|
| 1 | +// This file is part of Noteahead. |
| 2 | +// Copyright (C) 2026 Jussi Lind <jussi.lind@iki.fi> |
| 3 | +// |
| 4 | +// Noteahead is free software: you can redistribute it and/or modify |
| 5 | +// it under the terms of the GNU General Public License as published by |
| 6 | +// the Free Software Foundation, either version 3 of the License, or |
| 7 | +// (at your option) any later version. |
| 8 | +// Noteahead is distributed in the hope that it will be useful, |
| 9 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | +// GNU General Public License for more details. |
| 12 | +// |
| 13 | +// You should have received a copy of the GNU General Public License |
| 14 | +// along with Noteahead. If not, see <http://www.gnu.org/licenses/>. |
| 15 | + |
| 16 | +#include "limiter.hpp" |
| 17 | +#include "../dsp/audio_context.hpp" |
| 18 | + |
| 19 | +#include "../../common/constants.hpp" |
| 20 | +#include "../../common/parameter_mapper.hpp" |
| 21 | +#include "../../common/utils.hpp" |
| 22 | + |
| 23 | +#include <algorithm> |
| 24 | +#include <cmath> |
| 25 | + |
| 26 | +namespace noteahead { |
| 27 | + |
| 28 | +Limiter::Limiter() |
| 29 | +{ |
| 30 | + addParameter(Parameter { Constants::NahdXml::xmlKeyThreshold().toStdString(), 1.0f, -2400, 0, 0, 100 }); |
| 31 | + addParameter(Parameter { Constants::NahdXml::xmlKeyCeiling().toStdString(), 0.9f, -300, 0, -30, 100 }); |
| 32 | + addParameter(Parameter { Constants::NahdXml::xmlKeyRelease().toStdString(), 0.667f, 1, 1000, 667 }); |
| 33 | + addParameter(Parameter { Constants::NahdXml::xmlKeyLookahead().toStdString(), 0.5f, 0, 10, 5 }); |
| 34 | + addParameter(Parameter { Constants::NahdXml::xmlKeyBoost().toStdString(), 0.0f, 0, 1, 0, 1, Parameter::Type::Boolean }); |
| 35 | + |
| 36 | + syncParameters(); |
| 37 | +} |
| 38 | + |
| 39 | +void Limiter::process(double & left, double & right) |
| 40 | +{ |
| 41 | + if (m_sampleRate <= 0) { |
| 42 | + return; |
| 43 | + } |
| 44 | + |
| 45 | + updateBuffers(); |
| 46 | + updateCoefficients(); |
| 47 | + applyLimiter(left, right); |
| 48 | +} |
| 49 | + |
| 50 | +void Limiter::process(AudioContext & context) |
| 51 | +{ |
| 52 | + if (m_sampleRate <= 0) { |
| 53 | + return; |
| 54 | + } |
| 55 | + |
| 56 | + updateBuffers(); |
| 57 | + updateCoefficients(); |
| 58 | + |
| 59 | + for (uint32_t i = 0; i < context.frameCount; i++) { |
| 60 | + applyLimiter(context.buffer[i * 2], context.buffer[i * 2 + 1]); |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +void Limiter::updateBuffers() |
| 65 | +{ |
| 66 | + if (static_cast<uint32_t>(m_sampleRate) != m_lastSampleRate || m_shouldUpdateBuffers || m_delayBufferL.empty()) { |
| 67 | + syncParameters(); |
| 68 | + const uint32_t lookaheadSamples = static_cast<uint32_t>(m_lookaheadMs * m_sampleRate / 1000.0f); |
| 69 | + const uint32_t bufferSize = std::max(1u, lookaheadSamples + 1); |
| 70 | + if (bufferSize != m_delayBufferL.size()) { |
| 71 | + m_delayBufferL.assign(bufferSize, 0.0); |
| 72 | + m_delayBufferR.assign(bufferSize, 0.0); |
| 73 | + m_peakBuffer.assign(bufferSize, 0.0); |
| 74 | + m_maxIndices.clear(); |
| 75 | + m_writePos = 0; |
| 76 | + } |
| 77 | + m_delaySamples = lookaheadSamples; |
| 78 | + m_lastSampleRate = static_cast<uint32_t>(m_sampleRate); |
| 79 | + m_shouldUpdateBuffers = false; |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +void Limiter::updateCoefficients() |
| 84 | +{ |
| 85 | + if (m_sampleRate > 0) { |
| 86 | + m_releaseCoeff = std::exp(-1.0 / (static_cast<double>(m_releaseMs) * m_sampleRate / 1000.0)); |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +void Limiter::applyLimiter(double & left, double & right) |
| 91 | +{ |
| 92 | + if (m_delayBufferL.empty()) { |
| 93 | + updateBuffers(); |
| 94 | + } |
| 95 | + |
| 96 | + const double thresholdLin = std::max(1.0e-6, static_cast<double>(Utils::Dsp::dbToLinear(m_thresholdDb))); |
| 97 | + const double ceilingLin = std::max(1.0e-6, static_cast<double>(Utils::Dsp::dbToLinear(m_ceilingDb))); |
| 98 | + |
| 99 | + // With boost the signal is limited to the threshold and lifted so that the threshold reaches the |
| 100 | + // ceiling (make-up = ceiling / threshold). Without boost it is pure peak limiting, capped at whichever |
| 101 | + // of threshold and ceiling is lower. |
| 102 | + const double targetLevel = m_boost ? thresholdLin : std::min(thresholdLin, ceilingLin); |
| 103 | + const double postGain = m_boost ? ceilingLin / thresholdLin : 1.0; |
| 104 | + |
| 105 | + const uint32_t size = static_cast<uint32_t>(m_delayBufferL.size()); |
| 106 | + const double peak = std::max(std::abs(left), std::abs(right)); |
| 107 | + |
| 108 | + // Maintain a sliding-window maximum of the peak over the whole lookahead buffer using a monotonic |
| 109 | + // decreasing deque. The slot at m_writePos currently holds the sample that is leaving the window. |
| 110 | + if (!m_maxIndices.empty() && m_maxIndices.front() == m_writePos) { |
| 111 | + m_maxIndices.pop_front(); |
| 112 | + } |
| 113 | + while (!m_maxIndices.empty() && m_peakBuffer[m_maxIndices.back()] <= peak) { |
| 114 | + m_maxIndices.pop_back(); |
| 115 | + } |
| 116 | + m_peakBuffer[m_writePos] = peak; |
| 117 | + m_delayBufferL[m_writePos] = left; |
| 118 | + m_delayBufferR[m_writePos] = right; |
| 119 | + m_maxIndices.push_back(m_writePos); |
| 120 | + |
| 121 | + // The gain is derived from the loudest sample within the lookahead window, so full reduction is reached |
| 122 | + // before the peak reaches the output (instant attack); the gain only eases back up via the release. |
| 123 | + const double windowMax = m_peakBuffer[m_maxIndices.front()]; |
| 124 | + const double targetGain = windowMax > targetLevel ? targetLevel / windowMax : 1.0; |
| 125 | + if (targetGain < m_envGain) { |
| 126 | + m_envGain = targetGain; |
| 127 | + } else { |
| 128 | + m_envGain = m_releaseCoeff * m_envGain + (1.0 - m_releaseCoeff) * targetGain; |
| 129 | + } |
| 130 | + m_envGain = std::min(1.0, m_envGain); |
| 131 | + |
| 132 | + const uint32_t readPos = (m_writePos + 1) % size; |
| 133 | + double outL = m_delayBufferL[readPos] * m_envGain * postGain; |
| 134 | + double outR = m_delayBufferR[readPos] * m_envGain * postGain; |
| 135 | + |
| 136 | + m_writePos = readPos; |
| 137 | + |
| 138 | + // Brickwall backstop: guarantee the output never exceeds the ceiling. |
| 139 | + left = std::clamp(outL, -ceilingLin, ceilingLin); |
| 140 | + right = std::clamp(outR, -ceilingLin, ceilingLin); |
| 141 | + |
| 142 | + m_reductionDb = Utils::Dsp::linearToDb(static_cast<float>(m_envGain)); |
| 143 | + |
| 144 | + // Denormal protection |
| 145 | + if (std::abs(m_reductionDb) < 1.0e-15) { |
| 146 | + m_reductionDb = 0.0; |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +void Limiter::reset() |
| 151 | +{ |
| 152 | + m_envGain = 1.0; |
| 153 | + m_reductionDb = 0.0; |
| 154 | + std::fill(m_delayBufferL.begin(), m_delayBufferL.end(), 0.0); |
| 155 | + std::fill(m_delayBufferR.begin(), m_delayBufferR.end(), 0.0); |
| 156 | + std::fill(m_peakBuffer.begin(), m_peakBuffer.end(), 0.0); |
| 157 | + m_maxIndices.clear(); |
| 158 | + m_writePos = 0; |
| 159 | +} |
| 160 | + |
| 161 | +void Limiter::sync() |
| 162 | +{ |
| 163 | + m_shouldUpdateBuffers = true; |
| 164 | +} |
| 165 | + |
| 166 | +float Limiter::reductionDb() const |
| 167 | +{ |
| 168 | + return static_cast<float>(m_reductionDb); |
| 169 | +} |
| 170 | + |
| 171 | +void Limiter::syncParameters() |
| 172 | +{ |
| 173 | + if (const auto p = parameter(Constants::NahdXml::xmlKeyThreshold().toStdString()); p) { |
| 174 | + m_thresholdDb = -24.0f + p->get().value() * 24.0f; |
| 175 | + } |
| 176 | + if (const auto p = parameter(Constants::NahdXml::xmlKeyCeiling().toStdString()); p) { |
| 177 | + m_ceilingDb = -3.0f + p->get().value() * 3.0f; |
| 178 | + } |
| 179 | + if (const auto p = parameter(Constants::NahdXml::xmlKeyRelease().toStdString()); p) { |
| 180 | + m_releaseMs = static_cast<float>(ParameterMapper::mapExponential(p->get().value(), 1.0, 1000.0)); |
| 181 | + } |
| 182 | + if (const auto p = parameter(Constants::NahdXml::xmlKeyLookahead().toStdString()); p) { |
| 183 | + m_lookaheadMs = p->get().value() * 10.0f; |
| 184 | + } |
| 185 | + if (const auto p = parameter(Constants::NahdXml::xmlKeyBoost().toStdString()); p) { |
| 186 | + m_boost = p->get().value() > 0.5f; |
| 187 | + } |
| 188 | +} |
| 189 | + |
| 190 | +std::string Limiter::typeIdString() |
| 191 | +{ |
| 192 | + return "3f8c1a2e-6b7d-4e9a-8f0c-2d3e4f5a6b7c"; |
| 193 | +} |
| 194 | + |
| 195 | +std::string Limiter::type() const |
| 196 | +{ |
| 197 | + return Constants::RackEffectType::limiter().toStdString(); |
| 198 | +} |
| 199 | + |
| 200 | +std::string Limiter::typeId() const |
| 201 | +{ |
| 202 | + return typeIdString(); |
| 203 | +} |
| 204 | + |
| 205 | +} // namespace noteahead |
0 commit comments