HISE Logo Forum
    • Categories
    • Register
    • Login

    Filter Display in External C++ Node

    Scheduled Pinned Locked Moved C++ Development
    1 Posts 1 Posters 34 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • HISEnbergH
      HISEnberg
      last edited by HISEnberg

      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)

      g.gif

      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; 
      		}
      	};
      
      }
      
      1 Reply Last reply Reply Quote 3
      • First post
        Last post

      24

      Online

      1.8k

      Users

      12.1k

      Topics

      105.0k

      Posts