-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Timothy Place
committed
Oct 21, 2015
1 parent
feaecb1
commit ad78419
Showing
4 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<int> 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<int> delay = { this, "delay", 1, [this] { size = (int)delay; } }; | ||
|
||
|
||
/** Decay time -- is this really a dataspace conversion of the feedback coefficient? maybe not... | ||
*/ | ||
Parameter<double> decay = { this, "decay", 0.0, | ||
[this]{ | ||
// mDeciResonance = resonance * 0.1; | ||
// calculateCoefficients(); | ||
} | ||
}; | ||
|
||
|
||
/** Feedback coefficient. | ||
*/ | ||
Parameter<double> 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<bool> clip = {this, "clip", true, }; | ||
|
||
|
||
/** Cutoff Frequency for the internal lowpass filter | ||
*/ | ||
Parameter<double> 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
cmake_minimum_required(VERSION 3.0) | ||
|
||
|
||
project(Comb) | ||
|
||
include(${PROJECT_SOURCE_DIR}/../UnitTest.cmake NO_POLICY_SCOPE) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CombTest>* mTest; | ||
|
||
public: | ||
CombTest(Jamoma::UnitTest<CombTest>* 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<CombTest> aUnitTestInstance; | ||
return aUnitTestInstance.failureCount(); | ||
} |