From 422eecb9cd1c402a48bb424966347abe8a45a354 Mon Sep 17 00:00:00 2001 From: jatinchowdhury18 Date: Wed, 17 Jan 2024 22:59:32 -0800 Subject: [PATCH] Add WidthPanner processor class (#483) * Add WidthPanner processor class * Apply clang-format --------- Co-authored-by: github-actions[bot] --- .../Processors/chowdsp_Panner.cpp | 34 ++++++-- .../Processors/chowdsp_Panner.h | 31 ++++--- .../Processors/chowdsp_WidthPanner.h | 85 +++++++++++++++++++ .../dsp/chowdsp_dsp_utils/chowdsp_dsp_utils.h | 1 + .../chowdsp_dsp_utils_test/CMakeLists.txt | 1 + .../WidthPannerTest.cpp | 68 +++++++++++++++ 6 files changed, 198 insertions(+), 22 deletions(-) create mode 100644 modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_WidthPanner.h create mode 100644 tests/dsp_tests/chowdsp_dsp_utils_test/WidthPannerTest.cpp diff --git a/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.cpp b/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.cpp index 0398645d0..91ff620db 100644 --- a/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.cpp +++ b/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.cpp @@ -75,7 +75,7 @@ void Panner::reset() template void Panner::update() { - SampleType leftValue, rightValue, boostValue; + SampleType leftValue, rightValue; auto normalisedPan = static_cast (0.5) * (pan + static_cast (1.0)); @@ -84,56 +84,72 @@ void Panner::update() case Rule::balanced: leftValue = juce::jmin (static_cast (0.5), static_cast (1.0) - normalisedPan); rightValue = juce::jmin (static_cast (0.5), normalisedPan); - boostValue = static_cast (2.0); break; case Rule::linear: leftValue = static_cast (1.0) - normalisedPan; rightValue = normalisedPan; - boostValue = static_cast (2.0); break; case Rule::sin3dB: leftValue = static_cast (std::sin (0.5 * juce::MathConstants::pi * (1.0 - normalisedPan))); rightValue = static_cast (std::sin (0.5 * juce::MathConstants::pi * normalisedPan)); - boostValue = std::sqrt (static_cast (2.0)); break; case Rule::sin4p5dB: leftValue = static_cast (std::pow (std::sin (0.5 * juce::MathConstants::pi * (1.0 - normalisedPan)), 1.5)); rightValue = static_cast (std::pow (std::sin (0.5 * juce::MathConstants::pi * normalisedPan), 1.5)); - boostValue = static_cast (std::pow (2.0, 3.0 / 4.0)); break; case Rule::sin6dB: leftValue = static_cast (std::pow (std::sin (0.5 * juce::MathConstants::pi * (1.0 - normalisedPan)), 2.0)); rightValue = static_cast (std::pow (std::sin (0.5 * juce::MathConstants::pi * normalisedPan), 2.0)); - boostValue = static_cast (2.0); break; case Rule::squareRoot3dB: leftValue = std::sqrt (static_cast (1.0) - normalisedPan); rightValue = std::sqrt (normalisedPan); - boostValue = std::sqrt (static_cast (2.0)); break; case Rule::squareRoot4p5dB: leftValue = static_cast (std::pow (std::sqrt (1.0 - normalisedPan), 1.5)); rightValue = static_cast (std::pow (std::sqrt (normalisedPan), 1.5)); - boostValue = static_cast (std::pow (2.0, 3.0 / 4.0)); break; default: leftValue = juce::jmin (static_cast (0.5), static_cast (1.0) - normalisedPan); rightValue = juce::jmin (static_cast (0.5), normalisedPan); - boostValue = static_cast (2.0); break; } + const auto boostValue = getBoostForRule (currentRule); leftVolume.setTargetValue (leftValue * boostValue); rightVolume.setTargetValue (rightValue * boostValue); } +template +SampleType Panner::getBoostForRule (Rule rule) +{ + switch (rule) + { + case Rule::balanced: + case Rule::linear: + case Rule::sin6dB: + return static_cast (2.0); + + case Rule::sin3dB: + case Rule::squareRoot3dB: + return std::sqrt (static_cast (2.0)); + + case Rule::sin4p5dB: + case Rule::squareRoot4p5dB: + return static_cast (std::pow (2.0, 3.0 / 4.0)); + + default: + return static_cast (2.0); + } +} + //============================================================================== template class Panner; template class Panner; diff --git a/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.h b/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.h index 05d1204c8..ea8ecab25 100644 --- a/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.h +++ b/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_Panner.h @@ -27,6 +27,21 @@ namespace chowdsp { +/** Panning rules: determine the balance of left/right channels at a given pan setting */ +enum class PanningRule +{ + linear, /**< regular 6 dB or linear panning rule, allows the panned sound to be + perceived as having a constant level when summed to mono */ + balanced, /**< both left and right are 1 when pan value is 0.5, with left decreasing + to 0 above this value and right decreasing to 0 below it */ + sin3dB, /**< alternate version of the regular 3 dB panning rule with a sine curve */ + sin4p5dB, /**< alternate version of the regular 4.5 dB panning rule with a sine curve */ + sin6dB, /**< alternate version of the regular 6 dB panning rule with a sine curve */ + squareRoot3dB, /**< regular 3 dB or constant power panning rule, allows the panned sound + to be perceived as having a constant level regardless of the pan position */ + squareRoot4p5dB /**< regular 4.5 dB panning rule, a compromise option between 3 dB and 6 dB panning rules */ +}; + /** A processor to perform panning operations on stereo buffers. @@ -37,19 +52,7 @@ class Panner { public: /** Panning rules: determine the balance of left/right channels at a given pan setting */ - enum class Rule - { - linear, /**< regular 6 dB or linear panning rule, allows the panned sound to be - perceived as having a constant level when summed to mono */ - balanced, /**< both left and right are 1 when pan value is 0.5, with left decreasing - to 0 above this value and right decreasing to 0 below it */ - sin3dB, /**< alternate version of the regular 3 dB panning rule with a sine curve */ - sin4p5dB, /**< alternate version of the regular 4.5 dB panning rule with a sine curve */ - sin6dB, /**< alternate version of the regular 6 dB panning rule with a sine curve */ - squareRoot3dB, /**< regular 3 dB or constant power panning rule, allows the panned sound - to be perceived as having a constant level regardless of the pan position */ - squareRoot4p5dB /**< regular 4.5 dB panning rule, a compromise option between 3 dB and 6 dB panning rules */ - }; + using Rule = PanningRule; //============================================================================== /** Constructor. */ @@ -144,6 +147,8 @@ class Panner return { x * leftVolume.getNextValue(), x * rightVolume.getNextValue() }; } + static SampleType getBoostForRule (Rule rule); + private: //============================================================================== void update(); diff --git a/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_WidthPanner.h b/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_WidthPanner.h new file mode 100644 index 000000000..0e6ca5e34 --- /dev/null +++ b/modules/dsp/chowdsp_dsp_utils/Processors/chowdsp_WidthPanner.h @@ -0,0 +1,85 @@ +#pragma once + +#include "chowdsp_Panner.h" + +namespace chowdsp +{ +/** + * A panner-style processor that affects the "width" of the + * processed signal. + */ +template +class WidthPanner +{ +public: + WidthPanner() + { + setRule (rule); + setPan (1.0f); + } + + /** Sets the panning rule. */ + void setRule (PanningRule newRule) + { + rule = newRule; + leftPanner.setRule (rule); + rightPanner.setRule (rule); + } + + /** Sets the current panning value, between 1 (stereo), 0 (mono) and -1 (inverted stereo). */ + void setPan (SampleType newPan) + { + leftPanner.setPan (-newPan); + rightPanner.setPan (newPan); + } + + /** Initialises the processor. */ + void prepare (const juce::dsp::ProcessSpec& spec) + { + leftPanner.prepare (spec); + rightPanner.prepare (spec); + + leftPanBuffer.setMaxSize (2, static_cast (spec.maximumBlockSize)); + } + + /** Resets the internal state variables of the processor. */ + void reset() + { + leftPanner.reset(); + rightPanner.reset(); + } + + /** Processes a stereo buffer. */ + void processBlock (const BufferView& buffer) noexcept + { + jassert (buffer.getNumChannels() == 2); + + const auto numSamples = buffer.getNumSamples(); + leftPanBuffer.setCurrentSize (2, numSamples); + + // copy left signal into both channels of leftPanBuffer + for (int ch = 0; ch < 2; ++ch) + BufferMath::copyBufferChannels (buffer, leftPanBuffer, 0, ch); + + // copy right signal into both channels of buffer + BufferMath::copyBufferChannels (buffer, buffer, 1, 0); + + leftPanner.processBlock (leftPanBuffer); + rightPanner.processBlock (buffer); + + BufferMath::addBufferData (leftPanBuffer, buffer); + + BufferMath::applyGain (buffer, static_cast (1) / Panner::getBoostForRule (rule)); + } + +private: + PanningRule rule = PanningRule::linear; + + Panner leftPanner; + Panner rightPanner; + + Buffer leftPanBuffer {}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WidthPanner) +}; +} // namespace chowdsp diff --git a/modules/dsp/chowdsp_dsp_utils/chowdsp_dsp_utils.h b/modules/dsp/chowdsp_dsp_utils/chowdsp_dsp_utils.h index e6f56a605..8055fb024 100644 --- a/modules/dsp/chowdsp_dsp_utils/chowdsp_dsp_utils.h +++ b/modules/dsp/chowdsp_dsp_utils/chowdsp_dsp_utils.h @@ -69,6 +69,7 @@ BEGIN_JUCE_MODULE_DECLARATION #include "Processors/chowdsp_Gain.h" #include "Processors/chowdsp_LevelDetector.h" #include "Processors/chowdsp_Panner.h" +#include "Processors/chowdsp_WidthPanner.h" #include "Processors/chowdsp_TunerProcessor.h" #include "Processors/chowdsp_OvershootLimiter.h" diff --git a/tests/dsp_tests/chowdsp_dsp_utils_test/CMakeLists.txt b/tests/dsp_tests/chowdsp_dsp_utils_test/CMakeLists.txt index 204f0dd26..e85206301 100644 --- a/tests/dsp_tests/chowdsp_dsp_utils_test/CMakeLists.txt +++ b/tests/dsp_tests/chowdsp_dsp_utils_test/CMakeLists.txt @@ -9,6 +9,7 @@ target_sources(chowdsp_dsp_utils_test TunerTest.cpp LevelDetectorTest.cpp OvershootLimiterTest.cpp + WidthPannerTest.cpp BBDTest.cpp PitchShiftTest.cpp diff --git a/tests/dsp_tests/chowdsp_dsp_utils_test/WidthPannerTest.cpp b/tests/dsp_tests/chowdsp_dsp_utils_test/WidthPannerTest.cpp new file mode 100644 index 000000000..a63d4d5ef --- /dev/null +++ b/tests/dsp_tests/chowdsp_dsp_utils_test/WidthPannerTest.cpp @@ -0,0 +1,68 @@ +#include +#include + +TEST_CASE ("Width Panner Test", "[dsp][misc]") +{ + const auto spec = juce::dsp::ProcessSpec { 48000.0, 4, 2 }; + + chowdsp::Buffer buffer { 2, 4 }; + for (auto [ch, data] : chowdsp::buffer_iters::channels (buffer)) + std::fill (data.begin(), data.end(), ch == 0 ? -1.0f : 1.0f); + + const auto testPanner = [spec, &buffer] (chowdsp::PanningRule rule, float pan, auto&& getExpected) + { + chowdsp::WidthPanner panner; + panner.setRule (rule); + panner.setPan (pan); + panner.prepare (spec); + panner.processBlock (buffer); + for (auto [ch, data] : chowdsp::buffer_iters::channels (std::as_const (buffer))) + { + const auto expected = getExpected (ch); + for (auto& x : data) + REQUIRE (x == Catch::Approx { expected }.margin (1.0e-3)); + } + }; + + SECTION ("Stereo -> Mono") + { + testPanner (chowdsp::PanningRule::linear, 0.0f, [] (int) + { return 0.0f; }); + testPanner (chowdsp::PanningRule::sin3dB, 0.0f, [] (int) + { return 0.0f; }); + testPanner (chowdsp::PanningRule::sin6dB, 0.0f, [] (int) + { return 0.0f; }); + } + + SECTION ("Stereo -> Stereo") + { + testPanner (chowdsp::PanningRule::linear, 1.0f, [] (int ch) + { return ch == 0 ? -1.0f : 1.0f; }); + testPanner (chowdsp::PanningRule::sin3dB, 1.0f, [] (int ch) + { return ch == 0 ? -1.0f : 1.0f; }); + testPanner (chowdsp::PanningRule::sin6dB, 1.0f, [] (int ch) + { return ch == 0 ? -1.0f : 1.0f; }); + } + + SECTION ("Stereo -> Inverse Stereo") + { + testPanner (chowdsp::PanningRule::sin4p5dB, -1.0f, [] (int ch) + { return ch == 0 ? 1.0f : -1.0f; }); + testPanner (chowdsp::PanningRule::squareRoot3dB, -1.0f, [] (int ch) + { return ch == 0 ? -1.0f : 1.0f; }); + testPanner (chowdsp::PanningRule::squareRoot4p5dB, -1.0f, [] (int ch) + { return ch == 0 ? 1.0f : -1.0f; }); + } + + SECTION ("Stereo -> Half-Mid (Linear)") + { + testPanner (chowdsp::PanningRule::linear, 0.5f, [] (int ch) + { return ch == 0 ? -0.5f : 0.5f; }); + } + + SECTION ("Stereo -> Half-Mid (Sin 6dB)") + { + testPanner (chowdsp::PanningRule::sin6dB, 0.5f, [] (int ch) + { return ch == 0 ? -0.7071f : 0.7071f; }); + } +}