Skip to content

Instantly share code, notes, and snippets.

@linux-leo
Created September 12, 2025 21:03
Show Gist options
  • Save linux-leo/f1b56f2d386e29631cc814aeeafebaf8 to your computer and use it in GitHub Desktop.
Save linux-leo/f1b56f2d386e29631cc814aeeafebaf8 to your computer and use it in GitHub Desktop.
LOMM Ladspa Port
// 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