Just carrying on the discussion from here and creating a short primer on implementing the Filter Display in an external C++ node.
Disclaimer: I am not very experienced with C++ so there is potentially some/a lot of flaws with my implementation (open to suggestions as always).
Implementing it was actually a lot more straightforward than I anticipated. My example is just using a direct form biquad since that is what I was working on at the time. This should hypothetically work for any other IIR Filter.
I think for your own custom filters it's a bit more time consuming as you would need to approximate the coefficients to the ones found in JUCE IIRFilter class (more information about them is here & here)

Step 1: Change Base Class Inheritance
Inherit from the filter_base
class
template <int NV> struct MyFilter : public data::filter_base
Reference: FilterNode
Step 2: Enable Filter Display
static constexpr int NumFilters = 1;
Step 3: Store the Filter Coefficients
IIRCoefficients currentCoefficients;
Step 4: Add HISE Coefficient Data
FilterDataObject::CoefficientData coefficientData;
Step 5: Override setExternalData
void setExternalData(const ExternalData& data, int index) override
{
if (index == 0)
{
filter_base::setExternalData(data, index);
}
}
Step 6: Initial Display Notification Method
void prepare(PrepareSpecs specs)
{
// ... your code ...
this->sendCoefficientUpdateMessage(); // Notify HISE display system
}
Reference: sendCoefficientUpdateMessage()
Step 7: Implement getApproximateCoefficients
FilterDataObject::CoefficientData getApproximateCoefficients() const override
{
return coefficientData;
}
This is the key method HISE calls to get coefficients for display.
Step 8: Parameter Change Notification
template <int P> void setParameter(double v)
{
bool needsUpdate = false;
// ... check if parameters changed ...
if (needsUpdate)
{
calculateCoefficients();
this->sendCoefficientUpdateMessage();Notify display system
}
}
Step 9: Update Coefficient Data
void calculateCoefficients()
{
// Calculate your filter coefficients (example using JUCE)
switch (filterType)
{
case 0:
currentCoefficients = IIRCoefficients::makeLowPass(sampleRate, frequency, q);
break;
// ... more filter types ...
}
// Update HISE display data
coefficientData.first = currentCoefficients;
coefficientData.second = 1;
coefficientData.obj = nullptr;
coefficientData.customFunction = nullptr; // Use default HISE display function
}
Full code example:
#pragma once
#include <JuceHeader.h>
/*
==========================| HISE Filter Display Integration Guide |==========================
This demonstrates the COMPLETE process for adding filter display to any HISE node
STEP 1: Inherit from data::filter_base
- Change: public data::base → public data::filter_base
STEP 2: Enable filter display
- Set: static constexpr int NumFilters = 1;
STEP 3: Add coefficient storage
- Add: IIRCoefficients currentCoefficients;
STEP 4: Add HISE coefficient data
- Add: FilterDataObject::CoefficientData coefficientData;
STEP 5: Override setExternalData
- Add: void setExternalData(const ExternalData& data, int index) override
STEP 6: Initial display notification
- Add: this->sendCoefficientUpdateMessage(); in prepare()
STEP 7: Implement getApproximateCoefficients
- Add: FilterDataObject::CoefficientData getApproximateCoefficients() const override
STEP 8: Parameter change notification
- Add: this->sendCoefficientUpdateMessage(); when parameters change
STEP 9: Update coefficient data
- Set: coefficientData.first = currentCoefficients; when coefficients change
*/
namespace project
{
using namespace juce;
using namespace hise;
using namespace scriptnode;
template <int NV> struct FilterDisplay : public data::filter_base // STEP 1: Inherit from filter_base
{
// Metadata Definitions ------------------------------------------------------------------------
SNEX_NODE(FilterDisplay);
struct MetadataClass
{
SN_NODE_ID("FilterDisplay");
};
// Node characteristics
static constexpr bool isModNode() { return false; };
static constexpr bool isPolyphonic() { return NV > 1; };
static constexpr bool hasTail() { return false; };
static constexpr bool isSuspendedOnSilence() { return true; };
static constexpr int getFixChannelAmount() { return 2; };
// STEP 2: Enable filter display in external data requirements
static constexpr int NumTables = 0;
static constexpr int NumSliderPacks = 0;
static constexpr int NumAudioFiles = 0;
static constexpr int NumFilters = 1;
static constexpr int NumDisplayBuffers = 0;
// Filter Parameters ------------------------------------------------------------------------
double frequency = 1000.0;
double q = 0.707;
double gain = 0.0;
int filterType = 0;
double sampleRate = 44100.0;
// STEP 3: Use JUCE coefficients for processing AandND display
IIRCoefficients currentCoefficients;
// Biquad state variables for manual processing
double x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0; // Left channel
double x1r = 0.0, x2r = 0.0, y1r = 0.0, y2r = 0.0; // Right channel
// STEP 4: Filter coefficient data for HISE display system
FilterDataObject::CoefficientData coefficientData;
// Callbacks ------------------------------------------------------------------------
void prepare(PrepareSpecs specs)
{
sampleRate = specs.sampleRate;
calculateCoefficients();
reset();
// STEP 6: send the coefficient update message to notify HISE display
this->sendCoefficientUpdateMessage();
}
void reset() // Clear
{
x1 = x2 = y1 = y2 = 0.0;
x1r = x2r = y1r = y2r = 0.0;
}
void handleHiseEvent(HiseEvent& e)
{
// No MIDI processing
}
template <typename T> void process(T& data)
{
static constexpr int NumChannels = getFixChannelAmount();
auto& fixData = data.template as<ProcessData<NumChannels>>();
auto fd = fixData.toFrameData();
while (fd.next())
{
processFrame(fd.toSpan());
}
}
template <typename T> void processFrame(T& data)
{
// Process using JUCE coefficients
// JUCE coefficients format: [b0, b1, b2, a0, a1, a2] where a0 = 1.0
auto* coeffs = currentCoefficients.coefficients;
// Left channel biquad processing
double input = data[0];
double output = coeffs[0] * input + coeffs[1] * x1 + coeffs[2] * x2
- coeffs[4] * y1 - coeffs[5] * y2;
x2 = x1; x1 = input;
y2 = y1; y1 = output;
data[0] = static_cast<float>(output);
// Right channel (if stereo)
if (getFixChannelAmount() > 1)
{
double inputR = data[1];
double outputR = coeffs[0] * inputR + coeffs[1] * x1r + coeffs[2] * x2r
- coeffs[4] * y1r - coeffs[5] * y2r;
x2r = x1r; x1r = inputR;
y2r = y1r; y1r = outputR;
data[1] = static_cast<float>(outputR);
}
}
int handleModulation(double& value)
{
return 0;
}
// STEP 5: Override setExternalData for filter base class
void setExternalData(const ExternalData& data, int index) override
{
if (index == 0)
{
filter_base::setExternalData(data, index);
}
}
// STEP 7: Implement getApproximateCoefficients for HISE display
FilterDataObject::CoefficientData getApproximateCoefficients() const override
{
return coefficientData;
}
// Parameter Functions -------------------------------------------------------------------------
template <int P> void setParameter(double v)
{
bool needsUpdate = false;
if (P == 0) // Frequency
{
if (frequency != v)
{
frequency = jlimit(20.0, 20000.0, v);
needsUpdate = true;
}
}
else if (P == 1) // Q Factor
{
if (q != v)
{
q = jlimit(0.1, 30.0, v);
needsUpdate = true;
}
}
else if (P == 2) // Gain
{
if (gain != v)
{
gain = jlimit(-24.0, 24.0, v);
needsUpdate = true;
}
}
else if (P == 3) // Filter Type
{
int newType = jlimit(0, 7, (int)v);
if (filterType != newType)
{
filterType = newType;
needsUpdate = true;
}
}
// STEP 8: Update coefficients and notify display when parameters change
if (needsUpdate)
{
calculateCoefficients();
this->sendCoefficientUpdateMessage(); // Notify HISE display system
}
}
void createParameters(ParameterDataList& data)
{
// Frequency
{
parameter::data p("Frequency", { 20.0, 20000.0 });
registerCallback<0>(p);
p.setDefaultValue(1000.0);
p.setSkewForCentre(1000.0);
data.add(std::move(p));
}
// Q parameter
{
parameter::data p("Q", { 0.1, 10.0 });
registerCallback<1>(p);
p.setDefaultValue(0.707);
p.setSkewForCentre(1.0);
data.add(std::move(p));
}
// Gain
{
parameter::data p("Gain", { -24.0, 24.0 });
registerCallback<2>(p);
p.setDefaultValue(0.0);
data.add(std::move(p));
}
// Filter Type
{
parameter::data p("FilterType", { 0.0, 7.0, 1.0 });
registerCallback<3>(p);
p.setDefaultValue(0.0);
StringArray filterNames;
filterNames.add("LowPass");
filterNames.add("HighPass");
filterNames.add("BandPass");
filterNames.add("Notch");
filterNames.add("AllPass");
filterNames.add("LowShelf");
filterNames.add("HighShelf");
filterNames.add("Peak");
p.setParameterValueNames(filterNames);
data.add(std::move(p));
}
}
private:
// Helper Functions ----------------------------------------------------------------------------
void calculateCoefficients()
{
// Use JUCE's proven coefficient calculation
switch (filterType)
{
case 0: // LowPass
currentCoefficients = IIRCoefficients::makeLowPass(sampleRate, frequency, q);
break;
case 1: // HighPass
currentCoefficients = IIRCoefficients::makeHighPass(sampleRate, frequency, q);
break;
case 2: // BandPass
currentCoefficients = IIRCoefficients::makeBandPass(sampleRate, frequency, q);
break;
case 3: // Notch
currentCoefficients = IIRCoefficients::makeNotchFilter(sampleRate, frequency, q);
break;
case 4: // AllPass
currentCoefficients = IIRCoefficients::makeAllPass(sampleRate, frequency, q);
break;
case 5: // LowShelf
currentCoefficients = IIRCoefficients::makeLowShelf(sampleRate, frequency, q,
Decibels::decibelsToGain(gain));
break;
case 6: // HighShelf
currentCoefficients = IIRCoefficients::makeHighShelf(sampleRate, frequency, q,
Decibels::decibelsToGain(gain));
break;
case 7: // Peak
currentCoefficients = IIRCoefficients::makePeakFilter(sampleRate, frequency, q,
Decibels::decibelsToGain(gain));
break;
default:
currentCoefficients = IIRCoefficients::makeLowPass(sampleRate, frequency, q);
break;
}
// STEP 9: Update coefficient data for HISE display
coefficientData.first = currentCoefficients;
coefficientData.second = 1;
coefficientData.obj = nullptr;
coefficientData.customFunction = nullptr;
}
};
}