@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