| 231 | } |
| 232 | |
| 233 | void ClientComp::process(float* interleaved, int frames, int channels) noexcept |
| 234 | { |
| 235 | if (frames <= 0) return; |
| 236 | if (channels != 1 && channels != 2) return; |
| 237 | |
| 238 | recacheIfDirty(); |
| 239 | const bool enabled = m_atomics.enabled.load(std::memory_order_acquire); |
| 240 | |
| 241 | // Pre-comp PAPR shaping (#2887). The rotator runs first to |
| 242 | // symmetrize asymmetric voice peaks; the drive gain then pushes |
| 243 | // more material across the threshold so the existing comp curve |
| 244 | // engages harder and the brickwall limiter below contains the |
| 245 | // resulting hot peaks. These run independent of the comp's enabled |
| 246 | // flag — they're a useful pair even when the comp curve itself is |
| 247 | // bypassed (Drive + Limiter alone is the simplest broadcast PAPR |
| 248 | // setup). Their work happens *before* the input peak meter so the |
| 249 | // GR readout reflects the comp's workload at the boosted level. |
| 250 | if (m_cached.phaseRotatorStages > 0 && m_phaseRotator) { |
| 251 | m_phaseRotator->process(interleaved, frames, channels); |
| 252 | } |
| 253 | if (m_cached.driveLin != 1.0f) { |
| 254 | const float gain = m_cached.driveLin; |
| 255 | const int n = frames * channels; |
| 256 | for (int i = 0; i < n; ++i) interleaved[i] *= gain; |
| 257 | } |
| 258 | |
| 259 | float inPeakLin = 0.0f; |
| 260 | float outPeakLin = 0.0f; |
| 261 | float worstGrDb = 0.0f; // most negative (largest reduction) |
| 262 | float worstLimGrDb = 0.0f; // limiter-only GR, most negative |
| 263 | bool limFired = false; |
| 264 | |
| 265 | const float attackCoeff = m_cached.attackCoeff; |
| 266 | const float releaseCoeff = m_cached.releaseCoeff; |
| 267 | const float makeup = m_cached.makeupLin; |
| 268 | const float limCeiling = m_cached.limCeilingLin; |
| 269 | const float limAttack = m_cached.limAttackCoeff; |
| 270 | const float limRelease = m_cached.limReleaseCoeff; |
| 271 | |
| 272 | for (int f = 0; f < frames; ++f) { |
| 273 | float l = interleaved[f * channels]; |
| 274 | float r = (channels == 2) ? interleaved[f * channels + 1] : l; |
| 275 | |
| 276 | const float inAbs = std::max(std::fabs(l), std::fabs(r)); |
| 277 | if (inAbs > inPeakLin) inPeakLin = inAbs; |
| 278 | |
| 279 | // Compressor gain (only applied if enabled). Linear-domain peak |
| 280 | // envelope: smoothed |x| with asymmetric attack/release, converted |
| 281 | // to dB once to feed the static curve. This tracks the actual |
| 282 | // peak amplitude of the signal (unlike a dB-domain filter that |
| 283 | // would average log|x| and read ~4 dB below peak on a sine). |
| 284 | float gainLin = 1.0f; |
| 285 | if (enabled) { |
| 286 | const float alpha = (inAbs > m_envLin) ? attackCoeff : releaseCoeff; |
| 287 | m_envLin += alpha * (inAbs - m_envLin); |
| 288 | const float envDb = linToDb(std::max(m_envLin, 1e-6f)); |
| 289 | const float gainDb = staticCurveGainDb(envDb); |
| 290 | if (gainDb < worstGrDb) worstGrDb = gainDb; |