From ad7841955fd4fb328d89cda56cdbc30a7ec72cf1 Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Wed, 21 Oct 2015 10:34:49 -0500 Subject: [PATCH] Comb: initial leg-work. See #54. --- includes/Jamoma.h | 1 + includes/library/JamomaComb.h | 166 ++++++++++++++++++++++++++++++++++ tests/Comb/CMakeLists.txt | 8 ++ tests/Comb/Comb.cpp | 123 +++++++++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 includes/library/JamomaComb.h create mode 100755 tests/Comb/CMakeLists.txt create mode 100644 tests/Comb/Comb.cpp diff --git a/includes/Jamoma.h b/includes/Jamoma.h index 37d0f76..87e80d5 100644 --- a/includes/Jamoma.h +++ b/includes/Jamoma.h @@ -131,6 +131,7 @@ constexpr inline uint32_t Hash(const char *const str, const uint32_t seed = 0xAE // Units #include "JamomaAllpass1.h" +#include "JamomaComb.h" #include "JamomaDcblock.h" #include "JamomaGain.h" #include "JamomaLowpassOnePole.h" diff --git a/includes/library/JamomaComb.h b/includes/library/JamomaComb.h new file mode 100644 index 0000000..730d4cd --- /dev/null +++ b/includes/library/JamomaComb.h @@ -0,0 +1,166 @@ +/** @file + + @ingroup jamoma2 + + @brief IIR comb filter + + @author Timothy Place + @copyright Copyright (c) 2005-2015 The Jamoma Group, http://jamoma.org. + @license This project is released under the terms of the MIT License. + */ + +#pragma once + +#include "JamomaAudioObject.h" +#include "JamomaLowpassOnepole.h" + +namespace Jamoma { + + + /** This AudioObject implements an IIR comb filter with an additional lowpass filter in the feedback loop. + The result is a comb filter that is warmer or "less tinny" than the typical comb filter. + This filter is one of the key building blocks in for the TapVerb effect. + */ + class Comb : public AudioObject { + + + const std::size_t mCapacity; +// CircularSampleBufferGroup mFeedforwardHistory; +// Delay mFeedbackBuffer; + CircularSampleBufferGroup mFeedbackHistory; + LowpassOnePole mLowpassFilter; + Observer mChannelCountObserver = { std::bind(&Comb::resizeHistory, this) }; + + void resizeHistory() { + if ((mFeedbackHistory.size() && mFeedbackHistory[0].size() != size+frameCount) || mFeedbackHistory.size() != (size_t)channelCount) { + mFeedbackHistory.clear(); // ugly: doing this to force the reconstruction of the storage to the correct size + mFeedbackHistory.resize(channelCount, std::make_pair(mCapacity+frameCount, (size_t)size+frameCount)); + } + } + + + public: + static constexpr Classname classname = { "comb" }; + static constexpr auto tags = { "dspFilterLib", "audio", "processor", "filter", "comb" }; + + + /** TODO: Make capacity dynamic? It already sort of is when the channel count changes... + */ + Comb(std::size_t capacity = 4410) + : mCapacity(capacity) + , mFeedbackHistory(1, capacity) + { + channelCount.addObserver(mChannelCountObserver); + } + + + /** size of the history buffers -- i.e. the delay time + TODO: dataspace integration for units other than samples + */ + Parameter size = { this, "size", 1, + [this]{ + for (auto& channel : mFeedbackHistory) + channel.resize((int)size); + } + }; + + + /** Delay time. + An alias of the #size parameter. + TODO: dataspace with Native Unit in samples + */ + Parameter delay = { this, "delay", 1, [this] { size = (int)delay; } }; + + + /** Decay time -- is this really a dataspace conversion of the feedback coefficient? maybe not... + */ + Parameter decay = { this, "decay", 0.0, + [this]{ + // mDeciResonance = resonance * 0.1; + // calculateCoefficients(); + } + }; + + + /** Feedback coefficient. + */ + Parameter feedback = { this, "feedback", 0.0, + [this]{ +// mDeciResonance = resonance * 0.1; +// calculateCoefficients(); + } + }; + + + /** Clip the feedback internally to prevent the possibility of the filter blowing-up. + */ + Parameter clip = {this, "clip", true, }; + + + /** Cutoff Frequency for the internal lowpass filter + */ + Parameter cutoff = { this, "cutoff", 0.0, + [this]{ + mLowpassFilter.frequency = (double)cutoff; + // mDeciResonance = resonance * 0.1; + // calculateCoefficients(); + } + }; + + + + Message setDelayIndependently = { "setDelayIndependently", + Synopsis("Set the delay parameter without recalculating decay times or other dependent parameters."), + [this]{ + // TODO: implement + ; + } + }; + + + /** This algorithm is an IIR filter, meaning that it relies on feedback. If the filter should + not be producing any signal (such as turning audio off and then back on in a host) or if the + feedback has become corrupted (such as might happen if a NaN is fed in) then it may be + neccesary to clear the filter by calling this method. + */ + Message clear = { "clear", + Synopsis("Reset the Filter History"), + [this]{ + mFeedbackHistory.clear(); + mLowpassFilter.clear(); + } + }; + + + + + + Sample operator()(Sample x) + { + return (*this)(x, 0); + } + + + Sample operator()(Sample x, int channel) + { + Sample y = x; + + return y; + } + + + SharedSampleBundleGroup operator()(const SampleBundle& x) + { + auto out = adapt(x); + + for (int channel=0; channel < x.channelCount(); ++channel) { + for (int i=0; i < x.frameCount(); ++i) + out[0][channel][i] = (*this)(x[channel][i], channel); + } + return out; + } + + }; + + +} // namespace Jamoma diff --git a/tests/Comb/CMakeLists.txt b/tests/Comb/CMakeLists.txt new file mode 100755 index 0000000..53a990a --- /dev/null +++ b/tests/Comb/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.0) + + +project(Comb) + +include(${PROJECT_SOURCE_DIR}/../UnitTest.cmake NO_POLICY_SCOPE) + + diff --git a/tests/Comb/Comb.cpp b/tests/Comb/Comb.cpp new file mode 100644 index 0000000..ad5e3de --- /dev/null +++ b/tests/Comb/Comb.cpp @@ -0,0 +1,123 @@ +/** @file + @ingroup jamoma2 + + @brief Unit test for the Comb class + + @author Timothy Place + @copyright Copyright (c) 2005-2015 The Jamoma Group, http://jamoma.org. + @license This project is released under the terms of the MIT License. + + */ + +#include "Jamoma.h" + + +class CombTest { + + Jamoma::UnitTest* mTest; + +public: + CombTest(Jamoma::UnitTest* test) + : mTest(test) + { + testImpulseResponse(); + } + + + void testImpulseResponse() + { + Jamoma::Comb my_comb; + + my_comb.sampleRate = 44100; + my_comb.delay = 22; // 22 samples == 0.5 ms @ f_s = 44100 + my_comb.feedback = 0.9; // + my_comb.cutoff = 1.0; // normalized freq (no lowpass) + + Jamoma::UnitImpulse impulse; + + impulse.channelCount = 1; + impulse.frameCount = 64; + + auto out_samples = my_comb( impulse() ); + + + // coefficients calculated in Max using the "Comb" tab of the filterdetail object help patcher: + // a = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; % numerator (fir) + // b = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.9]; % denominator (iir) + // i = impz(a, b, 64); + + + // Max's filterdetail produces: + Jamoma::SampleVector expectedIR = { + 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.81, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.729, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.6561, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.59049, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.531441, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.478297, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.430467, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.38742, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.348678, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.313811, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.28243, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.254187, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.228768, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.205891, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.185302, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.166772, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.150095, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.135085, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.121577, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.109419, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.098477, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 , + 0.088629, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 + }; + + int badSampleCount = 0; + Jamoma::Sample temp = 0.0; + Jamoma::Sample tempExpected = 0.0; + + for (int i = 0; i < expectedIR.size(); i++) { + temp = out_samples[0][0][i]; + tempExpected = expectedIR[i]; + if (! mTest->compare(temp, tempExpected) ) { + badSampleCount++; + std::cout << "sample " << i << " had a difference of " << std::fabs(temp - tempExpected) << std::endl; + } + } + + std::cout << "the impulse response of my_comb has " << badSampleCount << " bad samples" << std::endl; + mTest->TEST_ASSERT("Bad Sample Count", badSampleCount == 0); + + + + // Test range limiting +// my_lowpass.frequency = 100.0; +// mTest->TEST_ASSERT("frequency setting is correct", my_lowpass.frequency == 100.0); +// +// my_lowpass.frequency = 5.0; +// mTest->TEST_ASSERT("low frequency is clipped", my_lowpass.frequency == 20.0); +// +// // TODO: boundaries for this object need to change when the sampleRate changes -- currently they don't! +// // So we do this test with the initial sampleRate instead of with `my_lowpass.sampleRate` +// my_lowpass.frequency = 100000; +// mTest->TEST_ASSERT("high frequency is clipped", my_lowpass.frequency < 96000 * 0.5); +// +// // resonance is not clipped at the moment, so we can do irrational bad things... we should change this +// my_lowpass.resonance = 100.0; +// mTest->TEST_ASSERT("insane resonance is not clipped", my_lowpass.resonance == 100.0); +// +// my_lowpass.resonance = -5.0; +// mTest->TEST_ASSERT("negative resonance is not clipped", my_lowpass.resonance == -5.0); + } + +}; + + +int main(int argc, const char * argv[]) +{ + Jamoma::UnitTest aUnitTestInstance; + return aUnitTestInstance.failureCount(); +}