Created
September 12, 2025 21:03
-
-
Save linux-leo/f1b56f2d386e29631cc814aeeafebaf8 to your computer and use it in GitHub Desktop.
LOMM Ladspa Port
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// LOMM LADSPA Port (no GUI) - Upward/Downward Multiband Compressor | |
// Derived from the LMMS LOMM plugin by Lost Robot (GPL-2.0-or-later). (more info at https://github.com/LMMS/lmms/pull/6925) | |
// This LADSPA port attempts to match the processing path closely, adapted to LADSPA. | |
// | |
// Build: g++ -O3 -fPIC -shared -std=c++17 -o lomm_ladspa.so lomm_ladspa.cpp (put ladspa.h file version 1.1 in the same directory) | |
// | |
// Install: place lomm_ladspa.so somewhere in your LADSPA_PATH (e.g. ~/.ladspa/) | |
// | |
// Author of this port: ChatGPT (automated port), 2025. | |
// | |
// License: GPL-2.0-or-later (due to derivation from LMMS LOMM, which is GPL-2.0-or-later). | |
#include <cmath> | |
#include <cstring> | |
#include <algorithm> | |
#include <array> | |
#include <vector> | |
#include <limits> | |
#include <string> | |
extern "C" { | |
#include "ladspa.h" | |
} | |
#ifndef M_PI | |
#define M_PI 3.14159265358979323846 | |
#endif | |
// ---------------------- Shared constants (mirroring LOMM) ---------------------- | |
static constexpr float LOMM_MIN_FLOOR = 0.00012589f; // -72 dBFS | |
static constexpr float LOMM_MAX_LOOKAHEAD = 20.f; // ms | |
static constexpr float LOMM_AUTO_TIME_ADJUST = 5.f; | |
// Utilities | |
template<typename T> | |
static inline T my_lerp(const T& a, const T& b, const T& t) { | |
return a + (b - a) * t; | |
} | |
static inline float dbfsToAmp(float db) { | |
return std::pow(10.0f, db / 20.0f); | |
} | |
static inline float ampToDbfs(float a) { | |
return 20.0f * std::log10(std::max(LOMM_MIN_FLOOR, a)); | |
} | |
// ---------------------- Simple Biquad (cookbook) ---------------------- | |
struct Biquad { | |
float a0=1, a1=0, a2=0, b1=0, b2=0; | |
float z1=0, z2=0; | |
inline float process(float x) { | |
// Direct Form I transposed | |
float y = a0*x + z1; | |
z1 = a1*x + z2 - b1*y; | |
z2 = a2*x - b2*y; | |
return y; | |
} | |
void reset() { z1 = z2 = 0; } | |
}; | |
// Calculate normalized biquad coefficients for a Butterworth lowpass/highpass | |
// Based on RBJ cookbook with Q = 1/sqrt(2) for 2nd-order Butterworth stage. | |
static void biquadLowpass(Biquad &bq, float sr, float fc) { | |
fc = std::max(20.0f, std::min(fc, sr*0.45f)); | |
const float Q = 1.0f / std::sqrt(2.0f); | |
const float w0 = 2.0f * M_PI * (fc / sr); | |
const float alpha = std::sin(w0) / (2.0f * Q); | |
const float cosw0 = std::cos(w0); | |
float b0 = (1 - cosw0) * 0.5f; | |
float b1 = (1 - cosw0); | |
float b2 = (1 - cosw0) * 0.5f; | |
float a0 = 1 + alpha; | |
float a1 = -2 * cosw0; | |
float a2 = 1 - alpha; | |
// normalize | |
bq.a0 = b0 / a0; bq.a1 = b1 / a0; bq.a2 = b2 / a0; | |
bq.b1 = a1 / a0; bq.b2 = a2 / a0; | |
} | |
static void biquadHighpass(Biquad &bq, float sr, float fc) { | |
fc = std::max(20.0f, std::min(fc, sr*0.45f)); | |
const float Q = 1.0f / std::sqrt(2.0f); | |
const float w0 = 2.0f * M_PI * (fc / sr); | |
const float alpha = std::sin(w0) / (2.0f * Q); | |
const float cosw0 = std::cos(w0); | |
float b0 = (1 + cosw0) * 0.5f; | |
float b1 = -(1 + cosw0); | |
float b2 = (1 + cosw0) * 0.5f; | |
float a0 = 1 + alpha; | |
float a1 = -2 * cosw0; | |
float a2 = 1 - alpha; | |
// normalize | |
bq.a0 = b0 / a0; bq.a1 = b1 / a0; bq.a2 = b2 / a0; | |
bq.b1 = a1 / a0; bq.b2 = a2 / a0; | |
} | |
// 2nd-order all-pass at frequency fc, Q provided (use ~0.7071) | |
static void biquadAllpass(Biquad &bq, float sr, float fc, float Q) { | |
fc = std::max(20.0f, std::min(fc, sr*0.45f)); | |
const float w0 = 2.0f * M_PI * (fc / sr); | |
const float alpha = std::sin(w0) / (2.0f * Q); | |
const float cosw0 = std::cos(w0); | |
float b0 = 1 - alpha; | |
float b1 = -2 * cosw0; | |
float b2 = 1 + alpha; | |
float a0 = 1 + alpha; | |
float a1 = -2 * cosw0; | |
float a2 = 1 - alpha; | |
// normalize | |
bq.a0 = b0 / a0; bq.a1 = b1 / a0; bq.a2 = b2 / a0; | |
bq.b1 = a1 / a0; bq.b2 = a2 / a0; | |
} | |
// ---------------------- Stereo Linkwitz-Riley (4th order via 2 cascaded 2nd) ---------------------- | |
struct LR4 { | |
float sr = 48000.0f; | |
Biquad s1[2], s2[2]; // cascade stages, per channel (0=L,1=R) | |
enum Type { LOWPASS, HIGHPASS } type = LOWPASS; | |
float fc = 1000.0f; | |
LR4() {} | |
explicit LR4(float sr_) : sr(sr_) {} | |
void setSampleRate(float sr_) { sr = sr_; updateCoeffs(); } | |
void setLowpass(float fc_) { type = LOWPASS; fc = fc_; updateCoeffs(); reset(); } | |
void setHighpass(float fc_) { type = HIGHPASS; fc = fc_; updateCoeffs(); reset(); } | |
void reset() { s1[0].reset(); s1[1].reset(); s2[0].reset(); s2[1].reset(); } | |
inline float process(float x, int ch) { | |
float y = s1[ch].process(x); | |
y = s2[ch].process(y); | |
return y; | |
} | |
void updateCoeffs() { | |
for (int ch=0; ch<2; ++ch) { | |
if (type == LOWPASS) { | |
biquadLowpass(s1[ch], sr, fc); | |
biquadLowpass(s2[ch], sr, fc); | |
} else { | |
biquadHighpass(s1[ch], sr, fc); | |
biquadHighpass(s2[ch], sr, fc); | |
} | |
} | |
} | |
}; | |
// ---------------------- Instance data ---------------------- | |
struct LOMMLADSPA { | |
// Ports | |
const LADSPA_Data* inL = nullptr; | |
const LADSPA_Data* inR = nullptr; | |
LADSPA_Data* outL = nullptr; | |
LADSPA_Data* outR = nullptr; | |
// Controls (mirroring LMMS controls closely) | |
// Global | |
const LADSPA_Data* depth = nullptr; // [0..1] | |
const LADSPA_Data* time = nullptr; // [0..10] | |
const LADSPA_Data* inVol_dB = nullptr; // [-48..48] | |
const LADSPA_Data* outVol_dB = nullptr; // [-48..48] | |
const LADSPA_Data* upward = nullptr; // [0..2] | |
const LADSPA_Data* downward = nullptr; // [0..2] | |
const LADSPA_Data* split1_Hz = nullptr; // [20..20000] | |
const LADSPA_Data* split2_Hz = nullptr; // [20..20000] | |
const LADSPA_Data* split1_enabled = nullptr;// toggle | |
const LADSPA_Data* split2_enabled = nullptr;// toggle | |
const LADSPA_Data* band1_enabled = nullptr; // High | |
const LADSPA_Data* band2_enabled = nullptr; // Mid | |
const LADSPA_Data* band3_enabled = nullptr; // Low | |
const LADSPA_Data* inHigh_dB = nullptr; const LADSPA_Data* inMid_dB = nullptr; const LADSPA_Data* inLow_dB = nullptr; | |
const LADSPA_Data* outHigh_dB = nullptr; const LADSPA_Data* outMid_dB = nullptr; const LADSPA_Data* outLow_dB = nullptr; | |
// Threshold/Ratio per band | |
const LADSPA_Data* aThreshH_dB = nullptr; const LADSPA_Data* aThreshM_dB = nullptr; const LADSPA_Data* aThreshL_dB = nullptr; | |
const LADSPA_Data* aRatioH = nullptr; const LADSPA_Data* aRatioM = nullptr; const LADSPA_Data* aRatioL = nullptr; // "above" ratios (downward) | |
const LADSPA_Data* bThreshH_dB = nullptr; const LADSPA_Data* bThreshM_dB = nullptr; const LADSPA_Data* bThreshL_dB = nullptr; | |
const LADSPA_Data* bRatioH = nullptr; const LADSPA_Data* bRatioM = nullptr; const LADSPA_Data* bRatioL = nullptr; // "below" ratios (upward) | |
// Times per band (ms) | |
const LADSPA_Data* atkH_ms = nullptr; const LADSPA_Data* atkM_ms = nullptr; const LADSPA_Data* atkL_ms = nullptr; | |
const LADSPA_Data* relH_ms = nullptr; const LADSPA_Data* relM_ms = nullptr; const LADSPA_Data* relL_ms = nullptr; | |
const LADSPA_Data* rmsTime_ms = nullptr; // 0..500 (0=peak) | |
const LADSPA_Data* knee_dB = nullptr; // 0..36 (we use half-knee as in LOMM) | |
const LADSPA_Data* range_dB = nullptr; // 0..96 cap for upward gain | |
const LADSPA_Data* balance_dB = nullptr; // -18..18 (bias L/R) | |
const LADSPA_Data* depthScaling = nullptr; // toggle | |
const LADSPA_Data* stereoLink = nullptr; // toggle | |
const LADSPA_Data* autoTime = nullptr; // 0..1 (squared in code) | |
const LADSPA_Data* mix = nullptr; // 0..1 | |
const LADSPA_Data* feedback = nullptr; // toggle | |
const LADSPA_Data* midside = nullptr; // toggle | |
const LADSPA_Data* lookaheadEnable = nullptr; // toggle | |
const LADSPA_Data* lookahead_ms = nullptr; // 0..20 | |
const LADSPA_Data* lowSideUpwardSuppress = nullptr; // toggle (only active in M/S) | |
// Sample rate and coefficients | |
float sr = 48000.0f; | |
float coeffPrecalc = 0.0f; | |
float crestTimeConst = 0.0f; | |
// Crossovers | |
LR4 lp1, lp2, hp1, hp2; | |
Biquad apL, apR; // all-pass for low band (phase align @ split1) | |
// States | |
std::array<std::array<float,2>,3> yL{}; // detector smoothed (per band, per ch) | |
std::array<std::array<float,2>,3> rms{}; // rms accumulators | |
std::array<std::array<float,2>,3> prevOut{}; // for feedback | |
std::array<float,2> crestPeakVal{}; | |
std::array<float,2> crestRmsVal{}; | |
std::array<float,2> crestFactorVal{}; | |
// Lookahead buffers | |
int lookWrite = 0; | |
int lookBufLength = 0; | |
std::array<std::array<std::vector<float>,2>,3> inLookBuf; // per band/ch | |
std::array<std::array<std::vector<float>,2>,3> scLookBuf; // sidechain history per band/ch | |
// Constructor-like init | |
void init(float sample_rate) { | |
sr = sample_rate; | |
coeffPrecalc = -2.2f / (sr * 0.001f); | |
crestTimeConst = std::exp(-1.0f / (0.2f * sr)); | |
lp1.setSampleRate(sr); | |
lp2.setSampleRate(sr); | |
hp1.setSampleRate(sr); | |
hp2.setSampleRate(sr); | |
// Default split freqs | |
lp1.setLowpass(2500.f); | |
hp1.setHighpass(2500.f); | |
lp2.setLowpass(88.3f); | |
hp2.setHighpass(88.3f); | |
biquadAllpass(apL, sr, 2500.f, 0.70710678f); | |
biquadAllpass(apR, sr, 2500.f, 0.70710678f); | |
lookBufLength = (int)std::ceil((LOMM_MAX_LOOKAHEAD / 1000.0f) * sr) + 2; | |
for (int i=0;i<2;++i) { | |
for (int j=0;j<3;++j) { | |
inLookBuf[j][i].assign(lookBufLength, 0.0f); | |
scLookBuf[j][i].assign(lookBufLength, LOMM_MIN_FLOOR); | |
} | |
} | |
lookWrite = 0; | |
for (auto &b : yL) b = {LOMM_MIN_FLOOR, LOMM_MIN_FLOOR}; | |
rms = yL; | |
prevOut = yL; | |
crestPeakVal = {LOMM_MIN_FLOOR, LOMM_MIN_FLOOR}; | |
crestRmsVal = crestPeakVal; | |
crestFactorVal = crestPeakVal; | |
} | |
inline float msToCoeff(float ms) const { | |
return (ms == 0.0f) ? 0.0f : std::exp(coeffPrecalc / ms); | |
} | |
void updateCrossovers() { | |
// Called when split freqs change | |
float s1 = std::max(20.0f, split1_Hz ? *split1_Hz : 2500.0f); | |
float s2 = std::max(20.0f, split2_Hz ? *split2_Hz : 88.3f); | |
lp1.setLowpass(s1); | |
hp1.setHighpass(s1); | |
lp2.setLowpass(s2); | |
hp2.setHighpass(s2); | |
biquadAllpass(apL, sr, s1, 0.70710678f); | |
biquadAllpass(apR, sr, s1, 0.70710678f); | |
} | |
// Process one frame (stereo) | |
inline void processFrame(float in0, float in1, float &out0, float &out1) { | |
auto V = [](const LADSPA_Data* p, float defv)->float { return p ? *p : defv; }; | |
// Read controls locally (hosts set control ports at block boundaries). | |
const float depth_ = std::clamp(V(depth, 0.0f), 0.f, 1.f); | |
const float time_ = std::max(0.f, V(time, 1.0f)); | |
const float inVol = dbfsToAmp(V(inVol_dB, 0.0f)); | |
const float outVol = dbfsToAmp(V(outVol_dB, 0.0f)); | |
const float upward_ = std::max(0.f, V(upward, 1.0f)); | |
const float downward_ = std::max(0.f, V(downward, 1.0f)); | |
const bool split1En = V(split1_enabled,1.0f) > 0.5f; | |
const bool split2En = V(split2_enabled,1.0f) > 0.5f; | |
const bool b1En = V(band1_enabled,1.0f) > 0.5f; | |
const bool b2En = V(band2_enabled,1.0f) > 0.5f; | |
const bool b3En = V(band3_enabled,1.0f) > 0.5f; | |
const float inHigh = dbfsToAmp(V(inHigh_dB, 0.0f)); | |
const float inMid = dbfsToAmp(V(inMid_dB, 0.0f)); | |
const float inLow = dbfsToAmp(V(inLow_dB, 0.0f)); | |
const float outHigh = dbfsToAmp(V(outHigh_dB, 0.0f)); | |
const float outMid = dbfsToAmp(V(outMid_dB, 0.0f)); | |
const float outLow = dbfsToAmp(V(outLow_dB, 0.0f)); | |
float inBandVol[3] = {inHigh, inMid, inLow}; | |
float outBandVol[3] = {outHigh, outMid, outLow}; | |
const float aThresh[3] = {V(aThreshH_dB, -24.0f),V(aThreshM_dB, -24.0f),V(aThreshL_dB, -24.0f)}; | |
const float bThresh[3] = {V(bThreshH_dB, -36.0f),V(bThreshM_dB, -36.0f),V(bThreshL_dB, -36.0f)}; | |
// LOMM inverts ratios as 1/ratio for math | |
const float aRatioInv[3] = { 1.0f/std::max(1e-6f,V(aRatioH, 4.0f)), | |
1.0f/std::max(1e-6f,V(aRatioM, 4.0f)), | |
1.0f/std::max(1e-6f,V(aRatioL, 4.0f)) }; | |
const float bRatioInv[3] = { 1.0f/std::max(1e-6f,V(bRatioH, 2.0f)), | |
1.0f/std::max(1e-6f,V(bRatioM, 2.0f)), | |
1.0f/std::max(1e-6f,V(bRatioL, 2.0f)) }; | |
const float atk_ms[3] = { V(atkH_ms, 10.0f) * time_, V(atkM_ms, 15.0f) * time_, V(atkL_ms, 30.0f) * time_ }; | |
const float rel_ms[3] = { V(relH_ms, 120.0f) * time_, V(relM_ms, 180.0f) * time_, V(relL_ms, 240.0f) * time_ }; | |
float atkCoef[3] = { msToCoeff(atk_ms[0]), msToCoeff(atk_ms[1]), msToCoeff(atk_ms[2]) }; | |
float relCoef[3] = { msToCoeff(rel_ms[0]), msToCoeff(rel_ms[1]), msToCoeff(rel_ms[2]) }; | |
const float rmsTime = std::max(0.f, V(rmsTime_ms, 20.0f)); | |
const float rmsTimeConst = (rmsTime == 0) ? 0.0f : std::exp(-1.0f / (rmsTime * 0.001f * sr)); | |
const float kneeHalf = std::max(0.f, V(knee_dB, 6.0f)) * 0.5f; | |
const float rangeAmp = dbfsToAmp(V(range_dB, 48.0f)); | |
const float balanceAmpTemp = dbfsToAmp(V(balance_dB, 0.0f)); | |
const float balanceAmp[2] = {1.0f / balanceAmpTemp, balanceAmpTemp}; | |
const bool depthScalingEn = V(depthScaling,1.0f) > 0.5f; | |
const bool stereoLinkEn = V(stereoLink,1.0f) > 0.5f; | |
const float autoTime_ = V(autoTime, 0.0f) * V(autoTime, 0.0f); // squared | |
const float mix_ = std::clamp(V(mix, 1.0f), 0.f, 1.f); | |
const bool midsideEn = V(midside,0.0f) > 0.5f; | |
const bool lookaheadEn = V(lookaheadEnable,0.0f) > 0.5f; | |
const int lookaheadSamp = (int)std::ceil((std::max(0.f,V(lookahead_ms, 5.0f)) / 1000.0f) * sr); | |
const bool feedbackEn = (V(feedback,0.0f) > 0.5f) && !lookaheadEn; | |
const bool lowSideUpwardSuppressEn = (V(lowSideUpwardSuppress,0.0f) > 0.5f) && midsideEn; | |
// Convert to M/S if requested. Side channel is made 6 dB louder (x2) in LMMS, | |
// but their math comments say "to bring it into comparable ranges". We'll follow exactly: | |
float s[2] = {in0, in1}; | |
// Global input gain applies to the entire plugin (dry and wet) | |
s[0] *= inVol; | |
s[1] *= inVol; | |
if (midsideEn) { | |
float tempS0 = s[0]; | |
s[0] = (s[0] + s[1]) * 0.5f; // mid | |
s[1] = tempS0 - s[1]; // side (2x of "usual" side) | |
} | |
// Split into 3 bands with linkwitz-riley | |
std::array<std::array<float,2>,3> bands{}; | |
std::array<std::array<float,2>,3> bandsDry{}; | |
for (int i=0;i<2;++i) { | |
// crest factor calc for auto-time | |
float inSq = s[i]*s[i]; | |
crestPeakVal[i] = std::max(std::max(LOMM_MIN_FLOOR, inSq), crestTimeConst*crestPeakVal[i] + (1-crestTimeConst)*(inSq)); | |
crestRmsVal[i] = std::max(LOMM_MIN_FLOOR, crestTimeConst*crestRmsVal[i] + (1-crestTimeConst)*(inSq)); | |
crestFactorVal[i] = crestPeakVal[i] / crestRmsVal[i]; | |
float crestFactorAdj = ((crestFactorVal[i] - LOMM_AUTO_TIME_ADJUST) * autoTime_) + LOMM_AUTO_TIME_ADJUST; | |
// crossover | |
bands[2][i] = lp2.process(s[i], i); // low | |
bands[1][i] = hp2.process(s[i], i); // mid+high | |
bands[0][i] = hp1.process(bands[1][i], i); // high | |
bands[1][i] = lp1.process(bands[1][i], i); // mid | |
// extra allpass on low band to align with hp1/lp1 path | |
float apIn = bands[2][i]; | |
if (i==0) bands[2][i] = apL.process(apIn); | |
else bands[2][i] = apR.process(apIn); | |
if (!split1En) { bands[1][i] += bands[0][i]; bands[0][i] = 0.0f; } | |
if (!split2En) { bands[1][i] += bands[2][i]; bands[2][i] = 0.0f; } | |
// Mute disabled bands | |
bands[0][i] *= b1En ? 1.0f : 0.0f; | |
bands[1][i] *= b2En ? 1.0f : 0.0f; | |
bands[2][i] *= b3En ? 1.0f : 0.0f; | |
float detect[3] = {0,0,0}; | |
for (int j=0;j<3;++j) { | |
bandsDry[j][i] = bands[j][i]; | |
if (feedbackEn && !lookaheadEn) { | |
bands[j][i] = prevOut[j][i]; // feedback uses previous output as the source for detection path | |
} | |
bands[j][i] *= inBandVol[j] * balanceAmp[i]; | |
if (rmsTime > 0.0f) { | |
rms[j][i] = rmsTimeConst * rms[j][i] + ((1 - rmsTimeConst) * (bands[j][i]*bands[j][i])); | |
detect[j] = std::max(LOMM_MIN_FLOOR, std::sqrt(rms[j][i])); | |
} else { | |
detect[j] = std::max(LOMM_MIN_FLOOR, std::fabs(bands[j][i])); | |
} | |
// attack/release smoothing with optional crest factor shortening | |
if (detect[j] > yL[j][i]) { | |
float currentAttack = autoTime_ ? msToCoeff(LOMM_AUTO_TIME_ADJUST * atk_ms[j] / crestFactorAdj) : atkCoef[j]; | |
yL[j][i] = yL[j][i] * currentAttack + (1 - currentAttack) * detect[j]; | |
} else { | |
float currentRelease = autoTime_ ? msToCoeff(LOMM_AUTO_TIME_ADJUST * rel_ms[j] / crestFactorAdj) : relCoef[j]; | |
yL[j][i] = yL[j][i] * currentRelease + (1 - currentRelease) * detect[j]; | |
} | |
yL[j][i] = std::max(LOMM_MIN_FLOOR, yL[j][i]); | |
float yAmp = yL[j][i]; | |
if (lookaheadEn) { | |
float tmp = yAmp; | |
// pick the larger of current and delayed sidechain | |
float delayed = scLookBuf[j][i][lookWrite]; | |
float delayed2 = scLookBuf[j][i][ (lookWrite + lookBufLength - lookaheadSamp) % lookBufLength ]; | |
yAmp = std::max(delayed, delayed2); | |
scLookBuf[j][i][lookWrite] = tmp; | |
} | |
float yDbfs = ampToDbfs(yAmp); | |
float aboveGain = 0.0f; | |
float belowGain = 0.0f; | |
// Downward compression (above threshold aThresh) with soft knee | |
if (yDbfs - aThresh[j] < -kneeHalf) { | |
aboveGain = yDbfs; // below knee region: no change | |
} else if (yDbfs - aThresh[j] < kneeHalf) { | |
float t = yDbfs - aThresh[j] + kneeHalf; | |
aboveGain = yDbfs + (aRatioInv[j] - 1.0f) * t * t / (4.0f * kneeHalf); | |
} else { | |
aboveGain = aThresh[j] + (yDbfs - aThresh[j]) * aRatioInv[j]; | |
} | |
if (aboveGain < yDbfs) { | |
if (downward_ * depth_ <= 1.0f) { | |
aboveGain = my_lerp(yDbfs, aboveGain, downward_ * depth_); | |
} else { | |
aboveGain = my_lerp(aboveGain, aThresh[j], downward_ * depth_ - 1.0f); | |
} | |
} | |
// Upward compression (below threshold bThresh) with soft knee | |
if (yDbfs - bThresh[j] > kneeHalf) { | |
belowGain = yDbfs; // above knee region: no change | |
} else if (bThresh[j] - yDbfs < kneeHalf) { | |
float t = bThresh[j] - yDbfs + kneeHalf; | |
belowGain = yDbfs + (1.0f - bRatioInv[j]) * t * t / (4.0f * kneeHalf); | |
} else { | |
belowGain = bThresh[j] + (yDbfs - bThresh[j]) * bRatioInv[j]; | |
} | |
if (belowGain > yDbfs) { | |
if (upward_ * depth_ <= 1.0f) { | |
belowGain = my_lerp(yDbfs, belowGain, upward_ * depth_); | |
} else { | |
belowGain = my_lerp(belowGain, bThresh[j], upward_ * depth_ - 1.0f); | |
} | |
} | |
float gain = (dbfsToAmp(aboveGain) / yAmp) * (dbfsToAmp(belowGain) / yAmp); | |
if (lowSideUpwardSuppressEn && gain > 1.0f && j == 2 && i == 1) { | |
gain = 1.0f; | |
} | |
gain = std::min(gain, rangeAmp); | |
// stereo link: apply min(gainL, gainR) | |
// We'll stage this by writing into prevOut for later output application. | |
// To reproduce LMMS: we must compute gains for both channels, then if link, equalize. | |
// Here we store the computed gain into prevOut as a temporary; we'll fix after both channels run. | |
prevOut[j][i] = gain; // temporarily store gain | |
} | |
} | |
// Stereo link adjust (per band): use minimum gain of both channels | |
if (stereoLinkEn) { | |
for (int j=0;j<3;++j) { | |
float gL = prevOut[j][0], gR = prevOut[j][1]; | |
float gMin = std::min(gL, gR); | |
prevOut[j][0] = gMin; | |
prevOut[j][1] = gMin; | |
} | |
} | |
// Now apply gain, lookahead input delay, feedback, out gains and mix | |
for (int i=0;i<2;++i) { | |
for (int j=0;j<3;++j) { | |
float gain = prevOut[j][i]; // now the true gain (after st-link step above) | |
if (lookaheadEn) { | |
float temp = bands[j][i]; | |
bands[j][i] = inLookBuf[j][i][lookWrite]; | |
inLookBuf[j][i][lookWrite] = temp; | |
bandsDry[j][i] = bands[j][i]; | |
} else if (feedbackEn) { | |
// when not using lookahead, the earlier detection path used prevOut (before overwrite) as 'feedback source'. | |
// To keep clean, re-apply input + balance here: | |
bands[j][i] = bandsDry[j][i] * inBandVol[j] * balanceAmp[i]; | |
} | |
// Apply gain reduction | |
bands[j][i] *= gain; | |
// Store band output (for next frame's feedback detection) | |
// NB: In LMMS they store band output *before* outBandVol and mix; we mimic that: | |
float storeVal = bands[j][i]; | |
// apply band output volume | |
bands[j][i] *= outBandVol[j]; | |
// mix per band | |
bands[j][i] = my_lerp(bandsDry[j][i], bands[j][i], mix_); | |
// Save for feedback's "use output as sidechain input" | |
prevOut[j][i] = storeVal; | |
} | |
} | |
float o[2]; | |
o[0] = bands[0][0] + bands[1][0] + bands[2][0]; | |
o[1] = bands[0][1] + bands[1][1] + bands[2][1]; | |
// scale output volume with depth if enabled (as in LMMS: outVol scaled by mix*(depthScaling?depth:1)) | |
float outVolEff = depthScalingEn ? my_lerp(1.0f, outVol, depth_) : outVol; | |
o[0] *= outVolEff; | |
o[1] *= outVolEff; | |
// Convert back to L/R if midside | |
if (midsideEn) { | |
float tempS0 = o[0]; | |
o[0] = o[0] + (o[1] * 0.5f); | |
o[1] = tempS0 - (o[1] * 0.5f); | |
} | |
// Advance lookahead buffer pointer | |
if (--lookWrite < 0) lookWrite = lookBufLength - 1; | |
out0 = o[0]; | |
out1 = o[1]; | |
} | |
}; | |
// ---------------------- LADSPA wiring ---------------------- | |
// Enumerate ports | |
enum Ports { | |
// audio | |
PORT_IN_L, PORT_IN_R, PORT_OUT_L, PORT_OUT_R, | |
// global controls (order chosen to mirror LMMS LOMMControls as much as possible) | |
PORT_DEPTH, PORT_TIME, PORT_IN_VOL, PORT_OUT_VOL, | |
PORT_UPWARD, PORT_DOWNWARD, | |
PORT_SPLIT1, PORT_SPLIT2, PORT_SPLIT1_EN, PORT_SPLIT2_EN, | |
PORT_BAND1_EN, PORT_BAND2_EN, PORT_BAND3_EN, | |
PORT_IN_HIGH, PORT_IN_MID, PORT_IN_LOW, | |
PORT_OUT_HIGH, PORT_OUT_MID, PORT_OUT_LOW, | |
PORT_ATHRESH_H, PORT_ATHRESH_M, PORT_ATHRESH_L, | |
PORT_ARATIO_H, PORT_ARATIO_M, PORT_ARATIO_L, | |
PORT_BTHRESH_H, PORT_BTHRESH_M, PORT_BTHRESH_L, | |
PORT_BRATIO_H, PORT_BRATIO_M, PORT_BRATIO_L, | |
PORT_ATK_H, PORT_ATK_M, PORT_ATK_L, | |
PORT_REL_H, PORT_REL_M, PORT_REL_L, | |
PORT_RMS_MS, PORT_KNEE_DB, PORT_RANGE_DB, | |
PORT_BALANCE_DB, | |
PORT_DEPTH_SCALING, PORT_STEREO_LINK, | |
PORT_AUTO_TIME, PORT_MIX, | |
PORT_FEEDBACK, PORT_MIDSIDE, | |
PORT_LOOKAHEAD_EN, PORT_LOOKAHEAD_MS, | |
PORT_LOWSIDE_UPSUPPRESS, | |
PORT_COUNT | |
}; | |
static LADSPA_Descriptor g_desc; | |
// Instantiate | |
static LADSPA_Handle instantiate(const LADSPA_Descriptor *desc, unsigned long s_rate) { | |
(void)desc; | |
LOMMLADSPA *p = new(std::nothrow) LOMMLADSPA(); | |
if (!p) return nullptr; | |
p->init((float)s_rate); | |
return p; | |
} | |
// Connect port | |
static void connect_port(LADSPA_Handle inst, unsigned long port, LADSPA_Data *data) { | |
LOMMLADSPA *p = (LOMMLADSPA*)inst; | |
switch (port) { | |
case PORT_IN_L: p->inL = data; break; | |
case PORT_IN_R: p->inR = data; break; | |
case PORT_OUT_L: p->outL = data; break; | |
case PORT_OUT_R: p->outR = data; break; | |
case PORT_DEPTH: p->depth = data; break; | |
case PORT_TIME: p->time = data; break; | |
case PORT_IN_VOL: p->inVol_dB = data; break; | |
case PORT_OUT_VOL: p->outVol_dB = data; break; | |
case PORT_UPWARD: p->upward = data; break; | |
case PORT_DOWNWARD: p->downward = data; break; | |
case PORT_SPLIT1: p->split1_Hz = data; p->updateCrossovers(); break; | |
case PORT_SPLIT2: p->split2_Hz = data; p->updateCrossovers(); break; | |
case PORT_SPLIT1_EN: p->split1_enabled = data; break; | |
case PORT_SPLIT2_EN: p->split2_enabled = data; break; | |
case PORT_BAND1_EN: p->band1_enabled = data; break; | |
case PORT_BAND2_EN: p->band2_enabled = data; break; | |
case PORT_BAND3_EN: p->band3_enabled = data; break; | |
case PORT_IN_HIGH: p->inHigh_dB = data; break; | |
case PORT_IN_MID: p->inMid_dB = data; break; | |
case PORT_IN_LOW: p->inLow_dB = data; break; | |
case PORT_OUT_HIGH: p->outHigh_dB = data; break; | |
case PORT_OUT_MID: p->outMid_dB = data; break; | |
case PORT_OUT_LOW: p->outLow_dB = data; break; | |
case PORT_ATHRESH_H: p->aThreshH_dB = data; break; | |
case PORT_ATHRESH_M: p->aThreshM_dB = data; break; | |
case PORT_ATHRESH_L: p->aThreshL_dB = data; break; | |
case PORT_ARATIO_H: p->aRatioH = data; break; | |
case PORT_ARATIO_M: p->aRatioM = data; break; | |
case PORT_ARATIO_L: p->aRatioL = data; break; | |
case PORT_BTHRESH_H: p->bThreshH_dB = data; break; | |
case PORT_BTHRESH_M: p->bThreshM_dB = data; break; | |
case PORT_BTHRESH_L: p->bThreshL_dB = data; break; | |
case PORT_BRATIO_H: p->bRatioH = data; break; | |
case PORT_BRATIO_M: p->bRatioM = data; break; | |
case PORT_BRATIO_L: p->bRatioL = data; break; | |
case PORT_ATK_H: p->atkH_ms = data; break; | |
case PORT_ATK_M: p->atkM_ms = data; break; | |
case PORT_ATK_L: p->atkL_ms = data; break; | |
case PORT_REL_H: p->relH_ms = data; break; | |
case PORT_REL_M: p->relM_ms = data; break; | |
case PORT_REL_L: p->relL_ms = data; break; | |
case PORT_RMS_MS: p->rmsTime_ms = data; break; | |
case PORT_KNEE_DB: p->knee_dB = data; break; | |
case PORT_RANGE_DB: p->range_dB = data; break; | |
case PORT_BALANCE_DB: p->balance_dB = data; break; | |
case PORT_DEPTH_SCALING: p->depthScaling = data; break; | |
case PORT_STEREO_LINK: p->stereoLink = data; break; | |
case PORT_AUTO_TIME: p->autoTime = data; break; | |
case PORT_MIX: p->mix = data; break; | |
case PORT_FEEDBACK: p->feedback = data; break; | |
case PORT_MIDSIDE: p->midside = data; break; | |
case PORT_LOOKAHEAD_EN: p->lookaheadEnable = data; break; | |
case PORT_LOOKAHEAD_MS: p->lookahead_ms = data; break; | |
case PORT_LOWSIDE_UPSUPPRESS: p->lowSideUpwardSuppress = data; break; | |
default: break; | |
} | |
} | |
// Activate (initialize state to safe defaults) | |
static void activate(LADSPA_Handle inst) { | |
LOMMLADSPA *p = (LOMMLADSPA*)inst; | |
p->init(p->sr); // reset state based on current sample rate | |
} | |
// Run | |
static void run(LADSPA_Handle inst, unsigned long nframes) { | |
LOMMLADSPA *p = (LOMMLADSPA*)inst; | |
if (!p->inL || !p->inR || !p->outL || !p->outR) return; | |
for (unsigned long i=0;i<nframes;++i) { | |
float oL, oR; | |
p->processFrame(p->inL[i], p->inR[i], oL, oR); | |
p->outL[i] = oL; | |
p->outR[i] = oR; | |
} | |
} | |
// Deactivate | |
static void deactivate(LADSPA_Handle inst) { | |
(void)inst; | |
} | |
// Cleanup | |
static void cleanup(LADSPA_Handle inst) { | |
LOMMLADSPA *p = (LOMMLADSPA*)inst; | |
delete p; | |
} | |
// Port descriptors and hints | |
static void init_descriptor() { | |
// Allocate and populate descriptor | |
g_desc.UniqueID = 19191101; // arbitrary; ensure unique in your system | |
g_desc.Label = "lomm_mb_comp"; | |
g_desc.Properties = LADSPA_PROPERTY_HARD_RT_CAPABLE; // we avoid heap in run() | |
g_desc.Name = "LOMM Multiband Compressor (Up/Down)"; | |
g_desc.Maker = "Lost Robot (LOMM) / LADSPA port by ChatGPT"; | |
g_desc.Copyright = "GPL-2.0-or-later"; | |
g_desc.PortCount = PORT_COUNT; | |
LADSPA_PortDescriptor *pd = (LADSPA_PortDescriptor*)std::calloc(PORT_COUNT, sizeof(LADSPA_PortDescriptor)); | |
LADSPA_PortRangeHint *ph = (LADSPA_PortRangeHint*)std::calloc(PORT_COUNT, sizeof(LADSPA_PortRangeHint)); | |
const char **pn = (const char**)std::calloc(PORT_COUNT, sizeof(char*)); | |
g_desc.PortDescriptors = pd; | |
g_desc.PortRangeHints = ph; | |
g_desc.PortNames = pn; | |
auto AIN = LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO; | |
auto AOUT = LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO; | |
auto CIN = LADSPA_PORT_INPUT | LADSPA_PORT_CONTROL; | |
auto setPort = [&](unsigned long idx, LADSPA_PortDescriptor d, const char* name, | |
LADSPA_PortRangeHintDescriptor hint, float lo, float hi) { | |
pd[idx] = d; | |
pn[idx] = name; | |
ph[idx].HintDescriptor = hint; | |
ph[idx].LowerBound = lo; | |
ph[idx].UpperBound = hi; | |
}; | |
// Audio | |
setPort(PORT_IN_L, AIN, "Input L", 0, 0,0); | |
setPort(PORT_IN_R, AIN, "Input R", 0, 0,0); | |
setPort(PORT_OUT_L, AOUT, "Output L",0, 0,0); | |
setPort(PORT_OUT_R, AOUT, "Output R",0, 0,0); | |
// Controls with reasonable ranges matching LMMS models | |
setPort(PORT_DEPTH, CIN, "Depth", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_LOW, 0, 1); | |
setPort(PORT_TIME, CIN, "Time", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_1, 0, 10); | |
setPort(PORT_IN_VOL, CIN, "Input Gain (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_OUT_VOL, CIN, "Output Gain (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_UPWARD, CIN, "Upward Depth", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, 0, 2); | |
setPort(PORT_DOWNWARD, CIN, "Downward Depth", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, 0, 2); | |
setPort(PORT_SPLIT1, CIN, "Split1 High/Mid (Hz)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 20, 20000); | |
setPort(PORT_SPLIT2, CIN, "Split2 Mid/Low (Hz)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 20, 20000); | |
setPort(PORT_SPLIT1_EN, CIN, "Split1 Enable", LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1, 0, 1); | |
setPort(PORT_SPLIT2_EN, CIN, "Split2 Enable", LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1, 0, 1); | |
setPort(PORT_BAND1_EN, CIN, "High Band Enable", LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1, 0, 1); | |
setPort(PORT_BAND2_EN, CIN, "Mid Band Enable", LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1, 0, 1); | |
setPort(PORT_BAND3_EN, CIN, "Low Band Enable", LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1, 0, 1); | |
setPort(PORT_IN_HIGH, CIN, "High In (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_IN_MID, CIN, "Mid In (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_IN_LOW, CIN, "Low In (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_OUT_HIGH, CIN, "High Out (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_OUT_MID, CIN, "Mid Out (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_OUT_LOW, CIN, "Low Out (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -48, 48); | |
setPort(PORT_ATHRESH_H, CIN, "Above Thresh High (dBFS)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -72, 0); | |
setPort(PORT_ATHRESH_M, CIN, "Above Thresh Mid (dBFS)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -72, 0); | |
setPort(PORT_ATHRESH_L, CIN, "Above Thresh Low (dBFS)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -72, 0); | |
setPort(PORT_ARATIO_H, CIN, "Above Ratio High", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 1, 99.99); | |
setPort(PORT_ARATIO_M, CIN, "Above Ratio Mid", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 1, 99.99); | |
setPort(PORT_ARATIO_L, CIN, "Above Ratio Low", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 1, 99.99); | |
setPort(PORT_BTHRESH_H, CIN, "Below Thresh High (dBFS)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -72, 0); | |
setPort(PORT_BTHRESH_M, CIN, "Below Thresh Mid (dBFS)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -72, 0); | |
setPort(PORT_BTHRESH_L, CIN, "Below Thresh Low (dBFS)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -72, 0); | |
setPort(PORT_BRATIO_H, CIN, "Below Ratio High", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 1, 99.99); | |
setPort(PORT_BRATIO_M, CIN, "Below Ratio Mid", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 1, 99.99); | |
setPort(PORT_BRATIO_L, CIN, "Below Ratio Low", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_LOGARITHMIC, 1, 99.99); | |
setPort(PORT_ATK_H, CIN, "Attack High (ms)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_LOW, 0, 1000); | |
setPort(PORT_ATK_M, CIN, "Attack Mid (ms)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_LOW, 0, 1000); | |
setPort(PORT_ATK_L, CIN, "Attack Low (ms)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_LOW, 0, 1000); | |
setPort(PORT_REL_H, CIN, "Release High (ms)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_HIGH, 0, 1000); | |
setPort(PORT_REL_M, CIN, "Release Mid (ms)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_HIGH, 0, 1000); | |
setPort(PORT_REL_L, CIN, "Release Low (ms)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_HIGH, 0, 1000); | |
setPort(PORT_RMS_MS, CIN, "RMS Window (ms, 0=Peak)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, 0, 500); | |
setPort(PORT_KNEE_DB, CIN, "Knee (dB)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, 0, 36); | |
setPort(PORT_RANGE_DB, CIN, "Range cap (dB up)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, 0, 96); | |
setPort(PORT_BALANCE_DB, CIN, "Balance (dB, L<->R)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, -18, 18); | |
setPort(PORT_DEPTH_SCALING, CIN, "Depth Link (scale output gain)", LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1, 0, 1); | |
setPort(PORT_STEREO_LINK, CIN, "Stereo Link", LADSPA_HINT_TOGGLED, 0, 1); | |
setPort(PORT_AUTO_TIME, CIN, "Auto Time", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, 0, 1); | |
setPort(PORT_MIX, CIN, "Mix (per-band)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_1, 0, 1); | |
setPort(PORT_FEEDBACK, CIN, "Feedback", LADSPA_HINT_TOGGLED, 0, 1); | |
setPort(PORT_MIDSIDE, CIN, "Mid/Side", LADSPA_HINT_TOGGLED, 0, 1); | |
setPort(PORT_LOOKAHEAD_EN, CIN, "Lookahead Enable", LADSPA_HINT_TOGGLED, 0, 1); | |
setPort(PORT_LOOKAHEAD_MS, CIN, "Lookahead (ms)", LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE, 0, LOMM_MAX_LOOKAHEAD); | |
setPort(PORT_LOWSIDE_UPSUPPRESS, CIN, "Low Side Upward Suppress", LADSPA_HINT_TOGGLED, 0, 1); | |
g_desc.instantiate = instantiate; | |
g_desc.connect_port = connect_port; | |
g_desc.activate = activate; | |
g_desc.run = run; | |
g_desc.deactivate = deactivate; | |
g_desc.cleanup = cleanup; | |
g_desc.run_adding = nullptr; | |
g_desc.set_run_adding_gain = nullptr;} | |
// LADSPA entry point | |
const LADSPA_Descriptor* ladspa_descriptor(unsigned long idx) { | |
if (idx != 0) return nullptr; | |
static bool inited = false; | |
if (!inited) { std::memset(&g_desc, 0, sizeof(g_desc)); init_descriptor(); inited = true; } | |
return &g_desc; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment