Forum
    • Categories
    • Register
    • Login

    # Bug report draft: compiled network passes audio UNFILTERED inside a HardcodedMasterFX (raw node works)

    Scheduled Pinned Locked Moved Bug Reports
    3 Posts 3 Posters 58 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.
    • P
      Phelan Kane
      last edited by

      Hiya @Christoph-Hart

      Me and my fav Bot (RIP Fable) has identified an issue with audio processing and modulations in HardcodedMasterFX that sit inside a master container (i.e. a top level filter for my device that is obs monophonic due to voicing reasons).

      I hope the below explains everything. Happy to drop a minimal script if needed.

      Thanks

      Phelan

      Adjacent to the resolved "HISE 4.1 Hardcoded Master & Poly FX P1 P2 Modulation" thread (forum topic 13276).


      Environment

      • HISE develop, version 4.9.2 (running commit 8606dea6), macOS, Apple Silicon.
      • A custom C++ SNEX filter node (ZDF filter, 6 params; param 0 = Cutoff/Frequency, 1 = Res/Resonance).

      Context

      After the 4.1 P1/P2 modulation breaking change (params no longer auto-modulated; must opt in per parameter), I'm trying to restore modulation on a global filter that sits in a HardcodedMasterFX.

      What works

      • The RAW C++ node placed directly in the HardcodedMasterFX FILTERS audio correctly. (But its params can no longer be modulated by the P1/P2 slots, as expected after the breaking change - createExternalModulationInfo is not implemented on it.)
      • The SAME node wrapped in a compiled network and used POLYPHONICALLY in a HardcodedPolyphonicFX (per-voice) filters AND modulates correctly. This is my sampler filter and it is fine.

      What doesn't (the bug)

      To get P1/P2 modulation back on the MASTER (mono) filter, I wrapped the node in a compiled network:

      • container.chain -> single C++ filter node
      • AllowCompilation="1", NO AllowPolyphonic (so it's monophonic for the master FX)
      • container params Cutoff/Res have ExternalModulation="Combined" connected to the node's Frequency/Resonance.

      Result in the HardcodedMasterFX:

      • P1/P2 modulation now works (the matrix/extra_mod drives Cutoff/Res).
      • Parameter changes reach the node (verified via scripting: getAttribute(0) returns the set cutoff, e.g. 2401 Hz).
      • The effect is NOT bypassed (isBypassed() == false).
      • BUT the audio passes through UNFILTERED at any cutoff/topology/response. The wrapped node never cooks its coefficients - it behaves as if prepare()/sampleRate never reached the inner node.

      Earlier I also tried the POLYPHONIC network (AllowPolyphonic=1) in the master FX: that produced SILENCE (channel/voice mismatch), which is why I made the monophonic variant.

      Questions

      1. Should a monophonic compiled network prepare/process its inner node inside a HardcodedMasterFX? It currently passes audio through unprocessed (inner node not prepared) while the raw node prepares fine.
      2. What is the recommended way to get a node to BOTH filter audio AND expose P1/P2 modulation inside a HardcodedMasterFX?
      3. For C++ nodes specifically: what is the exact signature/usage of createExternalModulationInfo to declare params 0 and 1 as Combined-modulatable on the raw node (so I can skip the network wrapper entirely)? You mentioned "with C++ nodes you can directly call a method that defines the parameter's modulation connections" - a minimal example would let me do this directly.
      ustkU griffinboyG 2 Replies Last reply Reply Quote 0
      • ustkU
        ustk @Phelan Kane
        last edited by

        @Phelan-Kane not my expertise field, but would that help?
        https://forum.hise.audio/topic/14270/how-do-you-set-up-external-modulation-slots-c

        Hise made me an F5 dude, any other app just suffers...

        1 Reply Last reply Reply Quote 0
        • griffinboyG
          griffinboy @Phelan Kane
          last edited by

          @Phelan-Kane

          Here's a real life example of c++ modslots.

          It's not minimal i'm afraid but it shows it in use.
          If you'd like to see the minimal example, it's just as @ustk said, have a look at this post:
          https://forum.hise.audio/topic/14270/how-do-you-set-up-external-modulation-slots-c

          // ==============================| Griffin Poly EQ Filter |===================================
          //
          //  File:        Griffin_EQFilter_Poly.h
          //  Node:        Griffin_EQFilter_Poly
          //  Package:     Griffin DSP Essentials for HISE
          //  Author:      Griffinboy
          //  HISE Forum:  https://forum.hise.audio/user/griffinboy
          //  Copyright:   Copyright (c) 2026 Griffinboy
          //
          //  Description:
          //      Minimum-phase EQ filter with per-voice state and frame processing.
          //      Designed for sample-accurate modulation in synthesis contexts.
          //      Uses more CPU than the block-rate Griffin_EQFilter.
          //
          //      Has Mod slots for frequency, Q, and gain.
          //
          //  License:
          //      GNU General Public License v3.0 or later (GPL-3.0-or-later).
          //
          //      This file is part of Griffin DSP Essentials for HISE.
          //
          //      This program is free software: you can redistribute it and/or modify it under the
          //      terms of the GNU General Public License as published by the Free Software Foundation,
          //      either version 3 of the License, or (at your option) any later version.
          //
          //      This program is distributed in the hope that it will be useful, but WITHOUT ANY
          //      WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
          //      PARTICULAR PURPOSE. See the GNU General Public License for more details.
          //
          //      You should have received a copy of the GNU General Public License along with this
          //      program. If not, see <https://www.gnu.org/licenses/>.
          //
          // ================================================================================================
          
          #pragma once
          #include <algorithm>
          #include <cmath>
          #include <JuceHeader.h>
          #include "src/griffinboy/modules/essentials/eq_filter/eq_filter.h"
          
          namespace project
          {
          using namespace juce;
          using namespace hise;
          using namespace scriptnode;
          
          template <int NV> struct Griffin_EQFilter_Poly: public data::filter_node_base
          {
              using VoiceState = griffin::modules::essentials::eq_filter::EQFilterFrameProcessor;
              using PlotModel = griffin::modules::essentials::eq_filter::EQPlotModel;
              using Defaults = griffin::modules::essentials::eq_filter::EQFilterDefaults;
          
              PolyData<VoiceState, NV> voices;
              PlotModel plotModel;
              SimpleReadWriteLock topologyLock;
              double plotSampleRate = 44100.0;
          
              SNEX_NODE(Griffin_EQFilter_Poly);
          
              struct MetadataClass
              {
                  SN_NODE_ID("Griffin_EQFilter_Poly");
              };
          
              static constexpr bool isModNode() { return false; };
              static constexpr bool isPolyphonic() { return NV > 1; };
              static constexpr bool hasTail() { return true; };
              static constexpr bool isSuspendedOnSilence() { return true; };
              static constexpr int getFixChannelAmount() { return 2; };
          
              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;
          
              void prepare(PrepareSpecs specs)
              {
                  SimpleReadWriteLock::ScopedWriteLock sl(topologyLock);
                  voices.prepare(specs);
                  for (auto& voice : voices)
                      voice.prepare(specs.sampleRate, specs.blockSize);
                  plotModel.prepare(specs.sampleRate);
          
                  plotSampleRate = specs.sampleRate > 0.0 ? specs.sampleRate : 44100.0;
          
                  if (auto fd = dynamic_cast<FilterDataObject*>(this->externalData.obj))
                      fd->setSampleRate(plotSampleRate);
          
                  sendCoefficientUpdateMessage();
          
                  voiceManager.prepare(specs);
                  voiceManager.setActive(1.0);
              }
          
              void reset()
              {
                  SimpleReadWriteLock::ScopedWriteLock sl(topologyLock);
          
                  for (auto& voice : voices)
                      voice.reset();
          
                  voiceManager.reset();
              }
          
              void handleHiseEvent(HiseEvent& e)
              {
                  voiceManager.handleHiseEvent(e);
              }
          
              template <typename T> void process(T& data)
              {
                  if (auto sl = SimpleReadWriteLock::ScopedTryReadLock(topologyLock))
                  {
                      static constexpr int NumChannels = getFixChannelAmount();
                      auto& fixData = data.template as<ProcessData<NumChannels>>();
                      auto& voice = voices.get();
          
                      auto fd = fixData.toFrameData();
                      while (fd.next())
                          voice.processFrame(fd.toSpan());
          
                      voiceManager.process(data);
                  }
              }
          
              template <typename T> void processFrame(T& data)
              {
                  if (auto sl = SimpleReadWriteLock::ScopedTryReadLock(topologyLock))
                      voices.get().processFrame(data);
              }
          
              int handleModulation(double& value)
              {
                  ignoreUnused(value);
                  return 0;
              }
          
              double getPlotValue(int getMagnitude, double freqNorm) override
              {
                  if (getMagnitude == 0)
                      return 0.0;
          
                  const auto frequency = std::clamp(freqNorm, 0.0, 0.5) * plotSampleRate;
                  return plotModel.getMagnitudeAtFrequency(frequency);
              }
          
              void setExternalData(const ExternalData& data, int index)
              {
                  data::filter_node_base::setExternalData(data, index);
                  ignoreUnused(index);
          
                  if (auto fd = dynamic_cast<FilterDataObject*>(data.obj))
                      fd->setSampleRate(plotSampleRate);
          
                  sendCoefficientUpdateMessage();
              }
          
              void createExternalModulationInfo(OpaqueNode::ModulationProperties& info)
              {
                  modulation::ParameterProperties::ConnectionList list;
          
                  auto addParameterSlot = [&list](int parameterIndex)
                  {
                      modulation::ConnectionInfo slot;
                      slot.connectedParameterIndex = parameterIndex;
                      slot.modColour = HiseModulationColours::ColourId::FX;
                      slot.connectionMode = modulation::ConnectionMode::Parameter;
                      slot.modulationMode = modulation::ParameterMode::ScaleAdd;
                      list.push_back(slot);
                  };
          
                  addParameterSlot(2);
                  addParameterSlot(3);
                  addParameterSlot(4);
          
                  info.fromConnectionList(list);
                  info.setModulationBlockSize(Defaults::modulationBlockSize);
              }
          
              template <int P> void setParameter(double v)
              {
                  if constexpr (P == 0)
                  {
                      SimpleReadWriteLock::ScopedWriteLock sl(topologyLock);
                      const auto nextShape = (int)std::round(v);
          
                      applyToVoices(
                          [nextShape](VoiceState& filter)
                          {
                              filter.setType(nextShape);
                          });
          
                      updatePlotFromFirstVoice(
                          [this, nextShape]
                          {
                              plotModel.setType(nextShape);
                          });
                  }
                  else if constexpr (P == 1)
                  {
                      SimpleReadWriteLock::ScopedWriteLock sl(topologyLock);
                      const auto nextSlope = (int)std::round(v);
          
                      applyToVoices(
                          [nextSlope](VoiceState& filter)
                          {
                              filter.setSlopeMode(nextSlope);
                          });
          
                      updatePlotFromFirstVoice(
                          [this, nextSlope]
                          {
                              plotModel.setSlopeMode(nextSlope);
                          });
                  }
                  else if constexpr (P == 2)
                  {
                      const auto value = (float)v;
                      applyToVoices(
                          [value](VoiceState& filter)
                          {
                              filter.setFrequencyHz(value);
                          });
          
                      updatePlotFromFirstVoice(
                          [this, value]
                          {
                              plotModel.setFrequencyHz(value);
                          });
                  }
                  else if constexpr (P == 3)
                  {
                      const auto value = (float)v;
                      applyToVoices(
                          [value](VoiceState& filter)
                          {
                              filter.setQ(value);
                          });
          
                      updatePlotFromFirstVoice(
                          [this, value]
                          {
                              plotModel.setQ(value);
                          });
                  }
                  else if constexpr (P == 4)
                  {
                      const auto value = (float)v;
                      applyToVoices(
                          [value](VoiceState& filter)
                          {
                              filter.setGainDb(value);
                          });
          
                      updatePlotFromFirstVoice(
                          [this, value]
                          {
                              plotModel.setGainDb(value);
                          });
                  }
              }
          
              void createParameters(ParameterDataList& data)
              {
                  {
                      parameter::data p("Shape", { 0.0, 6.0, 1.0 });
                      StringArray names;
                      addTypeLabels(names);
                      p.setParameterValueNames(names);
                      registerCallback<0>(p);
                      p.setDefaultValue((double)Defaults::type);
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Slope", { 0.0, 3.0, 1.0 });
                      StringArray names;
                      addSlopeLabels(names);
                      p.setParameterValueNames(names);
                      registerCallback<1>(p);
                      p.setDefaultValue((double)Defaults::slopeMode);
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Frequency", { 20.0, 20000.0, 0.01 });
                      p.setSkewForCentre(1000.0);
                      p.info.textConverter = parameter::pod::TextValueConverters::Frequency;
                      registerCallback<2>(p);
                      p.setDefaultValue(Defaults::frequencyHz);
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Q", { 0.025, 25.0, 0.001 });
                      p.setSkewForCentre(0.70710678);
                      registerCallback<3>(p);
                      p.setDefaultValue(Defaults::q);
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Gain", { -30.0, 30.0, 0.01 });
                      p.info.textConverter = parameter::pod::TextValueConverters::Decibel;
                      registerCallback<4>(p);
                      p.setDefaultValue(Defaults::gainDb);
                      data.add(std::move(p));
                  }
              }
          
          private:
              static constexpr int getNumTypes() noexcept
              {
                  return griffin::modules::essentials::eq_filter::EQFilterDesign::getNumTypes();
              }
          
              template <typename Function> void applyToVoices(Function&& function)
              {
                  // PolyData iteration follows HISE's poly callback.
                  // Each active voice receives the parameter update in its own DSP state.
                  for (auto& voice : voices)
                      function(voice);
              }
          
              template <typename Function> void updatePlotFromFirstVoice(Function&& function)
              {
                  // The plot is shared UI state. The first HISE voice owns graph updates
                  // so voices do not compete for the displayed response.
                  if (! voices.isVoiceRenderingActive() || voices.isFirst())
                  {
                      function();
                      sendCoefficientUpdateMessage();
                  }
              }
          
              static void addTypeLabels(StringArray& names)
              {
                  names.add("Lowpass");
                  names.add("Highpass");
                  names.add("Bandpass");
                  names.add("Notch");
                  names.add("Bell");
                  names.add("Low Shelf");
                  names.add("High Shelf");
              }
          
              static void addSlopeLabels(StringArray& names)
              {
                  names.add("12 dB/oct");
                  names.add("24 dB/oct");
                  names.add("48 dB/oct");
                  names.add("96 dB/oct");
              }
          
              envelope::silent_killer<NV> voiceManager;
          };
          } // namespace project
          
          
          1 Reply Last reply Reply Quote 0
          • First post
            Last post

          20

          Online

          2.4k

          Users

          13.8k

          Topics

          119.9k

          Posts