diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 4b2c172202..af89705c6e 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -310,10 +310,20 @@ jobs: - name: Checkout code uses: actions/checkout@v7 with: - # only Android needs the oboe submodule, so don't fetch it for other builds - submodules: ${{ matrix.config.target_os == 'android' }} + submodules: false fetch-depth: ${{ matrix.config.checkout_fetch_depth || '1' }} + - name: Initialize Submodules + shell: bash + run: | + # clone mverb for all platforms + git submodule update --init libs/mverb + + # clone oboe only for android builds + if [[ "${{ matrix.config.target_os }}" == "android" ]]; then + git submodule update --init libs/oboe + fi + - name: Cache Mac dependencies if: matrix.config.target_os == 'macos' uses: actions/cache@v6 diff --git a/.gitmodules b/.gitmodules index 819ad73047..f61a791095 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "libs/oboe"] path = libs/oboe url = ../../google/oboe.git +[submodule "libs/mverb"] + path = libs/mverb + url = https://github.com/martineastwood/mverb.git diff --git a/Jamulus.pro b/Jamulus.pro index fa956ef6d5..d5712b297d 100644 --- a/Jamulus.pro +++ b/Jamulus.pro @@ -403,7 +403,8 @@ HEADERS += src/plugins/audioreverb.h \ src/recorder/jamrecorder.h \ src/recorder/creaperproject.h \ src/recorder/cwavestream.h \ - src/signalhandler.h + src/signalhandler.h \ + libs/mverb/MVerb.h !contains(CONFIG, "serveronly") { HEADERS += src/client.h \ diff --git a/libs/mverb b/libs/mverb new file mode 160000 index 0000000000..9f9512e9d9 --- /dev/null +++ b/libs/mverb @@ -0,0 +1 @@ +Subproject commit 9f9512e9d990fcf265e00fc7c2ee47ad20c8019f diff --git a/src/client.cpp b/src/client.cpp index fb9bab5478..cbc71e436b 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -1345,7 +1345,7 @@ void CClient::Init() Channel.SetAudioStreamProperties ( eAudioCompressionType, iCeltNumCodedBytes, iSndCrdFrameSizeFactor, iNumAudioChannels ); // init reverberation - AudioReverb.Init ( eAudioChannelConf, iStereoBlockSizeSam, SYSTEM_SAMPLE_RATE_HZ ); + AudioReverb.Init ( eAudioChannelConf, iStereoBlockSizeSam ); // init the sound card conversion buffers if ( bSndCrdConversionBufferRequired ) diff --git a/src/client.h b/src/client.h index fb77930723..03478385ac 100644 --- a/src/client.h +++ b/src/client.h @@ -192,11 +192,7 @@ class CClient : public QObject void SetReverbLevel ( const int iNL ) { iReverbLevel = iNL; } bool IsReverbOnLeftChan() const { return bReverbOnLeftChan; } - void SetReverbOnLeftChan ( const bool bIL ) - { - bReverbOnLeftChan = bIL; - AudioReverb.Clear(); - } + void SetReverbOnLeftChan ( const bool bIL ) { bReverbOnLeftChan = bIL; } void SetDoAutoSockBufSize ( const bool bValue ); bool GetDoAutoSockBufSize() const { return Channel.GetDoAutoSockBufSize(); } diff --git a/src/plugins/audioreverb.cpp b/src/plugins/audioreverb.cpp index a10c415bb8..a6acc4373b 100644 --- a/src/plugins/audioreverb.cpp +++ b/src/plugins/audioreverb.cpp @@ -2,206 +2,67 @@ * Audio Reverberation * \******************************************************************************/ /* - The following code is based on "JCRev: John Chowning's reverberator class" - by Perry R. Cook and Gary P. Scavone, 1995 - 2004 - which is in "The Synthesis ToolKit in C++ (STK)" - http://ccrma.stanford.edu/software/stk - - Original description: - This class is derived from the CLM JCRev function, which is based on the use - of networks of simple allpass and comb delay filters. This class implements - three series allpass units, followed by four parallel comb filters, and two - decorrelation delay lines in parallel at the output. + The following code calls MVerb for reverberation. + MVerb was written by Martin Eastwood. + https://github.com/martineastwood/mverb */ #include "audioreverb.h" -void CAudioReverb::Init ( const EAudChanConf eNAudioChannelConf, const int iNStereoBlockSizeSam, const int iSampleRate, const float fT60 ) +void CAudioReverb::Init ( const EAudChanConf eNAudioChannelConf, const int iNStereoBlockSizeSam ) { - // store parameters eAudioChannelConf = eNAudioChannelConf; iStereoBlockSizeSam = iNStereoBlockSizeSam; - - // delay lengths for 44100 Hz sample rate - int lengths[9] = { 1116, 1356, 1422, 1617, 225, 341, 441, 211, 179 }; - const float scaler = static_cast ( iSampleRate ) / 44100.0f; - - if ( scaler != 1.0f ) - { - for ( int i = 0; i < 9; i++ ) - { - int delay = static_cast ( floorf ( scaler * lengths[i] ) ); - - if ( ( delay & 1 ) == 0 ) - { - delay++; - } - - while ( !isPrime ( delay ) ) - { - delay += 2; - } - - lengths[i] = delay; - } - } - - for ( int i = 0; i < 3; i++ ) - { - allpassDelays[i].Init ( lengths[i + 4] ); - } - - for ( int i = 0; i < 4; i++ ) - { - combDelays[i].Init ( lengths[i] ); - combFilters[i].setPole ( 0.2f ); - } - - setT60 ( fT60, iSampleRate ); - outLeftDelay.Init ( lengths[7] ); - outRightDelay.Init ( lengths[8] ); - allpassCoefficient = 0.7f; - Clear(); + fMaxShort = static_cast ( _MAXSHORT ); + mverb.setParameter ( MVerb::DAMPINGFREQ, 0.5 ); + mverb.setParameter ( MVerb::DENSITY, 0.5 ); + mverb.setParameter ( MVerb::BANDWIDTHFREQ, 0.5 ); + mverb.setParameter ( MVerb::DECAY, 0.5 ); + mverb.setParameter ( MVerb::PREDELAY, 0.5 ); + mverb.setParameter ( MVerb::GAIN, 1. ); + mverb.setParameter ( MVerb::MIX, 1.0 ); + mverb.setParameter ( MVerb::EARLYMIX, 0.5 ); + mverb.setParameter ( MVerb::SIZE, 0.75 ); } -bool CAudioReverb::isPrime ( const int number ) +void CAudioReverb::Process ( CVector& vecsStereoInOut, const bool bReverbOnLeftChan, const float fReverbGain ) { - /* - Returns true if argument value is prime. Taken from "class Effect" in - "STK abstract effects parent class". - */ - if ( number == 2 ) - { - return true; - } + // Jamulus uses interleaved stereo, mverb operates on a 2-dimensional array instead + // Calculate the number of frames for each channel ( iStereoBlockSizeSam / 2 ) + const int numFrames = iStereoBlockSizeSam >> 1; - if ( number & 1 ) + // These buffers get filled with dry signal and are then passed to mverb + // They need to be vectors as the windows builds fail when arrays are used + std::vector bufL ( numFrames ); + std::vector bufR ( numFrames ); + + // One buffer to pass to mverb's process function + float* fInput[2] = { bufL.data(), bufR.data() }; + + if ( eAudioChannelConf == CC_STEREO ) { - for ( int i = 3; i < static_cast ( sqrtf ( static_cast ( number ) ) ) + 1; i += 2 ) + for ( int i = 0, j = 0; j < numFrames; i += 2, j++ ) { - if ( ( number % i ) == 0 ) - { - return false; - } + // True stereo reverb + bufL[j] = static_cast ( vecsStereoInOut[i] ) / fMaxShort; + bufR[j] = static_cast ( vecsStereoInOut[i + 1] ) / fMaxShort; } - - return true; // prime } else { - return false; // even + for ( int i = 0, j = 0; j < numFrames; i += 2, j++ ) + { + // For Mono and Mono-in/Stereo-out only one channel is selected and copied into both fInput channels + bufR[j] = bufL[j] = static_cast ( vecsStereoInOut[i + !bReverbOnLeftChan] ) / fMaxShort; + } } -} -void CAudioReverb::Clear() -{ - // reset and clear all internal state - allpassDelays[0].Reset ( 0 ); - allpassDelays[1].Reset ( 0 ); - allpassDelays[2].Reset ( 0 ); - combDelays[0].Reset ( 0 ); - combDelays[1].Reset ( 0 ); - combDelays[2].Reset ( 0 ); - combDelays[3].Reset ( 0 ); - combFilters[0].Reset(); - combFilters[1].Reset(); - combFilters[2].Reset(); - combFilters[3].Reset(); - outRightDelay.Reset ( 0 ); - outLeftDelay.Reset ( 0 ); -} + mverb.process ( fInput, fInput, numFrames ); -void CAudioReverb::setT60 ( const float fT60, const int iSampleRate ) -{ - // set the reverberation T60 decay time - for ( int i = 0; i < 4; i++ ) + for ( int i = 0, j = 0; j < numFrames; i += 2, j++ ) { - combCoefficient[i] = powf ( 10.0f, static_cast ( -3.0f * combDelays[i].Size() / ( fT60 * iSampleRate ) ) ); + // Mix wet and dry signal + vecsStereoInOut[i] = Float2Short ( vecsStereoInOut[i] + bufL[j] * fMaxShort * fReverbGain ); + vecsStereoInOut[i + 1] = Float2Short ( vecsStereoInOut[i + 1] + bufR[j] * fMaxShort * fReverbGain ); } } - -void CAudioReverb::COnePole::setPole ( const float fPole ) -{ - // calculate IIR filter coefficients based on the pole value - fA = -fPole; - fB = 1.0f - fPole; -} - -float CAudioReverb::COnePole::Calc ( const float fIn ) -{ - // calculate IIR filter - fLastSample = fB * fIn - fA * fLastSample; - - return fLastSample; -} - -void CAudioReverb::Process ( CVector& vecsStereoInOut, const bool bReverbOnLeftChan, const float fAttenuation ) -{ - float fMixedInput, temp, temp0, temp1, temp2; - - for ( int i = 0; i < iStereoBlockSizeSam; i += 2 ) - { - // we sum up the stereo input channels (in case mono input is used, a zero - // shall be input for the right channel) - if ( eAudioChannelConf == CC_STEREO ) - { - fMixedInput = 0.5f * ( vecsStereoInOut[i] + vecsStereoInOut[i + 1] ); - } - else - { - if ( bReverbOnLeftChan ) - { - fMixedInput = vecsStereoInOut[i]; - } - else - { - fMixedInput = vecsStereoInOut[i + 1]; - } - } - - temp = allpassDelays[0].Get(); - temp0 = allpassCoefficient * temp; - temp0 += fMixedInput; - allpassDelays[0].Add ( temp0 ); - temp0 = -( allpassCoefficient * temp0 ) + temp; - - temp = allpassDelays[1].Get(); - temp1 = allpassCoefficient * temp; - temp1 += temp0; - allpassDelays[1].Add ( temp1 ); - temp1 = -( allpassCoefficient * temp1 ) + temp; - - temp = allpassDelays[2].Get(); - temp2 = allpassCoefficient * temp; - temp2 += temp1; - allpassDelays[2].Add ( temp2 ); - temp2 = -( allpassCoefficient * temp2 ) + temp; - - const float temp3 = temp2 + combFilters[0].Calc ( combCoefficient[0] * combDelays[0].Get() ); - const float temp4 = temp2 + combFilters[1].Calc ( combCoefficient[1] * combDelays[1].Get() ); - const float temp5 = temp2 + combFilters[2].Calc ( combCoefficient[2] * combDelays[2].Get() ); - const float temp6 = temp2 + combFilters[3].Calc ( combCoefficient[3] * combDelays[3].Get() ); - - combDelays[0].Add ( temp3 ); - combDelays[1].Add ( temp4 ); - combDelays[2].Add ( temp5 ); - combDelays[3].Add ( temp6 ); - - const float filtout = temp3 + temp4 + temp5 + temp6; - - outLeftDelay.Add ( filtout ); - outRightDelay.Add ( filtout ); - - // inplace apply the attenuated reverb signal (for stereo always apply - // reverberation effect on both channels) - if ( ( eAudioChannelConf == CC_STEREO ) || bReverbOnLeftChan ) - { - vecsStereoInOut[i] = Float2Short ( ( 1.0f - fAttenuation ) * vecsStereoInOut[i] + 0.5f * fAttenuation * outLeftDelay.Get() ); - } - - if ( ( eAudioChannelConf == CC_STEREO ) || !bReverbOnLeftChan ) - { - vecsStereoInOut[i + 1] = Float2Short ( ( 1.0f - fAttenuation ) * vecsStereoInOut[i + 1] + 0.5f * fAttenuation * outRightDelay.Get() ); - } - } -} \ No newline at end of file diff --git a/src/plugins/audioreverb.h b/src/plugins/audioreverb.h index a5da6fac61..d486326569 100644 --- a/src/plugins/audioreverb.h +++ b/src/plugins/audioreverb.h @@ -2,56 +2,29 @@ * Audio Reverberation * \******************************************************************************/ /* - The following code is based on "JCRev: John Chowning's reverberator class" - by Perry R. Cook and Gary P. Scavone, 1995 - 2004 - which is in "The Synthesis ToolKit in C++ (STK)" - http://ccrma.stanford.edu/software/stk - - Original description: - This class is derived from the CLM JCRev function, which is based on the use - of networks of simple allpass and comb delay filters. This class implements - three series allpass units, followed by four parallel comb filters, and two - decorrelation delay lines in parallel at the output. + The following code calls MVerb for reverberation. + MVerb was written by Martin Eastwood. + https://github.com/martineastwood/mverb */ #pragma once #include "util.h" +#include "libs/mverb/MVerb.h" class CAudioReverb { public: CAudioReverb() {} - void Init ( const EAudChanConf eNAudioChannelConf, const int iNStereoBlockSizeSam, const int iSampleRate, const float fT60 = 1.1f ); + void Init ( const EAudChanConf eNAudioChannelConf, const int iNStereoBlockSizeSam ); void Clear(); - void Process ( CVector& vecsStereoInOut, const bool bReverbOnLeftChan, const float fAttenuation ); + void Process ( CVector& vecsStereoInOut, const bool bReverbOnLeftChan, const float fReverbGain ); protected: - void setT60 ( const float fT60, const int iSampleRate ); - bool isPrime ( const int number ); - - class COnePole - { - public: - COnePole() : fA ( 0 ), fB ( 0 ) { Reset(); } - void setPole ( const float fPole ); - float Calc ( const float fIn ); - void Reset() { fLastSample = 0; } - - protected: - float fA; - float fB; - float fLastSample; - }; + MVerb mverb; EAudChanConf eAudioChannelConf; int iStereoBlockSizeSam; - CFIFO allpassDelays[3]; - CFIFO combDelays[4]; - COnePole combFilters[4]; - CFIFO outLeftDelay; - CFIFO outRightDelay; - float allpassCoefficient; - float combCoefficient[4]; + float fMaxShort; };