chromaflock/Tools/LevelNormalizer.cpp

330 lines
12 KiB
C++

// LevelNormalizer — offline preset loudness calibration tool.
//
// Renders every factory preset through the real DSP chain (voice + FX) at a
// fixed master gain of 1.0, measures the resulting peak, and computes the
// "masterLevel" value that brings the peak to the target. Used to keep all
// factory presets at a consistent loudness.
//
// ./level_normalizer dry run (prints a table, changes nothing)
// ./level_normalizer --apply rewrites masterLevel in PresetManager.h
//
// Note: matches presets to masterLevel entries by insertion order — every
// preset must contain exactly one "masterLevel" parameter.
#include <juce_core/juce_core.h>
#include <juce_audio_processors/juce_audio_processors.h>
#include "../Source/PresetManager.h"
#include "../Source/DSP/Voice.h"
#include "../Source/DSP/Distortion.h"
#include "../Source/DSP/Compressor.h"
#include "../Source/DSP/Limiter.h"
#include "../Source/DSP/Delay.h"
#include "../Source/DSP/Reverb.h"
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
namespace {
constexpr double sampleRate = 44100.0;
constexpr int blockSize = 512;
constexpr double holdSeconds = 3.0; // typical held-note reference length
constexpr double tailSeconds = 5.0;
constexpr float targetPeak = 0.8f;
constexpr float targetRms = 0.15f;
float param(const Preset& p, const char* id, float def) {
auto it = p.params.find(juce::String(id));
return it != p.params.end() ? it->second : def;
}
float paramInt(const Preset& p, const char* id, int def) {
return static_cast<float>(static_cast<int>(param(p, id, static_cast<float>(def))));
}
struct Result {
float peak = 0.0f;
float rms = 0.0f;
float masterLevel = 0.7f;
};
void renderPreset(const Preset& preset, Result& out) {
juce::Synthesiser synth;
synth.setCurrentPlaybackSampleRate(sampleRate);
synth.addSound(new SubtractiveSound());
synth.addVoice(new SubtractiveVoice());
auto* voice = dynamic_cast<SubtractiveVoice*>(synth.getVoice(0));
voice->setParameters(
static_cast<Waveform>(static_cast<int>(param(preset, "osc1Wave", 1))),
static_cast<Waveform>(static_cast<int>(param(preset, "osc2Wave", 2))),
param(preset, "osc1Oct", 0.0f),
param(preset, "osc2Oct", -1.0f),
param(preset, "osc1Semi", 0.0f),
param(preset, "osc2Semi", 0.0f),
param(preset, "osc1Fine", 0.0f),
param(preset, "osc2Fine", 7.0f),
param(preset, "osc1Level", 0.7f),
param(preset, "osc2Level", 0.7f),
param(preset, "phaseOffset", 0.5f),
param(preset, "filterCutoff", 8000.0f),
param(preset, "filterRes", 0.3f),
static_cast<FilterType>(static_cast<int>(param(preset, "filterType", 0))),
param(preset, "filterEnvAmt", 0.0f),
param(preset, "keyTrack", 0.5f),
param(preset, "envAttack", 0.01f),
param(preset, "envDecay", 0.3f),
param(preset, "envSustain", 0.7f),
param(preset, "envRelease", 0.5f),
param(preset, "fEnvAttack", 0.01f),
param(preset, "fEnvDecay", 0.3f),
param(preset, "fEnvSustain", 0.5f),
param(preset, "fEnvRelease", 0.5f),
param(preset, "pan", 0.0f),
param(preset, "drive", 1.5f),
1.0f, // outputLevel (master gain applied separately, see processBlock)
param(preset, "lfo1Rate", 2.0f),
param(preset, "lfo1Depth", 0.0f),
static_cast<int>(paramInt(preset, "lfo1Shape", 0)),
static_cast<int>(paramInt(preset, "lfo1Dest", 0)),
param(preset, "lfo2Rate", 2.0f),
param(preset, "lfo2Depth", 0.0f),
static_cast<int>(paramInt(preset, "lfo2Shape", 0)),
static_cast<int>(paramInt(preset, "lfo2Dest", 0)));
Distortion distortion;
Compression compression;
Limiter limiter;
Delay delay;
ReverbFX reverb;
distortion.prepare(sampleRate);
compression.prepare(sampleRate);
limiter.prepare(sampleRate);
delay.prepare(sampleRate);
reverb.prepare(sampleRate);
distortion.setParameters(
static_cast<DistortionType>(static_cast<int>(param(preset, "distType", 0))),
param(preset, "distAmount", 0.0f),
param(preset, "distSymmetry", 0.0f),
param(preset, "distTone", 1.0f));
compression.setParameters(
param(preset, "compThreshold", -20.0f),
param(preset, "compRatio", 4.0f),
param(preset, "compAttack", 10.0f),
param(preset, "compRelease", 100.0f),
param(preset, "compMakeup", 0.0f));
limiter.setParameters(1.0f, 0.9f, 50.0f); // plugin defaults (presets don't override)
delay.setParameters(param(preset, "delayTime", 200.0f),
param(preset, "delayFeedback", 0.0f),
param(preset, "delayMix", 0.0f),
param(preset, "delayPingPong", 0.0f) > 0.5f);
reverb.setParameters(param(preset, "reverbSize", 0.5f),
param(preset, "reverbDamping", 0.5f),
0.8f,
param(preset, "reverbMix", 0.0f));
const float autoPanRate = param(preset, "autoPanRate", 2.0f);
const float autoPanDepth = param(preset, "autoPanDepth", 0.0f);
float autoPanPhase = 0.0f;
constexpr float twoPi = 6.2831853f;
juce::AudioBuffer<float> buffer(2, blockSize);
std::vector<float> leftSamples, rightSamples;
const size_t capacity = static_cast<size_t>((holdSeconds + tailSeconds) * sampleRate);
leftSamples.reserve(capacity);
rightSamples.reserve(capacity);
auto renderAndApply = [&](double seconds) {
int total = static_cast<int>(seconds * sampleRate);
int done = 0;
while (done < total) {
const int n = std::min(blockSize, total - done);
buffer.clear();
juce::MidiBuffer midi;
synth.renderNextBlock(buffer, midi, 0, n);
float* l = buffer.getWritePointer(0);
float* r = buffer.getWritePointer(1);
for (int i = 0; i < n; ++i) {
float left = l[i];
float right = r[i];
left = distortion.process(left);
right = distortion.process(right);
left = compression.process(left);
right = compression.process(right);
left = limiter.process(left);
right = limiter.process(right);
if (autoPanDepth > 0.001f) {
const float panLfo = std::sin(autoPanPhase * twoPi);
const float leftGain = 1.0f - autoPanDepth * 0.5f * (1.0f + panLfo);
const float rightGain = 1.0f - autoPanDepth * 0.5f * (1.0f - panLfo);
left *= leftGain;
right *= rightGain;
autoPanPhase += autoPanRate / static_cast<float>(sampleRate);
if (autoPanPhase >= 1.0f) autoPanPhase -= 1.0f;
}
delay.process(left, right);
l[i] = left;
r[i] = right;
}
reverb.process(l, r, n);
for (int i = 0; i < n; ++i) {
leftSamples.push_back(l[i]);
rightSamples.push_back(r[i]);
}
done += n;
}
};
synth.noteOn(1, 60, 1.0f);
renderAndApply(holdSeconds);
synth.noteOff(1, 60, 0.0f, true);
renderAndApply(tailSeconds);
float peak = 0.0f;
double sumSquares = 0.0;
for (size_t i = 0; i < leftSamples.size(); ++i) {
const float mix = (leftSamples[i] + rightSamples[i]) * 0.5f;
peak = std::max(peak, std::abs(mix));
sumSquares += static_cast<double>(mix) * mix;
}
out.peak = peak;
out.rms = static_cast<float>(std::sqrt(sumSquares / static_cast<double>(leftSamples.size())));
// masterGain = masterLevel^2 (perceptual taper in processBlock).
// Pick the more conservative of the peak and RMS targets so loud presets
// are reined in on both fronts and nothing clips.
float masterLevel = 0.7f;
if (peak > 1.0e-4f) {
const float gainToPeak = targetPeak / peak;
const float gainToRms = out.rms > 1.0e-4f ? targetRms / out.rms : 1.0f;
const float gain = std::min(gainToPeak, gainToRms);
masterLevel = std::sqrt(std::min(gain, 1.0f));
}
out.masterLevel = juce::jlimit(0.0f, 1.0f, masterLevel);
}
std::string formatValue(float v) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%.3f", v);
std::string s(buf);
if (s.find('.') != std::string::npos) {
while (!s.empty() && s.back() == '0') s.pop_back();
if (!s.empty() && s.back() == '.') s.pop_back();
}
if (s.empty() || s == "-") return "0.0";
if (s.find('.') == std::string::npos) s += ".0"; // keep a decimal point
return s;
}
bool isNumberChar(char c) {
return (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E';
}
int applyToHeader(const std::vector<Result>& results) {
const char* path = "Source/PresetManager.h";
std::ifstream in(path, std::ios::binary);
if (!in) {
std::fprintf(stderr, "could not open %s\n", path);
return 1;
}
std::stringstream ss;
ss << in.rdbuf();
std::string content = ss.str();
const std::string marker = "\"masterLevel\"";
size_t pos = 0;
size_t idx = 0;
while ((pos = content.find(marker, pos)) != std::string::npos) {
const size_t comma = content.find(',', pos);
if (comma == std::string::npos) break;
size_t numStart = content.find_first_not_of(" \t", comma + 1);
if (numStart == std::string::npos) break;
size_t numEnd = numStart;
while (numEnd < content.size() && isNumberChar(content[numEnd])) ++numEnd;
const bool hadF = numEnd < content.size() && (content[numEnd] == 'f' || content[numEnd] == 'F');
const std::string token = content.substr(numStart, numEnd - numStart);
float oldVal = 0.0f;
try { oldVal = std::stof(token); } catch (...) {}
const float newVal = idx < results.size() ? results[idx].masterLevel : oldVal;
std::string replacement = formatValue(newVal);
if (hadF) replacement += "f";
if (std::abs(oldVal - newVal) > 1.0e-4f) {
std::printf(" %s : %s -> %s\n",
marker.c_str(), token.c_str(), replacement.c_str());
}
const size_t replaceEnd = numEnd + (hadF ? 1 : 0);
content.replace(numStart, replaceEnd - numStart, replacement);
pos = numStart + replacement.size();
++idx;
}
if (idx != results.size()) {
std::fprintf(stderr,
"preset/masterLevel count mismatch (%zu presets, %zu entries) — aborting.\n",
results.size(), idx);
return 1;
}
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (!out) {
std::fprintf(stderr, "could not write %s\n", path);
return 1;
}
out << content;
return 0;
}
} // namespace
int main(int argc, char** argv) {
const bool apply = argc > 1 && std::string(argv[1]) == "--apply";
PresetManager manager;
const auto& presets = manager.getPresets();
std::vector<Result> results;
results.reserve(presets.size());
std::printf("%-22s %8s %8s %8s %8s\n", "PRESET", "PEAK", "RMS", "MASTER", "NEW");
std::printf("%-22s %8s %8s %8s %8s\n", "------", "----", "---", "------", "---");
float minNew = 1.0f, maxNew = 0.0f;
for (const auto& preset : presets) {
Result res;
renderPreset(preset, res);
results.push_back(res);
const float oldMaster = param(preset, "masterLevel", 0.7f);
minNew = std::min(minNew, res.masterLevel);
maxNew = std::max(maxNew, res.masterLevel);
std::printf("%-22s %8.3f %8.3f %8.3f %8.3f\n",
preset.name.toRawUTF8(), res.peak, res.rms, oldMaster, res.masterLevel);
}
std::printf("\n%zu presets measured. new masterLevel range: %.3f .. %.3f (peak %.2f, rms %.2f)\n",
presets.size(), minNew, maxNew, targetPeak, targetRms);
if (apply) {
std::printf("\napplying changes to Source/PresetManager.h:\n");
return applyToHeader(results);
}
std::printf("\nrun with --apply to write the NEW column into PresetManager.h\n");
return 0;
}