1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
|
/*
This file is part of the SimpleCompressor project.
https://github.com/DanielRudrich/SimpleCompressor
Copyright (c) 2019 Daniel Rudrich
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "LookAheadGainReduction.h"
#include <cmath>
#include <algorithm>
namespace DanielRudrich {
void LookAheadGainReduction::setDelayTime (float delayTimeInSeconds)
{
if (delayTimeInSeconds <= 0.0f)
delay = 0.0f;
else
delay = delayTimeInSeconds;
if (sampleRate != 0.0)
prepare (sampleRate, blockSize);
}
void LookAheadGainReduction::prepare (const double newSampleRate, const int newBlockSize)
{
sampleRate = newSampleRate;
blockSize = newBlockSize;
delayInSamples = static_cast<int> (delay * sampleRate);
buffer.resize (blockSize + delayInSamples);
std::fill (buffer.begin(), buffer.end(), 0.0f);
writePosition = 0;
}
void LookAheadGainReduction::pushSamples (const float* src, const int numSamples)
{
int startIndex, blockSize1, blockSize2;
// write in delay line
getWritePositions (numSamples, startIndex, blockSize1, blockSize2);
for (int i = 0; i < blockSize1; ++i)
buffer[startIndex + i] = src[i];
if (blockSize2 > 0)
for (int i = 0; i < blockSize2; ++i)
buffer[i] = src[blockSize1 + i];
writePosition += numSamples;
writePosition = writePosition % buffer.size();
lastPushedSamples = numSamples;
}
void LookAheadGainReduction::process()
{
/** The basic idea here is to look for high gain-reduction values in the signal, and apply a fade which starts exactly `delayInSamples` many samples before that value appears. Depending on the value itself, the slope of the fade will vary.
Some things to note:
- as the samples are gain-reduction values in decibel, we actually look for negative peaks or local minima.
- it's easier for us to time-reverse our search, so we start with the last sample
- once we find a minimum, we calculate the slope, which will be called `step`
- with that slope, we can calculate the next value of our fade-in `nextGainReductionValue`
- once a value in the buffer is below our fade-in value, we found a new minimum, which might not be as deep as the previous one, but as it comes in earlier, it needs more attention, so we update our fade-in slope
- our buffer is a ring-buffer which makes things a little bit messy
*/
// As we don't know any samples of the future, yet, we assume we don't have to apply a fade-in right now, and initialize both `nextGainReductionValue` and step (slope of the fade-in) with zero.
float nextGainReductionValue = 0.0f;
float step = 0.0f;
// Get the position of the last sample in the buffer, which is the sample right before our new write position.
int index = writePosition - 1;
if (index < 0) // in case it's negative...
index += static_cast<int> (buffer.size()); // ... add the buffersize so we wrap around.
// == FIRST STEP: Process all recently pushed samples.
// We want to process all `lastPushedSamples` many samples, so let's find out, how many we can process in a first run before we have to wrap around our `index` variable (ring-buffer).
int size1, size2;
getProcessPositions (index, lastPushedSamples, size1, size2);
// first run
for (int i = 0; i < size1; ++i)
{
const float smpl = buffer[index];
if (smpl > nextGainReductionValue) // in case the sample is above our ramp...
{
buffer[index] = nextGainReductionValue; // ... replace it with the current ramp value
nextGainReductionValue += step; // and update the next ramp value
}
else // otherwise... (new peak)
{
step = - smpl / delayInSamples; // calculate the new slope
nextGainReductionValue = smpl + step; // and also the new ramp value
}
--index;
}
// second run
if (size2 > 0) // in case we have some samples left for the second run
{
index = static_cast<int> (buffer.size()) - 1; // wrap around: start from the last sample of the buffer
// exactly the same procedure as before... I guess I could have written that better...
for (int i = 0; i < size2; ++i)
{
const float smpl = buffer[index];
if (smpl > nextGainReductionValue)
{
buffer[index] = nextGainReductionValue;
nextGainReductionValue += step;
}
else
{
step = - smpl / delayInSamples;
nextGainReductionValue = smpl + step;
}
--index;
}
}
/*
At this point, we have processed all the new gain-reduction values. For this, we actually don't need a delay/lookahead at all.
!! However, we are not finished, yet !!
What if the first pushed sample has such a high gain-reduction value, that itself needs a fade-in? So we have to apply a gain-ramp even further into the past. And that is exactly the reason why we need lookahead, why we need to buffer our signal for a short amount of time: so we can apply that gain ramp for the first handful of gain-reduction samples.
*/
if (index < 0) // it's possible the index is exactly -1
index = static_cast<int> (buffer.size()) - 1; // so let's take care of that
/*
This time we only need to check `delayInSamples` many samples.
And there's another cool thing!
We know that the samples have been processed already, so in case one of the samples is below our ramp value, that's the new minimum, which has been faded-in already! So what we do is hit the break, and call it a day!
*/
getProcessPositions (index, delayInSamples, size1, size2);
bool breakWasUsed = false;
// first run
for (int i = 0; i < size1; ++i) // we iterate over the first size1 samples
{
const float smpl = buffer[index];
if (smpl > nextGainReductionValue) // in case the sample is above our ramp...
{
buffer[index] = nextGainReductionValue; // ... replace it with the current ramp value
nextGainReductionValue += step; // and update the next ramp value
}
else // otherwise... JACKPOT! Nothing left to do here!
{
breakWasUsed = true; // let the guys know we are finished
break;
}
--index;
}
// second run
if (! breakWasUsed && size2 > 0) // is there still some work to do?
{
index = static_cast<int> (buffer.size()) - 1; // wrap around (ring-buffer)
for (int i = 0; i < size2; ++i)
{
const float smpl = buffer[index];
// same as before
if (smpl > nextGainReductionValue) // in case the sample is above our ramp...
{
buffer[index] = nextGainReductionValue; // ... replace it with the current ramp value
nextGainReductionValue += step; // and update the next ramp value
}
else // otherwise... already processed -> byebye!
break;
--index;
}
}
}
void LookAheadGainReduction::readSamples (float* dest, int numSamples)
{
int startIndex, blockSize1, blockSize2;
// read from delay line
getReadPositions (numSamples, startIndex, blockSize1, blockSize2);
for (int i = 0; i < blockSize1; ++i)
dest[i] = buffer[startIndex + i];
if (blockSize2 > 0)
for (int i = 0; i < blockSize2; ++i)
dest[blockSize1 + i] = buffer[i];
}
inline void LookAheadGainReduction::getProcessPositions (int startIndex, int numSamples, int& blockSize1, int& blockSize2)
{
if (numSamples <= 0)
{
blockSize1 = 0;
blockSize2 = 0;
}
else
{
blockSize1 = std::min (startIndex + 1, numSamples);
numSamples -= blockSize1;
blockSize2 = numSamples <= 0 ? 0 : numSamples;
}
}
inline void LookAheadGainReduction::getWritePositions (int numSamples, int& startIndex, int& blockSize1, int& blockSize2)
{
const int L = static_cast<int> (buffer.size());
int pos = writePosition;
if (pos < 0)
pos = pos + L;
pos = pos % L;
if (numSamples <= 0)
{
startIndex = 0;
blockSize1 = 0;
blockSize2 = 0;
}
else
{
startIndex = pos;
blockSize1 = std::min (L - pos, numSamples);
numSamples -= blockSize1;
blockSize2 = numSamples <= 0 ? 0 : numSamples;
}
}
inline void LookAheadGainReduction::getReadPositions (int numSamples, int& startIndex, int& blockSize1, int& blockSize2)
{
const int L = static_cast<int> (buffer.size());
int pos = writePosition - lastPushedSamples - delayInSamples;
if (pos < 0)
pos = pos + L;
pos = pos % L;
if (numSamples <= 0)
{
startIndex = 0;
blockSize1 = 0;
blockSize2 = 0;
}
else
{
startIndex = pos;
blockSize1 = std::min (L - pos, numSamples);
numSamples -= blockSize1;
blockSize2 = numSamples <= 0 ? 0 : numSamples;
}
}
} // namespace DanielRudrich
|