diff --git a/CHANGELOG.md b/CHANGELOG.md index 1260c86f..6a4b2c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [UNRELEASED] +- Updated "Poly Octave" DSP to sound cleaner and use less CPU + ## [1.3.0] - 2024-03-15 - Added "Laser Trem" module. - Added "Poly Octave" module. diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fea0aa0..4ce5c948 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,9 +4,9 @@ if(WIN32) set(CMAKE_SYSTEM_VERSION 7.1 CACHE STRING INTERNAL FORCE) # Windows SDK for Windows 7 and up endif() if(APPLE AND (CMAKE_SYSTEM_NAME STREQUAL "iOS")) - project(BYOD VERSION 2.1.0) + project(BYOD VERSION 2.1.1) else() - project(BYOD VERSION 1.3.0) + project(BYOD VERSION 1.3.1) endif() set(CMAKE_CXX_STANDARD 20) diff --git a/res/presets/OctaVerb.chowpreset b/res/presets/OctaVerb.chowpreset index 0c3bafb5..4fd96358 100644 --- a/res/presets/OctaVerb.chowpreset +++ b/res/presets/OctaVerb.chowpreset @@ -1,13 +1,14 @@ - - + + + diff --git a/src/processors/other/poly_octave/PolyOctave.cpp b/src/processors/other/poly_octave/PolyOctave.cpp index 99c9ab11..3057c175 100644 --- a/src/processors/other/poly_octave/PolyOctave.cpp +++ b/src/processors/other/poly_octave/PolyOctave.cpp @@ -8,6 +8,7 @@ const String dryTag = "dry"; const String upOctaveTag = "up_octave"; const String up2OctaveTag = "up2_octave"; const String downOctaveTag = "down_octave"; +const String v1Tag = "v1_mode"; } // namespace PolyOctaveTags PolyOctave::PolyOctave (UndoManager* um) @@ -34,6 +35,9 @@ PolyOctave::PolyOctave (UndoManager* um) setupGainParam (PolyOctaveTags::upOctaveTag, upOctaveGain); setupGainParam (PolyOctaveTags::up2OctaveTag, up2OctaveGain); + ParameterHelpers::loadParameterPointer (v1_mode, vts, PolyOctaveTags::v1Tag); + addPopupMenuParameter (PolyOctaveTags::v1Tag); + uiOptions.backgroundColour = Colour { 0xff9fa09d }; uiOptions.powerColour = Colour { 0xffe70510 }; uiOptions.info.description = "A \"polyphonic octave generator\" effect."; @@ -49,6 +53,7 @@ ParamLayout PolyOctave::createParameterLayout() createPercentParameter (params, PolyOctaveTags::dryTag, "+0 Oct", 0.5f); createPercentParameter (params, PolyOctaveTags::upOctaveTag, "+1 Oct", 0.0f); createPercentParameter (params, PolyOctaveTags::up2OctaveTag, "+2 Oct", 0.0f); + emplace_param (params, PolyOctaveTags::v1Tag, "V1 Mode", false); // @TODO: version streaming return { params.begin(), params.end() }; } @@ -61,14 +66,22 @@ void PolyOctave::prepare (double sampleRate, int samplesPerBlock) downOctaveGain.prepare (sampleRate, samplesPerBlock); doubleBuffer.setMaxSize (2, samplesPerBlock); - up2OctaveBuffer.setMaxSize (2, static_cast (ComplexERBFilterBank::float_2::size) * samplesPerBlock); // allocate extra space for SIMD - upOctaveBuffer.setMaxSize (2, static_cast (ComplexERBFilterBank::float_2::size) * samplesPerBlock); // allocate extra space for SIMD - downOctaveBuffer.setMaxSize (2, samplesPerBlock); - - FilterBankHelpers::designFilterBank (octaveUpFilterBank, 2.0, 5.0, 6.0, sampleRate); - FilterBankHelpers::designFilterBank (octaveUp2FilterBank, 3.0, 7.0, 4.0, sampleRate); + up2OctaveBuffer_double.setMaxSize (2, 2 * samplesPerBlock); // allocate extra space for SIMD + upOctaveBuffer_double.setMaxSize (2, 2 * samplesPerBlock); // allocate extra space for SIMD + downOctaveBuffer_double.setMaxSize (2, samplesPerBlock); + poly_octave_v2::design_filter_bank (octaveUpFilterBank, 2.0, 5.0, 4.5, sampleRate); + poly_octave_v2::design_filter_bank (octaveUp2FilterBank, 3.0, 6.0, 2.75, sampleRate); for (auto& shifter : downOctavePitchShifters) + { + shifter.prepare (sampleRate); + shifter.set_pitch_factor (0.5f); + } + + poly_octave_v1::designFilterBank (octaveUpFilterBank_v1, 2.0, 5.0, 6.0, sampleRate); + poly_octave_v1::designFilterBank (octaveUp2FilterBank_v1, 3.0, 7.0, 4.0, sampleRate); + + for (auto& shifter : downOctavePitchShifters_v1) { shifter.prepare (sampleRate); shifter.set_pitch_factor (0.5); @@ -81,51 +94,117 @@ void PolyOctave::prepare (double sampleRate, int samplesPerBlock) } mixOutBuffer.setSize (2, samplesPerBlock); - up1OutBuffer.setSize (2, samplesPerBlock); - up2OutBuffer.setSize (2, samplesPerBlock); - down1OutBuffer.setSize (2, samplesPerBlock); + up1OutBuffer.setSize (2, 4 * samplesPerBlock + 8); // padding for SIMD + up2OutBuffer.setSize (2, 4 * samplesPerBlock + 8); // padding for SIMD + down1OutBuffer.setSize (2, 4 * samplesPerBlock + 8); // padding for SIMD } void PolyOctave::processAudio (AudioBuffer& buffer) +{ + if (v1_mode->get()) + { + processAudioV1 (buffer); + return; + } + + const auto numChannels = buffer.getNumChannels(); + const auto numSamples = buffer.getNumSamples(); + + mixOutBuffer.setSize (numChannels, numSamples, false, false, true); + up1OutBuffer.setSize (numChannels, numSamples, false, false, true); + up2OutBuffer.setSize (numChannels, numSamples, false, false, true); + down1OutBuffer.setSize (numChannels, numSamples, false, false, true); + + // "down" processing + for (auto [ch, data_in, data_out] : chowdsp::buffer_iters::zip_channels (std::as_const (buffer), down1OutBuffer)) + { + for (auto [x, y] : chowdsp::zip (data_in, data_out)) + y = downOctavePitchShifters[(size_t) ch].process_sample (x); + } + downOctaveGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (down1OutBuffer, downOctaveGain); + + // "up1" processing + for (auto [ch, data_in, data_out] : chowdsp::buffer_iters::zip_channels (std::as_const (buffer), up1OutBuffer)) + { + poly_octave_v2::process<1> (octaveUpFilterBank[ch], + data_in.data(), + data_out.data(), + numSamples); + } + upOctaveGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (up1OutBuffer, upOctaveGain); + + // "up2" processing + for (auto [ch, data_in, data_out] : chowdsp::buffer_iters::zip_channels (std::as_const (buffer), up2OutBuffer)) + { + poly_octave_v2::process<2> (octaveUp2FilterBank[ch], + data_in.data(), + data_out.data(), + numSamples); + } + up2OctaveGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (up2OutBuffer, up2OctaveGain); + + chowdsp::BufferMath::copyBufferData (buffer, mixOutBuffer); + dryGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (mixOutBuffer, dryGain); + + chowdsp::BufferMath::addBufferData (up1OutBuffer, mixOutBuffer); + chowdsp::BufferMath::addBufferData (up2OutBuffer, mixOutBuffer); + chowdsp::BufferMath::addBufferData (down1OutBuffer, mixOutBuffer); + + dcBlocker[MixOutput].processBlock (mixOutBuffer); + dcBlocker[Up1Output].processBlock (up1OutBuffer); + dcBlocker[Up2Output].processBlock (up2OutBuffer); + dcBlocker[Down1Output].processBlock (down1OutBuffer); + + outputBuffers.getReference (MixOutput) = &mixOutBuffer; + outputBuffers.getReference (Up1Output) = &up1OutBuffer; + outputBuffers.getReference (Up2Output) = &up2OutBuffer; + outputBuffers.getReference (Down1Output) = &down1OutBuffer; +} + +void PolyOctave::processAudioV1 (AudioBuffer& buffer) { const auto numChannels = buffer.getNumChannels(); const auto numSamples = buffer.getNumSamples(); doubleBuffer.setCurrentSize (numChannels, numSamples); - upOctaveBuffer.setCurrentSize (numChannels, numSamples); - up2OctaveBuffer.setCurrentSize (numChannels, numSamples); - downOctaveBuffer.setCurrentSize (numChannels, numSamples); + upOctaveBuffer_double.setCurrentSize (numChannels, numSamples); + up2OctaveBuffer_double.setCurrentSize (numChannels, numSamples); + downOctaveBuffer_double.setCurrentSize (numChannels, numSamples); chowdsp::BufferMath::copyBufferData (buffer, doubleBuffer); // "down" processing - for (auto [ch, data_in, data_out] : chowdsp::buffer_iters::zip_channels (std::as_const (doubleBuffer), downOctaveBuffer)) + for (auto [ch, data_in, data_out] : chowdsp::buffer_iters::zip_channels (std::as_const (doubleBuffer), downOctaveBuffer_double)) { for (auto [x, y] : chowdsp::zip (data_in, data_out)) - y = downOctavePitchShifters[(size_t) ch].process_sample (x); + y = downOctavePitchShifters_v1[(size_t) ch].process_sample (x); } // "up" processing - using float_2 = ComplexERBFilterBank::float_2; for (int ch = 0; ch < numChannels; ++ch) { + using poly_octave_v1::float_2; auto* dryData = doubleBuffer.getReadPointer (ch); - auto* upData = upOctaveBuffer.getWritePointer (ch); - auto* upDataSIMD = reinterpret_cast (upOctaveBuffer.getWritePointer (ch)); + auto* upData = upOctaveBuffer_double.getWritePointer (ch); + auto* upDataSIMD = reinterpret_cast (upOctaveBuffer_double.getWritePointer (ch)); jassert (juce::snapPointerToAlignment (upDataSIMD, xsimd::default_arch::alignment()) == upDataSIMD); std::fill (upDataSIMD, upDataSIMD + numSamples, float_2 {}); - auto* up2Data = up2OctaveBuffer.getWritePointer (ch); - auto* up2DataSIMD = reinterpret_cast (up2OctaveBuffer.getWritePointer (ch)); + auto* up2Data = up2OctaveBuffer_double.getWritePointer (ch); + auto* up2DataSIMD = reinterpret_cast (up2OctaveBuffer_double.getWritePointer (ch)); jassert (juce::snapPointerToAlignment (up2DataSIMD, xsimd::default_arch::alignment()) == up2DataSIMD); std::fill (up2DataSIMD, up2DataSIMD + numSamples, float_2 {}); - auto& upFilterBank = octaveUpFilterBank[static_cast (ch)]; - auto& up2FilterBank = octaveUp2FilterBank[static_cast (ch)]; + auto& upFilterBank = octaveUpFilterBank_v1[static_cast (ch)]; + auto& up2FilterBank = octaveUp2FilterBank_v1[static_cast (ch)]; static constexpr auto eps = std::numeric_limits::epsilon(); - for (size_t k = 0; k < ComplexERBFilterBank::numFilterBands; k += float_2::size) + for (size_t k = 0; k < poly_octave_v1::ComplexERBFilterBank::numFilterBands; k += float_2::size) { const auto filter_idx = k / float_2::size; auto& realFilter = upFilterBank.erbFilterReal[filter_idx]; @@ -134,8 +213,8 @@ void PolyOctave::processAudio (AudioBuffer& buffer) chowdsp::ScopedValue z_im { imagFilter.z[0] }; for (int n = 0; n < numSamples; ++n) { - auto x_re = FilterBankHelpers::processSample (realFilter, z_re.get(), dryData[n]); - auto x_im = FilterBankHelpers::processSample (imagFilter, z_im.get(), dryData[n]); + auto x_re = poly_octave_v1::processSample (realFilter, z_re.get(), dryData[n]); + auto x_im = poly_octave_v1::processSample (imagFilter, z_im.get(), dryData[n]); auto x_re_sq = x_re * x_re; auto x_im_sq = x_im * x_im; @@ -153,7 +232,7 @@ void PolyOctave::processAudio (AudioBuffer& buffer) for (int n = 0; n < numSamples; ++n) upData[n] = xsimd::reduce_add (upDataSIMD[n]); - for (size_t k = 0; k < ComplexERBFilterBank::numFilterBands; k += float_2::size) + for (size_t k = 0; k < poly_octave_v1::ComplexERBFilterBank::numFilterBands; k += float_2::size) { const auto filter_idx = k / float_2::size; auto& realFilter = up2FilterBank.erbFilterReal[filter_idx]; @@ -163,8 +242,8 @@ void PolyOctave::processAudio (AudioBuffer& buffer) for (int n = 0; n < numSamples; ++n) { - auto x_re = FilterBankHelpers::processSample (realFilter, z_re.get(), dryData[n]); - auto x_im = FilterBankHelpers::processSample (imagFilter, z_im.get(), dryData[n]); + auto x_re = poly_octave_v1::processSample (realFilter, z_re.get(), dryData[n]); + auto x_im = poly_octave_v1::processSample (imagFilter, z_im.get(), dryData[n]); auto x_re_sq = x_re * x_re; auto x_im_sq = x_im * x_im; @@ -178,23 +257,9 @@ void PolyOctave::processAudio (AudioBuffer& buffer) up2Data[n] = xsimd::reduce_add (up2DataSIMD[n]); } - chowdsp::BufferMath::applyGain (upOctaveBuffer, 2.0 / static_cast (ComplexERBFilterBank::numFilterBands)); - upOctaveGain.process (numSamples); - chowdsp::BufferMath::applyGainSmoothedBuffer (upOctaveBuffer, upOctaveGain); - - chowdsp::BufferMath::applyGain (up2OctaveBuffer, 2.0 / static_cast (ComplexERBFilterBank::numFilterBands)); - up2OctaveGain.process (numSamples); - chowdsp::BufferMath::applyGainSmoothedBuffer (up2OctaveBuffer, up2OctaveGain); - - chowdsp::BufferMath::applyGain (downOctaveBuffer, juce::Decibels::decibelsToGain (3.0f)); - downOctaveGain.process (numSamples); - chowdsp::BufferMath::applyGainSmoothedBuffer (downOctaveBuffer, downOctaveGain); - - dryGain.process (numSamples); - chowdsp::BufferMath::applyGainSmoothedBuffer (doubleBuffer, dryGain); - chowdsp::BufferMath::addBufferData (up2OctaveBuffer, doubleBuffer); - chowdsp::BufferMath::addBufferData (upOctaveBuffer, doubleBuffer); - chowdsp::BufferMath::addBufferData (downOctaveBuffer, doubleBuffer); + chowdsp::BufferMath::applyGain (upOctaveBuffer_double, 2.0 / static_cast (poly_octave_v1::ComplexERBFilterBank::numFilterBands)); + chowdsp::BufferMath::applyGain (up2OctaveBuffer_double, 2.0 / static_cast (poly_octave_v1::ComplexERBFilterBank::numFilterBands)); + chowdsp::BufferMath::applyGain (downOctaveBuffer_double, juce::Decibels::decibelsToGain (3.0f)); mixOutBuffer.setSize (numChannels, numSamples, false, false, true); up1OutBuffer.setSize (numChannels, numSamples, false, false, true); @@ -202,9 +267,22 @@ void PolyOctave::processAudio (AudioBuffer& buffer) down1OutBuffer.setSize (numChannels, numSamples, false, false, true); chowdsp::BufferMath::copyBufferData (doubleBuffer, mixOutBuffer); - chowdsp::BufferMath::copyBufferData (upOctaveBuffer, up1OutBuffer); - chowdsp::BufferMath::copyBufferData (up2OctaveBuffer, up2OutBuffer); - chowdsp::BufferMath::copyBufferData (downOctaveBuffer, down1OutBuffer); + chowdsp::BufferMath::copyBufferData (upOctaveBuffer_double, up1OutBuffer); + chowdsp::BufferMath::copyBufferData (up2OctaveBuffer_double, up2OutBuffer); + chowdsp::BufferMath::copyBufferData (downOctaveBuffer_double, down1OutBuffer); + + upOctaveGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (up1OutBuffer, upOctaveGain); + up2OctaveGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (up2OutBuffer, up2OctaveGain); + downOctaveGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (down1OutBuffer, downOctaveGain); + + dryGain.process (numSamples); + chowdsp::BufferMath::applyGainSmoothedBuffer (mixOutBuffer, dryGain); + chowdsp::BufferMath::addBufferData (up1OutBuffer, mixOutBuffer); + chowdsp::BufferMath::addBufferData (up2OutBuffer, mixOutBuffer); + chowdsp::BufferMath::addBufferData (down1OutBuffer, mixOutBuffer); dcBlocker[MixOutput].processBlock (mixOutBuffer); dcBlocker[Up1Output].processBlock (up1OutBuffer); @@ -256,3 +334,19 @@ String PolyOctave::getTooltipForPort (int portIndex, bool isInput) return BaseProcessor::getTooltipForPort (portIndex, isInput); } + +void PolyOctave::fromXML (XmlElement* xml, const chowdsp::Version& version, bool loadPosition) +{ + BaseProcessor::fromXML (xml, version, loadPosition); + + using namespace std::string_view_literals; +#if JUCE_IOS + if (version <= chowdsp::Version { "2.1.0"sv }) +#else + if (version <= chowdsp::Version { "1.3.0"sv }) +#endif + { + // For versions before 1.3.0, default to v1 mode + v1_mode->setValueNotifyingHost (1.0f); + } +} diff --git a/src/processors/other/poly_octave/PolyOctave.h b/src/processors/other/poly_octave/PolyOctave.h index 08ebf96c..2ef85d2c 100644 --- a/src/processors/other/poly_octave/PolyOctave.h +++ b/src/processors/other/poly_octave/PolyOctave.h @@ -1,6 +1,7 @@ #pragma once #include "DelayPitchShifter.h" +#include "PolyOctaveFilterBankTypes.h" #include "processors/BaseProcessor.h" class PolyOctave : public BaseProcessor @@ -15,14 +16,9 @@ class PolyOctave : public BaseProcessor void processAudio (AudioBuffer& buffer) override; void processAudioBypassed (AudioBuffer& buffer) override; - String getTooltipForPort (int portIndex, bool isInput) override; + void fromXML (XmlElement* xml, const chowdsp::Version& version, bool loadPosition) override; - struct ComplexERBFilterBank - { - static constexpr size_t numFilterBands = 44; - using float_2 = xsimd::batch; - std::array, numFilterBands / float_2::size> erbFilterReal, erbFilterImag; - }; + String getTooltipForPort (int portIndex, bool isInput) override; enum OutputPort { @@ -35,19 +31,28 @@ class PolyOctave : public BaseProcessor static constexpr auto numOutputs = (int) magic_enum::enum_count(); private: - chowdsp::SmoothedBufferValue dryGain {}; - chowdsp::SmoothedBufferValue upOctaveGain {}; - chowdsp::SmoothedBufferValue up2OctaveGain {}; - chowdsp::SmoothedBufferValue downOctaveGain {}; + void processAudioV1 (AudioBuffer& buffer); - chowdsp::Buffer doubleBuffer; - chowdsp::Buffer upOctaveBuffer; - chowdsp::Buffer up2OctaveBuffer; - chowdsp::Buffer downOctaveBuffer; + chowdsp::BoolParameter* v1_mode = nullptr; + + chowdsp::SmoothedBufferValue dryGain {}; + chowdsp::SmoothedBufferValue upOctaveGain {}; + chowdsp::SmoothedBufferValue up2OctaveGain {}; + chowdsp::SmoothedBufferValue downOctaveGain {}; - std::array octaveUpFilterBank; - std::array octaveUp2FilterBank; - std::array, 2> downOctavePitchShifters; + // V2 processing stuff... + std::array, 2> octaveUpFilterBank; + std::array, 2> octaveUp2FilterBank; + std::array, 2> downOctavePitchShifters; + + // V1 processing stuff... + chowdsp::Buffer doubleBuffer; + chowdsp::Buffer upOctaveBuffer_double; + chowdsp::Buffer up2OctaveBuffer_double; + chowdsp::Buffer downOctaveBuffer_double; + std::array octaveUpFilterBank_v1; + std::array octaveUp2FilterBank_v1; + std::array, 2> downOctavePitchShifters_v1; std::array, (size_t) numOutputs> dcBlocker; diff --git a/src/processors/other/poly_octave/PolyOctaveFilterBandHelpers.h b/src/processors/other/poly_octave/PolyOctaveFilterBandHelpers.h index 02d02a36..e1d8553b 100644 --- a/src/processors/other/poly_octave/PolyOctaveFilterBandHelpers.h +++ b/src/processors/other/poly_octave/PolyOctaveFilterBandHelpers.h @@ -1,13 +1,12 @@ #pragma once -#include "PolyOctave.h" +#include "PolyOctaveFilterBankTypes.h" -namespace FilterBankHelpers +namespace poly_octave_v1 { // Reference for filter-bank design and octave shifting: // https://aaltodoc.aalto.fi/server/api/core/bitstreams/ff9e52cf-fd79-45eb-b695-93038244ec0e/content -using float_2 = PolyOctave::ComplexERBFilterBank::float_2; static_assert (float_2::size == 2); static_assert (xsimd::batch::size == 2); static constexpr auto vec_size = float_2::size; @@ -89,13 +88,13 @@ static auto designERBFilter (size_t erb_index, return center_target_freq; } -static void designFilterBank (std::array& filterBank, +static void designFilterBank (std::array& filterBank, double gamma, double erb_start, double q_ERB, double sampleRate) { - for (size_t kiter = 0; kiter < PolyOctave::ComplexERBFilterBank::numFilterBands; kiter += vec_size) + for (size_t kiter = 0; kiter < ComplexERBFilterBank::numFilterBands; kiter += vec_size) { alignas (16) std::array b_coeffs_cplx_real_simd[5]; alignas (16) std::array b_coeffs_cplx_imag_simd[5]; @@ -160,4 +159,178 @@ static void designFilterBank (std::array& f } } } -} // namespace FilterBankHelpers +} // namespace poly_octave_v1 + +namespace poly_octave_v2 +{ +// Reference for filter-bank design and octave shifting: +// https://aaltodoc.aalto.fi/server/api/core/bitstreams/ff9e52cf-fd79-45eb-b695-93038244ec0e/content + +inline std::pair process_sample (const T& x, + const std::array& b_shared_coeffs, + const std::array& b_real_coeffs, + const std::array& b_imag_coeffs, + const std::array& a_coeffs, + std::array& z_shared, + std::array& z_real, + std::array& z_imag) +{ + const auto y_shared = z_shared[1] + x * b_shared_coeffs[0]; + z_shared[1] = z_shared[2] + x * b_shared_coeffs[1] - y_shared * a_coeffs[1]; + z_shared[2] = x * b_shared_coeffs[2] - y_shared * a_coeffs[2]; + + const auto y_real = z_real[1] + y_shared; // for the real filter, we know that b[0] == 1 + z_real[1] = z_real[2] + y_shared * b_real_coeffs[1] - y_real * a_coeffs[1]; + z_real[2] = y_shared * b_real_coeffs[2] - y_real * a_coeffs[2]; + + const auto y_imag = z_imag[1]; // for the imaginary filter, we know that b[0] == 0 + z_imag[1] = z_imag[2] + y_shared * b_imag_coeffs[1] - y_imag * a_coeffs[1]; + z_imag[2] = y_shared * b_imag_coeffs[2] - y_imag * a_coeffs[2]; + + return { y_real, y_imag }; +} + +static constexpr auto q_c = 4.0; +static auto design_erb_filter (size_t erb_index, + double gamma, + double erb_start, + double q_ERB, + double sample_rate, + double (&b_coeffs_cplx_shared)[3], + double (&b_coeffs_cplx_real)[3], + double (&b_coeffs_cplx_imag)[3], + double (&a_coeffs_cplx)[3]) +{ + const auto q_PS = gamma; + + const auto z = erb_start + static_cast (erb_index) * (q_c / q_ERB); + const auto center_target_freq = 228.7 * (std::pow (10.0, z / 21.3) - 1.0); + const auto filter_q = (1.0 / (q_PS * q_ERB)) * (24.7 + 0.108 * center_target_freq); + + double a_coeffs_proto[3]; + chowdsp::CoefficientCalculators::calcSecondOrderBPF (b_coeffs_cplx_shared, + a_coeffs_proto, + center_target_freq / gamma, + filter_q * 0.5, + sample_rate); + + auto pole = (std::sqrt (std::pow (std::complex { a_coeffs_proto[1] }, 2.0) - 4.0 * a_coeffs_proto[2]) - a_coeffs_proto[1]) / 2.0; + if (std::imag (pole) < 0.0) + pole = std::conj (pole); + const auto pr = std::real (pole); + const auto pi = std::imag (pole); + + // a[] = 1 - 2 pr z + (pi^2 + pr^2) z^2 + a_coeffs_cplx[0] = 1.0; + a_coeffs_cplx[1] = -2.0 * pr; + a_coeffs_cplx[2] = pi * pi + pr * pr; + + // b_real[] = 1 - 2 pr z + (-pi^2 + pr^2) z^2 + b_coeffs_cplx_real[0] = 1.0; + b_coeffs_cplx_real[1] = -2.0 * pr; + b_coeffs_cplx_real[2] = -pi * pi + pr * pr; + + // b_imag[] = 2 pi z - 2 pi pr z^2 + b_coeffs_cplx_imag[0] = 0.0; + b_coeffs_cplx_imag[1] = 2.0 * pi; + b_coeffs_cplx_imag[2] = -2.0 * pi * pr; + + return center_target_freq; +} + +template +static void design_filter_bank (std::array, 2>& filter_bank, + double gamma, + double erb_start, + double q_ERB, + double sample_rate) +{ + for (size_t kiter = 0; kiter < ComplexERBFilterBank::num_filter_bands; ++kiter) + { + double b_coeffs_cplx_shared_double[3] {}; + double b_coeffs_cplx_real_double[3] {}; + double b_coeffs_cplx_imag_double[3] {}; + double a_coeffs_cplx_double[3] {}; + design_erb_filter (kiter, + gamma, + erb_start, + q_ERB, + sample_rate, + b_coeffs_cplx_shared_double, + b_coeffs_cplx_real_double, + b_coeffs_cplx_imag_double, + a_coeffs_cplx_double); + + for (auto& bank : filter_bank) + { + const auto k_div = kiter / T::size; + const auto k_off = kiter - (k_div * T::size); + + bank.erb_filter_complex[k_div].z_shared = {}; + bank.erb_filter_complex[k_div].z_real = {}; + bank.erb_filter_complex[k_div].z_imag = {}; + + for (size_t i = 0; i < 3; ++i) + { + reinterpret_cast (&bank.erb_filter_complex[k_div].b_shared_coeffs[i])[k_off] = static_cast (b_coeffs_cplx_shared_double[i]); + reinterpret_cast (&bank.erb_filter_complex[k_div].b_real_coeffs[i])[k_off] = static_cast (b_coeffs_cplx_real_double[i]); + reinterpret_cast (&bank.erb_filter_complex[k_div].b_imag_coeffs[i])[k_off] = static_cast (b_coeffs_cplx_imag_double[i]); + reinterpret_cast (&bank.erb_filter_complex[k_div].a_coeffs[i])[k_off] = static_cast (a_coeffs_cplx_double[i]); + } + } + } +} + +template +static void process (ComplexERBFilterBank& filter_bank, + const float* buffer_in, + float* buffer_out, + int num_samples) noexcept +{ + // buffer_out size is padded by 4x + static constexpr auto eps = std::numeric_limits::epsilon(); + auto* buffer_out_simd = juce::snapPointerToAlignment (reinterpret_cast (buffer_out), xsimd::default_arch::alignment()); + std::fill (buffer_out_simd, buffer_out_simd + num_samples, T {}); + for (size_t k = 0; k < N; k += T::size) + { + const auto filter_idx = k / T::size; + auto& cplx_filter = filter_bank.erb_filter_complex[filter_idx]; + chowdsp::ScopedValue z_shared { cplx_filter.z_shared }; + chowdsp::ScopedValue z_re { cplx_filter.z_real }; + chowdsp::ScopedValue z_im { cplx_filter.z_imag }; + for (int n = 0; n < num_samples; ++n) + { + const auto x_in = static_cast (buffer_in[n]); + const auto [x_re, x_im] = process_sample (x_in, + cplx_filter.b_shared_coeffs, + cplx_filter.b_real_coeffs, + cplx_filter.b_imag_coeffs, + cplx_filter.a_coeffs, + z_shared.get(), + z_re.get(), + z_im.get()); + + auto x_re_sq = x_re * x_re; + auto x_im_sq = x_im * x_im; + auto x_abs_sq = x_re_sq + x_im_sq; + + if constexpr (num_octaves_up == 1) + { + auto x_abs_r = xsimd::select (x_abs_sq > eps, xsimd::rsqrt (x_abs_sq), {}); + buffer_out_simd[n] += (x_re_sq - x_im_sq) * x_abs_r; + } + else if constexpr (num_octaves_up == 2) + { + auto x_abs_sq_r = xsimd::select (x_abs_sq > eps, xsimd::reciprocal (x_abs_sq), {}); + buffer_out_simd[n] += x_re * (x_re_sq - (S) 3 * x_im_sq) * x_abs_sq_r; + } + } + } + + for (int n = 0; n < num_samples; ++n) + buffer_out[n] = xsimd::reduce_add (buffer_out_simd[n]); + + static constexpr auto norm_gain = 2.0f / static_cast (N); + juce::FloatVectorOperations::multiply (buffer_out, norm_gain, num_samples); +} +} // namespace poly_octave_v2 diff --git a/src/processors/other/poly_octave/PolyOctaveFilterBankTypes.h b/src/processors/other/poly_octave/PolyOctaveFilterBankTypes.h new file mode 100644 index 00000000..fd195caf --- /dev/null +++ b/src/processors/other/poly_octave/PolyOctaveFilterBankTypes.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +namespace poly_octave_v1 +{ +using float_2 = xsimd::batch; +struct ComplexERBFilterBank +{ + static constexpr size_t numFilterBands = 44; + std::array, numFilterBands / float_2::size> erbFilterReal, erbFilterImag; +}; +} // namespace poly_octave_v1 + +namespace poly_octave_v2 +{ +using T = xsimd::batch; +using S = float; +static constexpr auto N1 = 32; +template +struct ComplexERBFilterBank +{ + static constexpr size_t num_filter_bands = N; + + struct ComplexFilter + { + std::array b_shared_coeffs {}; + std::array b_real_coeffs {}; + std::array b_imag_coeffs {}; + std::array a_coeffs {}; + std::array z_shared {}; + std::array z_real {}; + std::array z_imag {}; + }; + + std::array erb_filter_complex; +}; +} // namespace poly_octave_v2