diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index fc6736ef3..925e1c481 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -51,6 +51,7 @@ endif(LIBSERIALDV_FOUND) set(sdrbase_SOURCES ${sdrbase_SOURCES} audio/audiocompressor.cpp + audio/audiocompressorsnd.cpp audio/audiodevicemanager.cpp audio/audiofifo.cpp audio/audiofilter.cpp @@ -152,6 +153,7 @@ set(sdrbase_SOURCES set(sdrbase_HEADERS ${sdrbase_HEADERS} audio/audiocompressor.h + audio/audiocompressorsnd.h audio/audiodevicemanager.h audio/audiofifo.h audio/audiofilter.h diff --git a/sdrbase/audio/audiocompressorsnd.cpp b/sdrbase/audio/audiocompressorsnd.cpp new file mode 100644 index 000000000..1f98a2a08 --- /dev/null +++ b/sdrbase/audio/audiocompressorsnd.cpp @@ -0,0 +1,324 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 F4EXB // +// written by Edouard Griffiths // +// // +// Audio compressor based on sndfilter by Sean Connelly (@voidqk) // +// https://github.com/voidqk/sndfilter // +// // +// Sample by sample interface to facilitate integration in SDRangel modulators. // +// Uses mono samples (just floats) // +// // +// 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "audiocompressorsnd.h" + + +AudioCompressorSnd::AudioCompressorSnd() +{ + m_sampleIndex = 0; + std::fill(m_processedBuffer, m_processedBuffer+AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE, 0.0f); +} + +AudioCompressorSnd::~AudioCompressorSnd() +{} + +void AudioCompressorSnd::initState() +{ + m_compressorState.sf_advancecomp( + m_rate, + m_pregain, + m_threshold, + m_knee, + m_ratio, + m_attack, + m_release, + m_predelay, + m_releasezone1, + m_releasezone2, + m_releasezone3, + m_releasezone4, + m_postgain, + m_wet + ); +} + +float AudioCompressorSnd::compress(float sample) +{ + if (m_sampleIndex >= AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE) + { + sf_compressor_process(&m_compressorState, AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE, m_storageBuffer, m_processedBuffer); + m_sampleIndex = 0; + } + else + { + m_storageBuffer[m_sampleIndex] = sample; + m_sampleIndex++; + } + + return m_processedBuffer[m_sampleIndex]; +} + +// populate the compressor state with advanced parameters +void AudioCompressorSnd::CompressorState::sf_advancecomp( + // these parameters are the same as the simple version above: + int rate, float pregain, float threshold, float knee, float ratio, float attack, float release, + // these are the advanced parameters: + float predelay, // seconds, length of the predelay buffer [0 to 1] + float releasezone1, // release zones should be increasing between 0 and 1, and are a fraction + float releasezone2, // of the release time depending on the input dB -- these parameters define + float releasezone3, // the adaptive release curve, which is discussed in further detail in the + float releasezone4, // demo: adaptive-release-curve.html + float postgain, // dB, amount of gain to apply after compression [0 to 100] + float wet) // amount to apply the effect [0 completely dry to 1 completely wet] +{ + // setup the predelay buffer + int delaybufsize = rate * predelay; + + if (delaybufsize < 1) + { + delaybufsize = 1; + } + else if (delaybufsize > AUDIOCOMPRESSORSND_SF_COMPRESSOR_MAXDELAY) + { + delaybufsize = AUDIOCOMPRESSORSND_SF_COMPRESSOR_MAXDELAY; + std::fill(delaybuf, delaybuf+delaybufsize, 0.0f); + } + + // useful values + float linearpregain = db2lin(pregain); + float linearthreshold = db2lin(threshold); + float slope = 1.0f / ratio; + float attacksamples = rate * attack; + float attacksamplesinv = 1.0f / attacksamples; + float releasesamples = rate * release; + float satrelease = 0.0025f; // seconds + float satreleasesamplesinv = 1.0f / ((float)rate * satrelease); + float dry = 1.0f - wet; + + // metering values (not used in core algorithm, but used to output a meter if desired) + float metergain = 1.0f; // gets overwritten immediately because gain will always be negative + float meterfalloff = 0.325f; // seconds + float meterrelease = 1.0f - expf(-1.0f / ((float)rate * meterfalloff)); + + // calculate knee curve parameters + float k = 5.0f; // initial guess + float kneedboffset = 0.0f; + float linearthresholdknee = 0.0f; + + if (knee > 0.0f) // if a knee exists, search for a good k value + { + float xknee = db2lin(threshold + knee); + float mink = 0.1f; + float maxk = 10000.0f; + + // search by comparing the knee slope at the current k guess, to the ideal slope + for (int i = 0; i < 15; i++) + { + if (kneeslope(xknee, k, linearthreshold) < slope) { + maxk = k; + } else { + mink = k; + } + + k = sqrtf(mink * maxk); + } + + kneedboffset = lin2db(kneecurve(xknee, k, linearthreshold)); + linearthresholdknee = db2lin(threshold + knee); + } + + // calculate a master gain based on what sounds good + float fulllevel = compcurve(1.0f, k, slope, linearthreshold, linearthresholdknee, threshold, knee, kneedboffset); + float mastergain = db2lin(postgain) * powf(1.0f / fulllevel, 0.6f); + + // calculate the adaptive release curve parameters + // solve a,b,c,d in `y = a*x^3 + b*x^2 + c*x + d` + // interescting points (0, y1), (1, y2), (2, y3), (3, y4) + float y1 = releasesamples * releasezone1; + float y2 = releasesamples * releasezone2; + float y3 = releasesamples * releasezone3; + float y4 = releasesamples * releasezone4; + float a = (-y1 + 3.0f * y2 - 3.0f * y3 + y4) / 6.0f; + float b = y1 - 2.5f * y2 + 2.0f * y3 - 0.5f * y4; + float c = (-11.0f * y1 + 18.0f * y2 - 9.0f * y3 + 2.0f * y4) / 6.0f; + float d = y1; + + // save everything + this->metergain = 1.0f; // large value overwritten immediately since it's always < 0 + this->meterrelease = meterrelease; + this->threshold = threshold; + this->knee = knee; + this->wet = wet; + this->linearpregain = linearpregain; + this->linearthreshold = linearthreshold; + this->slope = slope; + this->attacksamplesinv = attacksamplesinv; + this->satreleasesamplesinv = satreleasesamplesinv; + this->dry = dry; + this->k = k; + this->kneedboffset = kneedboffset; + this->linearthresholdknee = linearthresholdknee; + this->mastergain = mastergain; + this->a = a; + this->b = b; + this->c = c; + this->d = d; + this->detectoravg = 0.0f; + this->compgain = 1.0f; + this->maxcompdiffdb = -1.0f; + this->delaybufsize = delaybufsize; + this->delaywritepos = 0; + this->delayreadpos = delaybufsize > 1 ? 1 : 0; +} + +void AudioCompressorSnd::sf_compressor_process(AudioCompressorSnd::CompressorState *state, int size, float *input, float *output) +{ + // pull out the state into local variables + float metergain = state->metergain; + float meterrelease = state->meterrelease; + float threshold = state->threshold; + float knee = state->knee; + float linearpregain = state->linearpregain; + float linearthreshold = state->linearthreshold; + float slope = state->slope; + float attacksamplesinv = state->attacksamplesinv; + float satreleasesamplesinv = state->satreleasesamplesinv; + float wet = state->wet; + float dry = state->dry; + float k = state->k; + float kneedboffset = state->kneedboffset; + float linearthresholdknee = state->linearthresholdknee; + float mastergain = state->mastergain; + float a = state->a; + float b = state->b; + float c = state->c; + float d = state->d; + float detectoravg = state->detectoravg; + float compgain = state->compgain; + float maxcompdiffdb = state->maxcompdiffdb; + int delaybufsize = state->delaybufsize; + int delaywritepos = state->delaywritepos; + int delayreadpos = state->delayreadpos; + float *delaybuf = state->delaybuf; + + int samplesperchunk = AUDIOCOMPRESSORSND_SF_COMPRESSOR_SPU; + int chunks = size / samplesperchunk; + float ang90 = (float)M_PI * 0.5f; + float ang90inv = 2.0f / (float)M_PI; + int samplepos = 0; + float spacingdb = AUDIOCOMPRESSORSND_SF_COMPRESSOR_SPACINGDB; + + for (int ch = 0; ch < chunks; ch++) + { + detectoravg = fixf(detectoravg, 1.0f); + float desiredgain = detectoravg; + float scaleddesiredgain = asinf(desiredgain) * ang90inv; + float compdiffdb = lin2db(compgain / scaleddesiredgain); + + // calculate envelope rate based on whether we're attacking or releasing + float enveloperate; + if (compdiffdb < 0.0f) + { // compgain < scaleddesiredgain, so we're releasing + compdiffdb = fixf(compdiffdb, -1.0f); + maxcompdiffdb = -1; // reset for a future attack mode + // apply the adaptive release curve + // scale compdiffdb between 0-3 + float x = (clampf(compdiffdb, -12.0f, 0.0f) + 12.0f) * 0.25f; + float releasesamples = adaptivereleasecurve(x, a, b, c, d); + enveloperate = db2lin(spacingdb / releasesamples); + } + else + { // compresorgain > scaleddesiredgain, so we're attacking + compdiffdb = fixf(compdiffdb, 1.0f); + if (maxcompdiffdb == -1 || maxcompdiffdb < compdiffdb) + maxcompdiffdb = compdiffdb; + float attenuate = maxcompdiffdb; + if (attenuate < 0.5f) + attenuate = 0.5f; + enveloperate = 1.0f - powf(0.25f / attenuate, attacksamplesinv); + } + + // process the chunk + for (int chi = 0; chi < samplesperchunk; chi++, samplepos++, + delayreadpos = (delayreadpos + 1) % delaybufsize, + delaywritepos = (delaywritepos + 1) % delaybufsize) + { + + float inputL = input[samplepos] * linearpregain; + delaybuf[delaywritepos] = inputL; + + inputL = absf(inputL); + float inputmax = inputL; + + float attenuation; + if (inputmax < 0.0001f) + attenuation = 1.0f; + else + { + float inputcomp = compcurve(inputmax, k, slope, linearthreshold, + linearthresholdknee, threshold, knee, kneedboffset); + attenuation = inputcomp / inputmax; + } + + float rate; + if (attenuation > detectoravg) + { // if releasing + float attenuationdb = -lin2db(attenuation); + if (attenuationdb < 2.0f) + attenuationdb = 2.0f; + float dbpersample = attenuationdb * satreleasesamplesinv; + rate = db2lin(dbpersample) - 1.0f; + } + else + rate = 1.0f; + + detectoravg += (attenuation - detectoravg) * rate; + if (detectoravg > 1.0f) + detectoravg = 1.0f; + detectoravg = fixf(detectoravg, 1.0f); + + if (enveloperate < 1) // attack, reduce gain + compgain += (scaleddesiredgain - compgain) * enveloperate; + else + { // release, increase gain + compgain *= enveloperate; + if (compgain > 1.0f) + compgain = 1.0f; + } + + // the final gain value! + float premixgain = sinf(ang90 * compgain); + float gain = dry + wet * mastergain * premixgain; + + // calculate metering (not used in core algo, but used to output a meter if desired) + float premixgaindb = lin2db(premixgain); + if (premixgaindb < metergain) + metergain = premixgaindb; // spike immediately + else + metergain += (premixgaindb - metergain) * meterrelease; // fall slowly + + // apply the gain + output[samplepos] = delaybuf[delayreadpos] * gain; + } + } + + state->metergain = metergain; + state->detectoravg = detectoravg; + state->compgain = compgain; + state->maxcompdiffdb = maxcompdiffdb; + state->delaywritepos = delaywritepos; + state->delayreadpos = delayreadpos; +} diff --git a/sdrbase/audio/audiocompressorsnd.h b/sdrbase/audio/audiocompressorsnd.h new file mode 100644 index 000000000..c2bd55322 --- /dev/null +++ b/sdrbase/audio/audiocompressorsnd.h @@ -0,0 +1,229 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 F4EXB // +// written by Edouard Griffiths // +// // +// Audio compressor based on sndfilter by Sean Connelly (@voidqk) // +// https://github.com/voidqk/sndfilter // +// // +// Sample by sample interface to facilitate integration in SDRangel modulators. // +// Uses mono samples (just floats) // +// // +// 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRBASE_AUDIO_AUDIOCOMPRESSORSND_H_ +#define SDRBASE_AUDIO_AUDIOCOMPRESSORSND_H_ + +#include + +// maximum number of samples in the delay buffer +#define AUDIOCOMPRESSORSND_SF_COMPRESSOR_MAXDELAY 1024 + +// samples per update; the compressor works by dividing the input chunks into even smaller sizes, +// and performs heavier calculations after each mini-chunk to adjust the final envelope +#define AUDIOCOMPRESSORSND_SF_COMPRESSOR_SPU 32 + +// not sure what this does exactly, but it is part of the release curve +#define AUDIOCOMPRESSORSND_SF_COMPRESSOR_SPACINGDB 5.0f + +// the "chunk" size as defined in original sndfilter library +#define AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE 128 + +#include "export.h" + +class SDRBASE_API AudioCompressorSnd +{ +public: + AudioCompressorSnd(); + ~AudioCompressorSnd(); + + void initDefault(int rate) + { + m_rate = rate; + m_pregain = 0.000f; + m_threshold = -24.000f; + m_knee = 30.000f; + m_ratio = 12.000f; + m_attack = 0.003f; + m_release = 0.250f; + m_predelay = 0.006f; + m_releasezone1 = 0.090f; + m_releasezone2 = 0.160f; + m_releasezone3 = 0.420f; + m_releasezone4 = 0.980f; + m_postgain = 0.000f; + m_wet = 1.000f; + initState(); + } + + void initSimple( + int rate, // input sample rate (samples per second) + float pregain, // dB, amount to boost the signal before applying compression [0 to 100] + float threshold, // dB, level where compression kicks in [-100 to 0] + float knee, // dB, width of the knee [0 to 40] + float ratio, // unitless, amount to inversely scale the output when applying comp [1 to 20] + float attack, // seconds, length of the attack phase [0 to 1] + float release // seconds, length of the release phase [0 to 1] + ) + { + m_rate = rate; + m_pregain = pregain; + m_threshold = threshold; + m_knee = knee; + m_ratio = ratio; + m_attack = attack; + m_release = release; + m_predelay = 0.006f; + m_releasezone1 = 0.090f; + m_releasezone2 = 0.160f; + m_releasezone3 = 0.420f; + m_releasezone4 = 0.980f; + m_postgain = 0.000f; + m_wet = 1.000f; + initState(); + } + + void initState(); + float compress(float sample); + + float m_rate; + float m_pregain; + float m_threshold; + float m_knee; + float m_ratio; + float m_attack; + float m_release; + float m_predelay; + float m_releasezone1; + float m_releasezone2; + float m_releasezone3; + float m_releasezone4; + float m_postgain; + float m_wet; + +private: + static inline float db2lin(float db){ // dB to linear + return powf(10.0f, 0.05f * db); + } + + static inline float lin2db(float lin){ // linear to dB + return 20.0f * log10f(lin); + } + + // for more information on the knee curve, check out the compressor-curve.html demo + source code + // included in this repo + static inline float kneecurve(float x, float k, float linearthreshold){ + return linearthreshold + (1.0f - expf(-k * (x - linearthreshold))) / k; + } + + static inline float kneeslope(float x, float k, float linearthreshold){ + return k * x / ((k * linearthreshold + 1.0f) * expf(k * (x - linearthreshold)) - 1); + } + + static inline float compcurve(float x, float k, float slope, float linearthreshold, + float linearthresholdknee, float threshold, float knee, float kneedboffset){ + if (x < linearthreshold) + return x; + if (knee <= 0.0f) // no knee in curve + return db2lin(threshold + slope * (lin2db(x) - threshold)); + if (x < linearthresholdknee) + return kneecurve(x, k, linearthreshold); + return db2lin(kneedboffset + slope * (lin2db(x) - threshold - knee)); + } + + // for more information on the adaptive release curve, check out adaptive-release-curve.html demo + + // source code included in this repo + static inline float adaptivereleasecurve(float x, float a, float b, float c, float d){ + // a*x^3 + b*x^2 + c*x + d + float x2 = x * x; + return a * x2 * x + b * x2 + c * x + d; + } + + static inline float clampf(float v, float min, float max){ + return v < min ? min : (v > max ? max : v); + } + + static inline float absf(float v){ + return v < 0.0f ? -v : v; + } + + static inline float fixf(float v, float def){ + // fix NaN and infinity values that sneak in... not sure why this is needed, but it is + if (isnan(v) || isinf(v)) + return def; + return v; + } + + struct CompressorState + { // sf_compressor_state_st + // user can read the metergain state variable after processing a chunk to see how much dB the + // compressor would have liked to compress the sample; the meter values aren't used to shape the + // sound in any way, only used for output if desired + float metergain; + + // everything else shouldn't really be mucked with unless you read the algorithm and feel + // comfortable + float meterrelease; + float threshold; + float knee; + float linearpregain; + float linearthreshold; + float slope; + float attacksamplesinv; + float satreleasesamplesinv; + float wet; + float dry; + float k; + float kneedboffset; + float linearthresholdknee; + float mastergain; + float a; // adaptive release polynomial coefficients + float b; + float c; + float d; + float detectoravg; + float compgain; + float maxcompdiffdb; + int delaybufsize; + int delaywritepos; + int delayreadpos; + float delaybuf[AUDIOCOMPRESSORSND_SF_COMPRESSOR_MAXDELAY]; // predelay buffer + + // populate the compressor state with advanced parameters + void sf_advancecomp( + // these parameters are the same as the simple version above: + int rate, float pregain, float threshold, float knee, float ratio, float attack, float release, + // these are the advanced parameters: + float predelay, // seconds, length of the predelay buffer [0 to 1] + float releasezone1, // release zones should be increasing between 0 and 1, and are a fraction + float releasezone2, // of the release time depending on the input dB -- these parameters define + float releasezone3, // the adaptive release curve, which is discussed in further detail in the + float releasezone4, // demo: adaptive-release-curve.html + float postgain, // dB, amount of gain to apply after compression [0 to 100] + float wet // amount to apply the effect [0 completely dry to 1 completely wet] + ); + }; + + static void sf_compressor_process(CompressorState *state, int size, float *input, float *output); + + CompressorState m_compressorState; + float m_storageBuffer[AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE]; + float m_processedBuffer[AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE]; + int m_sampleIndex; +}; + + + + +#endif // SDRBASE_AUDIO_AUDIOCOMPRESSORSND_H_ \ No newline at end of file