From ede0652df929df5a83eb66b9f74e1a0bd6562500 Mon Sep 17 00:00:00 2001 From: Maciej Makowski Date: Fri, 25 Oct 2024 13:46:16 +0200 Subject: [PATCH] feat: implemented getFrequencyResponse --- .../BiquadFilterNodeHostObject.cpp | 38 +++++++++++ .../common/cpp/core/BiquadFilterNode.cpp | 65 ++++++++++++++----- .../common/cpp/core/BiquadFilterNode.h | 5 ++ .../cpp/wrappers/BiquadFilterNodeWrapper.cpp | 9 +++ .../cpp/wrappers/BiquadFilterNodeWrapper.h | 3 + packages/react-native-audio-api/src/types.ts | 5 ++ 6 files changed, 109 insertions(+), 16 deletions(-) diff --git a/packages/react-native-audio-api/common/cpp/HostObjects/BiquadFilterNodeHostObject.cpp b/packages/react-native-audio-api/common/cpp/HostObjects/BiquadFilterNodeHostObject.cpp index 89a325ab..65db7d45 100644 --- a/packages/react-native-audio-api/common/cpp/HostObjects/BiquadFilterNodeHostObject.cpp +++ b/packages/react-native-audio-api/common/cpp/HostObjects/BiquadFilterNodeHostObject.cpp @@ -30,6 +30,7 @@ std::vector BiquadFilterNodeHostObject::getPropertyNames( propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "Q")); propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "gain")); propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "type")); + propertyNames.push_back(jsi::PropNameID::forAscii(runtime, "getFrequencyResponse")); return propertyNames; } @@ -60,6 +61,43 @@ jsi::Value BiquadFilterNodeHostObject::get( return jsi::String::createFromUtf8(runtime, waveType); } + if (propName == "getFrequencyResponse") { + return jsi::Function::createFromHostFunction( + runtime, + propNameId, + 3, + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) -> jsi::Value { + auto frequencyArray = args[0].getObject(rt).asArray(rt); + auto magResponseOut = args[1].getObject(rt).asArray(rt); + auto phaseResponseOut = args[2].getObject(rt).asArray(rt); + + std::vector frequencyArrayVector(frequencyArray.length(rt)); + for (size_t i = 0; i < frequencyArray.length(rt); i++) { + frequencyArrayVector[i] = static_cast(frequencyArray.getValueAtIndex(rt, i).getNumber()); + } + + std::vector magResponseOutVector(magResponseOut.length(rt)); + std::vector phaseResponseOutVector(phaseResponseOut.length(rt)); + + auto wrapper = getBiquadFilterNodeWrapperFromAudioNodeWrapper(); + wrapper->getFrequencyResponse(frequencyArrayVector, magResponseOutVector, phaseResponseOutVector); + + for (size_t i = 0; i < magResponseOutVector.size(); i++) { + magResponseOut.setValueAtIndex(rt, i, magResponseOutVector[i]); + } + + for (size_t i = 0; i < phaseResponseOutVector.size(); i++) { + phaseResponseOut.setValueAtIndex(rt, i, phaseResponseOutVector[i]); + } + + return jsi::Value::undefined(); + }); + } + return AudioNodeHostObject::get(runtime, propNameId); } diff --git a/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.cpp b/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.cpp index cbe5ed7c..58724f8f 100644 --- a/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.cpp +++ b/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.cpp @@ -3,8 +3,6 @@ // https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html - math // formulas for filters -// https://github.com/LabSound/LabSound/blob/main/src/internal/src/Biquad.cpp - -// implementation of filters on which I based mine namespace audioapi { @@ -44,6 +42,45 @@ std::shared_ptr BiquadFilterNode::getGainParam() const { return gainParam_; } +// Compute Z-transform of the filter +// https://www.dsprelated.com/freebooks/filters/Frequency_Response_Analysis.html +// https://www.dsprelated.com/freebooks/filters/Transfer_Function_Analysis.html +// +// frequency response - H(z) = (b0 + b1*z^(-1) + b2*z^(-2))/(1 + a1*z^(-1) + a2*z^(-2)) +// = ((b0 * z + b1) * z + b2) / ((z + a1) * z + a2) +// phase response - angle of the frequency response + +void BiquadFilterNode::getFrequencyResponse( + const std::vector &frequencyArray, + std::vector &magResponseOutput, + std::vector &phaseResponseOutput) { + applyFilter(); + + auto frequencyArraySize = frequencyArray.size(); + auto magResponseOutputSize = magResponseOutput.size(); + auto phaseResponseOutputSize = phaseResponseOutput.size(); + + if (magResponseOutputSize != frequencyArraySize || + phaseResponseOutputSize != frequencyArraySize) { + throw std::invalid_argument("Output arrays must have the same size"); + } + + float b0 = b0_; + float b1 = b1_; + float b2 = b2_; + float a1 = a1_; + float a2 = a2_; + + for (size_t i = 0; i < frequencyArraySize; i++) + { + auto omega = static_cast(M_PI) * frequencyArray[i] / NYQUIST_FREQUENCY; + auto z = std::complex(cos(omega), sin(omega)); + auto response = ((b0 * z + b1) * z + b2) / ((z + a1) * z + a2); + magResponseOutput[i] = static_cast(abs(response)); + phaseResponseOutput[i] = static_cast(atan2(imag(response), real(response))); + } +} + float BiquadFilterNode::clamp(float value, float min, float max) { return std::min(std::max(value, min), max); } @@ -83,16 +120,14 @@ void BiquadFilterNode::setLowpassCoefficients(float frequency, float Q) { Q = std::max(0.0f, Q); float g = std::pow(10.0f, 0.05f * Q); - float d = std::sqrt((4 - std::sqrt(16 - 16 / (g * g))) / 2); float theta = M_PI * frequency; - float sn = 0.5f * d * std::sin(theta); - float beta = 0.5f * (1 - sn) / (1 + sn); - float gamma = (0.5f + beta) * std::cos(theta); - float alpha = 0.25f * (0.5f + beta - gamma); + float alpha = std::sin(theta) / (2 * g); + float cosw = std::cos(theta); + float beta = (1 - cosw) / 2; setNormalizedCoefficients( - 2 * alpha, 4 * alpha, 2 * alpha, 1.0f, -2 * gamma, 2 * beta); + beta, 2 * beta, beta, 1 + alpha, -2 * cosw, 1 - alpha); } void BiquadFilterNode::setHighpassCoefficients(float frequency, float Q) { @@ -108,16 +143,14 @@ void BiquadFilterNode::setHighpassCoefficients(float frequency, float Q) { Q = std::max(0.0f, Q); float g = std::pow(10.0f, 0.05f * Q); - float d = std::sqrt((4 - std::sqrt(16 - 16 / (g * g))) / 2); - float theta = M_PI * frequency; - float sn = 0.5f * d * std::sin(theta); - float beta = 0.5f * (1 - sn) / (1 + sn); - float gamma = (0.5f + beta) * std::cos(theta); - float alpha = 0.25f * (0.5f + beta - gamma); + float theta = M_PI * frequency; + float alpha = std::sin(theta) / (2 * g); + float cosw = std::cos(theta); + float beta = (1 - cosw) / 2; - setNormalizedCoefficients( - 2 * alpha, -4 * alpha, 2 * alpha, 1.0f, -2 * gamma, 2 * beta); + setNormalizedCoefficients( + beta, -2 * beta, beta, 1 + alpha, -2 * cosw, 1 - alpha); } void BiquadFilterNode::setBandpassCoefficients(float frequency, float Q) { diff --git a/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.h b/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.h index 8984ef36..656fe1e1 100644 --- a/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.h +++ b/packages/react-native-audio-api/common/cpp/core/BiquadFilterNode.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "AudioNode.h" #include "AudioParam.h" @@ -21,6 +23,9 @@ class BiquadFilterNode : public AudioNode { std::shared_ptr getDetuneParam() const; std::shared_ptr getQParam() const; std::shared_ptr getGainParam() const; + void getFrequencyResponse(const std::vector &frequencyArray, + std::vector &magResponseOutput, + std::vector &phaseResponseOutput); protected: bool processAudio(float *audioData, int32_t numFrames) override; diff --git a/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.cpp b/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.cpp index 87b7d484..ccfcf7f0 100644 --- a/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.cpp +++ b/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.cpp @@ -48,4 +48,13 @@ void BiquadFilterNodeWrapper::setType(const std::string &filterType) { auto biquadFilterNode_ = getBiquadFilterNodeFromAudioNode(); biquadFilterNode_->setType(filterType); } + +void BiquadFilterNodeWrapper::getFrequencyResponse( + const std::vector &frequencyArray, + std::vector &magResponseOutput, + std::vector &phaseResponseOutput) { + auto biquadFilterNode_ = getBiquadFilterNodeFromAudioNode(); + biquadFilterNode_->getFrequencyResponse(frequencyArray, magResponseOutput, + phaseResponseOutput); +} } // namespace audioapi diff --git a/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.h b/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.h index adf49d8f..823060ae 100644 --- a/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.h +++ b/packages/react-native-audio-api/common/cpp/wrappers/BiquadFilterNodeWrapper.h @@ -20,6 +20,9 @@ class BiquadFilterNodeWrapper : public AudioNodeWrapper { std::shared_ptr getGainParam() const; std::string getType(); void setType(const std::string &filterType); + void getFrequencyResponse(const std::vector &frequencyArray, + std::vector &magResponseOutput, + std::vector &phaseResponseOutput); private: std::shared_ptr frequencyParam_; diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/src/types.ts index e62afbcd..9642f90a 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/src/types.ts @@ -80,6 +80,11 @@ export interface BiquadFilterNode extends AudioNode { Q: AudioParam; gain: AudioParam; type: FilterType; + getFrequencyResponse( + frequencyArray: number[], + magResponseOut: number[], + phaseResponseOut: number[] + ): void; } export type ContextState = 'running' | 'closed' | 'suspended';