mirror of
https://codeberg.org/armin/ambivalence.git
synced 2026-09-01 04:10:48 +02:00
init
This commit is contained in:
parent
29c860587e
commit
42cf8432bf
57 changed files with 5782 additions and 0 deletions
349
Source/AlgorithmPresets.h
Normal file
349
Source/AlgorithmPresets.h
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
#pragma once
|
||||
// ============================================================
|
||||
// AlgorithmPresets.h FDN Reverb Acoustic Data Library
|
||||
// 7 algorithms x 10 bands of RT60 / EDT / D50 / C50 / C80
|
||||
// Bands: 31.25 / 62.5 / 125 / 250 / 500 / 1k / 2k / 4k / 8k / 16k Hz
|
||||
// ============================================================
|
||||
#include <array>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
static constexpr int NUM_BANDS = 10;
|
||||
static constexpr int NUM_ALGORITHMS = 7;
|
||||
|
||||
// Octave-band centre frequencies (Hz)
|
||||
static constexpr std::array<float, NUM_BANDS> BAND_FREQ = {
|
||||
31.25f, 62.5f, 125.0f, 250.0f, 500.0f,
|
||||
1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f
|
||||
};
|
||||
|
||||
struct AcousticData {
|
||||
std::array<float, NUM_BANDS> rt60; // RT60 (s)
|
||||
std::array<float, NUM_BANDS> edt; // EDT (s)
|
||||
std::array<float, NUM_BANDS> d50; // Definition 0-1
|
||||
std::array<float, NUM_BANDS> c50; // Clarity 50 ms (dB)
|
||||
std::array<float, NUM_BANDS> c80; // Clarity 80 ms (dB)
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// PresetDefaults: when a preset is selected load parameter
|
||||
// -----------------------------------------------------------------------------
|
||||
// when a preset is selected, these APVTS parameters are set automatically:
|
||||
// so the distinctive sound of the preset can be experienced immediately.
|
||||
// the user can still fine-tune every parameter afterwards.
|
||||
//
|
||||
// * v1.1.0 added : saturation
|
||||
// the saturation harmonic intensity that brings out each preset's character.
|
||||
// Room1/Room2: subtle (kept clean, restrained warmth)
|
||||
// Hall1/Hall2: moderate (emphasizes a rich ring)
|
||||
// Plate: moderate (brings out the metallic character)
|
||||
// Spring: high (doubles the distinctive metallic character)
|
||||
// Goldfoil: moderate (balanced)
|
||||
//
|
||||
// design rationale:
|
||||
// - decayTime is the preset's native mid-band (500 Hz) RT60,
|
||||
// giving decayScale = 1.0 so the source RT curve is reproduced exactly
|
||||
// - other parameters are empirical values based on each preset's physical traits
|
||||
// -----------------------------------------------------------------------------
|
||||
struct PresetDefaults {
|
||||
float roomSize; // 0.5 ~ 2.0
|
||||
float decayTime; // seconds ( source preset mid-band RT60)
|
||||
float hfDamp; // 0.0 ~ 1.0
|
||||
float lfAbsorb; // 0.0 ~ 1.0
|
||||
float diffusion; // 0.0 ~ 1.0
|
||||
float modAmount; // 0.0 ~ 1.0
|
||||
float modRate; // 0.1 ~ 10 Hz
|
||||
float erLevel; // 0.0 ~ 1.0
|
||||
float saturation; // * new in v1.1 : 0.0 ~ 1.0 ( harmonics intensity )
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// preset default values
|
||||
// -----------------------------------------------------------------------------
|
||||
// index corresponds to algorithmIndex (0=Room1, 1=Room2, ..., 6=Goldfoil)
|
||||
//
|
||||
// saturation value rationale:
|
||||
// - Room1/2 (0.15f/0.20f): natural, clean space; harmonics kept subtle.
|
||||
// - Hall1/2 (0.30f/0.35f): adds warmth to the rich reverb.
|
||||
// - Plate (0.40f): metal-plate resonance; moderate harmonics.
|
||||
// - Spring (0.55f): distinctive metallic character is the core; stronger harmonics highlight it.
|
||||
// - Goldfoil (0.35f): balanced, between plate and spring.
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr std::array<PresetDefaults, 7> PRESET_DEFAULTS = { {
|
||||
// Room1: small live room; decayTime = rt60[4] = 0.21 s
|
||||
// keep the natural sense of space; saturation minimal (0.00f)
|
||||
{ 0.85f, 0.21f, 0.55f, 0.45f, 0.55f, 0.20f, 0.40f, 0.70f, 0.00f },
|
||||
|
||||
// Room2: medium live room; decayTime = rt60[4] = 1.38 s
|
||||
// slightly wider than Room1, add a little warmth (0.05f)
|
||||
{ 1.00f, 1.38f, 0.50f, 0.40f, 0.60f, 0.25f, 0.45f, 0.65f, 0.05f },
|
||||
|
||||
// Hall1: medium hall; decayTime = rt60[4] = 1.89 s
|
||||
// bring out the hall's richness with moderate harmonics (0.10f)
|
||||
{ 1.30f, 1.89f, 0.45f, 0.35f, 0.70f, 0.30f, 0.30f, 0.60f, 0.10f },
|
||||
|
||||
// Hall2: large hall; decayTime = rt60[4] = 2.08 s
|
||||
// add harmonics for the grandeur of the large space (0.10f)
|
||||
{ 1.50f, 2.08f, 0.50f, 0.30f, 0.75f, 0.30f, 0.30f, 0.55f, 0.10f },
|
||||
|
||||
// Plate: plate reverb (metal plate); decayTime = rt60[4] = 1.1431 s
|
||||
// the metal-plate resonance is the core; moderate harmonics bring out its character (0.15f)
|
||||
{ 0.70f, 1.14f, 0.65f, 0.55f, 0.85f, 0.15f, 0.50f, 0.20f, 0.15f },
|
||||
|
||||
// Spring: spring decayTime = rt60[4] = 2.9252 s
|
||||
// distinctive metallic character spring essence . stronger harmonics character (0.20f)
|
||||
{ 0.50f, 2.93f, 0.70f, 0.60f, 0.65f, 0.26f, 0.33f, 0.10f, 0.20f },
|
||||
|
||||
// Goldfoil: gold foil decayTime = rt60[4] = 2.0642 s
|
||||
// plate spring in between . balanced (0.18f)
|
||||
{ 0.95f, 2.06f, 0.55f, 0.40f, 0.80f, 0.35f, 0.45f, 0.30f, 0.18f }
|
||||
} };
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ERPattern: preset ISM (Image Source Method) ER
|
||||
// -----------------------------------------------------------------------------
|
||||
// Allen-Berkley 1979 ISM theory , preset space characteristics
|
||||
// 12 early reflections .
|
||||
//
|
||||
// (delayMs, gain) :
|
||||
// - delayMs: input delay time ( ms )
|
||||
// - gain: amplitude (0.0 ~ 1.0)
|
||||
//
|
||||
// design principles :
|
||||
// - distance inverse-square law : gain prop 1/distance prop 1/delayMs
|
||||
// - wall absorption coefficient ( reflection about -3dB ~ -6dB)
|
||||
// - physical reflection : floor -> wall -> ceiling -> reflection
|
||||
//
|
||||
// Plate/Spring/Goldfoil space ER
|
||||
// (numTaps = 0). preset ER processing bypass .
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr int MAX_ER_TAPS = 12;
|
||||
|
||||
struct ERTap {
|
||||
float delayMs;
|
||||
float gain;
|
||||
};
|
||||
|
||||
struct ERPattern {
|
||||
int numTaps; // 0 ER bypass
|
||||
std::array<ERTap, MAX_ER_TAPS> taps;
|
||||
};
|
||||
|
||||
// preset ER
|
||||
static constexpr std::array<ERPattern, 7> PRESET_ER_PATTERNS = { {
|
||||
// -------------------------------------------------------------
|
||||
// Room1: small live room ( about 40 m^3)
|
||||
// early reflections density , near-field wall surface reflection strong
|
||||
// -------------------------------------------------------------
|
||||
{ 12, {{
|
||||
{ 5.2f, 0.65f }, // floor 1 reflection
|
||||
{ 8.7f, 0.58f }, // side wall 1 reflection
|
||||
{ 12.4f, 0.52f }, // side wall 1 reflection
|
||||
{ 15.8f, 0.46f }, // ceiling 1 reflection
|
||||
{ 19.3f, 0.42f }, // rear wall 1 reflection
|
||||
{ 24.1f, 0.36f }, // floor + wall 2 reflection
|
||||
{ 28.6f, 0.32f }, // wall + ceiling 2 reflection
|
||||
{ 33.5f, 0.28f }, // 2 reflection
|
||||
{ 38.9f, 0.24f }, // 2 reflection
|
||||
{ 45.2f, 0.20f }, // 3 reflection group
|
||||
{ 52.8f, 0.16f }, // 3 reflection group
|
||||
{ 62.4f, 0.13f } // 3 reflection group
|
||||
}}},
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Room2: medium live room ( about 100 m^3)
|
||||
// reflection times wide range , ER smooth
|
||||
// -------------------------------------------------------------
|
||||
{ 12, {{
|
||||
{ 7.5f, 0.62f },
|
||||
{ 12.3f, 0.55f },
|
||||
{ 17.1f, 0.49f },
|
||||
{ 22.8f, 0.43f },
|
||||
{ 28.5f, 0.38f },
|
||||
{ 34.2f, 0.33f },
|
||||
{ 41.6f, 0.28f },
|
||||
{ 49.3f, 0.24f },
|
||||
{ 57.8f, 0.20f },
|
||||
{ 67.5f, 0.17f },
|
||||
{ 78.2f, 0.14f },
|
||||
{ 90.6f, 0.11f }
|
||||
}}},
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Hall1: medium hall ( about 2000 m^3)
|
||||
// side wall reflection dominant , initial delay longer
|
||||
// -------------------------------------------------------------
|
||||
{ 12, {{
|
||||
{ 12.0f, 0.58f }, // floor 1 reflection
|
||||
{ 18.5f, 0.52f }, // side wall 1 reflection
|
||||
{ 25.7f, 0.47f }, // ceiling 1 reflection
|
||||
{ 33.4f, 0.42f }, // side wall 1 reflection
|
||||
{ 42.1f, 0.38f }, // rear wall 1 reflection
|
||||
{ 51.6f, 0.33f }, // 2 reflection
|
||||
{ 62.3f, 0.29f }, // 2 reflection
|
||||
{ 73.9f, 0.25f }, // 2 reflection
|
||||
{ 86.5f, 0.21f }, // 3 reflection
|
||||
{ 99.8f, 0.18f }, // 3 reflection
|
||||
{ 113.4f, 0.15f }, // 3 reflection
|
||||
{ 128.7f, 0.12f } // diffuse reflection
|
||||
}}},
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Hall2: large hall ( about 12000 m^3)
|
||||
// reflection times further , initial delay large
|
||||
// -------------------------------------------------------------
|
||||
{ 12, {{
|
||||
{ 16.5f, 0.55f },
|
||||
{ 24.8f, 0.50f },
|
||||
{ 33.7f, 0.45f },
|
||||
{ 43.5f, 0.40f },
|
||||
{ 54.2f, 0.36f },
|
||||
{ 65.8f, 0.32f },
|
||||
{ 78.4f, 0.28f },
|
||||
{ 92.1f, 0.24f },
|
||||
{ 107.3f, 0.21f },
|
||||
{ 123.6f, 0.18f },
|
||||
{ 141.2f, 0.15f },
|
||||
{ 160.5f, 0.12f }
|
||||
}}},
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Plate: space (ER bypass )
|
||||
// -------------------------------------------------------------
|
||||
{ 0, {{}} },
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Spring: space (ER bypass )
|
||||
// -------------------------------------------------------------
|
||||
{ 0, {{}} },
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Goldfoil: space (ER bypass )
|
||||
// -------------------------------------------------------------
|
||||
{ 0, {{}} }
|
||||
} };
|
||||
|
||||
struct AlgorithmPreset {
|
||||
const char* name;
|
||||
const char* description;
|
||||
float volumeM3; // estimated room volume (0 = non-room)
|
||||
AcousticData acoustics;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ROOM 1 : OpenAIR Measured Room
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr AlgorithmPreset PRESET_ROOM1 = {
|
||||
"ROOM1", "Real Room 1 (OpenAIR)", 40.0f,
|
||||
{
|
||||
// RT60 (s) 31 62 125 250 500 1k 2k 4k 8k 16k
|
||||
{{ 0.8f, 0.57f, 0.28f, 0.27f, 0.21f, 0.19f, 0.21f, 0.22f, 0.21f, 0.18f }},
|
||||
// EDT (s)
|
||||
{{ 0.98f, 0.73f, 0.34f, 0.22f, 0.22f, 0.22f, 0.22f, 0.22f, 0.22f, 0.22f }},
|
||||
// D50
|
||||
{{ 0.26f, 0.46f, 0.92f, 0.94f, 0.98f, 0.97f, 0.97f, 0.97f, 0.96f, 0.98f }},
|
||||
// C50 (dB)
|
||||
{{ -4.65f, -0.74f, 10.63f, 11.92f, 16.19f, 15.36f, 14.75f, 14.48f, 13.44f, 17.54f }},
|
||||
// C80 (dB)
|
||||
{{ -2.33f, 5.82f, 16.5f, 20.03f, 23.87f, 26.0f, 23.59f, 22.54f, 22.92f, 27.99f }}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ROOM 2 : OpenAIR Measured Room
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr AlgorithmPreset PRESET_ROOM2 = {
|
||||
"ROOM2", "Real Room 2 (OpenAIR)", 100.0f,
|
||||
{
|
||||
{{ 4.46f, 1.53f, 1.65f, 1.59f, 1.38f, 0.94f, 0.93f, 0.87f, 0.67f, 1.54f }},
|
||||
{{ 1.59f, 1.33f, 1.07f, 1.07f, 1.2f, 1.07f, 0.95f, 0.82f, 0.69f, 0.43f }},
|
||||
{{ 0.2f, 0.3f, 0.39f, 0.56f, 0.43f, 0.53f, 0.52f, 0.57f, 0.68f, 0.86f }},
|
||||
{{ -5.99f, -3.59f, -1.96f, 1.05f, -1.16f, 0.51f, 0.41f, 1.21f, 3.23f, 7.73f }},
|
||||
{{ 0.74f, 0.26f, 3.76f, 3.18f, 1.94f, 3.67f, 4.06f, 5.35f, 7.25f, 12.52f }}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// HALL 1 : OpenAIR Measured Hall
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr AlgorithmPreset PRESET_HALL1 = {
|
||||
"HALL1", "Real Hall 1 (OpenAIR)", 2000.0f,
|
||||
{
|
||||
{{ 3.55f, 2.28f, 2.18f, 2.05f, 1.89f, 1.86f, 1.69f, 1.28f, 0.9f, 5.53f }},
|
||||
{{ 2.72f, 1.95f, 1.7f, 2.21f, 2.08f, 2.08f, 1.82f, 1.44f, 1.06f, 0.42f }},
|
||||
{{ 0.06f, 0.19f, 0.25f, 0.12f, 0.15f, 0.19f, 0.2f, 0.33f, 0.41f, 0.83f }},
|
||||
{{ -12.08f, -6.4f, -4.76f, -8.49f, -7.57f, -6.27f, -6.11f, -3.16f, -1.62f, 6.9f }},
|
||||
{{ -7.02f, -2.45f, -0.66f, -3.54f, -2.86f, -2.84f, -2.37f, 0.46f, 2.63f, 11.94f }}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// HALL 2 : OpenAIR Measured Hall
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr AlgorithmPreset PRESET_HALL2 = {
|
||||
"HALL2", "Real Hall 2 (OpenAIR)", 12000.0f,
|
||||
{
|
||||
{{ 2.15f, 1.48f, 1.63f, 1.91f, 2.08f, 2.09f, 1.82f, 1.6f, 1.18f, 1.11f }},
|
||||
{{ 1.83f, 1.45f, 1.45f, 2.34f, 2.22f, 1.96f, 1.83f, 1.7f, 1.45f, 1.19f }},
|
||||
{{ 0.08f, 0.2f, 0.4f, 0.16f, 0.21f, 0.29f, 0.37f, 0.3f, 0.33f, 0.44f }},
|
||||
{{ -10.33f, -6.01f, -1.69f, -7.33f, -5.82f, -3.83f, -2.34f, -3.73f, -3.08f, -1.f }},
|
||||
{{ -2.99f, -3.18f, -0.69f, -3.02f, -3.05f, -0.92f, -0.36f, -0.77f, 0.13f, 2.59f }}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// PLATE : Studio Nord Bremen EMT-Style
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr AlgorithmPreset PRESET_PLATE = {
|
||||
"PLATE", "Vintage Plate (Studio Nord Bremen)", 0.0f,
|
||||
{
|
||||
{{ 4.6257f, 2.4647f, 1.6639f, 1.6039f, 1.1431f, 0.8664f, 0.6561f, 0.4921f, 0.3153f, 0.1973f }},
|
||||
{{ 4.0113f, 2.1374f, 1.4429f, 1.3909f, 1.1926f, 0.8981f, 0.6476f, 0.5082f, 0.3132f, 0.2154f }},
|
||||
{{ 0.2421f, 0.3389f, 0.4357f, 0.4841f, 0.3966f, 0.5246f, 0.5914f, 0.7155f, 0.8915f, 0.9695f }},
|
||||
{{ -0.795f, -0.4236f, -0.286f, -0.2757f, -1.8231f, 0.4282f, 1.6063f, 4.0053f, 9.1464f, 15.0212f }},
|
||||
{{ 5.9205f, 3.1547f, 2.1296f, 2.0529f, 1.1228f, 3.8402f, 5.8356f, 8.7699f, 14.5235f, 23.4324f }}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// SPRING : Studio Nord Bremen Vintage Spring
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr AlgorithmPreset PRESET_SPRING = {
|
||||
"SPRING", "Vintage Spring (Studio Nord Bremen)", 0.0f,
|
||||
{
|
||||
{{ 10.3635f, 5.522f, 3.7278f, 3.5934f, 2.9252f, 2.7681f, 2.0397f, 2.0373f, 2.1111f, 0.9319f }},
|
||||
{{ 4.9277f, 2.6256f, 1.7725f, 1.7086f, 1.5756f, 1.875f, 1.3502f, 1.3897f, 1.759f, 0.4177f }},
|
||||
{{ 0.1261f, 0.1765f, 0.227f, 0.2522f, 0.2939f, 0.1999f, 0.3321f, 0.3724f, 0.3551f, 0.9202f }},
|
||||
{{ -13.6143f, -7.2541f, -4.8971f, -4.7206f, -3.806f, -6.0225f, -3.0348f, -2.2663f, -2.591f, 10.6169f }},
|
||||
{{ -3.0172f, -1.6077f, -1.0853f, -1.0462f, -1.6692f, -2.0212f, 0.4169f, 0.5432f, 0.0761f, 13.5636f }}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// GOLD FOIL : Studio Nord Bremen Foil Reverb
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr AlgorithmPreset PRESET_GOLDFOIL = {
|
||||
"GOLDFOIL", "Gold Foil (Studio Nord Bremen)", 0.0f,
|
||||
{
|
||||
{{ 6.3421f, 3.3793f, 2.2813f, 2.1991f, 2.0642f, 2.255f, 2.17f, 1.4604f, 0.8313f, 0.4339f }},
|
||||
{{ 5.8718f, 3.1287f, 2.1121f, 2.036f, 1.8796f, 2.2713f, 2.0286f, 1.4122f, 0.7486f, 0.3458f }},
|
||||
{{ 0.1209f, 0.1693f, 0.2177f, 0.2419f, 0.2894f, 0.2261f, 0.2938f, 0.4516f, 0.6304f, 0.8808f }},
|
||||
{{ -14.3077f, -7.6236f, -5.1466f, -4.9611f, -3.9018f, -5.3429f, -3.8094f, -0.8434f, 2.3195f, 8.6843f }},
|
||||
{{ -2.5178f, -1.3416f, -0.9057f, -0.873f, -1.4987f, -1.424f, -1.4095f, 1.7577f, 5.4677f, 12.9829f }}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Master table
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr std::array<const AlgorithmPreset*, NUM_ALGORITHMS> ALL_PRESETS = { {
|
||||
&PRESET_ROOM1,
|
||||
&PRESET_ROOM2,
|
||||
&PRESET_HALL1,
|
||||
&PRESET_HALL2,
|
||||
&PRESET_PLATE,
|
||||
&PRESET_SPRING,
|
||||
&PRESET_GOLDFOIL
|
||||
} };
|
||||
|
||||
} // namespace FDNReverb
|
||||
5
Source/BuildInfo.h.in
Normal file
5
Source/BuildInfo.h.in
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#define AMBIVALENCE_GIT_BRANCH "@AMBIVALENCE_GIT_BRANCH@"
|
||||
#define AMBIVALENCE_GIT_COMMIT "@AMBIVALENCE_GIT_COMMIT@"
|
||||
#define AMBIVALENCE_GIT_DIRTY @AMBIVALENCE_GIT_DIRTY@
|
||||
168
Source/DSP/AcousticMetrics.cpp
Normal file
168
Source/DSP/AcousticMetrics.cpp
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#include "AcousticMetrics.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
void AcousticMetrics::prepare(double sr, float windowMs) {
|
||||
sampleRate = sr;
|
||||
analysisWindowMs = windowMs;
|
||||
|
||||
// sample rate time sample count
|
||||
samples50ms = static_cast<int>(0.050 * sr);
|
||||
samples80ms = static_cast<int>(0.080 * sr);
|
||||
analysisWindowSamples = static_cast<int>(windowMs * 0.001 * sr);
|
||||
|
||||
// read from the history buffer size analysis +
|
||||
size_t bufferSize = static_cast<size_t>(analysisWindowSamples + samples80ms + 64);
|
||||
energyHistory.assign(bufferSize, 0.0f);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void AcousticMetrics::reset() noexcept {
|
||||
std::fill(energyHistory.begin(), energyHistory.end(), 0.0f);
|
||||
historyWritePos = 0;
|
||||
recent50msEnergy = 0.0;
|
||||
recent80msEnergy = 0.0;
|
||||
totalEnergy = 0.0;
|
||||
energyPeak = 0.0f;
|
||||
energyPeakPos = 0;
|
||||
updateCounter = 0;
|
||||
|
||||
d50.store(0.0f, std::memory_order_relaxed);
|
||||
c50.store(0.0f, std::memory_order_relaxed);
|
||||
c80.store(0.0f, std::memory_order_relaxed);
|
||||
edt.store(0.0f, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void AcousticMetrics::processSample(float sample) noexcept {
|
||||
if (energyHistory.empty()) return;
|
||||
|
||||
const int bufferSize = static_cast<int>(energyHistory.size());
|
||||
|
||||
// current sample energy ( squared )
|
||||
float currentEnergy = sample * sample;
|
||||
|
||||
// read from the history buffer
|
||||
energyHistory[historyWritePos] = currentEnergy;
|
||||
|
||||
// update the running sums (50 ms / 80 ms / full window)
|
||||
// add the current sample, subtract the value from 50 ms ago
|
||||
const int read50Pos = (historyWritePos - samples50ms + bufferSize) % bufferSize;
|
||||
const int read80Pos = (historyWritePos - samples80ms + bufferSize) % bufferSize;
|
||||
const int readWindowPos = (historyWritePos - analysisWindowSamples + bufferSize) % bufferSize;
|
||||
|
||||
recent50msEnergy += currentEnergy - energyHistory[read50Pos];
|
||||
recent80msEnergy += currentEnergy - energyHistory[read80Pos];
|
||||
totalEnergy += currentEnergy - energyHistory[readWindowPos];
|
||||
|
||||
// peak detection (EDT estimate )
|
||||
if (currentEnergy > energyPeak) {
|
||||
energyPeak = currentEnergy;
|
||||
energyPeakPos = historyWritePos;
|
||||
}
|
||||
|
||||
//
|
||||
historyWritePos = (historyWritePos + 1) % bufferSize;
|
||||
|
||||
// value stable ( cumulative value 0 )
|
||||
if (recent50msEnergy < 0.0) recent50msEnergy = 0.0;
|
||||
if (recent80msEnergy < 0.0) recent80msEnergy = 0.0;
|
||||
if (totalEnergy < 0.0) totalEnergy = 0.0;
|
||||
|
||||
// value interval update
|
||||
if (++updateCounter >= kUpdateInterval) {
|
||||
updateMetrics();
|
||||
updateCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void AcousticMetrics::updateMetrics() noexcept {
|
||||
// 50ms energy
|
||||
double energy50ToInf = totalEnergy - recent50msEnergy;
|
||||
if (energy50ToInf < 1e-12) energy50ToInf = 1e-12;
|
||||
|
||||
// 80ms energy
|
||||
double energy80ToInf = totalEnergy - recent80msEnergy;
|
||||
if (energy80ToInf < 1e-12) energy80ToInf = 1e-12;
|
||||
|
||||
// entire energy ( minimum value clipping )
|
||||
double totalSafe = std::max(1e-12, totalEnergy);
|
||||
|
||||
// -- D50 compute (0~1 ) --
|
||||
float d50val = static_cast<float>(recent50msEnergy / totalSafe);
|
||||
d50val = std::min(1.0f, std::max(0.0f, d50val));
|
||||
d50.store(d50val, std::memory_order_relaxed);
|
||||
|
||||
// -- C50 compute (dB) --
|
||||
float c50val = static_cast<float>(10.0 * std::log10(recent50msEnergy / energy50ToInf));
|
||||
c50val = std::min(60.0f, std::max(-60.0f, c50val));
|
||||
c50.store(c50val, std::memory_order_relaxed);
|
||||
|
||||
// -- C80 compute (dB) --
|
||||
float c80val = static_cast<float>(10.0 * std::log10(recent80msEnergy / energy80ToInf));
|
||||
c80val = std::min(60.0f, std::max(-60.0f, c80val));
|
||||
c80.store(c80val, std::memory_order_relaxed);
|
||||
|
||||
// -- EDT estimate (running) --
|
||||
// after the peak, the time until energy falls to 1/10 (-10 dB decay)
|
||||
// * exact EDT needs offline IR analysis; here we estimate from the peak decay time
|
||||
float edtVal = 0.0f;
|
||||
if (energyPeak > 1e-9f) {
|
||||
// analysis peak
|
||||
// scan from the peak sample until the energy reaches 1/10
|
||||
const int bufferSize = static_cast<int>(energyHistory.size());
|
||||
int searchStart = energyPeakPos;
|
||||
float threshold = energyPeak * 0.1f; // 10dB decay
|
||||
int decaySamples = 0;
|
||||
for (int i = 1; i < analysisWindowSamples; ++i) {
|
||||
int pos = (searchStart + i) % bufferSize;
|
||||
if (energyHistory[pos] < threshold) {
|
||||
decaySamples = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
edtVal = static_cast<float>(decaySamples) / static_cast<float>(sampleRate) * 6.0f;
|
||||
// * 10 dB decay time x 6 ~= EDT (60 dB decay correction)
|
||||
}
|
||||
edt.store(edtVal, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// drawing: get instantaneous energy at a past time offset
|
||||
// -----------------------------------------------------------------------------
|
||||
// secondsAgo: how many seconds in the past to look up
|
||||
// returns: the energy value at that time (squared)
|
||||
//
|
||||
// reads the history buffer directly for the GUI.
|
||||
// out of range returns 0.
|
||||
// -----------------------------------------------------------------------------
|
||||
float AcousticMetrics::getEnergyAtTimeOffset(float secondsAgo) const noexcept {
|
||||
if (energyHistory.empty()) return 0.0f;
|
||||
|
||||
const int bufferSize = static_cast<int>(energyHistory.size());
|
||||
int offsetSamples = static_cast<int>(secondsAgo * static_cast<float>(sampleRate));
|
||||
|
||||
// clamp to range
|
||||
if (offsetSamples < 0) offsetSamples = 0;
|
||||
if (offsetSamples >= analysisWindowSamples) return 0.0f;
|
||||
|
||||
// read from the history buffer
|
||||
int readPos = (historyWritePos - 1 - offsetSamples + bufferSize) % bufferSize;
|
||||
return energyHistory[readPos];
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// input activity detection: energy over the last 50 ms
|
||||
// -----------------------------------------------------------------------------
|
||||
// used by the GUI to hide the "measured line" when inactive.
|
||||
// threshold: -60 dBFS (1e-6) energy
|
||||
// -----------------------------------------------------------------------------
|
||||
bool AcousticMetrics::isActive() const noexcept {
|
||||
// energy over the last 50 ms determines activity
|
||||
constexpr double kActivityThreshold = 1e-6; // -60dBFS
|
||||
return recent50msEnergy > kActivityThreshold;
|
||||
}
|
||||
|
||||
} // namespace FDNReverb
|
||||
104
Source/DSP/AcousticMetrics.h
Normal file
104
Source/DSP/AcousticMetrics.h
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
#pragma once
|
||||
#include "DSPConstants.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <vector> // <- 1 row added
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AcousticMetrics class
|
||||
// -----------------------------------------------------------------------------
|
||||
// Computes real-time acoustic metrics (D50, C50, C80, EDT).
|
||||
//
|
||||
// Principle:
|
||||
// Accumulate the squared energy of the input signal in a ring buffer,
|
||||
// then compare it against the energy from 50 ms / 80 ms ago,
|
||||
// and compute the D50 / C50 / C80 values.
|
||||
//
|
||||
// Sample-rate support:
|
||||
// times (ms) are converted to sample counts,
|
||||
// so 44.1 kHz through 192 kHz are supported automatically.
|
||||
//
|
||||
// CPU:
|
||||
// O(1) per-sample computation (energy accumulation)
|
||||
// CPU overhead: below ~0.5%
|
||||
// -----------------------------------------------------------------------------
|
||||
class AcousticMetrics {
|
||||
public:
|
||||
AcousticMetrics() = default;
|
||||
|
||||
// -- initialize --
|
||||
// sampleRate: sample rate (Hz)
|
||||
// analysisWindowMs: analysis window (ms). 2000 ms (2 s)
|
||||
void prepare(double sampleRate, float analysisWindowMs = 2000.0f);
|
||||
|
||||
// -- per-sample state update --
|
||||
// sample: current Wet signal sample (mono)
|
||||
void processSample(float sample) noexcept;
|
||||
|
||||
// -- value getters --
|
||||
// ranges:
|
||||
// D50: 0.0 ~ 1.0 ( 0.3~0.9)
|
||||
// C50: -10 ~ +30 dB
|
||||
// C80: -10 ~ +30 dB
|
||||
// EDT: 0.0 ~ 5.0 (s)
|
||||
float getD50() const noexcept { return d50.load(std::memory_order_relaxed); }
|
||||
float getC50() const noexcept { return c50.load(std::memory_order_relaxed); }
|
||||
float getC80() const noexcept { return c80.load(std::memory_order_relaxed); }
|
||||
float getEDT() const noexcept { return edt.load(std::memory_order_relaxed); }
|
||||
|
||||
// --- added: expose energy history for drawing ---
|
||||
// get the instantaneous energy (squared) at a time offset in the past
|
||||
float getEnergyAtTimeOffset(float secondsAgo) const noexcept;
|
||||
|
||||
// input activity detection (energy over the last 50 ms)
|
||||
bool isActive() const noexcept;
|
||||
|
||||
// -- reset --
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
// -- compute --
|
||||
void updateMetrics() noexcept;
|
||||
|
||||
// -- parameter --
|
||||
double sampleRate{ 48000.0 };
|
||||
float analysisWindowMs{ 2000.0f };
|
||||
|
||||
// 50ms / 80ms sample count ( sample rate depends on )
|
||||
int samples50ms{ 2400 }; // @ 48kHz
|
||||
int samples80ms{ 3840 }; // @ 48kHz
|
||||
int analysisWindowSamples{ 96000 }; // 2000ms @ 48kHz
|
||||
|
||||
// -- buffer --
|
||||
// energy history ( squared value )
|
||||
std::vector<float> energyHistory;
|
||||
int historyWritePos{ 0 };
|
||||
|
||||
// -- cumulative energy value --
|
||||
// 50ms cumulative energy ( time )
|
||||
double recent50msEnergy{ 0.0 };
|
||||
// 80ms cumulative energy ( time )
|
||||
double recent80msEnergy{ 0.0 };
|
||||
// entire cumulative energy
|
||||
double totalEnergy{ 0.0 };
|
||||
|
||||
// EDT : energy decay tracking
|
||||
float energyPeak{ 0.0f };
|
||||
int energyPeakPos{ 0 };
|
||||
|
||||
// -- output value (atomic for thread safety) --
|
||||
std::atomic<float> d50{ 0.0f };
|
||||
std::atomic<float> c50{ 0.0f };
|
||||
std::atomic<float> c80{ 0.0f };
|
||||
std::atomic<float> edt{ 0.0f };
|
||||
|
||||
// -- update --
|
||||
// sample compute ,
|
||||
// sample interval update
|
||||
int updateCounter{ 0 };
|
||||
static constexpr int kUpdateInterval = 1024; // about 21ms @ 48kHz
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
103
Source/DSP/BiquadFilters.cpp
Normal file
103
Source/DSP/BiquadFilters.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include "BiquadFilters.h"
|
||||
#include "MagnitudeResponseFitter.h"
|
||||
#include <JuceHeader.h>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace FDNReverb {
|
||||
namespace FilterDesign {
|
||||
|
||||
static float tanPi(float f, double fs) noexcept {
|
||||
return std::tan(juce::MathConstants<float>::pi * (float)(f / fs));
|
||||
}
|
||||
|
||||
BiquadCoeffs lowShelf(float fcHz, float gainDB, double sampleRate) {
|
||||
float A = std::pow(10.f, gainDB / 40.f);
|
||||
float K = tanPi(fcHz, sampleRate);
|
||||
BiquadCoeffs c;
|
||||
if (gainDB >= 0.f) {
|
||||
float norm = 1.f / (1.f + K);
|
||||
c.b0 = (1.f + A * K) * norm;
|
||||
c.b1 = (A * K - 1.f) * norm;
|
||||
c.b2 = 0.f;
|
||||
c.a1 = (K - 1.f) * norm;
|
||||
c.a2 = 0.f;
|
||||
}
|
||||
else {
|
||||
c.b0 = (1.f + K / A) / (1.f + K);
|
||||
c.b1 = (K / A - 1.f) / (1.f + K);
|
||||
c.b2 = 0.f;
|
||||
c.a1 = (K - 1.f) / (1.f + K);
|
||||
c.a2 = 0.f;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
BiquadCoeffs highShelf(float fcHz, float gainDB, double sampleRate) {
|
||||
float A = std::pow(10.f, gainDB / 40.f);
|
||||
float K = tanPi(fcHz, sampleRate);
|
||||
BiquadCoeffs c;
|
||||
if (gainDB >= 0.f) {
|
||||
float norm = 1.f / (1.f + K);
|
||||
c.b0 = (A + K) * norm;
|
||||
c.b1 = (K - A) * norm;
|
||||
c.b2 = 0.f;
|
||||
c.a1 = (K - 1.f) * norm;
|
||||
c.a2 = 0.f;
|
||||
}
|
||||
else {
|
||||
float norm = 1.f / (1.f + K);
|
||||
c.b0 = (1.f + A * K) * norm;
|
||||
c.b1 = (A * K - 1.f) * norm;
|
||||
c.b2 = 0.f;
|
||||
c.a1 = (K - 1.f) * norm;
|
||||
c.a2 = 0.f;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
BiquadCoeffs peak(float fcHz, float gainDB, float Q, double sampleRate) {
|
||||
float A = std::pow(10.f, gainDB / 40.f);
|
||||
float w0 = 2.f * juce::MathConstants<float>::pi * fcHz / (float)sampleRate;
|
||||
float alpha = std::sin(w0) / (2.f * Q);
|
||||
float cos0 = std::cos(w0);
|
||||
BiquadCoeffs c;
|
||||
c.a1 = 2.f * cos0 / (1.f + alpha / A);
|
||||
c.a2 = (1.f - alpha / A) / (1.f + alpha / A);
|
||||
c.b0 = (1.f + alpha * A) / (1.f + alpha / A);
|
||||
c.b1 = -2.f * cos0 / (1.f + alpha / A);
|
||||
c.b2 = (1.f - alpha * A) / (1.f + alpha / A);
|
||||
return c;
|
||||
}
|
||||
|
||||
BiquadCoeffs highPass1st(float fcHz, double sampleRate) {
|
||||
float K = tanPi(fcHz, sampleRate);
|
||||
float n = 1.f + K;
|
||||
BiquadCoeffs c;
|
||||
c.b0 = 1.f / n; c.b1 = -1.f / n; c.b2 = 0.f;
|
||||
c.a1 = (K - 1.f) / n; c.a2 = 0.f;
|
||||
return c;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// designAbsorption: MagnitudeResponseFitter
|
||||
// -------------------------------------------------------------------------
|
||||
// keeps the existing (UniversalEngine) helper functions,
|
||||
// preserving the internal Stage-1 MRF behavior.
|
||||
//
|
||||
// old implementation : gain + Low/High cascade
|
||||
// new implementation : Jot orthogonalizing 1 filter + LF/HF correction
|
||||
// -------------------------------------------------------------------------
|
||||
std::array<BiquadCoeffs, ABSO_STAGES> designAbsorption(
|
||||
int delaySamples, double sampleRate,
|
||||
const std::array<float, NUM_BANDS>& rt60,
|
||||
float hfDamping, float lfAbsorption)
|
||||
{
|
||||
// MagnitudeResponseFitter processing
|
||||
auto result = MagnitudeResponseFitter::design(
|
||||
delaySamples, sampleRate, rt60, hfDamping, lfAbsorption);
|
||||
return result.coeffs;
|
||||
}
|
||||
|
||||
} // namespace FilterDesign
|
||||
} // namespace FDNReverb
|
||||
43
Source/DSP/BiquadFilters.h
Normal file
43
Source/DSP/BiquadFilters.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
#include "DSPConstants.h"
|
||||
#include "../AlgorithmPresets.h"
|
||||
#include <array>
|
||||
|
||||
namespace FDNReverb {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Biquad helpers (Direct Form II Transposed - most robust)
|
||||
// -----------------------------------------------------------------------------
|
||||
struct BiquadCoeffs {
|
||||
float b0{ 1.f }, b1{ 0.f }, b2{ 0.f };
|
||||
float a1{ 0.f }, a2{ 0.f };
|
||||
};
|
||||
|
||||
struct BiquadState {
|
||||
float s1{ 0.f }, s2{ 0.f };
|
||||
inline float tick(float x, const BiquadCoeffs& c) noexcept {
|
||||
float y = c.b0 * x + s1;
|
||||
s1 = c.b1 * x - c.a1 * y + s2;
|
||||
s2 = c.b2 * x - c.a2 * y;
|
||||
return y;
|
||||
}
|
||||
void reset() noexcept { s1 = s2 = 0.f; }
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Filter design utilities
|
||||
// -----------------------------------------------------------------------------
|
||||
namespace FilterDesign {
|
||||
BiquadCoeffs lowShelf(float fcHz, float gainDB, double sampleRate);
|
||||
BiquadCoeffs highShelf(float fcHz, float gainDB, double sampleRate);
|
||||
BiquadCoeffs peak(float fcHz, float gainDB, float Q, double sampleRate);
|
||||
BiquadCoeffs highPass1st(float fcHz, double sampleRate);
|
||||
BiquadCoeffs allpass1st(float fcHz, double sampleRate);
|
||||
|
||||
// Design absorption filter cascade for delay lines
|
||||
// : function internal MagnitudeResponseFitter
|
||||
std::array<BiquadCoeffs, ABSO_STAGES> designAbsorption(
|
||||
int delaySamples, double sampleRate,
|
||||
const std::array<float, NUM_BANDS>& rt60,
|
||||
float hfDamping, float lfAbsorption);
|
||||
}
|
||||
} // namespace FDNReverb
|
||||
26
Source/DSP/DSPConstants.h
Normal file
26
Source/DSP/DSPConstants.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
#include <array>
|
||||
namespace FDNReverb {
|
||||
// -- Compile-time constants ----------------------------------------------------
|
||||
static constexpr int FDN_N = 8; // FDN order (channels; legacy definition kept for reference)
|
||||
static constexpr int SAPF_STAGES = 3; // allpass stages per delay line
|
||||
static constexpr int ABSO_STAGES = 3; // Stage 1: Jot first-order + LF/HF correction
|
||||
static constexpr int ER_TAPS = 16; // early-reflection FIR taps
|
||||
|
||||
// Stage 2 (Valimaki-Liski cumulative GEQ) stages:
|
||||
// 10: 10-band GEQ (interaction matrix + WLS)
|
||||
//
|
||||
// important design notes:
|
||||
// - the mid-band gain (midGain) of GEQ band 0 is absorbed into the b0/b1/b2 coefficients;
|
||||
// no separate DC gain stage is needed to avoid DC coloration,
|
||||
// just a single gain.
|
||||
// - LF Absorption / HF Damping are applied directly as GEQ target dB,
|
||||
// fully independent of each other.
|
||||
// - targets are clamped to 0 dB or below, mathematically guaranteeing loop gain <= 1.
|
||||
static constexpr int ABSO_STAGES_S2 = 10;
|
||||
|
||||
// Mutually-prime base delays (samples @ 48 kHz), log-distributed 30-130 ms
|
||||
static constexpr std::array<int, FDN_N> BASE_PRIMES_48K = {
|
||||
1451, 1693, 1979, 2311, 2683, 3067, 3491, 3923
|
||||
};
|
||||
} // namespace FDNReverb
|
||||
134
Source/DSP/DelayMemory.h
Normal file
134
Source/DSP/DelayMemory.h
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// memory pool (Single-Large Buffer)
|
||||
// -----------------------------------------------------------------------------
|
||||
class DelayMemoryPool {
|
||||
public:
|
||||
void allocate(size_t totalSamples) {
|
||||
buffer.assign(totalSamples, 0.0f);
|
||||
allocOffset = 0;
|
||||
}
|
||||
|
||||
// pointer sized up to the next power of two (also outputs an index mask)
|
||||
float* requestMemory(size_t samplesNeeded, int& outMask) {
|
||||
size_t powerOfTwoSize = 1;
|
||||
while (powerOfTwoSize < samplesNeeded) powerOfTwoSize *= 2;
|
||||
|
||||
if (allocOffset + powerOfTwoSize > buffer.size()) return nullptr;
|
||||
|
||||
float* ptr = buffer.data() + allocOffset;
|
||||
outMask = static_cast<int>(powerOfTwoSize - 1);
|
||||
allocOffset += powerOfTwoSize;
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void clear() { std::fill(buffer.begin(), buffer.end(), 0.0f); }
|
||||
|
||||
private:
|
||||
std::vector<float> buffer;
|
||||
size_t allocOffset{ 0 };
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// interpolation
|
||||
// -----------------------------------------------------------------------------
|
||||
class LinearDelayLine {
|
||||
public:
|
||||
void init(float* memory, int bitmask) {
|
||||
buffer = memory;
|
||||
mask = bitmask;
|
||||
writeIndex = 0;
|
||||
}
|
||||
|
||||
// linear interpolation ( high band natural Air Absorption )
|
||||
inline float read(float delayInSamples) const noexcept {
|
||||
int id = static_cast<int>(delayInSamples);
|
||||
float frac = delayInSamples - static_cast<float>(id);
|
||||
|
||||
// bitwise ops undefined behavior completely , uint32_t
|
||||
uint32_t uWrite = static_cast<uint32_t>(writeIndex);
|
||||
uint32_t uId = static_cast<uint32_t>(id);
|
||||
uint32_t uMask = static_cast<uint32_t>(mask);
|
||||
|
||||
int readIdx1 = static_cast<int>((uWrite - uId) & uMask);
|
||||
int readIdx2 = static_cast<int>((uWrite - uId - 1) & uMask);
|
||||
|
||||
return buffer[readIdx1] + frac * (buffer[readIdx2] - buffer[readIdx1]);
|
||||
}
|
||||
|
||||
inline void write(float input) noexcept {
|
||||
buffer[writeIndex] = input;
|
||||
writeIndex = (writeIndex + 1) & mask;
|
||||
}
|
||||
|
||||
private:
|
||||
float* buffer{ nullptr };
|
||||
int mask{ 0 };
|
||||
int writeIndex{ 0 };
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Thiran allpass interpolation (preserves the phase response)
|
||||
// linear interpolation would dull high-band decay (sinc(pi*f) rolloff), so use a Thiran allpass
|
||||
// which keeps |H(w)| = 1, preserving high-band clarity in the FDN feedback loops.
|
||||
// -----------------------------------------------------------------------------
|
||||
class ThiranDelayLine {
|
||||
public:
|
||||
void init(float* memory, int bitmask) {
|
||||
buffer = memory;
|
||||
mask = bitmask;
|
||||
writeIndex = 0;
|
||||
thiranX1 = 0.0f;
|
||||
thiranY1 = 0.0f;
|
||||
}
|
||||
|
||||
void resetState() noexcept {
|
||||
thiranX1 = 0.0f;
|
||||
thiranY1 = 0.0f;
|
||||
}
|
||||
|
||||
// Thiran first-order allpass: y[n] = a*x[n] + x[n-1] - a*y[n-1]
|
||||
// a = (1-D)/(1+D), D = fractional delay
|
||||
inline float read(float delayInSamples) noexcept {
|
||||
int id = static_cast<int>(delayInSamples);
|
||||
float frac = delayInSamples - static_cast<float>(id);
|
||||
|
||||
// clamp below to avoid instability as frac->0, a->1
|
||||
frac = std::max(frac, 0.1f);
|
||||
const float a = (1.0f - frac) / (1.0f + frac);
|
||||
|
||||
uint32_t uWrite = static_cast<uint32_t>(writeIndex);
|
||||
uint32_t uId = static_cast<uint32_t>(id);
|
||||
uint32_t uMask = static_cast<uint32_t>(mask);
|
||||
|
||||
float xn = buffer[static_cast<int>((uWrite - uId) & uMask)];
|
||||
|
||||
float yn = a * xn + thiranX1 - a * thiranY1;
|
||||
thiranX1 = xn;
|
||||
thiranY1 = yn;
|
||||
|
||||
return yn;
|
||||
}
|
||||
|
||||
inline void write(float input) noexcept {
|
||||
buffer[writeIndex] = input;
|
||||
writeIndex = (writeIndex + 1) & mask;
|
||||
}
|
||||
|
||||
private:
|
||||
float* buffer{ nullptr };
|
||||
int mask{ 0 };
|
||||
int writeIndex{ 0 };
|
||||
float thiranX1{ 0.0f };
|
||||
float thiranY1{ 0.0f };
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
66
Source/DSP/EarlyReflections.cpp
Normal file
66
Source/DSP/EarlyReflections.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#include "EarlyReflections.h"
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
void EarlyReflections::prepare(const juce::dsp::ProcessSpec& spec) {
|
||||
int maxSamples = (int)(0.7 * spec.sampleRate) + 8;
|
||||
juce::dsp::ProcessSpec mono = spec;
|
||||
mono.numChannels = 1;
|
||||
buf.prepare(mono);
|
||||
buf.setMaximumDelayInSamples(maxSamples);
|
||||
|
||||
erHPCoeffs = FilterDesign::highPass1st(80.f, spec.sampleRate);
|
||||
float K = std::tan(juce::MathConstants<float>::pi * 6000.f / (float)spec.sampleRate);
|
||||
erLPCoeffs.b0 = K / (1.f + K);
|
||||
erLPCoeffs.b1 = erLPCoeffs.b0;
|
||||
erLPCoeffs.b2 = 0.f;
|
||||
erLPCoeffs.a1 = (K - 1.f) / (K + 1.f);
|
||||
erLPCoeffs.a2 = 0.f;
|
||||
}
|
||||
|
||||
void EarlyReflections::buildTaps(const AlgorithmPreset& preset, float roomSizeScale, double sampleRate) {
|
||||
float erEnergy50 = preset.acoustics.d50[4];
|
||||
float V = preset.volumeM3 > 0.f ? preset.volumeM3 : 10.f;
|
||||
float mixTimeMs = std::min(0.0117f * V + 50.1f, 150.f);
|
||||
|
||||
float span = mixTimeMs * roomSizeScale;
|
||||
for (int i = 0; i < ER_TAPS; ++i) {
|
||||
float t01 = static_cast<float>(i + 1) / static_cast<float>(ER_TAPS);
|
||||
float delMs = span * std::pow(t01, 1.5f);
|
||||
taps[i].delaySamples = delMs * 0.001f * (float)sampleRate;
|
||||
|
||||
float rt60m = preset.acoustics.rt60[4];
|
||||
float amp = std::exp(-6.9f * delMs * 0.001f / rt60m);
|
||||
float factor = (i < ER_TAPS / 2) ? std::sqrt(erEnergy50) : std::sqrt(1.f - erEnergy50);
|
||||
amp *= factor * std::sqrt(2.f / ER_TAPS);
|
||||
|
||||
float pan = (i % 3 == 0) ? -0.707f : ((i % 3 == 1) ? 0.707f : 0.0f);
|
||||
taps[i].gainL = amp * std::sqrt(0.5f - 0.5f * pan);
|
||||
taps[i].gainR = amp * std::sqrt(0.5f + 0.5f * pan);
|
||||
}
|
||||
}
|
||||
|
||||
void EarlyReflections::setPreDelay(float ms, double sampleRate) noexcept {
|
||||
preDelaySamples = juce::roundToInt(ms * 0.001 * sampleRate);
|
||||
}
|
||||
|
||||
std::pair<float, float> EarlyReflections::tick(float mono) noexcept {
|
||||
buf.pushSample(0, mono);
|
||||
float L = 0.f, R = 0.f;
|
||||
for (const auto& t : taps) {
|
||||
float d = buf.popSample(0, t.delaySamples + preDelaySamples, false);
|
||||
L += t.gainL * d;
|
||||
R += t.gainR * d;
|
||||
}
|
||||
L = erHPL.tick(L, erHPCoeffs);
|
||||
R = erHPR.tick(R, erHPCoeffs);
|
||||
return { L, R };
|
||||
}
|
||||
|
||||
void EarlyReflections::reset() noexcept {
|
||||
buf.reset();
|
||||
erHPL.reset(); erHPR.reset();
|
||||
erLPL.reset(); erLPR.reset();
|
||||
}
|
||||
|
||||
} // namespace FDNReverb
|
||||
32
Source/DSP/EarlyReflections.h
Normal file
32
Source/DSP/EarlyReflections.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "DSPConstants.h"
|
||||
#include "BiquadFilters.h"
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
struct ERTap {
|
||||
float delaySamples{ 0.f };
|
||||
float gainL{ 0.f };
|
||||
float gainR{ 0.f };
|
||||
};
|
||||
|
||||
class EarlyReflections {
|
||||
public:
|
||||
void prepare(const juce::dsp::ProcessSpec& spec);
|
||||
void buildTaps(const AlgorithmPreset& preset, float roomSizeScale, double sampleRate);
|
||||
void setPreDelay(float ms, double sampleRate) noexcept;
|
||||
|
||||
std::pair<float, float> tick(float mono) noexcept;
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
juce::dsp::DelayLine<float, juce::dsp::DelayLineInterpolationTypes::Lagrange3rd> buf;
|
||||
std::array<ERTap, ER_TAPS> taps;
|
||||
int preDelaySamples{ 0 };
|
||||
|
||||
BiquadCoeffs erHPCoeffs, erLPCoeffs;
|
||||
BiquadState erHPL, erHPR, erLPL, erLPR;
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
388
Source/DSP/MagnitudeResponseFitter.cpp
Normal file
388
Source/DSP/MagnitudeResponseFitter.cpp
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
#include "MagnitudeResponseFitter.h"
|
||||
#include <JuceHeader.h>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <complex>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// static
|
||||
// -----------------------------------------------------------------------------
|
||||
std::array<std::array<double, NUM_BANDS>, NUM_BANDS> MagnitudeResponseFitter::cachedB;
|
||||
std::array<std::array<double, NUM_BANDS>, NUM_BANDS> MagnitudeResponseFitter::cachedBtWB;
|
||||
std::array<double, NUM_BANDS> MagnitudeResponseFitter::cachedW;
|
||||
double MagnitudeResponseFitter::cachedSampleRate = 0.0;
|
||||
bool MagnitudeResponseFitter::cacheValid = false;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// band Q value ( band : Q ~ sqrt2 / (2^(1/2) - 2^(-1/2)) ~ 1.414)
|
||||
// -----------------------------------------------------------------------------
|
||||
static const std::array<float, NUM_BANDS> kBandQs = {
|
||||
1.7f, // 31.25 Hz (: Q rise )
|
||||
1.414f, // 62.5 Hz
|
||||
1.414f, // 125 Hz
|
||||
1.414f, // 250 Hz
|
||||
1.414f, // 500 Hz
|
||||
1.414f, // 1 kHz
|
||||
1.414f, // 2 kHz
|
||||
1.414f, // 4 kHz
|
||||
1.414f, // 8 kHz
|
||||
1.7f // 16 kHz (: Q rise )
|
||||
};
|
||||
|
||||
const std::array<float, NUM_BANDS>& MagnitudeResponseFitter::getBandQs() noexcept {
|
||||
return kBandQs;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 1 ( existing )
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
float MagnitudeResponseFitter::t60ToLoopGain(float t60Seconds, int delaySamples, double sampleRate) noexcept {
|
||||
float t60Safe = std::max(0.01f, t60Seconds);
|
||||
float exponent = -3.0f * static_cast<float>(delaySamples) / (static_cast<float>(sampleRate) * t60Safe);
|
||||
return std::pow(10.0f, exponent);
|
||||
}
|
||||
|
||||
float MagnitudeResponseFitter::computeJotPole(float gDC, float alphaRatio) noexcept {
|
||||
float alphaSafe = juce::jlimit(0.05f, 20.0f, alphaRatio);
|
||||
float gDCSafe = juce::jlimit(1e-6f, 0.99999f, gDC);
|
||||
constexpr float kLn10Over4 = 0.5756462732485f;
|
||||
float log10g = std::log10(gDCSafe);
|
||||
float alphaSqInv = 1.0f / (alphaSafe * alphaSafe);
|
||||
float pole = kLn10Over4 * log10g * (1.0f - alphaSqInv);
|
||||
return juce::jlimit(-0.98f, 0.98f, pole);
|
||||
}
|
||||
|
||||
BiquadCoeffs MagnitudeResponseFitter::orthogonalizedFirstOrderToBiquad(float gain, float pole) noexcept {
|
||||
BiquadCoeffs c;
|
||||
c.b0 = gain * (1.0f - pole);
|
||||
c.b1 = 0.0f;
|
||||
c.b2 = 0.0f;
|
||||
c.a1 = -pole;
|
||||
c.a2 = 0.0f;
|
||||
return c;
|
||||
}
|
||||
|
||||
float MagnitudeResponseFitter::getT60AtDC(const std::array<float, NUM_BANDS>& rt60) noexcept {
|
||||
return (rt60[0] + rt60[1]) * 0.5f;
|
||||
}
|
||||
|
||||
float MagnitudeResponseFitter::getT60AtNyquist(const std::array<float, NUM_BANDS>& rt60, double sampleRate) noexcept {
|
||||
if (sampleRate <= 50000.0) {
|
||||
return rt60[9];
|
||||
}
|
||||
else {
|
||||
return (rt60[8] + rt60[9]) * 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 1 main design function ( existing )
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
MagnitudeResponseFitter::DesignResult MagnitudeResponseFitter::design(
|
||||
int delaySamples,
|
||||
double sampleRate,
|
||||
const std::array<float, NUM_BANDS>& rt60,
|
||||
float hfDamping,
|
||||
float lfAbsorption)
|
||||
{
|
||||
DesignResult result;
|
||||
|
||||
float t60DC = std::max(0.01f, getT60AtDC(rt60));
|
||||
float t60Nyq = std::max(0.01f, getT60AtNyquist(rt60, sampleRate));
|
||||
|
||||
float gDC = t60ToLoopGain(t60DC, delaySamples, sampleRate);
|
||||
float gNyq = t60ToLoopGain(t60Nyq, delaySamples, sampleRate);
|
||||
|
||||
float alpha = t60Nyq / t60DC;
|
||||
float pole = computeJotPole(gDC, alpha);
|
||||
|
||||
result.coeffs[0] = orthogonalizedFirstOrderToBiquad(gDC, pole);
|
||||
|
||||
float lfShelfDB = -lfAbsorption * 3.0f;
|
||||
result.coeffs[1] = FilterDesign::lowShelf(150.0f, lfShelfDB, sampleRate);
|
||||
|
||||
float hfShelfDB = -hfDamping * 6.0f;
|
||||
result.coeffs[2] = FilterDesign::highShelf(4000.0f, hfShelfDB, sampleRate);
|
||||
|
||||
result.dcGain = gDC;
|
||||
result.nyquistGain = gNyq;
|
||||
result.pole = pole;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 2 : Biquad peak filter
|
||||
// -----------------------------------------------------------------------------
|
||||
BiquadCoeffs MagnitudeResponseFitter::designSymmetricPeakBiquad(
|
||||
float fcHz, float gainDB, float Q, double sampleRate) noexcept
|
||||
{
|
||||
float fcSafe = juce::jlimit(10.0f, static_cast<float>(sampleRate) * 0.49f, fcHz);
|
||||
|
||||
float A = std::pow(10.0f, gainDB / 40.0f);
|
||||
float w0 = 2.0f * juce::MathConstants<float>::pi * fcSafe / static_cast<float>(sampleRate);
|
||||
float cosW0 = std::cos(w0);
|
||||
float sinW0 = std::sin(w0);
|
||||
float alpha = sinW0 / (2.0f * std::max(0.1f, Q));
|
||||
|
||||
float a0 = 1.0f + alpha / A;
|
||||
|
||||
BiquadCoeffs c;
|
||||
c.b0 = (1.0f + alpha * A) / a0;
|
||||
c.b1 = -2.0f * cosW0 / a0;
|
||||
c.b2 = (1.0f - alpha * A) / a0;
|
||||
c.a1 = -2.0f * cosW0 / a0;
|
||||
c.a2 = (1.0f - alpha / A) / a0;
|
||||
return c;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 2 : Biquad magnitude response (dB) compute
|
||||
// -----------------------------------------------------------------------------
|
||||
float MagnitudeResponseFitter::biquadMagnitudeDB(
|
||||
const BiquadCoeffs& c, float fEval, double sampleRate) noexcept
|
||||
{
|
||||
double w = 2.0 * juce::MathConstants<double>::pi * fEval / sampleRate;
|
||||
double cosW = std::cos(w);
|
||||
double sinW = std::sin(w);
|
||||
double cos2W = std::cos(2.0 * w);
|
||||
double sin2W = std::sin(2.0 * w);
|
||||
|
||||
double bRe = c.b0 + c.b1 * cosW + c.b2 * cos2W;
|
||||
double bIm = -c.b1 * sinW - c.b2 * sin2W;
|
||||
|
||||
double aRe = 1.0 + c.a1 * cosW + c.a2 * cos2W;
|
||||
double aIm = -c.a1 * sinW - c.a2 * sin2W;
|
||||
|
||||
double bMag2 = bRe * bRe + bIm * bIm;
|
||||
double aMag2 = aRe * aRe + aIm * aIm;
|
||||
|
||||
double mag2 = bMag2 / std::max(1e-30, aMag2);
|
||||
|
||||
return static_cast<float>(10.0 * std::log10(std::max(1e-30, mag2)));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 2 : 10x10 LDLT decomposition solver
|
||||
// -----------------------------------------------------------------------------
|
||||
void MagnitudeResponseFitter::solveLDLT10(
|
||||
const std::array<std::array<double, NUM_BANDS>, NUM_BANDS>& A,
|
||||
const std::array<double, NUM_BANDS>& b,
|
||||
std::array<double, NUM_BANDS>& x) noexcept
|
||||
{
|
||||
constexpr int N = NUM_BANDS;
|
||||
double L[N][N] = { 0 };
|
||||
double D[N] = { 0 };
|
||||
|
||||
for (int i = 0; i < N; ++i) L[i][i] = 1.0;
|
||||
|
||||
for (int j = 0; j < N; ++j) {
|
||||
double sum = A[j][j];
|
||||
for (int k = 0; k < j; ++k) {
|
||||
sum -= L[j][k] * L[j][k] * D[k];
|
||||
}
|
||||
D[j] = sum;
|
||||
|
||||
if (std::abs(D[j]) < 1e-12) {
|
||||
D[j] = (D[j] < 0.0 ? -1e-12 : 1e-12);
|
||||
}
|
||||
|
||||
for (int i = j + 1; i < N; ++i) {
|
||||
double s = A[i][j];
|
||||
for (int k = 0; k < j; ++k) {
|
||||
s -= L[i][k] * L[j][k] * D[k];
|
||||
}
|
||||
L[i][j] = s / D[j];
|
||||
}
|
||||
}
|
||||
|
||||
double z[N];
|
||||
for (int i = 0; i < N; ++i) {
|
||||
double s = b[i];
|
||||
for (int k = 0; k < i; ++k) s -= L[i][k] * z[k];
|
||||
z[i] = s;
|
||||
}
|
||||
|
||||
double y[N];
|
||||
for (int i = 0; i < N; ++i) y[i] = z[i] / D[i];
|
||||
|
||||
for (int i = N - 1; i >= 0; --i) {
|
||||
double s = y[i];
|
||||
for (int k = i + 1; k < N; ++k) s -= L[k][i] * x[k];
|
||||
x[i] = s;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 2 : Biquad coefficient linear gain absorption
|
||||
// -----------------------------------------------------------------------------
|
||||
// H(z) = (b0 + b1.z^{-1} + b2.z^{-2}) / (1 + a1.z^{-1} + a2.z^{-2})
|
||||
//
|
||||
// frequency amplitude linearGain , (b0, b1, b2) linearGain
|
||||
// . mathematically independent DC color apply completely .
|
||||
BiquadCoeffs MagnitudeResponseFitter::absorbGainIntoBiquad(
|
||||
const BiquadCoeffs& c, float linearGain) noexcept
|
||||
{
|
||||
BiquadCoeffs result = c;
|
||||
result.b0 *= linearGain;
|
||||
result.b1 *= linearGain;
|
||||
result.b2 *= linearGain;
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 2: Interaction Matrix before compute
|
||||
// -----------------------------------------------------------------------------
|
||||
void MagnitudeResponseFitter::precomputeInteractionMatrix(double sampleRate) {
|
||||
if (cacheValid && std::abs(cachedSampleRate - sampleRate) < 0.5) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr int N = NUM_BANDS;
|
||||
constexpr float kProbeGainDB = 1.0f;
|
||||
|
||||
for (int j = 0; j < N; ++j) {
|
||||
BiquadCoeffs c = designSymmetricPeakBiquad(
|
||||
BAND_FREQ[j], kProbeGainDB, kBandQs[j], sampleRate);
|
||||
|
||||
for (int i = 0; i < N; ++i) {
|
||||
float dB = biquadMagnitudeDB(c, BAND_FREQ[i], sampleRate);
|
||||
cachedB[i][j] = static_cast<double>(dB);
|
||||
}
|
||||
}
|
||||
|
||||
const std::array<double, NUM_BANDS> weights = {
|
||||
0.5, // 31.25 Hz
|
||||
0.7, // 62.5 Hz
|
||||
0.85, // 125 Hz
|
||||
1.0, // 250 Hz
|
||||
1.0, // 500 Hz
|
||||
1.0, // 1 kHz
|
||||
1.0, // 2 kHz
|
||||
1.0, // 4 kHz
|
||||
0.85, // 8 kHz
|
||||
0.6 // 16 kHz
|
||||
};
|
||||
for (int i = 0; i < N; ++i) cachedW[i] = weights[i];
|
||||
|
||||
for (int i = 0; i < N; ++i) {
|
||||
for (int j = 0; j < N; ++j) {
|
||||
double s = 0.0;
|
||||
for (int k = 0; k < N; ++k) {
|
||||
s += cachedB[k][i] * cachedW[k] * cachedB[k][j];
|
||||
}
|
||||
cachedBtWB[i][j] = s;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr double kRidge = 1e-4;
|
||||
for (int i = 0; i < N; ++i) cachedBtWB[i][i] += kRidge;
|
||||
|
||||
cachedSampleRate = sampleRate;
|
||||
cacheValid = true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Stage 2c: main design function ( fix )
|
||||
// -----------------------------------------------------------------------------
|
||||
// :
|
||||
// 1. band target dB compute (T60 dB )
|
||||
// t[i] = -60 . m / (fs . T60[i])
|
||||
// 2. LF/HF correction target dB directly
|
||||
// 3. target dB 0 below clamp -> loop gain <= 1 guarantee
|
||||
// 4. mid-band gain midGain (band 4 = 500Hz)
|
||||
// midGain = 10^(midDb/20)
|
||||
// 5. dB WLS
|
||||
// g_cmd = (B^T.W.B)^(-1).B^T.W.t_residual
|
||||
// 6. g_cmd[j] dB Biquad coefficient
|
||||
// 7. band 0 coefficient midGain absorption
|
||||
// -> independent DC color apply not needed
|
||||
MagnitudeResponseFitter::DesignResultStage2 MagnitudeResponseFitter::designStage2(
|
||||
int delaySamples,
|
||||
double sampleRate,
|
||||
const std::array<float, NUM_BANDS>& rt60,
|
||||
float hfDamping,
|
||||
float lfAbsorption)
|
||||
{
|
||||
precomputeInteractionMatrix(sampleRate);
|
||||
|
||||
DesignResultStage2 result;
|
||||
|
||||
constexpr int N = NUM_BANDS;
|
||||
const float fs = static_cast<float>(sampleRate);
|
||||
const float m = static_cast<float>(delaySamples);
|
||||
|
||||
// -- Step 1: band loop 1 gain dB target --
|
||||
std::array<float, NUM_BANDS> targetDb;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
float t60Safe = std::max(0.01f, rt60[i]);
|
||||
targetDb[i] = -60.0f * m / (fs * t60Safe);
|
||||
}
|
||||
|
||||
// -- Step 2: LF/HF correction target dB --
|
||||
// LF Absorption: low band (31Hz, 62Hz, 125Hz) added decay
|
||||
// lfAbsorption=0 -> correction , =1 -> -3dB added decay
|
||||
targetDb[0] += -lfAbsorption * 3.0f;
|
||||
targetDb[1] += -lfAbsorption * 2.5f;
|
||||
targetDb[2] += -lfAbsorption * 1.5f;
|
||||
// HF Damping: high band (4kHz, 8kHz, 16kHz) added decay
|
||||
// hfDamping=0 -> correction , =1 -> -6dB added decay
|
||||
targetDb[7] += -hfDamping * 3.0f;
|
||||
targetDb[8] += -hfDamping * 5.0f;
|
||||
targetDb[9] += -hfDamping * 6.0f;
|
||||
|
||||
// -- Step 3: target dB 0 below clamp --
|
||||
// loop gain <= 1 mathematically guarantee safe
|
||||
for (int i = 0; i < N; ++i) {
|
||||
targetDb[i] = std::min(targetDb[i], 0.0f);
|
||||
// decay precision influence below (-60dB/loop)
|
||||
targetDb[i] = std::max(targetDb[i], -60.0f);
|
||||
result.targetDb[i] = targetDb[i];
|
||||
}
|
||||
|
||||
// -- Step 4: mid-band gain midGain (band 4 = 500Hz) --
|
||||
float midDb = targetDb[4];
|
||||
float midGainLinear = std::pow(10.0f, midDb / 20.0f);
|
||||
result.midGainAbsorbed = midGainLinear;
|
||||
|
||||
// dB: mid-band deviation (GEQ frequency response )
|
||||
std::array<double, NUM_BANDS> residualDb;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
residualDb[i] = static_cast<double>(targetDb[i] - midDb);
|
||||
}
|
||||
|
||||
// -- Step 5: WLS GEQ coefficient --
|
||||
std::array<double, NUM_BANDS> rhs;
|
||||
for (int j = 0; j < N; ++j) {
|
||||
double s = 0.0;
|
||||
for (int k = 0; k < N; ++k) {
|
||||
s += cachedB[k][j] * cachedW[k] * residualDb[k];
|
||||
}
|
||||
rhs[j] = s;
|
||||
}
|
||||
|
||||
std::array<double, NUM_BANDS> gCmd;
|
||||
solveLDLT10(cachedBtWB, rhs, gCmd);
|
||||
|
||||
// -- Step 6: g_cmd[j] dB Biquad coefficient --
|
||||
// safe range clamp (+/-18 dB )
|
||||
for (int j = 0; j < N; ++j) {
|
||||
float gDb = static_cast<float>(juce::jlimit(-18.0, 18.0, gCmd[j]));
|
||||
result.commandDb[j] = gDb;
|
||||
result.geqStages[j] = designSymmetricPeakBiquad(
|
||||
BAND_FREQ[j], gDb, kBandQs[j], sampleRate);
|
||||
}
|
||||
|
||||
// -- Step 7: band 0 coefficient midGain absorption --
|
||||
// independent DC color apply not needed ,
|
||||
// filter cascade entire loop gain exact WLS .
|
||||
result.geqStages[0] = absorbGainIntoBiquad(result.geqStages[0], midGainLinear);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace FDNReverb
|
||||
130
Source/DSP/MagnitudeResponseFitter.h
Normal file
130
Source/DSP/MagnitudeResponseFitter.h
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#pragma once
|
||||
#include "DSPConstants.h"
|
||||
#include "BiquadFilters.h"
|
||||
#include "../AlgorithmPresets.h"
|
||||
#include <array>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MagnitudeResponseFitter
|
||||
// -----------------------------------------------------------------------------
|
||||
// designs the 10-band RT60 absorption filters for the FDN.
|
||||
//
|
||||
// design modes :
|
||||
// Stage 1 (Jot first-order orthogonalizing):
|
||||
// Jot-Chaigne (AES Preprint 3030, 1991) first-order orthogonalizing filters.
|
||||
// matched at DC and Nyquist with 2 design points.
|
||||
//
|
||||
// Stage 2c (Valimaki-Liski cumulative GEQ):
|
||||
// Valimaki & Liski (IEEE SPL 2017) Interaction Matrix + WLS
|
||||
// exact fit across the 10 bands.
|
||||
//
|
||||
// safety guarantee :
|
||||
// - targets are clamped to 0 dB or below -> loop gain <= 1 is guaranteed
|
||||
// - band 0 midGain and b0/b1/b2 are absorbed into the applied filter
|
||||
// - LF/HF corrections are independent GEQ targets in dB
|
||||
//
|
||||
// important :
|
||||
// - per-band decay in dB: -60*m / (fs*T60)
|
||||
// avoids the "2 kHz T60 assumption" of Schlecht-Habets (DAFx-17)
|
||||
// - design runs offline (message thread); the resulting Biquad coefficients
|
||||
// are used on the audio thread
|
||||
// -----------------------------------------------------------------------------
|
||||
class MagnitudeResponseFitter {
|
||||
public:
|
||||
enum class DesignMode {
|
||||
Stage1_Jot1stOrder, // Jot first-order orthogonalizing (2 pts: DC/Nyquist)
|
||||
Stage2_BiquadGEQ // Valimaki-Liski cumulative GEQ (exact at 10 bands)
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Stage 1 design result (existing)
|
||||
// -------------------------------------------------------------------------
|
||||
// ABSO_STAGES = 3 Biquads:
|
||||
// coeffs[0] = gain (Jot first-order orthogonalizing filter, Biquad form)
|
||||
// coeffs[1] = low-band correction (Low Shelf, LF Absorption)
|
||||
// coeffs[2] = high-band correction (High Shelf, HF Damping)
|
||||
struct DesignResult {
|
||||
std::array<BiquadCoeffs, ABSO_STAGES> coeffs;
|
||||
float dcGain{ 1.0f };
|
||||
float nyquistGain{ 1.0f };
|
||||
float pole{ 0.0f };
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Stage 2c design result
|
||||
// -------------------------------------------------------------------------
|
||||
// 10-band GEQ:
|
||||
// geqStages[0] = band 0 (31.25 Hz), midGain absorbed into the coefficient
|
||||
// geqStages[1..9] = bands 1-9 (62.5 Hz - 16 kHz), GEQ
|
||||
//
|
||||
// filter chain: geqStages[0] -> geqStages[1] -> ... -> geqStages[9]
|
||||
// no separate midGain stage is needed (absorbed into band 0).
|
||||
struct DesignResultStage2 {
|
||||
std::array<BiquadCoeffs, NUM_BANDS> geqStages; // 10-band GEQ
|
||||
|
||||
// visualization
|
||||
std::array<float, NUM_BANDS> targetDb; // per-band target dB (after clamping)
|
||||
std::array<float, NUM_BANDS> commandDb; // WLS-solved command dB
|
||||
float midGainAbsorbed{ 1.0f }; // midGain absorbed into band 0
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Stage 1 design function (existing)
|
||||
// -------------------------------------------------------------------------
|
||||
static DesignResult design(
|
||||
int delaySamples,
|
||||
double sampleRate,
|
||||
const std::array<float, NUM_BANDS>& rt60,
|
||||
float hfDamping,
|
||||
float lfAbsorption);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Stage 2c design function
|
||||
// -------------------------------------------------------------------------
|
||||
static DesignResultStage2 designStage2(
|
||||
int delaySamples,
|
||||
double sampleRate,
|
||||
const std::array<float, NUM_BANDS>& rt60,
|
||||
float hfDamping,
|
||||
float lfAbsorption);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// precompute the interaction matrix once (per sample rate)
|
||||
// -------------------------------------------------------------------------
|
||||
static void precomputeInteractionMatrix(double sampleRate);
|
||||
static double getCachedSampleRate() noexcept { return cachedSampleRate; }
|
||||
|
||||
private:
|
||||
// -- Stage 1 --
|
||||
static float t60ToLoopGain(float t60Seconds, int delaySamples, double sampleRate) noexcept;
|
||||
static float computeJotPole(float gDC, float alphaRatio) noexcept;
|
||||
static BiquadCoeffs orthogonalizedFirstOrderToBiquad(float gain, float pole) noexcept;
|
||||
static float getT60AtDC(const std::array<float, NUM_BANDS>& rt60) noexcept;
|
||||
static float getT60AtNyquist(const std::array<float, NUM_BANDS>& rt60, double sampleRate) noexcept;
|
||||
|
||||
// -- Stage 2 --
|
||||
static BiquadCoeffs designSymmetricPeakBiquad(
|
||||
float fcHz, float gainDB, float Q, double sampleRate) noexcept;
|
||||
static const std::array<float, NUM_BANDS>& getBandFreqs() noexcept { return BAND_FREQ; }
|
||||
static const std::array<float, NUM_BANDS>& getBandQs() noexcept;
|
||||
static float biquadMagnitudeDB(const BiquadCoeffs& c, float fEval, double sampleRate) noexcept;
|
||||
static void solveLDLT10(
|
||||
const std::array<std::array<double, NUM_BANDS>, NUM_BANDS>& A,
|
||||
const std::array<double, NUM_BANDS>& b,
|
||||
std::array<double, NUM_BANDS>& x) noexcept;
|
||||
|
||||
// absorb the entire DC gain of the Biquad (b0, b1, b2) into a gain
|
||||
// so an independent DC gain can be applied to the filter mathematically
|
||||
static BiquadCoeffs absorbGainIntoBiquad(const BiquadCoeffs& c, float linearGain) noexcept;
|
||||
|
||||
// -- Stage 2 static --
|
||||
static std::array<std::array<double, NUM_BANDS>, NUM_BANDS> cachedB;
|
||||
static std::array<std::array<double, NUM_BANDS>, NUM_BANDS> cachedBtWB;
|
||||
static std::array<double, NUM_BANDS> cachedW;
|
||||
static double cachedSampleRate;
|
||||
static bool cacheValid;
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
148
Source/DSP/OutputEQ.h
Normal file
148
Source/DSP/OutputEQ.h
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// OutputEQ: Wet output stage Lo/Hi Cut (Linkwitz-Riley 12dB/oct)
|
||||
// -----------------------------------------------------------------------------
|
||||
// design rationale:
|
||||
// - 1 IIR (6dB/oct) x 2 cascade = 12dB/oct
|
||||
// - Linkwitz-Riley topology: 2nd-order phase alignment
|
||||
// - keeps the reverb sounding musical
|
||||
//
|
||||
// filter equation (1 IIR):
|
||||
// HPF: y[n] = R . (y[n-1] + x[n] - x[n-1])
|
||||
// LPF: y[n] = (1 - R) . x[n] + R . y[n-1]
|
||||
// where R = exp(-2pi.fc/fs)
|
||||
//
|
||||
// real-time safety :
|
||||
// - no allocation at all
|
||||
// - per-sample cost: HPF 8 ops + LPF 6 ops (L/R combined)
|
||||
// - coefficients updated per block (no zipper noise, no SmoothedValue needed)
|
||||
//
|
||||
// bypass :
|
||||
// - Lo Cut below 20 Hz -> HPF fully bypassed
|
||||
// - Hi Cut above 20 kHz -> LPF fully bypassed
|
||||
// both bypasses are per-block coefficient updates, so CPU use is trivial.
|
||||
// -----------------------------------------------------------------------------
|
||||
class OutputEQ {
|
||||
public:
|
||||
OutputEQ() = default;
|
||||
|
||||
void prepare(double sampleRate) noexcept {
|
||||
fs = sampleRate;
|
||||
reset();
|
||||
setLoCutHz(20.0f);
|
||||
setHiCutHz(20000.0f);
|
||||
}
|
||||
|
||||
void reset() noexcept {
|
||||
// HPF state (two stages per channel, L/R)
|
||||
hpfX1_L_1 = hpfY1_L_1 = 0.0f;
|
||||
hpfX1_L_2 = hpfY1_L_2 = 0.0f;
|
||||
hpfX1_R_1 = hpfY1_R_1 = 0.0f;
|
||||
hpfX1_R_2 = hpfY1_R_2 = 0.0f;
|
||||
// LPF state (two stages per channel, L/R)
|
||||
lpfY1_L_1 = 0.0f;
|
||||
lpfY1_L_2 = 0.0f;
|
||||
lpfY1_R_1 = 0.0f;
|
||||
lpfY1_R_2 = 0.0f;
|
||||
}
|
||||
|
||||
// --- parameter setters (called per block) ---
|
||||
void setLoCutHz(float fcHz) noexcept {
|
||||
currentLoCutHz = fcHz;
|
||||
// bypass below 20 Hz (skip R computation)
|
||||
if (fcHz <= 20.0f) {
|
||||
loCutActive = false;
|
||||
return;
|
||||
}
|
||||
loCutActive = true;
|
||||
constexpr float twoPi = 6.28318530718f;
|
||||
const float clamped = std::clamp(fcHz, 20.0f, 500.0f);
|
||||
loCutR = std::exp(-twoPi * clamped / static_cast<float>(fs));
|
||||
}
|
||||
|
||||
void setHiCutHz(float fcHz) noexcept {
|
||||
currentHiCutHz = fcHz;
|
||||
// bypass above 20 kHz
|
||||
const float nyquist = static_cast<float>(fs) * 0.45f;
|
||||
const float clamped = std::clamp(fcHz, 1000.0f, std::min(20000.0f, nyquist));
|
||||
if (fcHz >= 20000.0f) {
|
||||
hiCutActive = false;
|
||||
return;
|
||||
}
|
||||
hiCutActive = true;
|
||||
constexpr float twoPi = 6.28318530718f;
|
||||
hiCutR = std::exp(-twoPi * clamped / static_cast<float>(fs));
|
||||
}
|
||||
|
||||
// --- per-sample processing (L/R interleaved) ---
|
||||
inline void process(float& l, float& r) noexcept {
|
||||
// -- Lo Cut: 1 HPF x 2 cascade --
|
||||
if (loCutActive) {
|
||||
// L stage 1
|
||||
const float l_in = l;
|
||||
const float l_1 = loCutR * (hpfY1_L_1 + l_in - hpfX1_L_1);
|
||||
hpfX1_L_1 = l_in;
|
||||
hpfY1_L_1 = l_1;
|
||||
// L stage 2
|
||||
const float l_2 = loCutR * (hpfY1_L_2 + l_1 - hpfX1_L_2);
|
||||
hpfX1_L_2 = l_1;
|
||||
hpfY1_L_2 = l_2;
|
||||
l = l_2;
|
||||
|
||||
// R stage 1
|
||||
const float r_in = r;
|
||||
const float r_1 = loCutR * (hpfY1_R_1 + r_in - hpfX1_R_1);
|
||||
hpfX1_R_1 = r_in;
|
||||
hpfY1_R_1 = r_1;
|
||||
// R stage 2
|
||||
const float r_2 = loCutR * (hpfY1_R_2 + r_1 - hpfX1_R_2);
|
||||
hpfX1_R_2 = r_1;
|
||||
hpfY1_R_2 = r_2;
|
||||
r = r_2;
|
||||
}
|
||||
|
||||
// -- Hi Cut: 1 LPF x 2 cascade --
|
||||
if (hiCutActive) {
|
||||
const float oneMinusR = 1.0f - hiCutR;
|
||||
// L stage 1
|
||||
lpfY1_L_1 = oneMinusR * l + hiCutR * lpfY1_L_1;
|
||||
// L stage 2
|
||||
lpfY1_L_2 = oneMinusR * lpfY1_L_1 + hiCutR * lpfY1_L_2;
|
||||
l = lpfY1_L_2;
|
||||
|
||||
// R stage 1
|
||||
lpfY1_R_1 = oneMinusR * r + hiCutR * lpfY1_R_1;
|
||||
// R stage 2
|
||||
lpfY1_R_2 = oneMinusR * lpfY1_R_1 + hiCutR * lpfY1_R_2;
|
||||
r = lpfY1_R_2;
|
||||
}
|
||||
}
|
||||
|
||||
float getCurrentLoCutHz() const noexcept { return currentLoCutHz; }
|
||||
float getCurrentHiCutHz() const noexcept { return currentHiCutHz; }
|
||||
|
||||
private:
|
||||
double fs{ 48000.0 };
|
||||
|
||||
// -- Lo Cut (HPF) --
|
||||
bool loCutActive{ false };
|
||||
float loCutR{ 0.0f };
|
||||
float currentLoCutHz{ 20.0f };
|
||||
float hpfX1_L_1{}, hpfY1_L_1{}, hpfX1_L_2{}, hpfY1_L_2{};
|
||||
float hpfX1_R_1{}, hpfY1_R_1{}, hpfX1_R_2{}, hpfY1_R_2{};
|
||||
|
||||
// -- Hi Cut (LPF) --
|
||||
bool hiCutActive{ false };
|
||||
float hiCutR{ 0.0f };
|
||||
float currentHiCutHz{ 20000.0f };
|
||||
float lpfY1_L_1{}, lpfY1_L_2{};
|
||||
float lpfY1_R_1{}, lpfY1_R_2{};
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
79
Source/DSP/OutputLimiter.h
Normal file
79
Source/DSP/OutputLimiter.h
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// OutputLimiter: safe output stage (true-peak limiter)
|
||||
// -----------------------------------------------------------------------------
|
||||
// design rationale :
|
||||
// - parameter values are chosen conservatively for safety
|
||||
// - Threshold = -0.5 dBFS (~0.944): suppresses peaks before the DAW limiter
|
||||
// - Look-ahead: introduces plugin latency
|
||||
// - Attack: 0.5 ms (peak-based)
|
||||
// - Release: 50 ms (prevents unnatural pumping)
|
||||
//
|
||||
// real-time safety :
|
||||
// - allocation: once in prepare(), never in processBlock
|
||||
// - per-sample gain: one comparison against targetGain, SIMD-friendly
|
||||
// - floating-point math: no branches or transcendental functions
|
||||
//
|
||||
// - layout: output stage of UniversalEngine::processBlock()
|
||||
// (after Dry/Wet mix, before the stereo output)
|
||||
// -----------------------------------------------------------------------------
|
||||
class OutputLimiter {
|
||||
public:
|
||||
OutputLimiter() = default;
|
||||
|
||||
// --- sample-rate dependent coefficient computation ---
|
||||
void prepare(double sampleRate) noexcept {
|
||||
fs = sampleRate;
|
||||
// 1 path filter coefficient : y[n] = y[n-1] + coeff * (x[n] - y[n-1])
|
||||
// coeff = 1 - exp(-T / tau) where T = 1/fs, tau = time constant
|
||||
attackCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.0005f)); // 0.5ms
|
||||
releaseCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.050f)); // 50ms
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset() noexcept {
|
||||
currentGain = 1.0f;
|
||||
}
|
||||
|
||||
// --- per-sample processing (called from the audio thread) ---
|
||||
inline void process(float& l, float& r) noexcept {
|
||||
// peak detection (max of L/R levels)
|
||||
const float absL = std::abs(l);
|
||||
const float absR = std::abs(r);
|
||||
const float peak = std::max(absL, absR);
|
||||
|
||||
// Threshold: -0.5 dBFS ~ 0.944
|
||||
// compute target gain from the signal
|
||||
constexpr float threshold = 0.944f;
|
||||
|
||||
// target gain :
|
||||
// peak <= threshold -> 1.0 (no reduction needed)
|
||||
// peak > threshold -> threshold/peak (pull signal to threshold)
|
||||
const float targetGain = (peak > threshold) ? (threshold / peak) : 1.0f;
|
||||
|
||||
// Attack/Release envelope
|
||||
// when targetGain < currentGain (gain must decrease): attack
|
||||
// when targetGain > currentGain (gain recovers): release
|
||||
// so peaks are suppressed smoothly
|
||||
const float coeff = (targetGain < currentGain) ? attackCoeff : releaseCoeff;
|
||||
currentGain += (targetGain - currentGain) * coeff;
|
||||
|
||||
// apply the same gain to L/R to preserve the stereo image
|
||||
l *= currentGain;
|
||||
r *= currentGain;
|
||||
}
|
||||
|
||||
private:
|
||||
double fs{ 44100.0 };
|
||||
float attackCoeff{ 0.0f };
|
||||
float releaseCoeff{ 0.0f };
|
||||
float currentGain{ 1.0f };
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
21
Source/DSP/SAPFStage.cpp
Normal file
21
Source/DSP/SAPFStage.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#include "SAPFStage.h"
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
void SAPFStage::prepare(const juce::dsp::ProcessSpec& spec, int delayTargetSamples) {
|
||||
M = delayTargetSamples;
|
||||
dl.prepare(spec);
|
||||
dl.setMaximumDelayInSamples(M + 4);
|
||||
dl.setDelay(static_cast<float>(M));
|
||||
}
|
||||
|
||||
float SAPFStage::tick(float x) noexcept {
|
||||
float d = dl.popSample(0);
|
||||
float w = x + gain * d;
|
||||
dl.pushSample(0, w);
|
||||
return d - gain * w;
|
||||
}
|
||||
|
||||
void SAPFStage::reset() noexcept { dl.reset(); }
|
||||
|
||||
} // namespace FDNReverb
|
||||
19
Source/DSP/SAPFStage.h
Normal file
19
Source/DSP/SAPFStage.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
class SAPFStage {
|
||||
public:
|
||||
void prepare(const juce::dsp::ProcessSpec& spec, int delayTargetSamples);
|
||||
void setGain(float g) noexcept { gain = juce::jlimit(0.3f, 0.72f, g); }
|
||||
float tick(float x) noexcept;
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
juce::dsp::DelayLine<float, juce::dsp::DelayLineInterpolationTypes::Thiran> dl;
|
||||
float gain{ 0.618f };
|
||||
int M{ 0 };
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
197
Source/DSP/Saturator.h
Normal file
197
Source/DSP/Saturator.h
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
#pragma once
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
enum class SaturationMode {
|
||||
Warm = 0,
|
||||
Tape = 1,
|
||||
Tube = 2,
|
||||
Hard = 3
|
||||
};
|
||||
|
||||
class Saturator {
|
||||
public:
|
||||
Saturator() = default;
|
||||
|
||||
void reset() noexcept {
|
||||
prevInput = 0.0f;
|
||||
switch (currentMode) {
|
||||
case SaturationMode::Warm: prevF = 1.0f; break;
|
||||
case SaturationMode::Tape: prevF = 0.0f; break;
|
||||
case SaturationMode::Tube: prevF = 1.0f; break;
|
||||
case SaturationMode::Hard: prevF = 0.0f; break;
|
||||
}
|
||||
}
|
||||
|
||||
void setMode(SaturationMode mode) noexcept {
|
||||
if (mode != currentMode) {
|
||||
currentMode = mode;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
void setMode(int modeIndex) noexcept {
|
||||
setMode(static_cast<SaturationMode>(std::clamp(modeIndex, 0, 3)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// * Step B fix: only the drive curve changed; ADAA structure fully preserved
|
||||
// -------------------------------------------------------------------------
|
||||
// drive = 1 + amount^3 x 1.0 ( maximum 2.0) -> 1 + amount^2 x 2.5 ( maximum 3.5)
|
||||
//
|
||||
// amount | old drive | new drive | effect
|
||||
// -------|----------|----------|--------------------
|
||||
// 0.30 | 1.027 | 1.225 | + about 7.5dB stronger
|
||||
// 0.50 | 1.125 | 1.625 | + about 3.2dB stronger
|
||||
// 0.70 | 1.343 | 2.225 | + about 4.4dB stronger
|
||||
// 1.00 | 2.000 | 3.500 | + about 4.9dB stronger
|
||||
//
|
||||
// -> plugin 24 harmonics visualization
|
||||
// -------------------------------------------------------------------------
|
||||
void setAmount(float amount) noexcept {
|
||||
amount = std::clamp(amount, 0.0f, 1.0f);
|
||||
currentAmount = amount;
|
||||
|
||||
// * Step B: amount^2 x 2.5 stronger
|
||||
drive = 1.0f + amount * amount * 2.5f;
|
||||
wetMix = amount * amount * 0.7f;
|
||||
dryMix = 1.0f - amount * 0.25f;
|
||||
}
|
||||
|
||||
inline float processSample(float input) noexcept {
|
||||
if (currentAmount < 1e-4f) return input;
|
||||
|
||||
const float dryInput = input;
|
||||
const float driven = input * drive;
|
||||
float saturated = 0.0f;
|
||||
|
||||
switch (currentMode) {
|
||||
case SaturationMode::Warm: saturated = processWarm(driven); break;
|
||||
case SaturationMode::Tape: saturated = processTape(driven); break;
|
||||
case SaturationMode::Tube: saturated = processTube(driven); break;
|
||||
case SaturationMode::Hard: saturated = processHard(driven); break;
|
||||
}
|
||||
|
||||
saturated /= drive;
|
||||
return dryInput * dryMix + saturated * wetMix;
|
||||
}
|
||||
|
||||
private:
|
||||
// --- Warm: Vicanek x/sqrt(1+x^2) + ADAA 1 ---
|
||||
inline float processWarm(float x) noexcept {
|
||||
const float F_x = std::sqrt(1.0f + x * x);
|
||||
const float dx = x - prevInput;
|
||||
float y;
|
||||
constexpr float kTol = 1e-5f;
|
||||
if (std::abs(dx) < kTol) {
|
||||
const float xAvg = (x + prevInput) * 0.5f;
|
||||
y = xAvg / std::sqrt(1.0f + xAvg * xAvg);
|
||||
}
|
||||
else {
|
||||
y = (F_x - prevF) / dx;
|
||||
}
|
||||
prevInput = x;
|
||||
prevF = F_x;
|
||||
return y;
|
||||
}
|
||||
|
||||
// --- Tape: Pade x(27+x^2)/(27+9x^2) (ADAA intentional ) ---
|
||||
inline float processTape(float x) noexcept {
|
||||
if (x > 3.0f) { prevInput = x; return 1.0f; }
|
||||
if (x < -3.0f) { prevInput = x; return -1.0f; }
|
||||
const float xsq = x * x;
|
||||
prevInput = x;
|
||||
return x * (27.0f + xsq) / (27.0f + 9.0f * xsq);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tube: asymmetric ADAA + * Step B: kNeg 1.5 -> 2.0
|
||||
// -------------------------------------------------------------------------
|
||||
// positive side : f(x) = x/sqrt(1+x^2) F(x) = sqrt(1+x^2)
|
||||
// negative side : f(x) = x/sqrt(1+(kNeg.x)^2) F(x) = (1/kNeg^2)sqrt(1+(kNeg.x)^2) + fShift
|
||||
//
|
||||
// C^1 : x=0 F_pos(0) = F_neg(0) = 1 fShift design
|
||||
// F_pos(0) = sqrt1 = 1
|
||||
// F_neg(0) = (1/kNeg^2).sqrt1 + fShift = 1
|
||||
// -> fShift = 1 - 1/kNeg^2
|
||||
//
|
||||
// kNeg=2.0 case : fShift = 1 - 0.25 = 0.75
|
||||
//
|
||||
// kNeg stronger effect :
|
||||
// 2 -> waveform asymmetric
|
||||
// -> harmonics (2f, 4f) plugin visualization
|
||||
// -------------------------------------------------------------------------
|
||||
inline float processTube(float x) noexcept {
|
||||
// * Step B: kNeg = 1.5f -> 2.0f
|
||||
constexpr float kNeg = 2.0f;
|
||||
constexpr float kNeg2 = kNeg * kNeg; // 4.0f
|
||||
constexpr float invKneg2 = 1.0f / kNeg2; // 0.25f
|
||||
constexpr float fShift = 1.0f - invKneg2; // 0.75f
|
||||
|
||||
float F_x;
|
||||
if (x >= 0.0f) {
|
||||
F_x = std::sqrt(1.0f + x * x);
|
||||
}
|
||||
else {
|
||||
const float kx = kNeg * x;
|
||||
F_x = invKneg2 * std::sqrt(1.0f + kx * kx) + fShift;
|
||||
}
|
||||
|
||||
const float dx = x - prevInput;
|
||||
const bool signChanged = (x >= 0.0f) != (prevInput >= 0.0f);
|
||||
|
||||
float y;
|
||||
constexpr float kTol = 1e-5f;
|
||||
if (std::abs(dx) < kTol || signChanged) {
|
||||
// input -> directly
|
||||
if (x >= 0.0f) {
|
||||
y = x / std::sqrt(1.0f + x * x);
|
||||
}
|
||||
else {
|
||||
const float kx = kNeg * x;
|
||||
y = x / std::sqrt(1.0f + kx * kx);
|
||||
}
|
||||
}
|
||||
else {
|
||||
y = (F_x - prevF) / dx;
|
||||
}
|
||||
|
||||
prevInput = x;
|
||||
prevF = F_x;
|
||||
return y;
|
||||
}
|
||||
|
||||
// --- Hard: clipping + ADAA 1 ---
|
||||
inline float processHard(float x) noexcept {
|
||||
float F_x;
|
||||
if (x > 1.0f) F_x = x - 0.5f;
|
||||
else if (x < -1.0f) F_x = -x - 0.5f;
|
||||
else F_x = x * x * 0.5f;
|
||||
|
||||
const float dx = x - prevInput;
|
||||
float y;
|
||||
constexpr float kTol = 1e-5f;
|
||||
if (std::abs(dx) < kTol) {
|
||||
y = std::clamp(x, -1.0f, 1.0f);
|
||||
}
|
||||
else {
|
||||
y = (F_x - prevF) / dx;
|
||||
}
|
||||
|
||||
prevInput = x;
|
||||
prevF = F_x;
|
||||
return y;
|
||||
}
|
||||
|
||||
float prevInput{ 0.0f };
|
||||
float prevF{ 1.0f };
|
||||
SaturationMode currentMode{ SaturationMode::Warm };
|
||||
float currentAmount{ 0.0f };
|
||||
float drive{ 1.0f };
|
||||
float wetMix{ 0.0f };
|
||||
float dryMix{ 1.0f };
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
663
Source/DSP/UniversalEngine.cpp
Normal file
663
Source/DSP/UniversalEngine.cpp
Normal file
|
|
@ -0,0 +1,663 @@
|
|||
#include "UniversalEngine.h"
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
namespace {
|
||||
static bool isMathPrime(int n) noexcept {
|
||||
if (n < 2) return false;
|
||||
if (n == 2) return true;
|
||||
if (n % 2 == 0) return false;
|
||||
for (int i = 3; i * i <= n; i += 2)
|
||||
if (n % i == 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int findNearestUniquePrime(int target,
|
||||
const std::array<int, 16>& usedPrimes,
|
||||
int usedCount) noexcept {
|
||||
target = std::max(target, 2);
|
||||
for (int offset = 0; offset < 100000; ++offset) {
|
||||
int hi = target + offset;
|
||||
if (isMathPrime(hi)) {
|
||||
bool used = false;
|
||||
for (int k = 0; k < usedCount; ++k)
|
||||
if (usedPrimes[k] == hi) { used = true; break; }
|
||||
if (!used) return hi;
|
||||
}
|
||||
int lo = target - offset;
|
||||
if (offset > 0 && lo >= 2 && isMathPrime(lo)) {
|
||||
bool used = false;
|
||||
for (int k = 0; k < usedCount; ++k)
|
||||
if (usedPrimes[k] == lo) { used = true; break; }
|
||||
if (!used) return lo;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
UniversalEngine::UniversalEngine() {
|
||||
fbVec.fill(0.0f);
|
||||
constexpr float phi = 1.6180339887f;
|
||||
for (int i = 0; i < FDN_ORDER; ++i) {
|
||||
lfos[i].state = 12345u + static_cast<uint32_t>(i) * 9876u;
|
||||
lfos[i].smoothed = 0.0f;
|
||||
const float angle = static_cast<float>(i) * phi;
|
||||
const float frac = angle - std::floor(angle);
|
||||
lfos[i].rateMultiplier = 0.80f + frac * 0.40f;
|
||||
|
||||
// * LFO: noise LFO offset
|
||||
const float cAngle = static_cast<float>(i + 5) * phi;
|
||||
chorusLFOs[i].phase = cAngle - std::floor(cAngle);
|
||||
const float cRateAngle = static_cast<float>(i + 11) * phi;
|
||||
chorusLFOs[i].rateScale = 0.30f + (cRateAngle - std::floor(cRateAngle)) * 0.50f;
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalEngine::prepare(double sampleRate, int /*maxBlockSize*/) {
|
||||
fs = sampleRate;
|
||||
|
||||
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
|
||||
MagnitudeResponseFitter::precomputeInteractionMatrix(sampleRate);
|
||||
#endif
|
||||
|
||||
auto getPow2 = [](size_t s) -> size_t {
|
||||
size_t p = 1;
|
||||
while (p < s) p *= 2;
|
||||
return p;
|
||||
};
|
||||
|
||||
size_t totalMemoryNeeded =
|
||||
getPow2(static_cast<size_t>(fs * 0.5)) // * preDelay (max 500ms)
|
||||
+ getPow2(static_cast<size_t>(fs * 1.0))
|
||||
+ getPow2(static_cast<size_t>(fs * 0.05)) * 4
|
||||
+ getPow2(static_cast<size_t>(fs * 0.5)) * FDN_ORDER
|
||||
+ getPow2(static_cast<size_t>(fs * 0.05)) * FDN_ORDER * SERIAL_APF_STAGES;
|
||||
|
||||
memoryPool.allocate(totalMemoryNeeded);
|
||||
|
||||
int mask = 0;
|
||||
float* ptr = nullptr;
|
||||
|
||||
// * PreDelay (max 500ms)
|
||||
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.5), mask);
|
||||
preDelayLine.init(ptr, mask);
|
||||
|
||||
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 1.0), mask);
|
||||
erDelay.init(ptr, mask);
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.05), mask);
|
||||
inputDiffusers[i].init(ptr, mask);
|
||||
}
|
||||
|
||||
for (int i = 0; i < FDN_ORDER; ++i) {
|
||||
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.5), mask);
|
||||
fdnDelays[i].init(ptr, mask);
|
||||
for (int s = 0; s < SERIAL_APF_STAGES; ++s) {
|
||||
ptr = memoryPool.requestMemory(static_cast<size_t>(fs * 0.05), mask);
|
||||
nestedAllpassDelays[i][s].init(ptr, mask);
|
||||
}
|
||||
}
|
||||
|
||||
acousticMetrics.prepare(sampleRate, 2000.0f);
|
||||
currentERTapCount = 0;
|
||||
currentERDelaySamples.fill(0.0f);
|
||||
currentERGains.fill(0.0f);
|
||||
outputLimiter.prepare(sampleRate);
|
||||
outputEQ.prepare(sampleRate);
|
||||
|
||||
duckingAttackCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.010f));
|
||||
duckingReleaseCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.200f));
|
||||
duckingEnvelope = 0.0f;
|
||||
|
||||
// * DC coefficient : fc ~ 5Hz 1HPF
|
||||
dcBlockerCoeff = 1.0f - (6.28318530718f * 5.0f / static_cast<float>(fs));
|
||||
dcX1.fill(0.0f);
|
||||
dcY1.fill(0.0f);
|
||||
|
||||
// * Soft-knee: RMS envelope coefficient (~3ms)
|
||||
fdnRmsEnv.fill(0.0f);
|
||||
rmsCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * 0.003f));
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void UniversalEngine::reset() {
|
||||
memoryPool.clear();
|
||||
fbVec.fill(0.0f);
|
||||
|
||||
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
|
||||
for (auto& lineFilters : absorptionFiltersS2)
|
||||
for (auto& f : lineFilters) f.reset();
|
||||
#else
|
||||
for (auto& f : absorptionFilters) f.reset();
|
||||
#endif
|
||||
|
||||
acousticMetrics.reset();
|
||||
saturatorL.reset();
|
||||
saturatorR.reset();
|
||||
outputLimiter.reset();
|
||||
outputEQ.reset();
|
||||
duckingEnvelope = 0.0f;
|
||||
dcX1.fill(0.0f);
|
||||
dcY1.fill(0.0f);
|
||||
fdnRmsEnv.fill(0.0f);
|
||||
for (auto& dl : fdnDelays) dl.resetState(); // * Thiran allpass state
|
||||
for (auto& lfo : lfos) lfo.smoothed = 0.0f;
|
||||
}
|
||||
|
||||
void UniversalEngine::setParams(const DSPParams& p) {
|
||||
activeParams = p;
|
||||
|
||||
switch (p.algorithmIndex) {
|
||||
case 0: case 1: currentTopology = ReverbTopology::Room; break;
|
||||
case 2: case 3: currentTopology = ReverbTopology::Hall; break;
|
||||
case 4: currentTopology = ReverbTopology::Plate; break;
|
||||
case 5: currentTopology = ReverbTopology::Spring; break;
|
||||
case 6: currentTopology = ReverbTopology::Goldfoil; break;
|
||||
}
|
||||
|
||||
const float attMs = juce::jmax(0.1f, p.duckingAttackMs);
|
||||
const float relMs = juce::jmax(0.1f, p.duckingRelMs);
|
||||
duckingAttackCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * attMs * 0.001f));
|
||||
duckingReleaseCoeff = 1.0f - std::exp(-1.0f / (static_cast<float>(fs) * relMs * 0.001f));
|
||||
|
||||
// * PreDelay: ms -> sample count
|
||||
preDelaySamples = p.preDelayMs * 0.001f * static_cast<float>(fs);
|
||||
|
||||
outputEQ.setLoCutHz(p.loCutHz);
|
||||
outputEQ.setHiCutHz(p.hiCutHz);
|
||||
|
||||
updateTopologyAndRouting();
|
||||
}
|
||||
|
||||
void UniversalEngine::calculatePrimePowerDelays() {
|
||||
const float fsf = static_cast<float>(fs);
|
||||
const float sizeCoeff = juce::jlimit(0.5f, 2.0f, activeParams.roomSizeScale + 1.0f);
|
||||
const float minDelayMs = 15.0f + sizeCoeff * 7.5f;
|
||||
const float maxDelayMs = 50.0f + sizeCoeff * 75.0f;
|
||||
const int minDelaySamples = std::max(11, static_cast<int>(minDelayMs * 0.001f * fsf));
|
||||
const int maxDelaySamples = static_cast<int>(maxDelayMs * 0.001f * fsf);
|
||||
|
||||
const float logMin = std::log(static_cast<float>(minDelaySamples));
|
||||
const float logMax = std::log(static_cast<float>(maxDelaySamples));
|
||||
|
||||
std::array<int, FDN_ORDER> usedPrimes;
|
||||
usedPrimes.fill(0);
|
||||
|
||||
for (int i = 0; i < FDN_ORDER; ++i) {
|
||||
const float t = static_cast<float>(i) / static_cast<float>(FDN_ORDER - 1);
|
||||
const float logTgt = logMin + t * (logMax - logMin);
|
||||
const int target = static_cast<int>(std::round(std::exp(logTgt)));
|
||||
const int prime = findNearestUniquePrime(target, usedPrimes, i);
|
||||
usedPrimes[i] = prime;
|
||||
fdnBaseDelaySamples[i] = static_cast<float>(prime);
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalEngine::updateTopologyAndRouting() {
|
||||
calculatePrimePowerDelays();
|
||||
|
||||
auto& preset = *ALL_PRESETS[activeParams.algorithmIndex];
|
||||
|
||||
std::array<float, NUM_BANDS> scaledRT60 = preset.acoustics.rt60;
|
||||
for (auto& v : scaledRT60) v *= activeParams.decayScale;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// * 2) fix : proMode always Tilt / band apply
|
||||
// -------------------------------------------------------------------------
|
||||
// old implementation : if (activeParams.proMode) { ... }
|
||||
// when ProMode is OFF, the Tilt / band coefficients were not applied,
|
||||
// so the RT60 graph kept the preset's original curve.
|
||||
//
|
||||
// new implementation: always apply; the coefficients default to 1.0f,
|
||||
// so changing them scales the RT60 graph,
|
||||
// and reset to 1.0f when loadPresetDefaults() is called.
|
||||
//
|
||||
// -------------------------------------------------------------------------
|
||||
scaledRT60[0] *= activeParams.tiltLow;
|
||||
scaledRT60[1] *= activeParams.tiltLow;
|
||||
scaledRT60[2] *= activeParams.tiltLow;
|
||||
scaledRT60[3] *= activeParams.tiltMid;
|
||||
scaledRT60[4] *= activeParams.tiltMid;
|
||||
scaledRT60[5] *= activeParams.tiltMid;
|
||||
scaledRT60[6] *= activeParams.tiltMid;
|
||||
scaledRT60[7] *= activeParams.tiltHigh;
|
||||
scaledRT60[8] *= activeParams.tiltHigh;
|
||||
scaledRT60[9] *= activeParams.tiltHigh;
|
||||
for (int b = 0; b < NUM_BANDS; ++b)
|
||||
scaledRT60[b] *= activeParams.rtBands[b];
|
||||
|
||||
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
|
||||
std::array<float, NUM_BANDS> targetDbAccum;
|
||||
targetDbAccum.fill(0.0f);
|
||||
|
||||
for (int i = 0; i < FDN_ORDER; ++i) {
|
||||
auto s2 = MagnitudeResponseFitter::designStage2(
|
||||
static_cast<int>(fdnBaseDelaySamples[i]), fs, scaledRT60,
|
||||
activeParams.hfDamping, activeParams.lfAbsorption);
|
||||
for (int b = 0; b < NUM_BANDS; ++b) {
|
||||
currentAbsorptionCoeffsS2[i][b] = s2.geqStages[b];
|
||||
targetDbAccum[b] += s2.targetDb[b];
|
||||
}
|
||||
}
|
||||
|
||||
const float representativeDelay = fdnBaseDelaySamples[FDN_ORDER / 2];
|
||||
for (int b = 0; b < NUM_BANDS; ++b) {
|
||||
const float avgTargetDb = targetDbAccum[b] / static_cast<float>(FDN_ORDER);
|
||||
if (avgTargetDb < -0.001f) {
|
||||
effectiveRT60[b] = -60.0f * representativeDelay
|
||||
/ (static_cast<float>(fs) * avgTargetDb);
|
||||
}
|
||||
else {
|
||||
effectiveRT60[b] = scaledRT60[b];
|
||||
}
|
||||
effectiveRT60[b] = juce::jlimit(0.05f, 30.0f, effectiveRT60[b]);
|
||||
}
|
||||
#else
|
||||
effectiveRT60 = scaledRT60;
|
||||
for (int i = 0; i < FDN_ORDER; ++i) {
|
||||
auto absoStages = FilterDesign::designAbsorption(
|
||||
static_cast<int>(fdnBaseDelaySamples[i]), fs, scaledRT60,
|
||||
activeParams.hfDamping, activeParams.lfAbsorption);
|
||||
currentAbsorptionCoeffs[i] = absoStages[0];
|
||||
}
|
||||
#endif
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// * EDT fix : band average LF/HF correction
|
||||
// -------------------------------------------------------------------------
|
||||
// old implementation : effectiveRT60[4] (500Hz) band use
|
||||
// -> HF Damping high band below EDT
|
||||
// -> LF Absorption low band below EDT
|
||||
//
|
||||
// new implementation : mid-band band (125Hz~4kHz = band 2~7) average value use
|
||||
// -> band LF/HF correction influence
|
||||
// -> (31Hz, 63Hz, 8kHz, 16kHz) ( psychoacoustically EDT
|
||||
// , value unstable )
|
||||
// -------------------------------------------------------------------------
|
||||
float rt60Mid = 0.0f;
|
||||
for (int b = 2; b <= 7; ++b)
|
||||
rt60Mid += effectiveRT60[b];
|
||||
rt60Mid = std::max(0.1f, rt60Mid / 6.0f);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// * metallic sound (1): Decay depends on saturation
|
||||
// -------------------------------------------------------------------------
|
||||
// each FDN loop pass runs processMicroSaturation(), and reverberation
|
||||
// nonlinear distortion accumulates in the reverb and shifts the filter
|
||||
// response, producing metallic ringing.
|
||||
//
|
||||
// policy: not applied below a 2.0 s mid-band RT60 average, scaled between 2.0 s and 6.0 s,
|
||||
// and fully bypassed above 6.0 s.
|
||||
// -------------------------------------------------------------------------
|
||||
microSatBlend = juce::jlimit(0.0f, 1.0f, 1.0f - (rt60Mid - 2.0f) / 4.0f);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// * metallic sound (2): Decay depends on modulation
|
||||
// -------------------------------------------------------------------------
|
||||
// longer reverb tails require deeper modulation at the filter peaks.
|
||||
// as used by Lexicon / Strymon.
|
||||
//
|
||||
// * modulation depth (scaled down for short reverbs)
|
||||
// RT60 <= 1.0 s -> 1.0x (min)
|
||||
// RT60 = 3.0s -> 2.0x
|
||||
// RT60 >= 5.0 s -> 3.0x (max)
|
||||
// -------------------------------------------------------------------------
|
||||
modDepthScale = 1.0f + juce::jlimit(0.0f, 2.0f, (rt60Mid - 1.0f) * 0.5f);
|
||||
|
||||
constexpr float baseDB = 16.0f;
|
||||
float decayCompDB = 7.0f * std::log10(rt60Mid);
|
||||
|
||||
static constexpr std::array<float, 7> algorithmOffsetDB = {
|
||||
+0.8f, +0.9f, +0.5f, +0.5f, +1.5f, +0.6f, +0.6f
|
||||
};
|
||||
float algoOffset = algorithmOffsetDB[juce::jlimit(0, 6, activeParams.algorithmIndex)];
|
||||
|
||||
switch (currentTopology) {
|
||||
case ReverbTopology::Room:
|
||||
bypassER = false; bypassInputDiffusers = false;
|
||||
apfGain = 0.3f; diffusionSensitivity = 1.0f;
|
||||
break;
|
||||
case ReverbTopology::Hall:
|
||||
bypassER = false; bypassInputDiffusers = false;
|
||||
apfGain = 0.618f; diffusionSensitivity = 1.0f;
|
||||
break;
|
||||
case ReverbTopology::Plate:
|
||||
bypassER = true; bypassInputDiffusers = false;
|
||||
apfGain = 0.7f; diffusionSensitivity = 0.7f;
|
||||
break;
|
||||
case ReverbTopology::Spring:
|
||||
bypassER = true; bypassInputDiffusers = false;
|
||||
apfGain = 0.5f; diffusionSensitivity = 0.5f;
|
||||
break;
|
||||
case ReverbTopology::Goldfoil:
|
||||
bypassER = true; bypassInputDiffusers = false;
|
||||
apfGain = 0.75f; diffusionSensitivity = 0.8f;
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& erPattern = PRESET_ER_PATTERNS[
|
||||
juce::jlimit(0, 6, activeParams.algorithmIndex)];
|
||||
currentERTapCount = erPattern.numTaps;
|
||||
float erSizeScale = 0.5f + activeParams.roomSizeScale;
|
||||
for (int i = 0; i < erPattern.numTaps; ++i) {
|
||||
currentERDelaySamples[i] = erPattern.taps[i].delayMs * 0.001f
|
||||
* static_cast<float>(fs) * erSizeScale;
|
||||
currentERGains[i] = erPattern.taps[i].gain;
|
||||
}
|
||||
if (erPattern.numTaps == 0) bypassER = true;
|
||||
|
||||
float edtCoeff = 0.7f;
|
||||
switch (currentTopology) {
|
||||
case ReverbTopology::Room: edtCoeff = 0.70f; break;
|
||||
case ReverbTopology::Hall: edtCoeff = 0.95f; break;
|
||||
case ReverbTopology::Plate: edtCoeff = 0.60f; break;
|
||||
case ReverbTopology::Spring: edtCoeff = 0.50f; break;
|
||||
case ReverbTopology::Goldfoil: edtCoeff = 0.85f; break;
|
||||
}
|
||||
theoreticalEDT = rt60Mid * edtCoeff;
|
||||
|
||||
float satMultiplier = 1.0f;
|
||||
switch (currentTopology) {
|
||||
case ReverbTopology::Room: satMultiplier = 0.90f; break;
|
||||
case ReverbTopology::Hall: satMultiplier = 0.93f; break;
|
||||
case ReverbTopology::Plate: satMultiplier = 1.00f; break;
|
||||
case ReverbTopology::Spring: satMultiplier = 1.05f; break;
|
||||
case ReverbTopology::Goldfoil: satMultiplier = 1.02f; break;
|
||||
}
|
||||
float effectiveSatAmount = juce::jlimit(0.0f, 1.0f,
|
||||
activeParams.saturation * satMultiplier);
|
||||
saturatorL.setAmount(effectiveSatAmount);
|
||||
saturatorR.setAmount(effectiveSatAmount);
|
||||
saturatorL.setMode(activeParams.satTypeIdx);
|
||||
saturatorR.setMode(activeParams.satTypeIdx);
|
||||
|
||||
lateMakeupGainLinear = juce::Decibels::decibelsToGain(baseDB + decayCompDB + algoOffset);
|
||||
}
|
||||
|
||||
inline void UniversalEngine::fastWalshHadamardTransform(
|
||||
std::array<float, 16>& v) noexcept
|
||||
{
|
||||
for (int h = 1; h < 16; h *= 2) {
|
||||
for (int i = 0; i < 16; i += h * 2) {
|
||||
for (int j = i; j < i + h; ++j) {
|
||||
float x = v[j], y = v[j + h];
|
||||
v[j] = x + y;
|
||||
v[j + h] = x - y;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 16; ++i) v[i] *= 0.25f;
|
||||
}
|
||||
|
||||
inline void UniversalEngine::applySignFlipping(
|
||||
std::array<float, 16>& v) noexcept
|
||||
{
|
||||
static constexpr std::array<float, 16> flip = {
|
||||
1.f, -1.f, 1.f, -1.f, -1.f, 1.f, -1.f, 1.f,
|
||||
1.f, 1.f, -1.f, -1.f, -1.f, -1.f, 1.f, 1.f
|
||||
};
|
||||
for (int i = 0; i < 16; ++i) v[i] *= flip[i];
|
||||
}
|
||||
|
||||
void UniversalEngine::processBlock(const float* inL, const float* inR,
|
||||
float* outL, float* outR,
|
||||
int numSamples) noexcept
|
||||
{
|
||||
// * CPU: fs float (processBlock throughout use )
|
||||
const float fsf = static_cast<float>(fs);
|
||||
|
||||
// * modulation : squared curve + coefficient suppress
|
||||
// modAmount^2 low band gradually , 0.001f entire
|
||||
// : modAmt=0.5 -> 48smp(1ms) / : modAmt=0.5 -> 12smp(0.25ms)
|
||||
const float modAmtCurved = activeParams.modAmount * activeParams.modAmount;
|
||||
const float depthSamples = modAmtCurved * 0.001f * fsf * modDepthScale;
|
||||
const float wetGain = juce::Decibels::decibelsToGain(activeParams.wetDB);
|
||||
const float stereoWidth = activeParams.stereoWidth;
|
||||
const float erLevel = activeParams.erLevel;
|
||||
const float lateLevel = activeParams.lateLevel;
|
||||
const bool erSolo = activeParams.erSolo;
|
||||
const float duckThreshLin = juce::Decibels::decibelsToGain(activeParams.duckingThreshDB);
|
||||
const float duckAmountDB = activeParams.duckingAmount;
|
||||
|
||||
const float effectiveDiffusion = activeParams.diffusion * diffusionSensitivity;
|
||||
const float diffuserGain = 0.25f + effectiveDiffusion * 0.55f;
|
||||
const float effectiveApfGain = apfGain * (0.60f + effectiveDiffusion * 0.40f);
|
||||
|
||||
const float sideBoost = stereoWidth * 1.5f;
|
||||
const float erLeakage = (1.0f - stereoWidth) * 0.7f;
|
||||
|
||||
// * CPU: apfGainStage loop -> before compute
|
||||
const float apfGainStage = effectiveApfGain * 0.78f;
|
||||
|
||||
// * CPU: freqModScale before compute (16ch)
|
||||
std::array<float, FDN_ORDER> freqModScales;
|
||||
constexpr float invFdnM1 = 1.0f / static_cast<float>(FDN_ORDER - 1);
|
||||
for (int i = 0; i < FDN_ORDER; ++i)
|
||||
freqModScales[i] = 0.5f + (1.0f - static_cast<float>(i) * invFdnM1) * 1.0f;
|
||||
|
||||
// * CPU: input diffuser time before compute
|
||||
std::array<float, 4> diffuserDelaySmp;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
diffuserDelaySmp[i] = (3.0f + i * 2.0f) * 0.001f * fsf;
|
||||
|
||||
// * CPU: Allpass before compute (16ch x 3)
|
||||
constexpr float apfBaseMs[SERIAL_APF_STAGES] = { 1.5f, 2.3f, 3.7f };
|
||||
constexpr float apfSpreadMs[SERIAL_APF_STAGES] = { 0.30f, 0.37f, 0.47f };
|
||||
constexpr float apfModFrac[SERIAL_APF_STAGES] = { 0.15f, 0.10f, 0.07f };
|
||||
const float msToSmp = 0.001f * fsf;
|
||||
std::array<std::array<float, SERIAL_APF_STAGES>, FDN_ORDER> apfBaseDelaySmp;
|
||||
for (int i = 0; i < FDN_ORDER; ++i)
|
||||
for (int s = 0; s < SERIAL_APF_STAGES; ++s)
|
||||
apfBaseDelaySmp[i][s] = (apfBaseMs[s] + i * apfSpreadMs[s]) * msToSmp;
|
||||
|
||||
// * CPU: ER tapGain * 0.5f before compute
|
||||
std::array<float, MAX_ER_TAPS> erTapGainsHalf;
|
||||
for (int t = 0; t < currentERTapCount; ++t)
|
||||
erTapGainsHalf[t] = currentERGains[t] * 0.5f;
|
||||
|
||||
// * CPU: soft-knee threshold squared before compute (sqrt avoid )
|
||||
constexpr float compThresh = 0.35f;
|
||||
constexpr float compThreshSq = compThresh * compThresh;
|
||||
|
||||
std::array<float, FDN_ORDER> lfoCoeffs;
|
||||
{
|
||||
constexpr float twoPi = 6.28318530718f;
|
||||
for (int i = 0; i < FDN_ORDER; ++i) {
|
||||
const float fc = activeParams.modRate * lfos[i].rateMultiplier;
|
||||
lfoCoeffs[i] = juce::jlimit(0.0001f, 0.9999f,
|
||||
1.0f - std::exp(-twoPi * fc / fsf));
|
||||
// * LFO update
|
||||
chorusLFOs[i].phaseInc = activeParams.modRate * chorusLFOs[i].rateScale / fsf;
|
||||
}
|
||||
}
|
||||
|
||||
for (int n = 0; n < numSamples; ++n) {
|
||||
const float leftIn = inL[n];
|
||||
const float rightIn = inR[n];
|
||||
const float midIn = (leftIn + rightIn) * 0.5f;
|
||||
const float sideIn = (leftIn - rightIn) * 0.5f;
|
||||
float erOutL = 0.0f, erOutR = 0.0f;
|
||||
|
||||
// * PreDelay: dry time
|
||||
// ERFDN input .
|
||||
// dry attack after ,
|
||||
// clarity (D50/C50) significantly above .
|
||||
preDelayLine.write(midIn);
|
||||
const float delayedMid = (preDelaySamples > 0.5f)
|
||||
? preDelayLine.read(preDelaySamples)
|
||||
: midIn;
|
||||
|
||||
const float inputPeak = juce::jmax(std::abs(leftIn), std::abs(rightIn));
|
||||
const float envCoeff = (inputPeak > duckingEnvelope)
|
||||
? duckingAttackCoeff : duckingReleaseCoeff;
|
||||
duckingEnvelope += (inputPeak - duckingEnvelope) * envCoeff;
|
||||
|
||||
float duckGainLinear = 1.0f;
|
||||
if (duckAmountDB > 0.001f && duckingEnvelope > duckThreshLin) {
|
||||
const float envDB = 20.0f * std::log10(juce::jmax(duckingEnvelope, 1e-6f));
|
||||
const float overDB = envDB - activeParams.duckingThreshDB;
|
||||
const float gainRedDB = -juce::jmin(overDB, duckAmountDB);
|
||||
duckGainLinear = juce::Decibels::decibelsToGain(gainRedDB);
|
||||
}
|
||||
|
||||
float fdnInputMid = delayedMid;
|
||||
if (!bypassInputDiffusers) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
float d = inputDiffusers[i].read(diffuserDelaySmp[i]);
|
||||
float w = fdnInputMid + diffuserGain * d;
|
||||
inputDiffusers[i].write(w);
|
||||
fdnInputMid = d - diffuserGain * w;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bypassER) {
|
||||
erDelay.write(delayedMid);
|
||||
float erTotalL = 0.0f, erTotalR = 0.0f;
|
||||
for (int t = 0; t < currentERTapCount; ++t) {
|
||||
const float tapValue = erDelay.read(currentERDelaySamples[t]);
|
||||
const float tapGain = erTapGainsHalf[t];
|
||||
const float tg = tapValue * tapGain;
|
||||
const float tgLeak = tg * erLeakage;
|
||||
if (t % 2 == 0) {
|
||||
erTotalL += tg;
|
||||
erTotalR += tgLeak;
|
||||
}
|
||||
else {
|
||||
erTotalR += tg;
|
||||
erTotalL += tgLeak;
|
||||
}
|
||||
}
|
||||
erOutL = erTotalL;
|
||||
erOutR = erTotalR;
|
||||
}
|
||||
|
||||
// * ER -> Late: feed the ER output into the FDN input
|
||||
// the early reflections are wall-surface reflections that seed the Late Reverb,
|
||||
// making the ER-to-Late transition natural and smooth.
|
||||
if (!bypassER) {
|
||||
fdnInputMid += (erOutL + erOutR) * 0.5f * 0.15f;
|
||||
}
|
||||
|
||||
std::array<float, 16> currentFb = fbVec;
|
||||
fastWalshHadamardTransform(currentFb);
|
||||
applySignFlipping(currentFb);
|
||||
|
||||
float fdnOutL = 0.0f, fdnOutR = 0.0f;
|
||||
std::array<float, 16> nextFb;
|
||||
|
||||
for (int i = 0; i < FDN_ORDER; ++i) {
|
||||
const float lfoVal = lfos[i].tick(lfoCoeffs[i]);
|
||||
// * modulation: sine-wave LFO + noise LFO
|
||||
// noise = random (suppresses metallic ringing)
|
||||
// chorus = smoothly accumulated (rich tail)
|
||||
const float chorusVal = chorusLFOs[i].tick();
|
||||
const float combinedLfo = lfoVal + chorusVal * 0.6f;
|
||||
|
||||
// * frequency-dependent modulation: high bands modulate less than low bands
|
||||
const float freqModScale = freqModScales[i];
|
||||
const float delaySmp = fdnBaseDelaySamples[i]
|
||||
+ combinedLfo * depthSamples * freqModScale;
|
||||
float d = fdnDelays[i].read(delaySmp);
|
||||
|
||||
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
|
||||
for (int s = 0; s < ABSO_STAGES_S2; ++s)
|
||||
d = absorptionFiltersS2[i][s].tick(d, currentAbsorptionCoeffsS2[i][s]);
|
||||
#else
|
||||
d = absorptionFilters[i].tick(d, currentAbsorptionCoeffs[i]);
|
||||
#endif
|
||||
|
||||
// * metallic sound (3): DC blocker (1st-order HPF, fc ~ 5 Hz)
|
||||
// saturation in the FDN loop absorption filters can
|
||||
// accumulate DC; blocking it prevents low-band asymmetric distortion.
|
||||
{
|
||||
const float dcIn = d;
|
||||
const float dcOut = dcIn - dcX1[i] + dcBlockerCoeff * dcY1[i];
|
||||
dcX1[i] = dcIn;
|
||||
dcY1[i] = dcOut;
|
||||
d = dcOut;
|
||||
}
|
||||
|
||||
// * soft-knee compression (in the FDN feedback loop)
|
||||
// an RMS envelope over the threshold triggers compression.
|
||||
// * CPU: sqrt only runs above threshold (compare on squared values)
|
||||
{
|
||||
fdnRmsEnv[i] += (d * d - fdnRmsEnv[i]) * rmsCoeff;
|
||||
if (fdnRmsEnv[i] > compThreshSq) {
|
||||
const float env = std::sqrt(fdnRmsEnv[i]);
|
||||
const float over = env - compThresh;
|
||||
d *= compThresh / (compThresh + over * 0.65f);
|
||||
}
|
||||
}
|
||||
|
||||
// * metallic sound (1): Decay depends on saturation
|
||||
// microSatBlend=1.0 -> applied (into the reverb loop)
|
||||
// microSatBlend=0.0 -> fully bypassed
|
||||
if (microSatBlend > 0.001f) {
|
||||
const float sat = processMicroSaturation(d);
|
||||
d = d + (sat - d) * microSatBlend;
|
||||
}
|
||||
|
||||
// * 3 nested allpass filters (echo density)
|
||||
// * CPU: apfGainStage precomputed per block
|
||||
float apfOut = d;
|
||||
{
|
||||
for (int s = 0; s < SERIAL_APF_STAGES; ++s) {
|
||||
const float apfModDepth = depthSamples * apfModFrac[s];
|
||||
const float apfDelaySmp = apfBaseDelaySmp[i][s]
|
||||
+ combinedLfo * apfModDepth * freqModScale;
|
||||
float apfD = nestedAllpassDelays[i][s].read(apfDelaySmp);
|
||||
float apfW = apfOut + apfGainStage * apfD;
|
||||
nestedAllpassDelays[i][s].write(apfW);
|
||||
apfOut = apfD - apfGainStage * apfW;
|
||||
}
|
||||
}
|
||||
|
||||
nextFb[i] = apfOut;
|
||||
|
||||
const float sideForCh = (i % 2 == 0 ? +sideIn : -sideIn) * sideBoost;
|
||||
const float fdnInputForThisCh = (fdnInputMid + sideForCh) * 0.25f;
|
||||
fdnDelays[i].write(fdnInputForThisCh + currentFb[i]);
|
||||
|
||||
const float crossLeak = 1.0f - stereoWidth;
|
||||
if (i % 2 == 0) {
|
||||
fdnOutL += apfOut;
|
||||
fdnOutR += apfOut * crossLeak;
|
||||
}
|
||||
else {
|
||||
fdnOutR += apfOut;
|
||||
fdnOutL += apfOut * crossLeak;
|
||||
}
|
||||
}
|
||||
|
||||
fdnOutL *= 0.125f;
|
||||
fdnOutR *= 0.125f;
|
||||
fbVec = nextFb;
|
||||
|
||||
const float erMixL = bypassER ? 0.0f : erOutL * erLevel;
|
||||
const float erMixR = bypassER ? 0.0f : erOutR * erLevel;
|
||||
const float lateMixL = fdnOutL * lateMakeupGainLinear * lateLevel;
|
||||
const float lateMixR = fdnOutR * lateMakeupGainLinear * lateLevel;
|
||||
|
||||
acousticMetrics.processSample((lateMixL + lateMixR) * 0.5f);
|
||||
|
||||
float satL = saturatorL.processSample(lateMixL);
|
||||
float satR = saturatorR.processSample(lateMixR);
|
||||
|
||||
if (erSolo) { satL = 0.0f; satR = 0.0f; }
|
||||
|
||||
float wetL = erMixL + satL;
|
||||
float wetR = erMixR + satR;
|
||||
outputEQ.process(wetL, wetR);
|
||||
|
||||
const float finalWetGain = wetGain * duckGainLinear;
|
||||
outL[n] = wetL * finalWetGain;
|
||||
outR[n] = wetR * finalWetGain;
|
||||
|
||||
outputLimiter.process(outL[n], outR[n]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FDNReverb
|
||||
174
Source/DSP/UniversalEngine.h
Normal file
174
Source/DSP/UniversalEngine.h
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#pragma once
|
||||
#include "DelayMemory.h"
|
||||
#include "BiquadFilters.h"
|
||||
#include "MagnitudeResponseFitter.h"
|
||||
#include "AcousticMetrics.h"
|
||||
#include "Saturator.h"
|
||||
#include "OutputLimiter.h"
|
||||
#include "OutputEQ.h"
|
||||
#include "../PluginParameters.h"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#define AMBIVALENCE_USE_STAGE2_ABSORPTION 1
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
enum class ReverbTopology { Room, Hall, Plate, Spring, Goldfoil };
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// BandlimitedNoiseLFO: color noise + 1 IIR LPF
|
||||
// -----------------------------------------------------------------------------
|
||||
struct BandlimitedNoiseLFO {
|
||||
uint32_t state{ 12345u };
|
||||
float smoothed{ 0.0f };
|
||||
float rateMultiplier{ 1.0f };
|
||||
|
||||
inline float nextNoise() noexcept {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 17;
|
||||
state ^= state << 5;
|
||||
return static_cast<float>(state) * 2.3283064365386963e-10f * 2.0f - 1.0f;
|
||||
}
|
||||
|
||||
inline float tick(float lpfCoeff) noexcept {
|
||||
smoothed += (nextNoise() - smoothed) * lpfCoeff;
|
||||
return smoothed;
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ChorusLFO: sine-wave phase (modulation)
|
||||
// -----------------------------------------------------------------------------
|
||||
struct ChorusLFO {
|
||||
float phase{ 0.0f };
|
||||
float phaseInc{ 0.0f };
|
||||
float rateScale{ 1.0f }; // per-channel rate coefficient (multiplier)
|
||||
|
||||
// * CPU: std::sin() replaced by a parabolic approximation (max error ~0.06%, 5-10x faster)
|
||||
inline float tick() noexcept {
|
||||
phase += phaseInc;
|
||||
if (phase >= 1.0f) phase -= 1.0f;
|
||||
// Parabolic sine: phase [0,1) -> sin(2pi.phase)
|
||||
const float x = phase < 0.5f ? phase : phase - 1.0f;
|
||||
const float para = 16.0f * x * (0.5f - std::abs(x));
|
||||
return para * (0.775f + 0.225f * std::abs(para));
|
||||
}
|
||||
};
|
||||
|
||||
class UniversalEngine {
|
||||
public:
|
||||
UniversalEngine();
|
||||
void prepare(double sampleRate, int maxBlockSize);
|
||||
void reset();
|
||||
void setParams(const DSPParams& p);
|
||||
void processBlock(const float* inL, const float* inR,
|
||||
float* outL, float* outR, int numSamples) noexcept;
|
||||
|
||||
std::array<float, NUM_BANDS> getEffectiveRT60() const noexcept { return effectiveRT60; }
|
||||
float getD50() const noexcept { return acousticMetrics.getD50(); }
|
||||
float getC50() const noexcept { return acousticMetrics.getC50(); }
|
||||
float getC80() const noexcept { return acousticMetrics.getC80(); }
|
||||
float getEDT() const noexcept { return theoreticalEDT; }
|
||||
const AcousticMetrics& getAcousticMetrics() const noexcept { return acousticMetrics; }
|
||||
|
||||
int getERTapCount() const noexcept { return currentERTapCount; }
|
||||
float getERTapDelaySamples(int index) const noexcept {
|
||||
return (index >= 0 && index < currentERTapCount) ? currentERDelaySamples[index] : 0.0f;
|
||||
}
|
||||
float getERTapGain(int index) const noexcept {
|
||||
return (index >= 0 && index < currentERTapCount) ? currentERGains[index] : 0.0f;
|
||||
}
|
||||
double getSampleRate() const noexcept { return fs; }
|
||||
bool isERBypassed() const noexcept { return bypassER; }
|
||||
|
||||
private:
|
||||
void updateTopologyAndRouting();
|
||||
void calculatePrimePowerDelays();
|
||||
inline void fastWalshHadamardTransform(std::array<float, 16>& v) noexcept;
|
||||
inline void applySignFlipping(std::array<float, 16>& v) noexcept;
|
||||
|
||||
// --- FDN loop saturation ---
|
||||
inline static float processMicroSaturation(float x) noexcept {
|
||||
constexpr float kInScale = 0.15f;
|
||||
constexpr float kOutScale = 1.0f / kInScale;
|
||||
const float xs = x * kInScale;
|
||||
if (xs > 3.0f) return kOutScale;
|
||||
if (xs < -3.0f) return -kOutScale;
|
||||
const float xsq = xs * xs;
|
||||
return (xs * (27.0f + xsq) / (27.0f + 9.0f * xsq)) * kOutScale;
|
||||
}
|
||||
|
||||
DelayMemoryPool memoryPool;
|
||||
double fs{ 48000.0 };
|
||||
DSPParams activeParams;
|
||||
ReverbTopology currentTopology{ ReverbTopology::Room };
|
||||
|
||||
static constexpr int FDN_ORDER = 16;
|
||||
static constexpr int SERIAL_APF_STAGES = 3; // * Allpass stages
|
||||
|
||||
// * PreDelay (max 500 ms)
|
||||
LinearDelayLine preDelayLine;
|
||||
float preDelaySamples{ 0.0f };
|
||||
|
||||
LinearDelayLine erDelay;
|
||||
std::array<float, 16> erTaps;
|
||||
std::array<LinearDelayLine, 4> inputDiffusers;
|
||||
std::array<ThiranDelayLine, FDN_ORDER> fdnDelays; // * Thiran allpass interpolation
|
||||
std::array<std::array<LinearDelayLine, SERIAL_APF_STAGES>, FDN_ORDER> nestedAllpassDelays;
|
||||
|
||||
int currentERTapCount{ 0 };
|
||||
std::array<float, MAX_ER_TAPS> currentERDelaySamples;
|
||||
std::array<float, MAX_ER_TAPS> currentERGains;
|
||||
|
||||
OutputLimiter outputLimiter;
|
||||
OutputEQ outputEQ; // * Phase 5 added
|
||||
|
||||
float duckingEnvelope{ 0.0f };
|
||||
float duckingAttackCoeff{ 0.0f };
|
||||
float duckingReleaseCoeff{ 0.0f };
|
||||
|
||||
#if AMBIVALENCE_USE_STAGE2_ABSORPTION
|
||||
std::array<std::array<BiquadState, ABSO_STAGES_S2>, FDN_ORDER> absorptionFiltersS2;
|
||||
std::array<std::array<BiquadCoeffs, ABSO_STAGES_S2>, FDN_ORDER> currentAbsorptionCoeffsS2;
|
||||
#else
|
||||
std::array<BiquadState, FDN_ORDER> absorptionFilters;
|
||||
std::array<BiquadCoeffs, FDN_ORDER> currentAbsorptionCoeffs;
|
||||
#endif
|
||||
|
||||
std::array<BandlimitedNoiseLFO, FDN_ORDER> lfos;
|
||||
std::array<ChorusLFO, FDN_ORDER> chorusLFOs; // * modulation
|
||||
std::array<float, FDN_ORDER> fdnBaseDelaySamples;
|
||||
std::array<float, FDN_ORDER> fbVec;
|
||||
|
||||
float apfGain{ 0.618f };
|
||||
bool bypassER{ false };
|
||||
bool bypassInputDiffusers{ false }; // * new: default false
|
||||
float lateMixScale{ 1.0f };
|
||||
float lateMakeupGainLinear{ 1.0f };
|
||||
|
||||
// * Phase 5 addition: Diffusion
|
||||
float diffusionSensitivity{ 1.0f };
|
||||
|
||||
// * metallic sound: DecayTime depends on parameters
|
||||
float microSatBlend{ 1.0f }; // FDN loop saturation blend (0 = bypass, 1 = full)
|
||||
float modDepthScale{ 1.0f }; // modulation depth scale (increases with Decay time)
|
||||
|
||||
// * DC: prevent DC accumulation in the FDN loop
|
||||
std::array<float, FDN_ORDER> dcX1;
|
||||
std::array<float, FDN_ORDER> dcY1;
|
||||
float dcBlockerCoeff{ 0.999f };
|
||||
|
||||
// * soft-knee compression: in the FDN feedback loop
|
||||
std::array<float, FDN_ORDER> fdnRmsEnv;
|
||||
float rmsCoeff{ 0.002f };
|
||||
|
||||
std::array<float, NUM_BANDS> effectiveRT60;
|
||||
float theoreticalEDT{ 0.0f };
|
||||
|
||||
AcousticMetrics acousticMetrics;
|
||||
Saturator saturatorL;
|
||||
Saturator saturatorR;
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
354
Source/GUI/AmbivalenceUI.cpp
Normal file
354
Source/GUI/AmbivalenceUI.cpp
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
#include "AmbivalenceUI.h"
|
||||
#include "../PluginProcessor.h"
|
||||
|
||||
// --- AmbivalenceLookAndFeel ---------------------------------------------
|
||||
AmbivalenceLookAndFeel::AmbivalenceLookAndFeel()
|
||||
{
|
||||
setColour(juce::Slider::backgroundColourId, AmbivalenceColors::ArcTrack);
|
||||
setColour(juce::Slider::thumbColourId, AmbivalenceColors::Accent);
|
||||
setColour(juce::Slider::trackColourId, AmbivalenceColors::ArcFill);
|
||||
setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
|
||||
setColour(juce::ComboBox::backgroundColourId, AmbivalenceColors::Surface);
|
||||
setColour(juce::ComboBox::textColourId, AmbivalenceColors::TextPrimary);
|
||||
setColour(juce::ComboBox::outlineColourId, AmbivalenceColors::Border);
|
||||
mainFont = juce::Font(juce::FontOptions("Helvetica Neue", 11.f, juce::Font::plain));
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::drawRotarySlider(juce::Graphics& g,
|
||||
int x, int y, int w, int h,
|
||||
float sliderPos, float startAngle, float endAngle, juce::Slider&)
|
||||
{
|
||||
auto b = juce::Rectangle<float>((float)x, (float)y, (float)w, (float)h).reduced(4.f);
|
||||
float cx = b.getCentreX(), cy = b.getCentreY();
|
||||
float r = juce::jmin(b.getWidth(), b.getHeight()) * 0.45f;
|
||||
float th = r * 0.22f;
|
||||
|
||||
juce::Path track;
|
||||
track.addCentredArc(cx, cy, r, r, 0.f, startAngle, endAngle, true);
|
||||
g.setColour(AmbivalenceColors::ArcTrack);
|
||||
g.strokePath(track, juce::PathStrokeType(th,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
|
||||
float angle = startAngle + sliderPos * (endAngle - startAngle);
|
||||
juce::Path fill;
|
||||
fill.addCentredArc(cx, cy, r, r, 0.f, startAngle, angle, true);
|
||||
juce::ColourGradient grad(AmbivalenceColors::AccentBlue, cx - r, cy,
|
||||
AmbivalenceColors::Accent, cx + r, cy, false);
|
||||
g.setGradientFill(grad);
|
||||
g.strokePath(fill, juce::PathStrokeType(th,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
|
||||
g.setColour(AmbivalenceColors::Panel);
|
||||
g.fillEllipse(cx - r * 0.28f, cy - r * 0.28f, r * 0.56f, r * 0.56f);
|
||||
|
||||
float ix = cx + r * 0.6f * std::sin(angle);
|
||||
float iy = cy - r * 0.6f * std::cos(angle);
|
||||
g.setColour(AmbivalenceColors::TextPrimary);
|
||||
g.drawLine(cx, cy, ix, iy, 2.f);
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::drawLinearSlider(juce::Graphics& g,
|
||||
int x, int y, int w, int h,
|
||||
float sliderPos, float, float, juce::Slider::SliderStyle, juce::Slider&)
|
||||
{
|
||||
auto b = juce::Rectangle<int>(x, y, w, h).toFloat();
|
||||
float ty = b.getCentreY() - 2.f;
|
||||
g.setColour(AmbivalenceColors::ArcTrack);
|
||||
g.fillRoundedRectangle(b.getX(), ty, b.getWidth(), 4.f, 2.f);
|
||||
g.setColour(AmbivalenceColors::Accent);
|
||||
g.fillRoundedRectangle(b.getX(), ty, sliderPos - b.getX(), 4.f, 2.f);
|
||||
float r = 7.f;
|
||||
g.setColour(AmbivalenceColors::TextPrimary);
|
||||
g.fillEllipse(sliderPos - r, b.getCentreY() - r, r * 2.f, r * 2.f);
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::drawComboBox(juce::Graphics& g,
|
||||
int w, int h, bool isDown, int, int, int, int, juce::ComboBox&)
|
||||
{
|
||||
auto b = juce::Rectangle<int>(0, 0, w, h).toFloat();
|
||||
g.setColour(isDown ? AmbivalenceColors::Panel : AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(b, 3.f);
|
||||
g.setColour(AmbivalenceColors::Border);
|
||||
g.drawRoundedRectangle(b.reduced(0.5f), 3.f, 1.f);
|
||||
juce::Path arrow;
|
||||
arrow.addTriangle(w - 16.f, h * 0.5f - 3.f,
|
||||
w - 8.f, h * 0.5f - 3.f,
|
||||
w - 12.f, h * 0.5f + 3.f);
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.fillPath(arrow);
|
||||
}
|
||||
|
||||
void AmbivalenceLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label) {
|
||||
label.setBounds(6, 1, box.getWidth() - 22, box.getHeight() - 2);
|
||||
label.setFont(getComboBoxFont(box));
|
||||
}
|
||||
|
||||
juce::Font AmbivalenceLookAndFeel::getLabelFont(juce::Label&) { return mainFont.withHeight(10.f); }
|
||||
juce::Font AmbivalenceLookAndFeel::getComboBoxFont(juce::ComboBox&) { return mainFont.withHeight(11.f); }
|
||||
|
||||
void AmbivalenceLookAndFeel::drawGroupComponentOutline(juce::Graphics& g,
|
||||
int w, int h, const juce::String& text,
|
||||
const juce::Justification&, juce::GroupComponent&)
|
||||
{
|
||||
float textH = 12.f, indent = 8.f, yOff = textH * 0.5f;
|
||||
juce::Path p;
|
||||
p.startNewSubPath(indent + 4.f, yOff); p.lineTo(indent, yOff);
|
||||
p.lineTo(indent, (float)h - 1.f);
|
||||
p.lineTo((float)w - indent, (float)h - 1.f);
|
||||
p.lineTo((float)w - indent, yOff);
|
||||
|
||||
// * redraw after changes
|
||||
juce::GlyphArrangement ga;
|
||||
ga.addLineOfText(mainFont.withHeight(textH), text, 0.f, 0.f);
|
||||
float tw = ga.getBoundingBox(0, -1, true).getWidth() + 6.f;
|
||||
|
||||
p.lineTo(indent + 14.f + tw, yOff);
|
||||
g.setColour(AmbivalenceColors::Border);
|
||||
g.strokePath(p, juce::PathStrokeType(1.f));
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.setFont(mainFont.withHeight(textH).boldened());
|
||||
g.drawText(text, (int)(indent + 14.f), 0, (int)tw, (int)textH,
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
// --- RT60Visualizer --------------------------------------------------
|
||||
RT60Visualizer::RT60Visualizer() {
|
||||
displayRT60.fill(1.0f);
|
||||
startTimerHz(30);
|
||||
}
|
||||
RT60Visualizer::~RT60Visualizer() { stopTimer(); }
|
||||
|
||||
void RT60Visualizer::timerCallback() {
|
||||
if (!processor) return;
|
||||
|
||||
auto live = processor->getRT60ForDisplay();
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i)
|
||||
displayRT60[i] += 0.25f * (live[i] - displayRT60[i]);
|
||||
|
||||
// * dynamic Y axis above : current maximum RT60 value x 1.3 smoothly
|
||||
float maxVal = *std::max_element(displayRT60.begin(), displayRT60.end());
|
||||
float targetMax = std::max(MAX_RT60_DISPLAY_FLOOR, maxVal * 1.3f);
|
||||
// exponential ( rise quickly , fall gentle -> frequently )
|
||||
float smoothFactor = (targetMax > dynamicMaxRT60) ? 0.15f : 0.03f;
|
||||
dynamicMaxRT60 += smoothFactor * (targetMax - dynamicMaxRT60);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void RT60Visualizer::paint(juce::Graphics& g)
|
||||
{
|
||||
auto b = getLocalBounds().toFloat().reduced(2.f);
|
||||
float W = b.getWidth(), H = b.getHeight();
|
||||
float x0 = b.getX(), y0 = b.getY();
|
||||
|
||||
g.setColour(AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(b, 4.f);
|
||||
g.setColour(AmbivalenceColors::Border);
|
||||
g.drawRoundedRectangle(b.reduced(0.5f), 4.f, 1.f);
|
||||
|
||||
// * dynamic Y axis
|
||||
float logMin = std::log10(MIN_RT60_DISPLAY);
|
||||
float logMax = std::log10(dynamicMaxRT60);
|
||||
|
||||
// grid value dynamic Y axis
|
||||
// fixed value dynamicMaxRT60 below drawing
|
||||
static constexpr float kAllGridVals[] = {
|
||||
0.1f, 0.3f, 0.5f, 1.0f, 2.0f, 4.0f,
|
||||
8.0f, 12.0f, 16.0f, 20.0f
|
||||
};
|
||||
|
||||
// grid
|
||||
g.setColour(AmbivalenceColors::Separator);
|
||||
for (float v : kAllGridVals) {
|
||||
if (v > dynamicMaxRT60 * 1.05f) break;
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
g.drawHorizontalLine((int)(y0 + ny * H), x0 + 36.f, x0 + W - 4.f);
|
||||
}
|
||||
|
||||
// frequency label (X axis )
|
||||
g.setFont(8.5f);
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
static const char* fLbls[] = {
|
||||
"31","63","125","250","500","1k","2k","4k","8k","16k"
|
||||
};
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
|
||||
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
|
||||
g.drawText(fLbls[i], (int)(px - 12.f), (int)(y0 + H - 14.f),
|
||||
24, 13, juce::Justification::centred);
|
||||
}
|
||||
|
||||
// seconds label (Y axis ) - dynamic
|
||||
for (float v : kAllGridVals) {
|
||||
if (v > dynamicMaxRT60 * 1.05f) break;
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
float py = y0 + ny * H;
|
||||
juce::String lbl = (v < 1.f)
|
||||
? juce::String(v, 1) + "s"
|
||||
: (v < 10.f ? juce::String(v, 1) : juce::String((int)v)) + "s";
|
||||
g.drawText(lbl, (int)(x0 + 2.f), (int)(py - 7.f), 32, 14,
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
auto plotCurve = [&](const std::array<float, FDNReverb::NUM_BANDS>& rt60,
|
||||
juce::Colour col, float thick)
|
||||
{
|
||||
juce::Path path;
|
||||
bool first = true;
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
|
||||
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
|
||||
float py = y0 + ny * H;
|
||||
if (first) { path.startNewSubPath(px, py); first = false; }
|
||||
else path.lineTo(px, py);
|
||||
}
|
||||
g.setColour(col);
|
||||
g.strokePath(path, juce::PathStrokeType(thick,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
|
||||
for (int i = 0; i < FDNReverb::NUM_BANDS; ++i) {
|
||||
float v = std::clamp(rt60[i], MIN_RT60_DISPLAY, dynamicMaxRT60);
|
||||
float ny = 1.f - (std::log10(v) - logMin) / (logMax - logMin);
|
||||
float px = x0 + 36.f + (float)i / (FDNReverb::NUM_BANDS - 1) * (W - 40.f);
|
||||
float py = y0 + ny * H;
|
||||
g.fillEllipse(px - 3.f, py - 3.f, 6.f, 6.f);
|
||||
}
|
||||
};
|
||||
|
||||
// source preset curve (from the selected algorithm)
|
||||
if (processor) {
|
||||
int algo = (int)*processor->apvts.getRawParameterValue("algorithm");
|
||||
auto& preset = *FDNReverb::ALL_PRESETS[
|
||||
juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1, algo)];
|
||||
plotCurve(preset.acoustics.rt60,
|
||||
AmbivalenceColors::TextSecondary.withAlpha(0.5f), 1.f);
|
||||
}
|
||||
|
||||
// current RT60 curve (measured)
|
||||
plotCurve(displayRT60, AmbivalenceColors::Accent, 2.f);
|
||||
|
||||
// top right title
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.setFont(9.f);
|
||||
g.drawText("RT60 (s) per band",
|
||||
(int)x0 + 36, (int)y0 + 3, (int)W - 40, 12,
|
||||
juce::Justification::right);
|
||||
}
|
||||
|
||||
// --- VUMeter ---------------------------------------------------------
|
||||
VUMeter::VUMeter(const juce::String& lbl, Side s) : label(lbl), side(s) {}
|
||||
|
||||
void VUMeter::paint(juce::Graphics& g)
|
||||
{
|
||||
auto b = getLocalBounds().toFloat().reduced(1.f);
|
||||
g.setColour(AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(b, 3.f);
|
||||
|
||||
float bx = b.getX() + 22.f, bw = b.getWidth() - 22.f;
|
||||
auto bar = [&](float y, float level) {
|
||||
float n = juce::jlimit(0.f, 1.f, juce::jmap(
|
||||
juce::Decibels::gainToDecibels(level + 1e-9f), -60.f, 0.f, 0.f, 1.f));
|
||||
g.setColour(AmbivalenceColors::ArcTrack);
|
||||
g.fillRoundedRectangle(bx, y, bw, 7.f, 2.f);
|
||||
juce::ColourGradient gr(AmbivalenceColors::AccentBlue, bx, y,
|
||||
AmbivalenceColors::Accent, bx + bw, y, false);
|
||||
g.setGradientFill(gr);
|
||||
g.fillRoundedRectangle(bx, y, bw * n, 7.f, 2.f);
|
||||
};
|
||||
bar(b.getY() + 2.f, levelL);
|
||||
bar(b.getY() + 11.f, levelR);
|
||||
|
||||
g.setColour(AmbivalenceColors::TextSecondary);
|
||||
g.setFont(8.f);
|
||||
g.drawText(label, (int)b.getX(), (int)b.getY(), 20, (int)b.getHeight(),
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
// --- ArcKnob ---------------------------------------------------------
|
||||
void ArcKnob::build(juce::AudioProcessorValueTreeState& apvts,
|
||||
const juce::String& paramID,
|
||||
const juce::String& labelText,
|
||||
juce::Component* parent,
|
||||
AmbivalenceLookAndFeel& laf)
|
||||
{
|
||||
slider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag);
|
||||
slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 62, 14);
|
||||
slider.setLookAndFeel(&laf);
|
||||
slider.setColour(juce::Slider::textBoxTextColourId,
|
||||
AmbivalenceColors::TextSecondary);
|
||||
slider.setColour(juce::Slider::textBoxOutlineColourId,
|
||||
juce::Colours::transparentBlack);
|
||||
parent->addAndMakeVisible(slider);
|
||||
|
||||
label.setText(labelText, juce::dontSendNotification);
|
||||
label.setJustificationType(juce::Justification::centred);
|
||||
label.setFont(juce::Font(juce::FontOptions(9.f)));
|
||||
label.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
|
||||
parent->addAndMakeVisible(label);
|
||||
|
||||
// * after changes
|
||||
attachment.reset(
|
||||
new juce::AudioProcessorValueTreeState::SliderAttachment(
|
||||
apvts, paramID, slider));
|
||||
}
|
||||
|
||||
// --- AlgorithmSelector -----------------------------------------------
|
||||
AlgorithmSelector::AlgorithmSelector(juce::AudioProcessorValueTreeState& a)
|
||||
: apvts(a)
|
||||
{
|
||||
static const char* names[] = {
|
||||
"ROOM1","ROOM2","HALL1","HALL2","PLATE","SPRING","GOLDFOIL"
|
||||
};
|
||||
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
|
||||
buttons[i].setButtonText(names[i]);
|
||||
addAndMakeVisible(buttons[i]);
|
||||
int idx = i;
|
||||
buttons[i].onClick = [this, idx] {
|
||||
if (auto* param = apvts.getParameter("algorithm"))
|
||||
param->setValueNotifyingHost(
|
||||
(float)idx / (float)(FDNReverb::NUM_ALGORITHMS - 1));
|
||||
};
|
||||
}
|
||||
apvts.addParameterListener("algorithm", this);
|
||||
currentAlgo = juce::roundToInt(
|
||||
*apvts.getRawParameterValue("algorithm")
|
||||
* (FDNReverb::NUM_ALGORITHMS - 1));
|
||||
}
|
||||
|
||||
AlgorithmSelector::~AlgorithmSelector() {
|
||||
apvts.removeParameterListener("algorithm", this);
|
||||
}
|
||||
|
||||
void AlgorithmSelector::parameterChanged(const juce::String&, float newVal) {
|
||||
int newAlgo = juce::jlimit(0, FDNReverb::NUM_ALGORITHMS - 1,
|
||||
juce::roundToInt(newVal));
|
||||
juce::MessageManager::callAsync([this, newAlgo] {
|
||||
currentAlgo = newAlgo;
|
||||
updateButtonColors();
|
||||
});
|
||||
}
|
||||
|
||||
void AlgorithmSelector::updateButtonColors() {
|
||||
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i) {
|
||||
bool on = (i == currentAlgo);
|
||||
buttons[i].setColour(juce::TextButton::buttonColourId,
|
||||
on ? AmbivalenceColors::Accent : AmbivalenceColors::Surface);
|
||||
buttons[i].setColour(juce::TextButton::textColourOffId,
|
||||
on ? AmbivalenceColors::Background : AmbivalenceColors::TextSecondary);
|
||||
buttons[i].repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void AlgorithmSelector::paint(juce::Graphics& g) {
|
||||
g.setColour(AmbivalenceColors::Surface);
|
||||
g.fillRoundedRectangle(getLocalBounds().toFloat(), 4.f);
|
||||
}
|
||||
|
||||
void AlgorithmSelector::resized() {
|
||||
auto area = getLocalBounds().reduced(2);
|
||||
int btnW = area.getWidth() / FDNReverb::NUM_ALGORITHMS;
|
||||
for (int i = 0; i < FDNReverb::NUM_ALGORITHMS; ++i)
|
||||
buttons[i].setBounds(area.getX() + i * btnW, area.getY(),
|
||||
btnW - 1, area.getHeight());
|
||||
updateButtonColors();
|
||||
}
|
||||
110
Source/GUI/AmbivalenceUI.h
Normal file
110
Source/GUI/AmbivalenceUI.h
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "../AlgorithmPresets.h"
|
||||
|
||||
class FDNReverbAudioProcessor;
|
||||
|
||||
// --- Ambivalence Design System ------------------------------------------
|
||||
namespace AmbivalenceColors {
|
||||
const juce::Colour Background{ 0xFF1A1A1A };
|
||||
const juce::Colour Surface{ 0xFF242424 };
|
||||
const juce::Colour Panel{ 0xFF2C2C2C };
|
||||
const juce::Colour Border{ 0xFF3C3C3C };
|
||||
const juce::Colour Accent{ 0xFFFF6B00 };
|
||||
const juce::Colour AccentBlue{ 0xFF4090FF };
|
||||
const juce::Colour TextPrimary{ 0xFFE8E8E8 };
|
||||
const juce::Colour TextSecondary{ 0xFF888888 };
|
||||
const juce::Colour ArcTrack{ 0xFF3A3A3A };
|
||||
const juce::Colour ArcFill{ 0xFFFF6B00 };
|
||||
const juce::Colour Separator{ 0xFF383838 };
|
||||
}
|
||||
|
||||
// --- Ambivalence LookAndFeel --------------------------------------------
|
||||
class AmbivalenceLookAndFeel : public juce::LookAndFeel_V4
|
||||
{
|
||||
public:
|
||||
AmbivalenceLookAndFeel();
|
||||
void drawRotarySlider(juce::Graphics&, int x, int y, int w, int h,
|
||||
float sliderPos, float startAngle, float endAngle,
|
||||
juce::Slider&) override;
|
||||
void drawLinearSlider(juce::Graphics&, int x, int y, int w, int h,
|
||||
float sliderPos, float, float,
|
||||
juce::Slider::SliderStyle, juce::Slider&) override;
|
||||
void drawComboBox(juce::Graphics&, int w, int h, bool isDown,
|
||||
int, int, int, int, juce::ComboBox&) override;
|
||||
void positionComboBoxText(juce::ComboBox&, juce::Label&) override;
|
||||
juce::Font getLabelFont(juce::Label&) override;
|
||||
juce::Font getComboBoxFont(juce::ComboBox&) override;
|
||||
void drawGroupComponentOutline(juce::Graphics&, int w, int h,
|
||||
const juce::String&, const juce::Justification&,
|
||||
juce::GroupComponent&) override;
|
||||
private:
|
||||
juce::Font mainFont;
|
||||
};
|
||||
|
||||
// --- RT60 Visualizer -------------------------------------------------
|
||||
class RT60Visualizer : public juce::Component, private juce::Timer
|
||||
{
|
||||
public:
|
||||
RT60Visualizer();
|
||||
~RT60Visualizer() override;
|
||||
void setProcessor(FDNReverbAudioProcessor* p) { processor = p; }
|
||||
void paint(juce::Graphics&) override;
|
||||
private:
|
||||
void timerCallback() override;
|
||||
FDNReverbAudioProcessor* processor{ nullptr };
|
||||
std::array<float, FDNReverb::NUM_BANDS> displayRT60;
|
||||
|
||||
static constexpr float MIN_RT60_DISPLAY = 0.05f;
|
||||
static constexpr float MAX_RT60_DISPLAY_FLOOR = 4.0f; // Y axis above value
|
||||
|
||||
// * dynamic Y axis above : effectiveRT60 maximum value smoothly
|
||||
float dynamicMaxRT60{ MAX_RT60_DISPLAY_FLOOR };
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(RT60Visualizer)
|
||||
};
|
||||
|
||||
// --- VU Meter --------------------------------------------------------
|
||||
class VUMeter : public juce::Component
|
||||
{
|
||||
public:
|
||||
enum class Side { Input, Output };
|
||||
VUMeter(const juce::String& label, Side side);
|
||||
void paint(juce::Graphics&) override;
|
||||
void setLevels(float l, float r) noexcept { levelL = l; levelR = r; }
|
||||
private:
|
||||
juce::String label;
|
||||
Side side;
|
||||
float levelL{ 0.f }, levelR{ 0.f };
|
||||
};
|
||||
|
||||
// --- Labelled Arc Knob -----------------------------------------------
|
||||
struct ArcKnob {
|
||||
juce::Slider slider;
|
||||
juce::Label label;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> attachment;
|
||||
|
||||
void build(juce::AudioProcessorValueTreeState& apvts,
|
||||
const juce::String& paramID,
|
||||
const juce::String& labelText,
|
||||
juce::Component* parent,
|
||||
AmbivalenceLookAndFeel& laf);
|
||||
};
|
||||
|
||||
// --- Algorithm Selector ----------------------------------------------
|
||||
class AlgorithmSelector : public juce::Component,
|
||||
private juce::AudioProcessorValueTreeState::Listener
|
||||
{
|
||||
public:
|
||||
AlgorithmSelector(juce::AudioProcessorValueTreeState& apvts);
|
||||
~AlgorithmSelector() override;
|
||||
void paint(juce::Graphics&) override;
|
||||
void resized() override;
|
||||
private:
|
||||
void parameterChanged(const juce::String&, float) override;
|
||||
void updateButtonColors();
|
||||
std::array<juce::TextButton, FDNReverb::NUM_ALGORITHMS> buttons;
|
||||
juce::AudioProcessorValueTreeState& apvts;
|
||||
int currentAlgo{ 0 };
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AlgorithmSelector)
|
||||
};
|
||||
324
Source/GUI/DecayCurveViz.cpp
Normal file
324
Source/GUI/DecayCurveViz.cpp
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
#include "DecayCurveViz.h"
|
||||
|
||||
DecayCurveViz::DecayCurveViz() {
|
||||
cachedERDelayMs.fill(0.0f);
|
||||
cachedERGains.fill(0.0f);
|
||||
startTimerHz(15);
|
||||
}
|
||||
|
||||
DecayCurveViz::~DecayCurveViz() {
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void DecayCurveViz::timerCallback() {
|
||||
if (processor == nullptr) return;
|
||||
const auto& engine = processor->getEngine();
|
||||
|
||||
auto rt60 = engine.getEffectiveRT60();
|
||||
cachedRT60Mid = std::max(0.1f, rt60[4]);
|
||||
|
||||
cachedERBypassed = engine.isERBypassed();
|
||||
cachedERTapCount = engine.getERTapCount();
|
||||
if (cachedERTapCount > MAX_DISPLAY_TAPS)
|
||||
cachedERTapCount = MAX_DISPLAY_TAPS;
|
||||
|
||||
double sr = engine.getSampleRate();
|
||||
if (sr < 1.0) sr = 48000.0;
|
||||
|
||||
for (int i = 0; i < cachedERTapCount; ++i) {
|
||||
float delaySamples = engine.getERTapDelaySamples(i);
|
||||
cachedERDelayMs[i] = delaySamples / static_cast<float>(sr) * 1000.0f;
|
||||
cachedERGains[i] = engine.getERTapGain(i);
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
void DecayCurveViz::resized() {}
|
||||
|
||||
void DecayCurveViz::paint(juce::Graphics& g)
|
||||
{
|
||||
auto bounds = getLocalBounds().toFloat();
|
||||
if (bounds.getWidth() < 10.0f || bounds.getHeight() < 10.0f) return;
|
||||
|
||||
g.fillAll(AmbivalenceColors::Background);
|
||||
|
||||
const float topMargin = 10.0f;
|
||||
const float bottomMargin = 18.0f;
|
||||
const float leftMargin = 30.0f;
|
||||
const float rightMargin = 8.0f;
|
||||
const float plotX = bounds.getX() + leftMargin;
|
||||
const float plotY = bounds.getY() + topMargin;
|
||||
const float plotW = bounds.getWidth() - leftMargin - rightMargin;
|
||||
const float plotH = bounds.getHeight() - topMargin - bottomMargin;
|
||||
|
||||
const float maxTimeSec = juce::jlimit(0.5f, 8.0f, cachedRT60Mid * 1.5f);
|
||||
const float minDB = -60.0f;
|
||||
const float maxDB = 0.0f;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// time axis
|
||||
// 0~splitSec -> full width splitRatio expanded (ER zone )
|
||||
// splitSec~max -> width (Late zone )
|
||||
// -------------------------------------------------------------------------
|
||||
constexpr float splitSec = 0.20f; // 200ms expanded
|
||||
constexpr float splitRatio = 0.30f; // full width 30% ER zone
|
||||
|
||||
auto timeToX = [&](float timeSec) -> float {
|
||||
if (timeSec <= splitSec) {
|
||||
const float ratio = timeSec / splitSec;
|
||||
return plotX + ratio * plotW * splitRatio;
|
||||
}
|
||||
else {
|
||||
const float lateRange = maxTimeSec - splitSec;
|
||||
if (lateRange <= 0.0f) return plotX + plotW;
|
||||
const float ratio = (timeSec - splitSec) / lateRange;
|
||||
return plotX + plotW * splitRatio + ratio * plotW * (1.0f - splitRatio);
|
||||
}
|
||||
};
|
||||
|
||||
auto dbToY = [&](float db) -> float {
|
||||
const float normalized = (db - minDB) / (maxDB - minDB);
|
||||
return plotY + (1.0f - normalized) * plotH;
|
||||
};
|
||||
|
||||
// --- ER zone background ---
|
||||
{
|
||||
const float erZoneW = plotW * splitRatio;
|
||||
g.setColour(juce::Colour(0xFF1A2535));
|
||||
g.fillRect(plotX, plotY, erZoneW, plotH);
|
||||
}
|
||||
|
||||
// --- grid : horizontal (dB) ---
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.3f));
|
||||
for (float db = 0.0f; db >= -60.0f; db -= 20.0f)
|
||||
g.drawHorizontalLine((int)dbToY(db), plotX, plotX + plotW);
|
||||
|
||||
// --- grid : ER zone vertical (ms) ---
|
||||
{
|
||||
static const float erGridMs[] = { 20.0f, 50.0f, 100.0f, 150.0f, 200.0f };
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.5f));
|
||||
for (float ms : erGridMs) {
|
||||
const float t = ms * 0.001f;
|
||||
if (t >= maxTimeSec) break;
|
||||
g.drawVerticalLine((int)timeToX(t), plotY, plotY + plotH);
|
||||
}
|
||||
}
|
||||
|
||||
// --- grid : Late zone vertical (s) ---
|
||||
float timeStep;
|
||||
if (maxTimeSec <= 2.0f) timeStep = 0.5f;
|
||||
else if (maxTimeSec <= 4.0f) timeStep = 1.0f;
|
||||
else timeStep = 2.0f;
|
||||
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.3f));
|
||||
for (float t = splitSec + timeStep; t <= maxTimeSec; t += timeStep)
|
||||
g.drawVerticalLine((int)timeToX(t), plotY, plotY + plotH);
|
||||
|
||||
// --- ---
|
||||
{
|
||||
const float splitX = timeToX(splitSec);
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.9f));
|
||||
g.drawVerticalLine((int)splitX, plotY, plotY + plotH);
|
||||
}
|
||||
|
||||
// --- axis label ---
|
||||
g.setFont(juce::Font(juce::FontOptions(8.5f)));
|
||||
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.6f));
|
||||
|
||||
for (float db = 0.0f; db >= -60.0f; db -= 20.0f) {
|
||||
const float y = dbToY(db);
|
||||
g.drawText(juce::String((int)db) + "dB",
|
||||
(int)(plotX - leftMargin + 2), (int)(y - 6),
|
||||
(int)(leftMargin - 4), 12,
|
||||
juce::Justification::centredRight);
|
||||
}
|
||||
|
||||
// ER zone time label (ms)
|
||||
{
|
||||
static const float erGridMs[] = { 20.0f, 50.0f, 100.0f, 150.0f, 200.0f };
|
||||
for (float ms : erGridMs) {
|
||||
const float t = ms * 0.001f;
|
||||
if (t >= maxTimeSec) break;
|
||||
const float x = timeToX(t);
|
||||
g.drawText(juce::String((int)ms) + "ms",
|
||||
(int)(x - 20), (int)(plotY + plotH + 2),
|
||||
40, 14, juce::Justification::centred);
|
||||
}
|
||||
}
|
||||
|
||||
// Late zone time label (s)
|
||||
for (float t = splitSec + timeStep; t <= maxTimeSec; t += timeStep) {
|
||||
const float x = timeToX(t);
|
||||
g.drawText(juce::String(t, 1) + "s",
|
||||
(int)(x - 20), (int)(plotY + plotH + 2),
|
||||
40, 14, juce::Justification::centred);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Late Reverb decay curve ( 2D gradient )
|
||||
// -------------------------------------------------------------------------
|
||||
{
|
||||
const int numPoints = 80;
|
||||
juce::Colour orangeColor = AmbivalenceColors::Accent;
|
||||
|
||||
juce::Path latePath;
|
||||
latePath.startNewSubPath(plotX, dbToY(maxDB));
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
const float t = (i / static_cast<float>(numPoints)) * maxTimeSec;
|
||||
const float db = std::max(minDB, -60.0f * t / cachedRT60Mid);
|
||||
latePath.lineTo(timeToX(t), dbToY(db));
|
||||
}
|
||||
latePath.lineTo(timeToX(maxTimeSec), dbToY(minDB));
|
||||
latePath.lineTo(plotX, dbToY(minDB));
|
||||
latePath.closeSubPath();
|
||||
|
||||
juce::ColourGradient lateGrad(
|
||||
orangeColor.withAlpha(0.55f), plotX, plotY,
|
||||
orangeColor.withAlpha(0.0f), plotX + plotW, plotY + plotH,
|
||||
false);
|
||||
g.setGradientFill(lateGrad);
|
||||
g.fillPath(latePath);
|
||||
|
||||
juce::Path lateOutline;
|
||||
lateOutline.startNewSubPath(plotX, dbToY(maxDB));
|
||||
for (int i = 0; i <= numPoints; ++i) {
|
||||
const float t = (i / static_cast<float>(numPoints)) * maxTimeSec;
|
||||
const float db = std::max(minDB, -60.0f * t / cachedRT60Mid);
|
||||
lateOutline.lineTo(timeToX(t), dbToY(db));
|
||||
}
|
||||
g.setColour(orangeColor.withAlpha(0.75f));
|
||||
g.strokePath(lateOutline, juce::PathStrokeType(1.5f,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ER drawing (* )
|
||||
// : vertical line 2px + marker
|
||||
// : wide 5px + marker + envelope fill
|
||||
// -------------------------------------------------------------------------
|
||||
if (!cachedERBypassed && cachedERTapCount > 0) {
|
||||
const juce::Colour blueColor = juce::Colour::fromRGB(80, 160, 230);
|
||||
|
||||
// -- ER envelope fill region --
|
||||
if (cachedERTapCount >= 2) {
|
||||
juce::Path erFill;
|
||||
bool started = false;
|
||||
float lastX = plotX;
|
||||
|
||||
for (int t = 0; t < cachedERTapCount; ++t) {
|
||||
const float timeSec = cachedERDelayMs[t] * 0.001f;
|
||||
if (timeSec > maxTimeSec) continue;
|
||||
|
||||
float gainDB = (cachedERGains[t] > 1e-6f)
|
||||
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
|
||||
gainDB = juce::jlimit(minDB, maxDB, gainDB);
|
||||
|
||||
const float x = timeToX(timeSec);
|
||||
const float y = dbToY(gainDB);
|
||||
|
||||
if (!started) {
|
||||
erFill.startNewSubPath(plotX, dbToY(minDB));
|
||||
erFill.lineTo(x, y);
|
||||
started = true;
|
||||
}
|
||||
else {
|
||||
erFill.lineTo(x, y);
|
||||
}
|
||||
lastX = x;
|
||||
}
|
||||
|
||||
if (started) {
|
||||
erFill.lineTo(lastX, dbToY(minDB));
|
||||
erFill.closeSubPath();
|
||||
|
||||
juce::ColourGradient erAreaGrad(
|
||||
blueColor.withAlpha(0.20f), plotX, plotY,
|
||||
blueColor.withAlpha(0.03f), plotX + plotW * splitRatio, plotY + plotH,
|
||||
false);
|
||||
g.setGradientFill(erAreaGrad);
|
||||
g.fillPath(erFill);
|
||||
}
|
||||
}
|
||||
|
||||
// -- : wide + marker --
|
||||
for (int t = 0; t < cachedERTapCount; ++t) {
|
||||
const float timeSec = cachedERDelayMs[t] * 0.001f;
|
||||
if (timeSec > maxTimeSec) continue;
|
||||
|
||||
float gainDB = (cachedERGains[t] > 1e-6f)
|
||||
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
|
||||
gainDB = juce::jlimit(minDB, maxDB, gainDB);
|
||||
|
||||
const float x = timeToX(timeSec);
|
||||
const float yTop = dbToY(gainDB);
|
||||
const float yBottom = dbToY(minDB);
|
||||
const float barW = 5.0f;
|
||||
|
||||
juce::ColourGradient tapGrad(
|
||||
blueColor.withAlpha(0.90f), x, yTop,
|
||||
blueColor.withAlpha(0.10f), x, yBottom,
|
||||
false);
|
||||
g.setGradientFill(tapGrad);
|
||||
g.fillRect(x - barW * 0.5f, yTop, barW, yBottom - yTop);
|
||||
|
||||
// diamond marker
|
||||
g.setColour(blueColor);
|
||||
juce::Path diamond;
|
||||
diamond.startNewSubPath(x, yTop - 5.0f);
|
||||
diamond.lineTo(x + 4.0f, yTop);
|
||||
diamond.lineTo(x, yTop + 3.0f);
|
||||
diamond.lineTo(x - 4.0f, yTop);
|
||||
diamond.closeSubPath();
|
||||
g.fillPath(diamond);
|
||||
}
|
||||
|
||||
// -- ER envelope outline --
|
||||
if (cachedERTapCount >= 2) {
|
||||
juce::Path erOutline;
|
||||
bool started = false;
|
||||
|
||||
for (int t = 0; t < cachedERTapCount; ++t) {
|
||||
const float timeSec = cachedERDelayMs[t] * 0.001f;
|
||||
if (timeSec > maxTimeSec) continue;
|
||||
|
||||
float gainDB = (cachedERGains[t] > 1e-6f)
|
||||
? juce::Decibels::gainToDecibels(cachedERGains[t]) : minDB;
|
||||
gainDB = juce::jlimit(minDB, maxDB, gainDB);
|
||||
|
||||
const float x = timeToX(timeSec);
|
||||
const float y = dbToY(gainDB);
|
||||
|
||||
if (!started) { erOutline.startNewSubPath(x, y); started = true; }
|
||||
else erOutline.lineTo(x, y);
|
||||
}
|
||||
|
||||
g.setColour(blueColor.withAlpha(0.65f));
|
||||
g.strokePath(erOutline, juce::PathStrokeType(1.5f,
|
||||
juce::PathStrokeType::curved, juce::PathStrokeType::rounded));
|
||||
}
|
||||
}
|
||||
|
||||
// --- zone label ---
|
||||
g.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 8.0f, juce::Font::bold)));
|
||||
|
||||
g.setColour(juce::Colour::fromRGB(80, 160, 230).withAlpha(0.9f));
|
||||
g.drawText("ER",
|
||||
(int)(plotX + 4), (int)(plotY + 2),
|
||||
30, 12, juce::Justification::centredLeft);
|
||||
|
||||
{
|
||||
const float splitX = timeToX(splitSec);
|
||||
g.setColour(AmbivalenceColors::Accent.withAlpha(0.9f));
|
||||
g.drawText("LATE",
|
||||
(int)(splitX + 6), (int)(plotY + 2),
|
||||
40, 12, juce::Justification::centredLeft);
|
||||
}
|
||||
|
||||
g.setFont(juce::Font(juce::FontOptions(7.5f)));
|
||||
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.4f));
|
||||
g.drawText("0-200ms (x2)",
|
||||
(int)(plotX + 2), (int)(plotY + plotH - 14),
|
||||
(int)(plotW * splitRatio - 4), 12,
|
||||
juce::Justification::centredLeft);
|
||||
}
|
||||
42
Source/GUI/DecayCurveViz.h
Normal file
42
Source/GUI/DecayCurveViz.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
#include "../PluginProcessor.h"
|
||||
#include "AmbivalenceUI.h"
|
||||
|
||||
class DecayCurveViz : public juce::Component, private juce::Timer {
|
||||
public:
|
||||
DecayCurveViz();
|
||||
~DecayCurveViz() override;
|
||||
|
||||
void setProcessor(FDNReverbAudioProcessor* p) noexcept { processor = p; }
|
||||
void paint(juce::Graphics& g) override;
|
||||
void resized() override;
|
||||
|
||||
private:
|
||||
void timerCallback() override;
|
||||
|
||||
// --- time axis -----------------------------------------------
|
||||
// time axis :
|
||||
// 0~splitSec : plotW x splitRatio width expanded
|
||||
// splitSec~max: width
|
||||
// ER(0~200ms) 2
|
||||
float timeToX(float timeSec, float plotX, float plotW,
|
||||
float maxTimeSec) const noexcept;
|
||||
|
||||
FDNReverbAudioProcessor* processor{ nullptr };
|
||||
|
||||
float cachedRT60Mid{ 1.0f };
|
||||
int cachedERTapCount{ 0 };
|
||||
bool cachedERBypassed{ false };
|
||||
|
||||
static constexpr int MAX_DISPLAY_TAPS = 12;
|
||||
std::array<float, MAX_DISPLAY_TAPS> cachedERDelayMs;
|
||||
std::array<float, MAX_DISPLAY_TAPS> cachedERGains;
|
||||
|
||||
// --- time axis setting ---
|
||||
// splitSec below time splitRatio width expanded
|
||||
static constexpr float splitSec = 0.20f; // 0~200ms expanded
|
||||
static constexpr float splitRatio = 0.30f; // full width 30% ER zone
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DecayCurveViz)
|
||||
};
|
||||
690
Source/PluginEditor.cpp
Normal file
690
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,690 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
#include "BuildInfo.h"
|
||||
|
||||
static constexpr int Y_HEADER = 8;
|
||||
static constexpr int Y_ALGO = 48;
|
||||
static constexpr int Y_SLABEL1 = 86;
|
||||
static constexpr int Y_ROW1 = 104;
|
||||
static constexpr int Y_SLABEL2 = 204;
|
||||
static constexpr int Y_ROW2 = 222;
|
||||
static constexpr int Y_SEP = 322;
|
||||
static constexpr int Y_VIZ = 326;
|
||||
|
||||
static constexpr int SEC_TIME = 8;
|
||||
static constexpr int SEC_FREQUENCY = 254;
|
||||
static constexpr int SEC_DIFFUSION = 418;
|
||||
static constexpr int SEC_STEREO = 664;
|
||||
static constexpr int SEC_CHARACTER = 746;
|
||||
static constexpr int SEP_TF = 245;
|
||||
static constexpr int SEP_FD = 409;
|
||||
static constexpr int SEP_DS = 655;
|
||||
static constexpr int SEP_SC = 737;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// constructor
|
||||
// -----------------------------------------------------------------------------
|
||||
FDNReverbEditor::FDNReverbEditor(FDNReverbAudioProcessor& p)
|
||||
: AudioProcessorEditor(&p),
|
||||
audioProcessor(p),
|
||||
algoSelector(p.apvts),
|
||||
vuIn("IN", VUMeter::Side::Input),
|
||||
vuOut("OUT", VUMeter::Side::Output)
|
||||
{
|
||||
setLookAndFeel(&laf);
|
||||
setSize(W, H);
|
||||
|
||||
// -- Title --
|
||||
titleLabel.setText("AMBIVALENCE 1.1", juce::dontSendNotification);
|
||||
titleLabel.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 14.f, juce::Font::bold)));
|
||||
titleLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary);
|
||||
addAndMakeVisible(titleLabel);
|
||||
|
||||
addAndMakeVisible(algoSelector);
|
||||
|
||||
auto BK = [&](ArcKnob& k, const char* id, const char* lbl) {
|
||||
k.build(p.apvts, id, lbl, this, laf);
|
||||
};
|
||||
|
||||
BK(kPreDelay, "predelay", "PRE-DELAY");
|
||||
BK(kRoomSize, "roomsize", "ROOM SIZE");
|
||||
BK(kDecay, "decaytime", "DECAY");
|
||||
BK(kHFDamp, "hfdamping", "HF DAMP");
|
||||
BK(kLFAbsorb, "lfabsorption", "LF ABSORB");
|
||||
BK(kDiffusion, "diffusion", "DIFFUSION");
|
||||
BK(kModAmt, "modamount", "MOD AMT");
|
||||
BK(kModRate, "modrate", "MOD RATE");
|
||||
BK(kStereoW, "stereowidth", "WIDTH");
|
||||
BK(kERLevel, "erlevel", "ER LEVEL");
|
||||
BK(kSaturation, "saturation", "SATURATE");
|
||||
BK(kWet, "wetlevel", "WET");
|
||||
BK(kDry, "drylevel", "DRY");
|
||||
BK(kDuckAmt, "duckamount", "AMOUNT");
|
||||
BK(kDuckThr, "duckthresh", "THRESH");
|
||||
BK(kDuckAtt, "duckattack", "ATTACK");
|
||||
BK(kDuckRel, "duckrelease", "RELEASE");
|
||||
BK(kLoCutNorm, "locut", "LO CUT");
|
||||
BK(kHiCutNorm, "hicut", "HI CUT");
|
||||
|
||||
// -- ProMode button --
|
||||
proModeButton.setButtonText("PRO");
|
||||
proModeButton.setClickingTogglesState(true);
|
||||
proModeButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::Accent);
|
||||
proModeButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
|
||||
proModeButton.setColour(juce::TextButton::textColourOnId, AmbivalenceColors::Background);
|
||||
proModeButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
|
||||
addAndMakeVisible(proModeButton);
|
||||
proModeAttachment.reset(
|
||||
new juce::AudioProcessorValueTreeState::ButtonAttachment(
|
||||
p.apvts, "promode", proModeButton));
|
||||
|
||||
// -- ER SOLO button --
|
||||
erSoloButton.setButtonText("ER SOLO");
|
||||
erSoloButton.setClickingTogglesState(true);
|
||||
erSoloButton.setColour(juce::TextButton::buttonOnColourId, AmbivalenceColors::AccentBlue);
|
||||
erSoloButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
|
||||
erSoloButton.setColour(juce::TextButton::textColourOnId, AmbivalenceColors::Background);
|
||||
erSoloButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
|
||||
addAndMakeVisible(erSoloButton);
|
||||
erSoloAttachment.reset(
|
||||
new juce::AudioProcessorValueTreeState::ButtonAttachment(
|
||||
p.apvts, "ersolo", erSoloButton));
|
||||
|
||||
// -- ProMode: RT60 band --
|
||||
static const char* rtBandIDs[] = {
|
||||
"rtband0","rtband1","rtband2","rtband3","rtband4",
|
||||
"rtband5","rtband6","rtband7","rtband8","rtband9"
|
||||
};
|
||||
static const char* rtBandLbls[] = {
|
||||
"31Hz","63Hz","125Hz","250Hz","500Hz",
|
||||
"1kHz","2kHz","4kHz","8kHz","16kHz"
|
||||
};
|
||||
for (int i = 0; i < 10; ++i)
|
||||
kRTBands[i].build(p.apvts, rtBandIDs[i], rtBandLbls[i], this, laf);
|
||||
|
||||
// -- ProMode: SatType --
|
||||
satTypeLabel.setText("SAT TYPE", juce::dontSendNotification);
|
||||
satTypeLabel.setFont(juce::Font(juce::FontOptions(9.f)));
|
||||
satTypeLabel.setColour(juce::Label::textColourId, AmbivalenceColors::TextSecondary);
|
||||
satTypeLabel.setJustificationType(juce::Justification::centred);
|
||||
addAndMakeVisible(satTypeLabel);
|
||||
|
||||
satTypeCombo.addItemList({ "Warm","Tape","Tube","Hard" }, 1);
|
||||
satTypeCombo.setLookAndFeel(&laf);
|
||||
addAndMakeVisible(satTypeCombo);
|
||||
satTypeAttachment.reset(
|
||||
new juce::AudioProcessorValueTreeState::ComboBoxAttachment(
|
||||
p.apvts, "sattype", satTypeCombo));
|
||||
|
||||
// -- ProMode: Tilt EQ + Output EQ --
|
||||
BK(kTiltLow, "tiltlow", "TILT LOW");
|
||||
BK(kTiltMid, "tiltmid", "TILT MID");
|
||||
BK(kTiltHigh, "tilthigh", "TILT HIGH");
|
||||
BK(kLoCutPro, "locut", "LO CUT");
|
||||
BK(kHiCutPro, "hicut", "HI CUT");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// preset UI
|
||||
// -------------------------------------------------------------------------
|
||||
presetManager = std::make_unique<PresetManager>(p);
|
||||
|
||||
// < PREV
|
||||
presetPrevButton.setButtonText("<");
|
||||
presetPrevButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
|
||||
presetPrevButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
|
||||
addAndMakeVisible(presetPrevButton);
|
||||
presetPrevButton.onClick = [this] {
|
||||
presetManager->loadPrevPreset();
|
||||
};
|
||||
|
||||
// preset name combo
|
||||
presetCombo.setLookAndFeel(&laf);
|
||||
addAndMakeVisible(presetCombo);
|
||||
presetCombo.onChange = [this] {
|
||||
int idx = presetCombo.getSelectedItemIndex();
|
||||
auto names = presetManager->getPresetNames();
|
||||
if (idx >= 0 && idx < names.size())
|
||||
presetManager->loadPreset(names[idx]);
|
||||
};
|
||||
|
||||
// > NEXT
|
||||
presetNextButton.setButtonText(">");
|
||||
presetNextButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
|
||||
presetNextButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextPrimary);
|
||||
addAndMakeVisible(presetNextButton);
|
||||
presetNextButton.onClick = [this] {
|
||||
presetManager->loadNextPreset();
|
||||
};
|
||||
|
||||
// SAVE
|
||||
presetSaveButton.setButtonText("SAVE");
|
||||
presetSaveButton.setColour(juce::TextButton::buttonColourId,
|
||||
AmbivalenceColors::Accent.withAlpha(0.75f));
|
||||
presetSaveButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::Background);
|
||||
addAndMakeVisible(presetSaveButton);
|
||||
presetSaveButton.onClick = [this] { savePresetWithDialog(); };
|
||||
|
||||
// --- after changes ---
|
||||
// LOAD
|
||||
presetLoadButton.setButtonText("LOAD");
|
||||
presetLoadButton.setColour(juce::TextButton::buttonColourId,
|
||||
AmbivalenceColors::AccentBlue.withAlpha(0.75f));
|
||||
presetLoadButton.setColour(juce::TextButton::textColourOffId,
|
||||
AmbivalenceColors::Background);
|
||||
addAndMakeVisible(presetLoadButton);
|
||||
presetLoadButton.onClick = [this] {
|
||||
auto names = presetManager->getPresetNames();
|
||||
int idx = presetCombo.getSelectedItemIndex();
|
||||
if (idx >= 0 && idx < names.size())
|
||||
presetManager->loadPreset(names[idx]);
|
||||
};
|
||||
|
||||
// DELETE
|
||||
presetDeleteButton.setButtonText("DELETE");
|
||||
presetDeleteButton.setColour(juce::TextButton::buttonColourId, AmbivalenceColors::Surface);
|
||||
presetDeleteButton.setColour(juce::TextButton::textColourOffId, AmbivalenceColors::TextSecondary);
|
||||
addAndMakeVisible(presetDeleteButton);
|
||||
presetDeleteButton.onClick = [this] { deleteCurrentPreset(); };
|
||||
|
||||
// callback setup
|
||||
// in the constructor callback setup
|
||||
presetManager->onPresetListChanged = [this] { refreshPresetCombo(); };
|
||||
|
||||
// * fix : whenever the preset name changes Processor notify
|
||||
presetManager->onPresetLoaded = [this](const juce::String& name) {
|
||||
audioProcessor.setLastSavedPresetName(name);
|
||||
refreshPresetCombo();
|
||||
};
|
||||
// -- Visualizers --
|
||||
rt60Viz.setProcessor(&p);
|
||||
decayCurveViz.setProcessor(&p);
|
||||
addAndMakeVisible(rt60Viz);
|
||||
addAndMakeVisible(decayCurveViz);
|
||||
|
||||
addAndMakeVisible(vuIn);
|
||||
addAndMakeVisible(vuOut);
|
||||
|
||||
// -- AcousticMetrics --
|
||||
labelMetricsTitle.setText("ACOUSTICS", juce::dontSendNotification);
|
||||
labelMetricsTitle.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 8.5f, juce::Font::bold)));
|
||||
labelMetricsTitle.setColour(juce::Label::textColourId,
|
||||
AmbivalenceColors::Accent.withAlpha(0.75f));
|
||||
labelMetricsTitle.setJustificationType(juce::Justification::centredLeft);
|
||||
addAndMakeVisible(labelMetricsTitle);
|
||||
|
||||
auto setupCaption = [this](juce::Label& label, const juce::String& text) {
|
||||
label.setText(text, juce::dontSendNotification);
|
||||
label.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 8.0f, juce::Font::plain)));
|
||||
label.setColour(juce::Label::textColourId,
|
||||
AmbivalenceColors::TextSecondary.withAlpha(0.85f));
|
||||
label.setJustificationType(juce::Justification::centredRight);
|
||||
addAndMakeVisible(label);
|
||||
};
|
||||
auto setupValue = [this](juce::Label& label) {
|
||||
label.setText("--", juce::dontSendNotification);
|
||||
label.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 9.5f, juce::Font::bold)));
|
||||
label.setColour(juce::Label::textColourId, AmbivalenceColors::TextPrimary);
|
||||
label.setJustificationType(juce::Justification::centredLeft);
|
||||
addAndMakeVisible(label);
|
||||
};
|
||||
|
||||
setupCaption(labelD50Caption, "D50:");
|
||||
setupCaption(labelC50Caption, "C50:");
|
||||
setupCaption(labelC80Caption, "C80:");
|
||||
setupCaption(labelEDTCaption, "EDT:");
|
||||
setupValue(labelD50Value);
|
||||
setupValue(labelC50Value);
|
||||
setupValue(labelC80Value);
|
||||
setupValue(labelEDTValue);
|
||||
|
||||
// -- Git build info --
|
||||
juce::String gitInfo = "Git: " + juce::String(AMBIVALENCE_GIT_BRANCH) + " @ " +
|
||||
juce::String(AMBIVALENCE_GIT_COMMIT);
|
||||
#if AMBIVALENCE_GIT_DIRTY
|
||||
gitInfo += " (dirty)";
|
||||
#endif
|
||||
gitInfo += " | Build: " + juce::String(__DATE__) + " " + juce::String(__TIME__) + " (local)";
|
||||
|
||||
statusLabel.setText(gitInfo, juce::dontSendNotification);
|
||||
statusLabel.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 8.5f, juce::Font::plain)));
|
||||
statusLabel.setColour(juce::Label::textColourId,
|
||||
AmbivalenceColors::TextSecondary.withAlpha(0.6f));
|
||||
statusLabel.setJustificationType(juce::Justification::centred);
|
||||
addAndMakeVisible(statusLabel);
|
||||
|
||||
// --- after changes ---
|
||||
refreshPresetCombo(); // added: initialize the combo at startup
|
||||
updatePanelVisibility();
|
||||
startTimerHz(60);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// destructor
|
||||
// -----------------------------------------------------------------------------
|
||||
FDNReverbEditor::~FDNReverbEditor() {
|
||||
stopTimer();
|
||||
setLookAndFeel(nullptr);
|
||||
satTypeCombo.setLookAndFeel(nullptr);
|
||||
presetCombo.setLookAndFeel(nullptr);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// timerCallback
|
||||
// -----------------------------------------------------------------------------
|
||||
void FDNReverbEditor::timerCallback()
|
||||
{
|
||||
vuIn.setLevels(audioProcessor.getInputRMSL(),
|
||||
audioProcessor.getInputRMSR());
|
||||
vuOut.setLevels(audioProcessor.getOutputRMSL(),
|
||||
audioProcessor.getOutputRMSR());
|
||||
vuIn.repaint();
|
||||
vuOut.repaint();
|
||||
|
||||
static int metricsCounter = 0;
|
||||
if (++metricsCounter >= 2) {
|
||||
metricsCounter = 0;
|
||||
labelD50Value.setText(
|
||||
juce::String(audioProcessor.getD50() * 100.0f, 1) + "%",
|
||||
juce::dontSendNotification);
|
||||
labelC50Value.setText(
|
||||
juce::String(audioProcessor.getC50(), 1) + "dB",
|
||||
juce::dontSendNotification);
|
||||
labelC80Value.setText(
|
||||
juce::String(audioProcessor.getC80(), 1) + "dB",
|
||||
juce::dontSendNotification);
|
||||
labelEDTValue.setText(
|
||||
juce::String(audioProcessor.getEDT(), 2) + "s",
|
||||
juce::dontSendNotification);
|
||||
}
|
||||
|
||||
bool newProMode = (*audioProcessor.apvts.getRawParameterValue("promode") > 0.5f);
|
||||
if (newProMode != isProMode) {
|
||||
isProMode = newProMode;
|
||||
updatePanelVisibility();
|
||||
resized();
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// updatePanelVisibility
|
||||
// -----------------------------------------------------------------------------
|
||||
void FDNReverbEditor::updatePanelVisibility()
|
||||
{
|
||||
auto setKnob = [](ArcKnob& k, bool vis) {
|
||||
k.slider.setVisible(vis);
|
||||
k.label.setVisible(vis);
|
||||
};
|
||||
|
||||
const bool showNormal = !isProMode;
|
||||
const bool showPro = isProMode;
|
||||
|
||||
setKnob(kPreDelay, showNormal);
|
||||
setKnob(kRoomSize, showNormal);
|
||||
setKnob(kDecay, showNormal);
|
||||
setKnob(kHFDamp, showNormal);
|
||||
setKnob(kLFAbsorb, showNormal);
|
||||
setKnob(kDiffusion, showNormal);
|
||||
setKnob(kModAmt, showNormal);
|
||||
setKnob(kModRate, showNormal);
|
||||
setKnob(kStereoW, showNormal);
|
||||
setKnob(kERLevel, showNormal);
|
||||
setKnob(kSaturation, showNormal);
|
||||
setKnob(kWet, showNormal);
|
||||
setKnob(kDry, showNormal);
|
||||
setKnob(kDuckAmt, showNormal);
|
||||
setKnob(kDuckThr, showNormal);
|
||||
setKnob(kDuckAtt, showNormal);
|
||||
setKnob(kDuckRel, showNormal);
|
||||
setKnob(kLoCutNorm, showNormal);
|
||||
setKnob(kHiCutNorm, showNormal);
|
||||
|
||||
for (auto& k : kRTBands) setKnob(k, showPro);
|
||||
satTypeLabel.setVisible(showPro);
|
||||
satTypeCombo.setVisible(showPro);
|
||||
setKnob(kTiltLow, showPro);
|
||||
setKnob(kTiltMid, showPro);
|
||||
setKnob(kTiltHigh, showPro);
|
||||
setKnob(kLoCutPro, showPro);
|
||||
setKnob(kHiCutPro, showPro);
|
||||
|
||||
// preset UI always visible
|
||||
presetPrevButton.setVisible(true);
|
||||
presetCombo.setVisible(true);
|
||||
presetNextButton.setVisible(true);
|
||||
// --- after changes ---
|
||||
presetSaveButton.setVisible(true);
|
||||
presetLoadButton.setVisible(true); // * added
|
||||
presetDeleteButton.setVisible(true);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// resized
|
||||
// -----------------------------------------------------------------------------
|
||||
void FDNReverbEditor::resized()
|
||||
{
|
||||
titleLabel.setBounds(PAD, Y_HEADER, 180, 32);
|
||||
proModeButton.setBounds(196, Y_HEADER + 5, 52, 22);
|
||||
erSoloButton.setBounds(256, Y_HEADER + 5, 72, 22);
|
||||
vuIn.setBounds(W - 220, Y_HEADER + 2, 96, 28);
|
||||
vuOut.setBounds(W - 120, Y_HEADER + 2, 96, 28);
|
||||
|
||||
algoSelector.setBounds(PAD, Y_ALGO, W - PAD * 2, 30);
|
||||
|
||||
auto place1 = [&](ArcKnob& k, int& x, int y) {
|
||||
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
|
||||
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
|
||||
x += KNOB_W + ROW1_GAP;
|
||||
};
|
||||
auto place2 = [&](ArcKnob& k, int& x, int y) {
|
||||
k.label.setBounds(x, y, KNOB_W, KNOB_LBL_H);
|
||||
k.slider.setBounds(x, y + KNOB_LBL_H, KNOB_W, KNOB_H);
|
||||
x += KNOB_W + PAD;
|
||||
};
|
||||
|
||||
if (!isProMode) {
|
||||
// -- Row 1 --
|
||||
int kx = PAD;
|
||||
place1(kPreDelay, kx, Y_ROW1);
|
||||
place1(kRoomSize, kx, Y_ROW1);
|
||||
place1(kDecay, kx, Y_ROW1);
|
||||
place1(kHFDamp, kx, Y_ROW1);
|
||||
place1(kLFAbsorb, kx, Y_ROW1);
|
||||
place1(kDiffusion, kx, Y_ROW1);
|
||||
place1(kModAmt, kx, Y_ROW1);
|
||||
place1(kModRate, kx, Y_ROW1);
|
||||
place1(kStereoW, kx, Y_ROW1);
|
||||
place1(kERLevel, kx, Y_ROW1);
|
||||
place1(kSaturation, kx, Y_ROW1);
|
||||
|
||||
// -- Row 2: MIX | OUT EQ | DUCKING --
|
||||
kx = PAD;
|
||||
place2(kWet, kx, Y_ROW2);
|
||||
place2(kDry, kx, Y_ROW2);
|
||||
kx += 16;
|
||||
place2(kLoCutNorm, kx, Y_ROW2);
|
||||
place2(kHiCutNorm, kx, Y_ROW2);
|
||||
kx += 16;
|
||||
place2(kDuckAmt, kx, Y_ROW2);
|
||||
place2(kDuckThr, kx, Y_ROW2);
|
||||
place2(kDuckAtt, kx, Y_ROW2);
|
||||
place2(kDuckRel, kx, Y_ROW2);
|
||||
|
||||
}
|
||||
else {
|
||||
// -- ProMode row 1 --
|
||||
int kx = PAD;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
place1(kRTBands[i], kx, Y_ROW1);
|
||||
|
||||
// -- ProMode row 2 --
|
||||
int kx2 = PAD;
|
||||
satTypeLabel.setBounds(kx2, Y_SLABEL2, KNOB_W, KNOB_LBL_H);
|
||||
satTypeCombo.setBounds(kx2, Y_SLABEL2 + KNOB_LBL_H + 2, KNOB_W + PAD, 24);
|
||||
kx2 += KNOB_W + PAD + PAD + 8;
|
||||
place2(kTiltLow, kx2, Y_ROW2);
|
||||
place2(kTiltMid, kx2, Y_ROW2);
|
||||
place2(kTiltHigh, kx2, Y_ROW2);
|
||||
kx2 += 16;
|
||||
place2(kLoCutPro, kx2, Y_ROW2);
|
||||
place2(kHiCutPro, kx2, Y_ROW2);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// preset UI (always, mode-independent)
|
||||
// -------------------------------------------------------------------------
|
||||
// layout (PRESET_PANEL_X=632 starting from ):
|
||||
//
|
||||
// top row Y=Y_ROW2: [<(26)] gap4 [combo(154)] gap4 [>(26)] -> right edge 848
|
||||
// bottom row Y=Y_ROW2+34:[SAVE(104)] gap8 [DELETE(104)] -> right edge 848
|
||||
//
|
||||
// separator vertical line : PRESET_PANEL_X - 9 = 623
|
||||
// -------------------------------------------------------------------------
|
||||
{
|
||||
const int px = PRESET_PANEL_X;
|
||||
const int btnH = 26;
|
||||
|
||||
// top row
|
||||
presetPrevButton.setBounds(px, Y_ROW2, 26, btnH);
|
||||
presetCombo.setBounds(px + 30, Y_ROW2, 154, btnH);
|
||||
presetNextButton.setBounds(px + 188, Y_ROW2, 26, btnH);
|
||||
|
||||
// --- after changes ---
|
||||
// bottom row : [SAVE(68)] [LOAD(68)] [DELETE(68)]
|
||||
presetSaveButton.setBounds(px, Y_ROW2 + 34, 68, btnH);
|
||||
presetLoadButton.setBounds(px + 72, Y_ROW2 + 34, 68, btnH);
|
||||
presetDeleteButton.setBounds(px + 144, Y_ROW2 + 34, 68, btnH);
|
||||
}
|
||||
|
||||
// -- Visualizers --
|
||||
const int vizTotalH = H - Y_VIZ - STATUS_H - PAD;
|
||||
const int rt60Height = vizTotalH / 2 - 2;
|
||||
const int decayHeight = vizTotalH / 2 - 2;
|
||||
const int decayY = Y_VIZ + rt60Height + 4;
|
||||
|
||||
rt60Viz.setBounds(PAD, Y_VIZ, W - PAD * 2, rt60Height);
|
||||
decayCurveViz.setBounds(PAD, decayY, W - PAD * 2, decayHeight);
|
||||
|
||||
// -- AcousticMetrics --
|
||||
const int metricsRight = W - PAD - 8;
|
||||
const int metricsW = 280;
|
||||
const int metricsLeft = metricsRight - metricsW;
|
||||
const int metricsTop = decayY + 6;
|
||||
const int metricsRowH = 14;
|
||||
const int metricsRow1Y = metricsTop + 14;
|
||||
const int metricsRow2Y = metricsRow1Y + metricsRowH;
|
||||
const int captionW = 28;
|
||||
const int valueW = 52;
|
||||
const int colSpacing = 8;
|
||||
const int colW = captionW + valueW;
|
||||
|
||||
labelMetricsTitle.setBounds(metricsLeft, metricsTop, 80, 12);
|
||||
labelD50Caption.setBounds(metricsLeft, metricsRow1Y, captionW, metricsRowH);
|
||||
labelD50Value.setBounds(metricsLeft + captionW, metricsRow1Y, valueW, metricsRowH);
|
||||
labelC50Caption.setBounds(metricsLeft + colW + colSpacing, metricsRow1Y, captionW, metricsRowH);
|
||||
labelC50Value.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow1Y, valueW, metricsRowH);
|
||||
labelC80Caption.setBounds(metricsLeft, metricsRow2Y, captionW, metricsRowH);
|
||||
labelC80Value.setBounds(metricsLeft + captionW, metricsRow2Y, valueW, metricsRowH);
|
||||
labelEDTCaption.setBounds(metricsLeft + colW + colSpacing, metricsRow2Y, captionW, metricsRowH);
|
||||
labelEDTValue.setBounds(metricsLeft + colW + colSpacing + captionW, metricsRow2Y, valueW, metricsRowH);
|
||||
|
||||
// -- Git build-info status bar --
|
||||
statusLabel.setBounds(PAD, H - STATUS_H, W - PAD * 2, STATUS_H);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// paint
|
||||
// -----------------------------------------------------------------------------
|
||||
void FDNReverbEditor::paint(juce::Graphics& g)
|
||||
{
|
||||
g.fillAll(AmbivalenceColors::Background);
|
||||
juce::ColourGradient grad(
|
||||
AmbivalenceColors::Surface.withAlpha(0.12f), 0.f, 0.f,
|
||||
AmbivalenceColors::Background, 0.f, (float)H, false);
|
||||
g.setGradientFill(grad);
|
||||
g.fillAll();
|
||||
|
||||
g.setFont(juce::Font(juce::FontOptions(8.f)));
|
||||
g.setColour(AmbivalenceColors::TextSecondary.withAlpha(0.35f));
|
||||
g.drawText("16ch FDN | SAPF | ISM-ER | 44.1-192kHz",
|
||||
PAD + 336, Y_HEADER + 10, W / 2, 12,
|
||||
juce::Justification::centredLeft);
|
||||
|
||||
g.setColour(AmbivalenceColors::Separator);
|
||||
g.drawHorizontalLine(Y_SEP, (float)PAD, (float)(W - PAD));
|
||||
|
||||
g.setFont(juce::Font(juce::FontOptions(
|
||||
"Helvetica Neue", 8.5f, juce::Font::bold)));
|
||||
|
||||
auto sl = [&](int x, int y, const char* t) {
|
||||
g.drawText(t, x, y, 200, 14, juce::Justification::centredLeft);
|
||||
};
|
||||
|
||||
if (!isProMode) {
|
||||
// -- Row 1 separator --
|
||||
g.setColour(AmbivalenceColors::Separator);
|
||||
g.drawVerticalLine(SEP_TF, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
|
||||
g.drawVerticalLine(SEP_FD, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
|
||||
g.drawVerticalLine(SEP_DS, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
|
||||
g.drawVerticalLine(SEP_SC, (float)Y_SLABEL1, (float)(Y_ROW1 + UNIT_H));
|
||||
|
||||
// -- Row 2 separator --
|
||||
const int row2_outeq_x = PAD + 2 * (KNOB_W + PAD) + 16;
|
||||
const int row2_duck_x = row2_outeq_x + 2 * (KNOB_W + PAD) + 16;
|
||||
g.drawVerticalLine(row2_outeq_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
|
||||
g.drawVerticalLine(row2_duck_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
|
||||
|
||||
// -- section --
|
||||
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
|
||||
sl(SEC_TIME, Y_SLABEL1, "TIME");
|
||||
sl(SEC_FREQUENCY, Y_SLABEL1, "FREQUENCY");
|
||||
sl(SEC_DIFFUSION, Y_SLABEL1, "DIFFUSION");
|
||||
sl(SEC_STEREO, Y_SLABEL1, "STEREO");
|
||||
sl(SEC_CHARACTER, Y_SLABEL1, "CHARACTER");
|
||||
sl(PAD, Y_SLABEL2, "MIX");
|
||||
sl(row2_outeq_x, Y_SLABEL2, "OUT EQ");
|
||||
sl(row2_duck_x, Y_SLABEL2, "DUCKING");
|
||||
|
||||
}
|
||||
else {
|
||||
// -- ProMode --
|
||||
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
|
||||
sl(PAD, Y_SLABEL1, "RT60 PER BAND");
|
||||
|
||||
g.setColour(AmbivalenceColors::Separator.withAlpha(0.5f));
|
||||
g.drawHorizontalLine(Y_SLABEL2 - 4, (float)PAD, (float)(W - PAD));
|
||||
|
||||
const int tilt_x = PAD + KNOB_W + PAD + PAD + 8;
|
||||
const int outeq_x = tilt_x + 3 * (KNOB_W + PAD) + 16;
|
||||
g.setColour(AmbivalenceColors::Separator);
|
||||
g.drawVerticalLine(outeq_x - 9, (float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
|
||||
|
||||
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
|
||||
sl(tilt_x, Y_SLABEL2, "TILT EQ");
|
||||
sl(outeq_x, Y_SLABEL2, "OUT EQ");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// preset section (always, mode-independent)
|
||||
// -------------------------------------------------------------------------
|
||||
g.setColour(AmbivalenceColors::Separator);
|
||||
g.drawVerticalLine(PRESET_PANEL_X - 9,
|
||||
(float)Y_SLABEL2, (float)(Y_ROW2 + UNIT_H));
|
||||
g.setColour(AmbivalenceColors::Accent.withAlpha(0.75f));
|
||||
sl(PRESET_PANEL_X, Y_SLABEL2, "PRESET");
|
||||
|
||||
// -- Git build-info status bar divider --
|
||||
g.setColour(AmbivalenceColors::Separator);
|
||||
g.drawHorizontalLine(H - STATUS_H - 1, (float)PAD, (float)(W - PAD));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Preset UI helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
void FDNReverbEditor::refreshPresetCombo()
|
||||
{
|
||||
presetCombo.clear(juce::dontSendNotification);
|
||||
auto names = presetManager->getPresetNames();
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// * fix: restore the preset name saved in the Processor at startup
|
||||
// ---------------------------------------------------------------------
|
||||
// when the editor is closed the PresetManager is destroyed,
|
||||
// so currentPresetName becomes empty.
|
||||
// the name persisted in the Processor as lastSavedPresetName is
|
||||
// re-set in the new PresetManager,
|
||||
// so the combo box selection is restored correctly.
|
||||
// ---------------------------------------------------------------------
|
||||
if (presetManager->getCurrentPresetName().isEmpty()) {
|
||||
auto saved = audioProcessor.getLastSavedPresetName();
|
||||
if (saved.isNotEmpty())
|
||||
presetManager->setCurrentPresetName(saved);
|
||||
}
|
||||
|
||||
if (names.isEmpty()) {
|
||||
presetCombo.addItem("-- No Presets --", 1);
|
||||
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
|
||||
presetDeleteButton.setEnabled(false);
|
||||
presetLoadButton.setEnabled(false);
|
||||
presetPrevButton.setEnabled(false);
|
||||
presetNextButton.setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < names.size(); ++i)
|
||||
presetCombo.addItem(names[i], i + 1);
|
||||
|
||||
int idx = presetManager->getCurrentPresetIndex();
|
||||
if (idx >= 0)
|
||||
presetCombo.setSelectedItemIndex(idx, juce::dontSendNotification);
|
||||
else
|
||||
presetCombo.setSelectedItemIndex(0, juce::dontSendNotification);
|
||||
|
||||
presetDeleteButton.setEnabled(true);
|
||||
presetLoadButton.setEnabled(true);
|
||||
presetPrevButton.setEnabled(names.size() > 1);
|
||||
presetNextButton.setEnabled(names.size() > 1);
|
||||
}
|
||||
|
||||
void FDNReverbEditor::savePresetWithDialog()
|
||||
{
|
||||
// ---------------------------------------------------------------------
|
||||
// AlertWindow preset name input dialog
|
||||
// uses enterModalState(async callback): no runModalLoop() needed
|
||||
// SafePointer handles the case where the editor is destroyed first
|
||||
// ---------------------------------------------------------------------
|
||||
auto* dialog = new juce::AlertWindow(
|
||||
"Save Preset",
|
||||
"Enter a name for this preset:",
|
||||
juce::MessageBoxIconType::NoIcon);
|
||||
|
||||
dialog->addTextEditor("name", presetManager->getCurrentPresetName());
|
||||
dialog->addButton("Save", 1, juce::KeyPress(juce::KeyPress::returnKey));
|
||||
dialog->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey));
|
||||
|
||||
juce::Component::SafePointer<FDNReverbEditor> safeThis(this);
|
||||
|
||||
dialog->enterModalState(
|
||||
true,
|
||||
juce::ModalCallbackFunction::create(
|
||||
[safeThis, dialog](int result) {
|
||||
if (safeThis != nullptr && result == 1) {
|
||||
auto name = dialog->getTextEditorContents("name").trim();
|
||||
if (name.isNotEmpty())
|
||||
safeThis->presetManager->savePreset(name);
|
||||
}
|
||||
}),
|
||||
true // deleteWhenDismissed
|
||||
);
|
||||
}
|
||||
|
||||
void FDNReverbEditor::deleteCurrentPreset()
|
||||
{
|
||||
auto name = presetManager->getCurrentPresetName();
|
||||
if (name.isEmpty()) return;
|
||||
|
||||
auto* dialog = new juce::AlertWindow(
|
||||
"Delete Preset",
|
||||
"Delete \"" + name + "\"?",
|
||||
juce::MessageBoxIconType::WarningIcon);
|
||||
|
||||
dialog->addButton("Delete", 1);
|
||||
dialog->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey));
|
||||
|
||||
juce::Component::SafePointer<FDNReverbEditor> safeThis(this);
|
||||
|
||||
dialog->enterModalState(
|
||||
true,
|
||||
juce::ModalCallbackFunction::create(
|
||||
[safeThis, name](int result) {
|
||||
if (safeThis != nullptr && result == 1)
|
||||
safeThis->presetManager->deletePreset(name);
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
97
Source/PluginEditor.h
Normal file
97
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "PluginProcessor.h"
|
||||
#include "PresetManager.h"
|
||||
#include "GUI/AmbivalenceUI.h"
|
||||
#include "GUI/DecayCurveViz.h"
|
||||
|
||||
class FDNReverbEditor : public juce::AudioProcessorEditor,
|
||||
private juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit FDNReverbEditor(FDNReverbAudioProcessor&);
|
||||
~FDNReverbEditor() override;
|
||||
|
||||
void paint(juce::Graphics&) override;
|
||||
void resized() override;
|
||||
|
||||
private:
|
||||
void timerCallback() override;
|
||||
void updatePanelVisibility();
|
||||
|
||||
// --- Preset UI helpers ---
|
||||
void refreshPresetCombo();
|
||||
void savePresetWithDialog();
|
||||
void deleteCurrentPreset();
|
||||
|
||||
FDNReverbAudioProcessor& audioProcessor;
|
||||
AmbivalenceLookAndFeel laf;
|
||||
|
||||
// --- common ---
|
||||
AlgorithmSelector algoSelector;
|
||||
RT60Visualizer rt60Viz;
|
||||
DecayCurveViz decayCurveViz;
|
||||
VUMeter vuIn, vuOut;
|
||||
juce::Label titleLabel;
|
||||
|
||||
juce::Label labelMetricsTitle;
|
||||
juce::Label labelD50Caption, labelD50Value;
|
||||
juce::Label labelC50Caption, labelC50Value;
|
||||
juce::Label labelC80Caption, labelC80Value;
|
||||
juce::Label labelEDTCaption, labelEDTValue;
|
||||
|
||||
juce::Label statusLabel;
|
||||
|
||||
juce::TextButton proModeButton;
|
||||
juce::TextButton erSoloButton;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> proModeAttachment;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> erSoloAttachment;
|
||||
|
||||
bool isProMode{ false };
|
||||
|
||||
// --- Normal Mode ---
|
||||
ArcKnob kPreDelay, kRoomSize, kDecay;
|
||||
ArcKnob kHFDamp, kLFAbsorb;
|
||||
ArcKnob kDiffusion, kModAmt, kModRate;
|
||||
ArcKnob kStereoW;
|
||||
ArcKnob kERLevel, kSaturation;
|
||||
ArcKnob kWet, kDry;
|
||||
ArcKnob kDuckAmt, kDuckThr, kDuckAtt, kDuckRel;
|
||||
ArcKnob kLoCutNorm, kHiCutNorm;
|
||||
|
||||
// --- ProMode panel ---
|
||||
std::array<ArcKnob, 10> kRTBands;
|
||||
juce::Label satTypeLabel;
|
||||
juce::ComboBox satTypeCombo;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> satTypeAttachment;
|
||||
ArcKnob kTiltLow, kTiltMid, kTiltHigh;
|
||||
ArcKnob kLoCutPro, kHiCutPro;
|
||||
|
||||
// --- preset UI ---
|
||||
std::unique_ptr<PresetManager> presetManager;
|
||||
juce::TextButton presetPrevButton;
|
||||
juce::ComboBox presetCombo;
|
||||
juce::TextButton presetNextButton;
|
||||
// existing presetSaveButton / presetDeleteButton next to added
|
||||
juce::TextButton presetSaveButton;
|
||||
juce::TextButton presetLoadButton; // * added
|
||||
juce::TextButton presetDeleteButton;
|
||||
|
||||
// --- layout constants ---
|
||||
static constexpr int W = 900;
|
||||
static constexpr int H = 540;
|
||||
static constexpr int PAD = 8;
|
||||
static constexpr int KNOB_W = 64;
|
||||
static constexpr int KNOB_H = 72;
|
||||
static constexpr int KNOB_LBL_H = 14;
|
||||
static constexpr int UNIT_H = 88;
|
||||
static constexpr int ROW1_GAP = 18;
|
||||
static constexpr int STATUS_H = 16;
|
||||
|
||||
// Fixed X of the preset panel
|
||||
// DUCKING end (616) + gap(16) = 632
|
||||
static constexpr int PRESET_PANEL_X = 632;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FDNReverbEditor)
|
||||
};
|
||||
87
Source/PluginParameters.cpp
Normal file
87
Source/PluginParameters.cpp
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#include "PluginParameters.h"
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout ParameterHelper::createLayout()
|
||||
{
|
||||
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
|
||||
|
||||
auto addFloat = [&](const juce::String& id,
|
||||
const juce::String& name,
|
||||
float min, float max, float def,
|
||||
float skew = 1.0f,
|
||||
const juce::String& label = "")
|
||||
{
|
||||
params.push_back(std::make_unique<juce::AudioParameterFloat>(
|
||||
id, name,
|
||||
juce::NormalisableRange<float>(min, max, 0.01f, skew),
|
||||
def,
|
||||
juce::AudioParameterFloatAttributes().withLabel(label)));
|
||||
};
|
||||
|
||||
params.push_back(std::make_unique<juce::AudioParameterChoice>(
|
||||
ParamID::Algorithm, "Algorithm",
|
||||
juce::StringArray{ "ROOM1","ROOM2","HALL1","HALL2","PLATE","SPRING","GOLDFOIL" }, 0));
|
||||
|
||||
addFloat(ParamID::PreDelay, "Pre-Delay", 0.0f, 500.0f, 10.0f, 1.0f, "ms");
|
||||
addFloat(ParamID::RoomSize, "Room Size", 0.3f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::DecayTime, "Decay Time", 0.1f, 20.0f, 1.5f, 0.35f, "s");
|
||||
addFloat(ParamID::HFDamping, "HF Damping", 0.0f, 1.0f, 0.0f);
|
||||
addFloat(ParamID::LFAbsorption, "LF Absorption", 0.0f, 1.0f, 0.0f);
|
||||
|
||||
addFloat(ParamID::Diffusion, "Diffusion", 0.0f, 1.0f, 0.7f);
|
||||
addFloat(ParamID::ModAmount, "Mod Amount", 0.0f, 1.0f, 0.25f);
|
||||
addFloat(ParamID::ModRate, "Mod Rate", 0.05f, 2.0f, 0.5f, 1.0f, "Hz");
|
||||
|
||||
addFloat(ParamID::StereoWidth, "Stereo Width", 0.0f, 1.0f, 0.8f);
|
||||
|
||||
addFloat(ParamID::ERLevel, "ER Level", 0.0f, 1.0f, 0.6f);
|
||||
addFloat(ParamID::Saturation, "Saturation", 0.0f, 1.0f, 0.0f);
|
||||
|
||||
params.push_back(std::make_unique<juce::AudioParameterChoice>(
|
||||
ParamID::SatType, "Sat Type",
|
||||
juce::StringArray{ "Warm","Tape","Tube","Hard" },
|
||||
0,
|
||||
juce::AudioParameterChoiceAttributes().withAutomatable(false)));
|
||||
|
||||
// * Step B: Wet -6dB / Dry 0dB change
|
||||
// internal offset -3dB (PluginProcessor.cpp)
|
||||
// effective Wet displayed value -3dB .
|
||||
// Wet=-6dB -> effective -9dB, Wet=0dB -> effective -3dB
|
||||
addFloat(ParamID::WetLevel, "Wet", -60.0f, 0.0f, -4.0f, 1.0f, "dB");
|
||||
addFloat(ParamID::DryLevel, "Dry", -60.0f, 0.0f, 0.0f, 1.0f, "dB");
|
||||
|
||||
addFloat(ParamID::DuckAmount, "Ducking", 0.0f, 20.0f, 0.0f, 1.0f, "dB");
|
||||
addFloat(ParamID::DuckAttack, "Duck Attack", 0.5f, 100.0f, 10.0f, 0.4f, "ms");
|
||||
addFloat(ParamID::DuckRelease, "Duck Release", 10.0f, 2000.0f, 200.0f, 0.4f, "ms");
|
||||
addFloat(ParamID::DuckThresh, "Duck Thresh", -60.0f, 0.0f, -20.0f, 1.0f, "dB");
|
||||
|
||||
params.push_back(std::make_unique<juce::AudioParameterBool>(
|
||||
ParamID::ERSolo, "ER Solo", false,
|
||||
juce::AudioParameterBoolAttributes().withAutomatable(false)));
|
||||
params.push_back(std::make_unique<juce::AudioParameterBool>(
|
||||
ParamID::ProMode, "Pro Mode", false,
|
||||
juce::AudioParameterBoolAttributes().withAutomatable(false)));
|
||||
|
||||
addFloat(ParamID::TiltLow, "Tilt Low", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::TiltMid, "Tilt Mid", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::TiltHigh, "Tilt High", 0.5f, 2.0f, 1.0f);
|
||||
|
||||
addFloat(ParamID::RTBand0, "RT 31Hz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand1, "RT 62Hz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand2, "RT 125Hz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand3, "RT 250Hz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand4, "RT 500Hz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand5, "RT 1kHz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand6, "RT 2kHz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand7, "RT 4kHz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand8, "RT 8kHz", 0.5f, 2.0f, 1.0f);
|
||||
addFloat(ParamID::RTBand9, "RT 16kHz", 0.5f, 2.0f, 1.0f);
|
||||
|
||||
addFloat(ParamID::LoCut, "Lo Cut", 20.0f, 500.0f, 20.0f, 0.3f, "Hz");
|
||||
addFloat(ParamID::HiCut, "Hi Cut", 1000.0f, 20000.0f, 20000.0f, 0.3f, "Hz");
|
||||
|
||||
return { params.begin(), params.end() };
|
||||
}
|
||||
|
||||
} // namespace FDNReverb
|
||||
117
Source/PluginParameters.h
Normal file
117
Source/PluginParameters.h
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include <array>
|
||||
|
||||
namespace FDNReverb {
|
||||
|
||||
namespace ParamID {
|
||||
inline const juce::String Algorithm = "algorithm";
|
||||
inline const juce::String PreDelay = "predelay";
|
||||
inline const juce::String RoomSize = "roomsize";
|
||||
inline const juce::String DecayTime = "decaytime";
|
||||
inline const juce::String HFDamping = "hfdamping";
|
||||
inline const juce::String LFAbsorption = "lfabsorption";
|
||||
inline const juce::String Diffusion = "diffusion";
|
||||
inline const juce::String ModAmount = "modamount";
|
||||
inline const juce::String ModRate = "modrate";
|
||||
inline const juce::String StereoWidth = "stereowidth";
|
||||
inline const juce::String ERLevel = "erlevel";
|
||||
inline const juce::String Saturation = "saturation";
|
||||
inline const juce::String SatType = "sattype";
|
||||
inline const juce::String WetLevel = "wetlevel";
|
||||
inline const juce::String DryLevel = "drylevel";
|
||||
inline const juce::String DuckAmount = "duckamount";
|
||||
inline const juce::String DuckAttack = "duckattack";
|
||||
inline const juce::String DuckRelease = "duckrelease";
|
||||
inline const juce::String DuckThresh = "duckthresh";
|
||||
inline const juce::String ERSolo = "ersolo";
|
||||
inline const juce::String ProMode = "promode";
|
||||
inline const juce::String TiltLow = "tiltlow";
|
||||
inline const juce::String TiltMid = "tiltmid";
|
||||
inline const juce::String TiltHigh = "tilthigh";
|
||||
inline const juce::String RTBand0 = "rtband0";
|
||||
inline const juce::String RTBand1 = "rtband1";
|
||||
inline const juce::String RTBand2 = "rtband2";
|
||||
inline const juce::String RTBand3 = "rtband3";
|
||||
inline const juce::String RTBand4 = "rtband4";
|
||||
inline const juce::String RTBand5 = "rtband5";
|
||||
inline const juce::String RTBand6 = "rtband6";
|
||||
inline const juce::String RTBand7 = "rtband7";
|
||||
inline const juce::String RTBand8 = "rtband8";
|
||||
inline const juce::String RTBand9 = "rtband9";
|
||||
inline const juce::String LoCut = "locut";
|
||||
inline const juce::String HiCut = "hicut";
|
||||
}
|
||||
|
||||
struct DSPParams {
|
||||
int algorithmIndex{ 0 };
|
||||
float decayScale{ 1.0f };
|
||||
float roomSizeScale{ 1.0f };
|
||||
float hfDamping{ 0.0f };
|
||||
float lfAbsorption{ 0.0f };
|
||||
float diffusion{ 0.70f };
|
||||
float preDelayMs{ 10.0f };
|
||||
float modAmount{ 0.25f };
|
||||
float modRate{ 0.5f };
|
||||
float stereoWidth{ 0.80f };
|
||||
float erLevel{ 0.6f };
|
||||
float lateLevel{ 1.0f };
|
||||
// * Step B: Wet -4dB / Dry 0dB
|
||||
float wetDB{ -4.0f };
|
||||
float dryDB{ 0.0f };
|
||||
float saturation{ 0.0f };
|
||||
int satTypeIdx{ 0 };
|
||||
float duckingAmount{ 0.0f };
|
||||
float duckingAttackMs{ 10.0f };
|
||||
float duckingRelMs{ 200.0f };
|
||||
float duckingThreshDB{ -20.0f };
|
||||
bool erSolo{ false };
|
||||
bool proMode{ false };
|
||||
float tiltLow{ 1.0f };
|
||||
float tiltMid{ 1.0f };
|
||||
float tiltHigh{ 1.0f };
|
||||
std::array<float, 10> rtBands{ { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
|
||||
1.0f, 1.0f, 1.0f, 1.0f, 1.0f } };
|
||||
float loCutHz{ 20.0f };
|
||||
float hiCutHz{ 20000.0f };
|
||||
|
||||
bool operator==(const DSPParams& o) const noexcept {
|
||||
return algorithmIndex == o.algorithmIndex
|
||||
&& decayScale == o.decayScale
|
||||
&& roomSizeScale == o.roomSizeScale
|
||||
&& hfDamping == o.hfDamping
|
||||
&& lfAbsorption == o.lfAbsorption
|
||||
&& diffusion == o.diffusion
|
||||
&& preDelayMs == o.preDelayMs
|
||||
&& modAmount == o.modAmount
|
||||
&& modRate == o.modRate
|
||||
&& stereoWidth == o.stereoWidth
|
||||
&& erLevel == o.erLevel
|
||||
&& lateLevel == o.lateLevel
|
||||
&& wetDB == o.wetDB
|
||||
&& dryDB == o.dryDB
|
||||
&& saturation == o.saturation
|
||||
&& satTypeIdx == o.satTypeIdx
|
||||
&& duckingAmount == o.duckingAmount
|
||||
&& duckingAttackMs == o.duckingAttackMs
|
||||
&& duckingRelMs == o.duckingRelMs
|
||||
&& duckingThreshDB == o.duckingThreshDB
|
||||
&& erSolo == o.erSolo
|
||||
&& proMode == o.proMode
|
||||
&& tiltLow == o.tiltLow
|
||||
&& tiltMid == o.tiltMid
|
||||
&& tiltHigh == o.tiltHigh
|
||||
&& rtBands == o.rtBands
|
||||
&& loCutHz == o.loCutHz
|
||||
&& hiCutHz == o.hiCutHz;
|
||||
}
|
||||
bool operator!=(const DSPParams& o) const noexcept { return !(*this == o); }
|
||||
};
|
||||
|
||||
class ParameterHelper {
|
||||
public:
|
||||
static juce::AudioProcessorValueTreeState::ParameterLayout createLayout();
|
||||
};
|
||||
|
||||
} // namespace FDNReverb
|
||||
223
Source/PluginProcessor.cpp
Normal file
223
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
using namespace FDNReverb;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// * Step A: Wet internal offset
|
||||
// -----------------------------------------------------------------------------
|
||||
// the UI shows -60 to 0 dB, but the effective Wet maximum is -3 dB.
|
||||
// reason: at Wet=0 dB, combined with the FDN makeup gain the OutputLimiter
|
||||
// would engage constantly and clip. -3 dB of headroom is required.
|
||||
// implementation: rather than adding -3 dB (= 0.708x) to the APVTS value,
|
||||
// multiply after Decibels::decibelsToGain() - numerically safer.
|
||||
// -----------------------------------------------------------------------------
|
||||
static constexpr float kWetInternalOffsetDB = -1.0f;
|
||||
|
||||
FDNReverbAudioProcessor::FDNReverbAudioProcessor()
|
||||
: AudioProcessor(BusesProperties()
|
||||
.withInput("Input", juce::AudioChannelSet::stereo(), true)
|
||||
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
|
||||
apvts(*this, nullptr, "FDNReverbState", ParameterHelper::createLayout())
|
||||
{
|
||||
}
|
||||
|
||||
void FDNReverbAudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
int osIdx = 0;
|
||||
oversampler = std::make_unique<juce::dsp::Oversampling<float>>(
|
||||
2, osIdx,
|
||||
juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR, true);
|
||||
oversampler->initProcessing(static_cast<size_t>(samplesPerBlock));
|
||||
|
||||
engine.prepare(sampleRate, samplesPerBlock);
|
||||
|
||||
wetBuffer.setSize(2, samplesPerBlock);
|
||||
smoothWetGain.reset(sampleRate, 0.05);
|
||||
smoothDryGain.reset(sampleRate, 0.05);
|
||||
|
||||
lastSampleRate = sampleRate;
|
||||
paramsNeedUpdate = true;
|
||||
}
|
||||
|
||||
void FDNReverbAudioProcessor::updateEngineParams()
|
||||
{
|
||||
int currentAlgo = (int)*apvts.getRawParameterValue(ParamID::Algorithm);
|
||||
if (currentAlgo != lastAlgorithmIndex) {
|
||||
if (lastAlgorithmIndex >= 0)
|
||||
loadPresetDefaults(currentAlgo);
|
||||
lastAlgorithmIndex = currentAlgo;
|
||||
paramsNeedUpdate = true;
|
||||
}
|
||||
|
||||
DSPParams p;
|
||||
p.algorithmIndex = (int)*apvts.getRawParameterValue(ParamID::Algorithm);
|
||||
p.preDelayMs = *apvts.getRawParameterValue(ParamID::PreDelay);
|
||||
p.roomSizeScale = *apvts.getRawParameterValue(ParamID::RoomSize) - 0.5f;
|
||||
|
||||
p.decayScale = *apvts.getRawParameterValue(ParamID::DecayTime)
|
||||
/ ALL_PRESETS[p.algorithmIndex]->acoustics.rt60[4];
|
||||
|
||||
p.hfDamping = *apvts.getRawParameterValue(ParamID::HFDamping);
|
||||
p.lfAbsorption = *apvts.getRawParameterValue(ParamID::LFAbsorption);
|
||||
p.diffusion = *apvts.getRawParameterValue(ParamID::Diffusion);
|
||||
p.modAmount = *apvts.getRawParameterValue(ParamID::ModAmount);
|
||||
p.modRate = *apvts.getRawParameterValue(ParamID::ModRate);
|
||||
p.stereoWidth = *apvts.getRawParameterValue(ParamID::StereoWidth);
|
||||
p.erLevel = *apvts.getRawParameterValue(ParamID::ERLevel);
|
||||
p.saturation = *apvts.getRawParameterValue(ParamID::Saturation);
|
||||
p.wetDB = *apvts.getRawParameterValue(ParamID::WetLevel);
|
||||
p.dryDB = *apvts.getRawParameterValue(ParamID::DryLevel);
|
||||
p.duckingAmount = *apvts.getRawParameterValue(ParamID::DuckAmount);
|
||||
p.duckingAttackMs = *apvts.getRawParameterValue(ParamID::DuckAttack);
|
||||
p.duckingRelMs = *apvts.getRawParameterValue(ParamID::DuckRelease);
|
||||
p.duckingThreshDB = *apvts.getRawParameterValue(ParamID::DuckThresh);
|
||||
p.satTypeIdx = (int)*apvts.getRawParameterValue(ParamID::SatType);
|
||||
p.erSolo = (*apvts.getRawParameterValue(ParamID::ERSolo)) > 0.5f;
|
||||
p.proMode = (*apvts.getRawParameterValue(ParamID::ProMode)) > 0.5f;
|
||||
p.tiltLow = *apvts.getRawParameterValue(ParamID::TiltLow);
|
||||
p.tiltMid = *apvts.getRawParameterValue(ParamID::TiltMid);
|
||||
p.tiltHigh = *apvts.getRawParameterValue(ParamID::TiltHigh);
|
||||
|
||||
p.rtBands[0] = *apvts.getRawParameterValue(ParamID::RTBand0);
|
||||
p.rtBands[1] = *apvts.getRawParameterValue(ParamID::RTBand1);
|
||||
p.rtBands[2] = *apvts.getRawParameterValue(ParamID::RTBand2);
|
||||
p.rtBands[3] = *apvts.getRawParameterValue(ParamID::RTBand3);
|
||||
p.rtBands[4] = *apvts.getRawParameterValue(ParamID::RTBand4);
|
||||
p.rtBands[5] = *apvts.getRawParameterValue(ParamID::RTBand5);
|
||||
p.rtBands[6] = *apvts.getRawParameterValue(ParamID::RTBand6);
|
||||
p.rtBands[7] = *apvts.getRawParameterValue(ParamID::RTBand7);
|
||||
p.rtBands[8] = *apvts.getRawParameterValue(ParamID::RTBand8);
|
||||
p.rtBands[9] = *apvts.getRawParameterValue(ParamID::RTBand9);
|
||||
|
||||
p.loCutHz = *apvts.getRawParameterValue(ParamID::LoCut);
|
||||
p.hiCutHz = *apvts.getRawParameterValue(ParamID::HiCut);
|
||||
|
||||
// * Step A: Wet internal -3dB offset apply
|
||||
// -60~0dB, effective value -63~-3dB .
|
||||
smoothWetGain.setTargetValue(
|
||||
juce::Decibels::decibelsToGain(p.wetDB + kWetInternalOffsetDB));
|
||||
smoothDryGain.setTargetValue(juce::Decibels::decibelsToGain(p.dryDB));
|
||||
|
||||
if (paramsNeedUpdate || p != lastSentParams) {
|
||||
engine.setParams(p);
|
||||
lastSentParams = p;
|
||||
paramsNeedUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
void FDNReverbAudioProcessor::processBlock(
|
||||
juce::AudioBuffer<float>& buffer, juce::MidiBuffer&)
|
||||
{
|
||||
juce::ScopedNoDenormals noDenormals;
|
||||
|
||||
updateEngineParams();
|
||||
|
||||
inputRMS_L.store(buffer.getRMSLevel(0, 0, buffer.getNumSamples()));
|
||||
inputRMS_R.store(buffer.getRMSLevel(1, 0, buffer.getNumSamples()));
|
||||
|
||||
juce::dsp::AudioBlock<float> block(buffer);
|
||||
auto osBlock = oversampler->processSamplesUp(block);
|
||||
int numSamples = static_cast<int>(osBlock.getNumSamples());
|
||||
|
||||
wetBuffer.setSize(2, numSamples, false, false, true);
|
||||
|
||||
engine.processBlock(osBlock.getChannelPointer(0), osBlock.getChannelPointer(1),
|
||||
wetBuffer.getWritePointer(0), wetBuffer.getWritePointer(1),
|
||||
numSamples);
|
||||
|
||||
for (int i = 0; i < numSamples; ++i) {
|
||||
float w = smoothWetGain.getNextValue();
|
||||
float d = smoothDryGain.getNextValue();
|
||||
osBlock.setSample(0, i, osBlock.getSample(0, i) * d
|
||||
+ wetBuffer.getSample(0, i) * w);
|
||||
osBlock.setSample(1, i, osBlock.getSample(1, i) * d
|
||||
+ wetBuffer.getSample(1, i) * w);
|
||||
}
|
||||
|
||||
oversampler->processSamplesDown(block);
|
||||
|
||||
outputRMS_L.store(buffer.getRMSLevel(0, 0, buffer.getNumSamples()));
|
||||
outputRMS_R.store(buffer.getRMSLevel(1, 0, buffer.getNumSamples()));
|
||||
}
|
||||
|
||||
void FDNReverbAudioProcessor::getStateInformation(juce::MemoryBlock& d) {
|
||||
auto state = apvts.copyState();
|
||||
|
||||
// * fix: save the current preset name in the ValueTree
|
||||
// when the editor exists, get the name from the PresetManager
|
||||
// the editor is not held directly by the AudioProcessor,
|
||||
// so a preset-name field managed by the Processor was added.
|
||||
if (lastSavedPresetName.isNotEmpty())
|
||||
state.setProperty("currentPresetName", lastSavedPresetName, nullptr);
|
||||
|
||||
std::unique_ptr<juce::XmlElement> xml(state.createXml());
|
||||
copyXmlToBinary(*xml, d);
|
||||
}
|
||||
|
||||
void FDNReverbAudioProcessor::setStateInformation(const void* d, int s) {
|
||||
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(d, s));
|
||||
if (xml && xml->hasTagName(apvts.state.getType())) {
|
||||
auto tree = juce::ValueTree::fromXml(*xml);
|
||||
|
||||
// * fix : preset name restore
|
||||
lastSavedPresetName = tree.getProperty("currentPresetName", "").toString();
|
||||
|
||||
apvts.replaceState(tree);
|
||||
paramsNeedUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* FDNReverbAudioProcessor::createEditor() {
|
||||
return new FDNReverbEditor(*this);
|
||||
}
|
||||
|
||||
void FDNReverbAudioProcessor::loadPresetDefaults(int algorithmIndex)
|
||||
{
|
||||
if (algorithmIndex < 0 || algorithmIndex >= 7) return;
|
||||
|
||||
const auto& def = PRESET_DEFAULTS[algorithmIndex];
|
||||
|
||||
auto setParam = [this](const juce::String& paramID, float value) {
|
||||
if (auto* param = apvts.getParameter(paramID)) {
|
||||
param->setValueNotifyingHost(param->convertTo0to1(value));
|
||||
}
|
||||
};
|
||||
|
||||
setParam(ParamID::RoomSize, def.roomSize);
|
||||
setParam(ParamID::DecayTime, def.decayTime);
|
||||
|
||||
// * Step A: HF Damping / LF Absorption always 0
|
||||
// AlgorithmPresets.h def.hfDamp / def.lfAbsorb .
|
||||
// reason : after " preset RT60 curve "
|
||||
// correct . intentional correction
|
||||
// before correction .
|
||||
setParam(ParamID::HFDamping, 0.0f);
|
||||
setParam(ParamID::LFAbsorption, 0.0f);
|
||||
|
||||
setParam(ParamID::Diffusion, def.diffusion);
|
||||
setParam(ParamID::ModAmount, def.modAmount);
|
||||
setParam(ParamID::ModRate, def.modRate);
|
||||
setParam(ParamID::ERLevel, def.erLevel);
|
||||
setParam(ParamID::Saturation, def.saturation);
|
||||
|
||||
setParam(ParamID::RTBand0, 1.0f);
|
||||
setParam(ParamID::RTBand1, 1.0f);
|
||||
setParam(ParamID::RTBand2, 1.0f);
|
||||
setParam(ParamID::RTBand3, 1.0f);
|
||||
setParam(ParamID::RTBand4, 1.0f);
|
||||
setParam(ParamID::RTBand5, 1.0f);
|
||||
setParam(ParamID::RTBand6, 1.0f);
|
||||
setParam(ParamID::RTBand7, 1.0f);
|
||||
setParam(ParamID::RTBand8, 1.0f);
|
||||
setParam(ParamID::RTBand9, 1.0f);
|
||||
setParam(ParamID::TiltLow, 1.0f);
|
||||
setParam(ParamID::TiltMid, 1.0f);
|
||||
setParam(ParamID::TiltHigh, 1.0f);
|
||||
|
||||
paramsNeedUpdate = true;
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() {
|
||||
return new FDNReverbAudioProcessor();
|
||||
}
|
||||
91
Source/PluginProcessor.h
Normal file
91
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "DSP/UniversalEngine.h"
|
||||
#include "PluginParameters.h"
|
||||
|
||||
class FDNReverbAudioProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
FDNReverbAudioProcessor();
|
||||
|
||||
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override { engine.reset(); }
|
||||
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||
void processBlockBypassed(juce::AudioBuffer<float>&, juce::MidiBuffer&) override {}
|
||||
|
||||
juce::AudioProcessorEditor* createEditor() override;
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
const juce::String getName() const override { return "Ambivalence1.1"; }
|
||||
double getTailLengthSeconds() const override { return 20.0; }
|
||||
bool acceptsMidi() const override { return false; }
|
||||
bool producesMidi() const override { return false; }
|
||||
bool isMidiEffect() const override { return false; }
|
||||
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram(int) override {}
|
||||
const juce::String getProgramName(int) override { return {}; }
|
||||
void changeProgramName(int, const juce::String&) override {}
|
||||
|
||||
void getStateInformation(juce::MemoryBlock& destData) override;
|
||||
void setStateInformation(const void* data, int sizeInBytes) override;
|
||||
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
|
||||
std::array<float, FDNReverb::NUM_BANDS> getRT60ForDisplay() const noexcept {
|
||||
return engine.getEffectiveRT60();
|
||||
}
|
||||
|
||||
float getInputRMSL() const noexcept { return inputRMS_L.load(); }
|
||||
float getInputRMSR() const noexcept { return inputRMS_R.load(); }
|
||||
float getOutputRMSL() const noexcept { return outputRMS_L.load(); }
|
||||
float getOutputRMSR() const noexcept { return outputRMS_R.load(); }
|
||||
|
||||
float getD50() const noexcept { return engine.getD50(); }
|
||||
float getC50() const noexcept { return engine.getC50(); }
|
||||
float getC80() const noexcept { return engine.getC80(); }
|
||||
float getEDT() const noexcept { return engine.getEDT(); }
|
||||
|
||||
const FDNReverb::UniversalEngine& getEngine() const noexcept { return engine; }
|
||||
|
||||
void loadPresetDefaults(int algorithmIndex);
|
||||
|
||||
private:
|
||||
void updateEngineParams();
|
||||
|
||||
FDNReverb::UniversalEngine engine;
|
||||
|
||||
// --- dirty flag: skip setParams() when no parameter changed ---
|
||||
// processBlock() calls updateEngineParams() every buffer,
|
||||
// but the designStage2() x 16 WLS computation is skipped when nothing changed
|
||||
FDNReverb::DSPParams lastSentParams;
|
||||
bool paramsNeedUpdate{ true }; // the first run
|
||||
|
||||
int lastAlgorithmIndex{ -1 };
|
||||
|
||||
std::unique_ptr<juce::dsp::Oversampling<float>> oversampler;
|
||||
juce::AudioBuffer<float> wetBuffer;
|
||||
juce::SmoothedValue<float> smoothWetGain, smoothDryGain;
|
||||
std::atomic<float> inputRMS_L{ 0.f }, inputRMS_R{ 0.f };
|
||||
std::atomic<float> outputRMS_L{ 0.f }, outputRMS_R{ 0.f };
|
||||
double lastSampleRate{ 0.0 };
|
||||
|
||||
// * added : for session saving preset name
|
||||
// PresetManager editor ,
|
||||
// Processor preset name save support .
|
||||
juce::String lastSavedPresetName;
|
||||
|
||||
public:
|
||||
// editor call preset name Processor notify
|
||||
void setLastSavedPresetName(const juce::String& name) noexcept {
|
||||
lastSavedPresetName = name;
|
||||
}
|
||||
juce::String getLastSavedPresetName() const noexcept {
|
||||
return lastSavedPresetName;
|
||||
}
|
||||
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FDNReverbAudioProcessor)
|
||||
};
|
||||
134
Source/PresetManager.cpp
Normal file
134
Source/PresetManager.cpp
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#include "PresetManager.h"
|
||||
#include "PluginProcessor.h"
|
||||
|
||||
PresetManager::PresetManager(FDNReverbAudioProcessor& p)
|
||||
: processor(p)
|
||||
{
|
||||
refreshPresetList();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// filesystem
|
||||
// -----------------------------------------------------------------------------
|
||||
juce::File PresetManager::getPresetsFolder() const
|
||||
{
|
||||
auto folder = juce::File::getSpecialLocation(
|
||||
juce::File::userDocumentsDirectory)
|
||||
.getChildFile(kSubFolder);
|
||||
if (!folder.exists())
|
||||
folder.createDirectory();
|
||||
return folder;
|
||||
}
|
||||
|
||||
juce::File PresetManager::getPresetFile(const juce::String& name) const
|
||||
{
|
||||
return getPresetsFolder().getChildFile(name + kExtension);
|
||||
}
|
||||
|
||||
void PresetManager::refreshPresetList()
|
||||
{
|
||||
presetNames.clear();
|
||||
auto files = getPresetsFolder().findChildFiles(
|
||||
juce::File::findFiles, false,
|
||||
juce::String("*") + kExtension);
|
||||
files.sort();
|
||||
for (const auto& f : files)
|
||||
presetNames.add(f.getFileNameWithoutExtension());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// save
|
||||
// -----------------------------------------------------------------------------
|
||||
// reuses PluginProcessor::getStateInformation() directly,
|
||||
// so no PluginProcessor changes are needed.
|
||||
// -----------------------------------------------------------------------------
|
||||
// --- after changes ---
|
||||
bool PresetManager::savePreset(const juce::String& name)
|
||||
{
|
||||
if (name.isEmpty()) return false;
|
||||
// * fix: notify the Processor of the name before getStateInformation()
|
||||
// so getStateInformation() writes the correct name into the ValueTree
|
||||
processor.setLastSavedPresetName(name);
|
||||
juce::MemoryBlock data;
|
||||
processor.getStateInformation(data);
|
||||
|
||||
auto file = getPresetFile(name);
|
||||
if (!file.replaceWithData(data.getData(), data.getSize()))
|
||||
return false;
|
||||
|
||||
currentPresetName = name;
|
||||
refreshPresetList();
|
||||
if (onPresetListChanged) onPresetListChanged();
|
||||
if (onPresetLoaded) onPresetLoaded(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// load
|
||||
// -----------------------------------------------------------------------------
|
||||
// reuses PluginProcessor::setStateInformation() directly.
|
||||
// setStateInformation() sets paramsNeedUpdate=true, so
|
||||
// the next processBlock() sends the new parameters to the engine.
|
||||
// -----------------------------------------------------------------------------
|
||||
bool PresetManager::loadPreset(const juce::String& name)
|
||||
{
|
||||
auto file = getPresetFile(name);
|
||||
if (!file.exists()) return false;
|
||||
|
||||
juce::MemoryBlock data;
|
||||
if (!file.loadFileAsData(data)) return false;
|
||||
|
||||
processor.setStateInformation(data.getData(), static_cast<int>(data.getSize()));
|
||||
|
||||
// * after loading a preset always normal view (Normal Mode)
|
||||
if (auto* param = processor.apvts.getParameter("promode"))
|
||||
param->setValueNotifyingHost(0.0f);
|
||||
|
||||
currentPresetName = name;
|
||||
if (onPresetLoaded) onPresetLoaded(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// delete
|
||||
// -----------------------------------------------------------------------------
|
||||
bool PresetManager::deletePreset(const juce::String& name)
|
||||
{
|
||||
auto file = getPresetFile(name);
|
||||
if (!file.exists()) return false;
|
||||
|
||||
if (!file.deleteFile()) return false;
|
||||
|
||||
if (currentPresetName == name)
|
||||
currentPresetName.clear();
|
||||
|
||||
refreshPresetList();
|
||||
if (onPresetListChanged) onPresetListChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// navigation
|
||||
// -----------------------------------------------------------------------------
|
||||
int PresetManager::getCurrentPresetIndex() const noexcept
|
||||
{
|
||||
return presetNames.indexOf(currentPresetName);
|
||||
}
|
||||
|
||||
void PresetManager::loadPrevPreset()
|
||||
{
|
||||
if (presetNames.isEmpty()) return;
|
||||
int idx = getCurrentPresetIndex();
|
||||
if (idx <= 0)
|
||||
idx = presetNames.size();
|
||||
loadPreset(presetNames[idx - 1]);
|
||||
}
|
||||
|
||||
void PresetManager::loadNextPreset()
|
||||
{
|
||||
if (presetNames.isEmpty()) return;
|
||||
int idx = getCurrentPresetIndex();
|
||||
if (idx < 0 || idx >= presetNames.size() - 1)
|
||||
idx = -1;
|
||||
loadPreset(presetNames[idx + 1]);
|
||||
}
|
||||
59
Source/PresetManager.h
Normal file
59
Source/PresetManager.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
#include <JuceHeader.h>
|
||||
|
||||
class FDNReverbAudioProcessor;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// PresetManager
|
||||
// -----------------------------------------------------------------------------
|
||||
// preset file manage .
|
||||
//
|
||||
// save location : ~/Documents/Ambivalence/Presets/*.ambpreset
|
||||
// : APVTS getStateInformation/setStateInformation
|
||||
// -> existing save fully reused , minimizes added code
|
||||
//
|
||||
// real-time safety :
|
||||
// - file I/O all message thread (UI) assumed to be called
|
||||
// - processBlock not involved at all
|
||||
// -----------------------------------------------------------------------------
|
||||
class PresetManager
|
||||
{
|
||||
public:
|
||||
explicit PresetManager(FDNReverbAudioProcessor& processor);
|
||||
|
||||
// --- preset operations ---
|
||||
bool savePreset(const juce::String& name);
|
||||
bool loadPreset(const juce::String& name);
|
||||
bool deletePreset(const juce::String& name);
|
||||
|
||||
// --- navigation ---
|
||||
void loadPrevPreset();
|
||||
void loadNextPreset();
|
||||
|
||||
// --- state access ---
|
||||
juce::StringArray getPresetNames() const noexcept { return presetNames; }
|
||||
// --- after changes ---
|
||||
juce::String getCurrentPresetName() const noexcept { return currentPresetName; }
|
||||
void setCurrentPresetName(const juce::String& name) noexcept { currentPresetName = name; } // * added
|
||||
int getCurrentPresetIndex() const noexcept;
|
||||
|
||||
bool hasPresets() const noexcept { return !presetNames.isEmpty(); }
|
||||
|
||||
// --- folder access ---
|
||||
juce::File getPresetsFolder() const;
|
||||
|
||||
// --- UI update callback ---
|
||||
std::function<void()> onPresetListChanged;
|
||||
std::function<void(const juce::String&)> onPresetLoaded;
|
||||
|
||||
private:
|
||||
void refreshPresetList();
|
||||
juce::File getPresetFile(const juce::String& name) const;
|
||||
|
||||
FDNReverbAudioProcessor& processor;
|
||||
juce::StringArray presetNames;
|
||||
juce::String currentPresetName;
|
||||
|
||||
static constexpr const char* kExtension = ".ambpreset";
|
||||
static constexpr const char* kSubFolder = "Ambivalence/Presets";
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue