mirror of
				https://github.com/f4exb/sdrangel.git
				synced 2025-10-30 20:40:20 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			324 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			324 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| ///////////////////////////////////////////////////////////////////////////////////
 | |
| // Copyright (C) 2019-2020 Edouard Griffiths, F4EXB <f4exb06@gmail.com>          //
 | |
| //                                                                               //
 | |
| // 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 <http://www.gnu.org/licenses/>.          //
 | |
| ///////////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| #include <algorithm>
 | |
| #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)
 | |
| {
 | |
|     float compressedSample;
 | |
| 
 | |
|     if (m_sampleIndex >= AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE)
 | |
|     {
 | |
|         sf_compressor_process(&m_compressorState, AUDIOCOMPRESSORSND_SF_COMPRESSOR_CHUNKSIZE, m_storageBuffer, m_processedBuffer);
 | |
|         m_sampleIndex = 0;
 | |
|     }
 | |
| 
 | |
|     compressedSample = m_processedBuffer[m_sampleIndex];
 | |
|     m_storageBuffer[m_sampleIndex] = sample;
 | |
|     m_sampleIndex++;
 | |
| 
 | |
|     return compressedSample;
 | |
| }
 | |
| 
 | |
| // 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 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;
 | |
| }
 |