| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #define LOG_TAG "EffectHG_Processors" |
| //#define LOG_NDEBUG 0 |
| #include <utils/Log.h> |
| |
| #include <assert.h> |
| |
| #include <cmath> |
| |
| #include "Processors.h" |
| |
| #if defined(__aarch64__) || defined(__ARM_NEON__) |
| #ifndef USE_NEON |
| #define USE_NEON (true) |
| #endif |
| #else |
| #define USE_NEON (false) |
| #endif |
| #if USE_NEON |
| #include <arm_neon.h> |
| #endif |
| |
| namespace android::audio_effect::haptic_generator { |
| |
| float getRealPoleZ(float cornerFrequency, float sampleRate) { |
| // This will be a pole of a first order filter. |
| float realPoleS = -2 * M_PI * cornerFrequency; |
| return exp(realPoleS / sampleRate); // zero-pole matching |
| } |
| |
| std::pair<float, float> getComplexPoleZ(float ringingFrequency, float q, float sampleRate) { |
| // This is the pole for 1/(s^2 + s/q + 1) in normalized frequency. The other pole is |
| // the complex conjugate of this. |
| float poleImagS = 2 * M_PI * ringingFrequency; |
| float poleRealS = -poleImagS / (2 * q); |
| float poleRadius = exp(poleRealS / sampleRate); |
| float poleImagZ = poleRadius * sin(poleImagS / sampleRate); |
| float poleRealZ = poleRadius * cos(poleImagS / sampleRate); |
| return {poleRealZ, poleImagZ}; |
| } |
| |
| // Implementation of Ramp |
| |
| Ramp::Ramp(size_t channelCount) : mChannelCount(channelCount) {} |
| |
| void Ramp::process(float *out, const float *in, size_t frameCount) { |
| size_t i = 0; |
| #if USE_NEON |
| size_t sampleCount = frameCount * mChannelCount; |
| float32x2_t allZero = vdup_n_f32(0.0f); |
| while (i + 1 < sampleCount) { |
| vst1_f32(out, vmax_f32(vld1_f32(in), allZero)); |
| in += 2; |
| out += 2; |
| i += 2; |
| } |
| #endif // USE_NEON |
| for (; i < frameCount * mChannelCount; ++i) { |
| *out = *in >= 0.0f ? *in : 0.0f; |
| out++; |
| in++; |
| } |
| } |
| |
| // Implementation of SlowEnvelope |
| |
| SlowEnvelope::SlowEnvelope( |
| float cornerFrequency, |
| float sampleRate, |
| float normalizationPower, |
| float envOffset, |
| size_t channelCount) |
| : mLpf(createLPF(cornerFrequency, sampleRate, channelCount)), |
| mNormalizationPower(normalizationPower), |
| mEnvOffset(envOffset), |
| mChannelCount(channelCount) {} |
| |
| void SlowEnvelope::process(float* out, const float* in, size_t frameCount) { |
| size_t sampleCount = frameCount * mChannelCount; |
| if (sampleCount > mLpfOutBuffer.size()) { |
| mLpfOutBuffer.resize(sampleCount); |
| mLpfInBuffer.resize(sampleCount); |
| } |
| for (size_t i = 0; i < sampleCount; ++i) { |
| mLpfInBuffer[i] = fabs(in[i]); |
| } |
| mLpf->process(mLpfOutBuffer.data(), mLpfInBuffer.data(), frameCount); |
| for (size_t i = 0; i < sampleCount; ++i) { |
| out[i] = in[i] * pow(mLpfOutBuffer[i] + mEnvOffset, mNormalizationPower); |
| } |
| } |
| |
| void SlowEnvelope::setNormalizationPower(float normalizationPower) { |
| mNormalizationPower = normalizationPower; |
| } |
| |
| void SlowEnvelope::clear() { |
| mLpf->clear(); |
| } |
| |
| // Implementation of distortion |
| |
| Distortion::Distortion( |
| float cornerFrequency, |
| float sampleRate, |
| float inputGain, |
| float cubeThreshold, |
| float outputGain, |
| size_t channelCount) |
| : mLpf(createLPF2(cornerFrequency, sampleRate, channelCount)), |
| mSampleRate(sampleRate), |
| mCornerFrequency(cornerFrequency), |
| mInputGain(inputGain), |
| mCubeThreshold(cubeThreshold), |
| mOutputGain(outputGain), |
| mChannelCount(channelCount) {} |
| |
| void Distortion::process(float *out, const float *in, size_t frameCount) { |
| size_t sampleCount = frameCount * mChannelCount; |
| if (sampleCount > mLpfInBuffer.size()) { |
| mLpfInBuffer.resize(sampleCount); |
| } |
| for (size_t i = 0; i < sampleCount; ++i) { |
| const float x = mInputGain * in[i]; |
| mLpfInBuffer[i] = x * x * x / (mCubeThreshold + x * x); // "Coring" nonlinearity. |
| } |
| mLpf->process(out, mLpfInBuffer.data(), frameCount); // Reduce 3*F components. |
| for (size_t i = 0; i < sampleCount; ++i) { |
| const float x = out[i]; |
| out[i] = mOutputGain * x / (1.0f + fabs(x)); // Soft limiter. |
| } |
| } |
| |
| void Distortion::setCornerFrequency(float cornerFrequency) { |
| mCornerFrequency = cornerFrequency; |
| BiquadFilterCoefficients coefficient = lpfCoefs(cornerFrequency, mSampleRate); |
| mLpf->setCoefficients(coefficient); |
| } |
| |
| void Distortion::setInputGain(float inputGain) { |
| mInputGain = inputGain; |
| } |
| |
| void Distortion::setCubeThrehold(float cubeThreshold) { |
| mCubeThreshold = cubeThreshold; |
| } |
| |
| void Distortion::setOutputGain(float outputGain) { |
| mOutputGain = outputGain; |
| } |
| |
| void Distortion::clear() { |
| mLpf->clear(); |
| } |
| |
| |
| // Implementation of helper functions |
| |
| BiquadFilterCoefficients cascadeFirstOrderFilters(const BiquadFilterCoefficients &coefs1, |
| const BiquadFilterCoefficients &coefs2) { |
| assert(coefs1[2] == 0.0f); |
| assert(coefs2[2] == 0.0f); |
| assert(coefs1[4] == 0.0f); |
| assert(coefs2[4] == 0.0f); |
| return {coefs1[0] * coefs2[0], |
| coefs1[0] * coefs2[1] + coefs1[1] * coefs2[0], |
| coefs1[1] * coefs2[1], |
| coefs1[3] + coefs2[3], |
| coefs1[3] * coefs2[3]}; |
| } |
| |
| BiquadFilterCoefficients lpfCoefs(const float cornerFrequency, const float sampleRate) { |
| BiquadFilterCoefficients coefficient; |
| float realPoleZ = getRealPoleZ(cornerFrequency, sampleRate); |
| // This is a zero at nyquist |
| coefficient[0] = 0.5f * (1 - realPoleZ); |
| coefficient[1] = coefficient[0]; |
| coefficient[2] = 0.0f; |
| coefficient[3] = -realPoleZ; // This is traditional 1/(s+1) filter |
| coefficient[4] = 0.0f; |
| return coefficient; |
| } |
| |
| BiquadFilterCoefficients bpfCoefs(const float ringingFrequency, |
| const float q, |
| const float sampleRate) { |
| BiquadFilterCoefficients coefficient; |
| const auto [real, img] = getComplexPoleZ(ringingFrequency, q, sampleRate); |
| // Note: this is not a standard cookbook BPF, but a low pass filter with zero at DC |
| coefficient[0] = 1.0f; |
| coefficient[1] = -1.0f; |
| coefficient[2] = 0.0f; |
| coefficient[3] = -2 * real; |
| coefficient[4] = real * real + img * img; |
| return coefficient; |
| } |
| |
| BiquadFilterCoefficients bsfCoefs(const float ringingFrequency, |
| const float zq, |
| const float pq, |
| const float sampleRate) { |
| BiquadFilterCoefficients coefficient; |
| const auto [zeroReal, zeroImg] = getComplexPoleZ(ringingFrequency, zq, sampleRate); |
| float zeroCoeff1 = -2 * zeroReal; |
| float zeroCoeff2 = zeroReal* zeroReal + zeroImg * zeroImg; |
| const auto [poleReal, poleImg] = getComplexPoleZ(ringingFrequency, pq, sampleRate); |
| float poleCoeff1 = -2 * poleReal; |
| float poleCoeff2 = poleReal * poleReal + poleImg * poleImg; |
| const float norm = (1.0f + poleCoeff1 + poleCoeff2) / (1.0f + zeroCoeff1 + zeroCoeff2); |
| coefficient[0] = 1.0f * norm; |
| coefficient[1] = zeroCoeff1 * norm; |
| coefficient[2] = zeroCoeff2 * norm; |
| coefficient[3] = poleCoeff1; |
| coefficient[4] = poleCoeff2; |
| return coefficient; |
| } |
| |
| std::shared_ptr<HapticBiquadFilter> createLPF(const float cornerFrequency, |
| const float sampleRate, |
| const size_t channelCount) { |
| BiquadFilterCoefficients coefficient = lpfCoefs(cornerFrequency, sampleRate); |
| return std::make_shared<HapticBiquadFilter>(channelCount, coefficient); |
| } |
| |
| std::shared_ptr<HapticBiquadFilter> createLPF2(const float cornerFrequency, |
| const float sampleRate, |
| const size_t channelCount) { |
| BiquadFilterCoefficients coefficient = lpfCoefs(cornerFrequency, sampleRate); |
| return std::make_shared<HapticBiquadFilter>( |
| channelCount, cascadeFirstOrderFilters(coefficient, coefficient)); |
| } |
| |
| std::shared_ptr<HapticBiquadFilter> createHPF2(const float cornerFrequency, |
| const float sampleRate, |
| const size_t channelCount) { |
| BiquadFilterCoefficients coefficient; |
| // Note: this is valid only when corner frequency is less than nyquist / 2. |
| float realPoleZ = getRealPoleZ(cornerFrequency, sampleRate); |
| |
| // Note: this is a zero at DC |
| coefficient[0] = 0.5f * (1 + realPoleZ); |
| coefficient[1] = -coefficient[0]; |
| coefficient[2] = 0.0f; |
| coefficient[3] = -realPoleZ; |
| coefficient[4] = 0.0f; |
| return std::make_shared<HapticBiquadFilter>( |
| channelCount, cascadeFirstOrderFilters(coefficient, coefficient)); |
| } |
| |
| std::shared_ptr<HapticBiquadFilter> createBPF(const float ringingFrequency, |
| const float q, |
| const float sampleRate, |
| const size_t channelCount) { |
| BiquadFilterCoefficients coefficient = bpfCoefs(ringingFrequency, q, sampleRate); |
| return std::make_shared<HapticBiquadFilter>(channelCount, coefficient); |
| } |
| |
| std::shared_ptr<HapticBiquadFilter> createBSF(const float ringingFrequency, |
| const float zq, |
| const float pq, |
| const float sampleRate, |
| const size_t channelCount) { |
| BiquadFilterCoefficients coefficient = bsfCoefs(ringingFrequency, zq, pq, sampleRate); |
| return std::make_shared<HapticBiquadFilter>(channelCount, coefficient); |
| } |
| |
| } // namespace android::audio_effect::haptic_generator |