Filter Display in External C++ Node
-
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
classtemplate <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; } }; }