Skip to content

Commit

Permalink
Comb: initial leg-work. See #54.
Browse files Browse the repository at this point in the history
  • Loading branch information
Timothy Place committed Oct 21, 2015
1 parent feaecb1 commit ad78419
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 0 deletions.
1 change: 1 addition & 0 deletions includes/Jamoma.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
166 changes: 166 additions & 0 deletions includes/library/JamomaComb.h
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
8 changes: 8 additions & 0 deletions tests/Comb/CMakeLists.txt
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)


123 changes: 123 additions & 0 deletions tests/Comb/Comb.cpp
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();
}

0 comments on commit ad78419

Please sign in to comment.