Custom Parametric EQ Display using DraggableFilterPanel
-
I am trying to design a parametric EQ plugin from scratch. The DSP network has nine biquad filters, corresponding to up to nine allowed bands in the EQ. Each parameter (frequency, gain, Q, Enabled) in each filter has a corresponding knob in the overarching parameter hub. The UI has nine buttons corresponding to up to nine bands, frequency, gain, and Q knobs, Create and Delete buttons for adding a removing bands from the EQ display, and a ComboBox which I will later turn into a dropdown menu for selecting the band shape. I have gotten all of the UI components to work (except the dropdown menu, which I will worry about later), but I have been struggling to get the EQ points and curve to display nicely in the DraggableFilterPanel. I've attached pictures of my UI and DSP network, as well as my code in onInit and onControl tabs below.
Some technical notes about the code:- The tile is intentionally kept in IdPattern mode so that it binds directly to the Parameter Hub attributes (FrequencyN, GainN, QN, EnabledN). I do not use the custom data block that creates default bands; instead, I only set ParameterOrder, DragActions, and Range via setDraggableFilterData so that the axes match the knob ranges and the mapping is correct (X = Frequency, Y = Gain).
- The hub resolver works by scanning fx.getAttributeId(i) to find the correct indices for each band’s parameters (FrequencyN, GainN, QN, and EnabledN). This method is more reliable across different HISE builds than calling getAttributeIndex directly.
- The Create and Delete buttons simply flip the EnabledN parameter in the hub, which in turn makes the panel show or hide the corresponding points.
- The knobs update the Parameter Hub directly using helper functions that write into FrequencyN, GainN, and QN. Because the panel is bound via IdPatterns, the dots move in response to knob changes. When switching between bands, an updateKnobs() function reads back the current hub values so the knobs always reflect the active point’s state.
- If needed, a sync timer could also be added so that dragging points on the panel updates the knobs in real time.
I've only started using HISE about 2 weeks ago, but I have been learning how the different modules interact. I've tried my best to draw from the DraggableFilterPanel example snippet, but so far I've been unsuccessful in getting the EQ curve display to work. I'd greatly appreciate any advice/tips for troubleshooting!
// ============================================================================
// Parametric EQ UI (9 bands) wired to a Parameter Hub + DraggableFilterPanel
// - Hub exposes: Frequency1..9, Gain1..9, Q1..9, Enabled1..9
// - The FloatingTile DraggableFilterPanel binds via IdPatterns
// - UI radio buttons select an active band; Create/Delete toggle EnabledN
// - Knobs write to hub (Freq/Gain/Q) and the panel follows
// ============================================================================Content.makeFrontInterface(800, 600);
// Reference to the Script FX that owns the Parameter Hub attributes
const var fx = Synth.getEffect("Script FX1");// --- UI Components -----------------------------------------------------------
const var buttons = [];
for (i = 0; i < 9; i++) buttons[i] = Content.getComponent("Button" + (i + 1));const var freqKnob = Content.getComponent("Knob1");
const var gainKnob = Content.getComponent("Knob2");
const var qKnob = Content.getComponent("Knob3");
const var create = Content.getComponent("Button10");
const var delBtn = Content.getComponent("Button11");// Knob ranges must match panel ranges (see setDraggableFilterData below)
freqKnob.set("min", 20); freqKnob.set("max", 20000); freqKnob.set("defaultValue", 1000); freqKnob.set("mode", "Frequency"); freqKnob.set("suffix", " Hz");
gainKnob.set("min", -24); gainKnob.set("max", 24); gainKnob.set("defaultValue", 0); gainKnob.set("mode", "Linear"); gainKnob.set("suffix", " dB");
qKnob.set("min", 0.1); qKnob.set("max", 10); qKnob.set("defaultValue", 1); qKnob.set("mode", "Linear");// --- Local model of band state (for button visuals + selection) -------------
var activeBand = 0;
var bandParameters = [];
for (i = 0; i < 9; i++)
bandParameters[i] = { freq: 1000, gain: 0, q: 1, active: (i == 0), Enabled: false };// --- Hub attribute resolver (by name) ---------------------------------------
const var KIND = ["Frequency","Gain","Q","Enabled"];
const var KIND_FREQ = 0, KIND_GAIN = 1, KIND_Q = 2, KIND_ENABLED = 3;// Cache of resolved attribute indices: paramIdx[band][kind]
var paramIdx = [];
for (i = 0; i < 9; i++) { paramIdx[i] = []; for (j = 0; j < 4; j++) paramIdx[i][j] = -1; }// Resolve "" to numeric attribute index by scanning attribute names.
// (Robust across HISE versions where getAttributeIndex(name) may differ)
inline function resolveIndex(b, k)
{
reg bandNum, wanted, N, ii;
bandNum = b + 1;
wanted = KIND[k] + bandNum; // e.g. "Frequency1"
N = fx.getNumAttributes();for (ii = 0; ii < N; ii++) if (fx.getAttributeId(ii) == wanted) { paramIdx[b][k] = ii; return ii; } return -1; // don't cache a miss to allow re-try later
}
inline function idxOf(b, k)
{
reg idx;
idx = paramIdx[b][k];
if (idx == -1) idx = resolveIndex(b, k);
return idx;
}inline function setP(b, k, v) { reg id = idxOf(b, k); if (id != -1) fx.setAttribute(id, v); }
inline function getP(b, k) { reg id = idxOf(b, k); if (id != -1) return fx.getAttribute(id); return undefined; }// --- DraggableFilterPanel binding -------------------------------------------
// Keep the tile in IdPattern mode but align axes/ranges to the knobs.
fx.setDraggableFilterData({
ParameterOrder: ["Gain","Freq","Q","Enabled"], // logical param order
DragActions: { "DragX":"Freq", "DragY":"Gain", "ShiftDrag":"Q", "DoubleClick":"", "RightClick":"" },
Range: { // visible grid ranges
"Gain": { "min": -24.0, "max": 24.0 },
"Frequency": { "min": 20.0, "max": 20000.0 },
"Q": { "min": 0.1, "max": 10.0 }
}
});const var eqPanel = Content.getComponent("FloatingTile2");
const var ft_data = {
"Type": "DraggableFilterPanel",
"ProcessorId": "Script FX1", // must match the Script FX name
"ItemCount": 9,
"FrequencyIdPattern": "Frequency$N",
"GainIdPattern": "Gain$N",
"QIdPattern": "Q$N",
"EnabledIdPattern": "Enabled$N",
"AllowAdd": false,
"AllowRemove": false
};
eqPanel.setContentData(ft_data);// --- UI helpers --------------------------------------------------------------
function updateButtonAppearance()
{
for (i = 0; i < 9; i++)
{
if (bandParameters[i].Enabled)
{
buttons[i].set("enabled", 1);
buttons[i].setColour(0, 0xFFFFFFFF);
buttons[i].setColour(1, 0xFF888888);
}
else
{
buttons[i].set("enabled", 0); // ignore clicks on disabled bands
buttons[i].setValue(0);
buttons[i].setColour(0, 0xFF444444);
buttons[i].setColour(1, 0xFF666666);
}
}
}inline function setBandEnabled(idx, on)
{
setP(idx, KIND_ENABLED, on ? 1 : 0); // EnabledN in the hub
bandParameters[idx].Enabled = on;
updateButtonAppearance();
}inline function initBandParams(idx, f, g, qv)
{
setP(idx, KIND_FREQ, f);
setP(idx, KIND_GAIN, g);
setP(idx, KIND_Q, qv);
bandParameters[idx].freq = f;
bandParameters[idx].gain = g;
bandParameters[idx].q = qv;
}inline function findFirstInactiveBand()
{
for (i = 0; i < 9; i++) if (!bandParameters[i].Enabled) return i;
return -1;
}inline function findNextActiveBand(fromIdx)
{
reg k, idx;
for (k = 1; k <= 9; k++)
{
idx = (fromIdx + k) % 9;
if (bandParameters[idx].Enabled) return idx;
}
return -1;
}// Read the current hub values into the knobs when switching bands
inline function updateKnobs()
{
reg f, g, qv;
f = getP(activeBand, KIND_FREQ);
g = getP(activeBand, KIND_GAIN);
qv = getP(activeBand, KIND_Q);if (f != undefined) freqKnob.setValue(f); if (g != undefined) gainKnob.setValue(g); if (qv != undefined) qKnob.setValue(qv); bandParameters[activeBand].freq = freqKnob.getValue(); bandParameters[activeBand].gain = gainKnob.getValue(); bandParameters[activeBand].q = qKnob.getValue();
}
function storeBandParameters()
{
bandParameters[activeBand].freq = freqKnob.getValue();
bandParameters[activeBand].gain = gainKnob.getValue();
bandParameters[activeBand].q = qKnob.getValue();
}function switchToBand(bandIndex)
{
if (bandIndex < 0 || bandIndex >= 9) return;
storeBandParameters();
for (i = 0; i < 9; i++) { bandParameters[i].active = (i == bandIndex); buttons[i].setValue(i == bandIndex ? 1 : 0); }
activeBand = bandIndex;
updateKnobs();
}// --- Initial state: pull hub values and sync buttons/knobs -------------------
for (i = 0; i < 9; i++)
{
// resolve & read once (ok if some bands don't exist yet)
var idF = idxOf(i, KIND_FREQ);
var idG = idxOf(i, KIND_GAIN);
var idQ = idxOf(i, KIND_Q);
var idE = idxOf(i, KIND_ENABLED);if (idF != -1) bandParameters[i].freq = fx.getAttribute(idF); if (idG != -1) bandParameters[i].gain = fx.getAttribute(idG); if (idQ != -1) bandParameters[i].q = fx.getAttribute(idQ); if (idE != -1) bandParameters[i].Enabled = fx.getAttribute(idE) > 0;
}
updateButtonAppearance();
switchToBand(0);
function onControl(component, value)
{
// Radio buttons: only allow selecting enabled bands
for (i = 0; i < 9; i++)
{
if (component == buttons[i])
{
if (!bandParameters[i].Enabled) { buttons[i].setValue(0); return; }
if (value > 0) { switchToBand(i); }
return;
}
}// Knobs write directly to the Parameter Hub (panel follows) if (component == freqKnob) { setP(activeBand, KIND_FREQ, value); bandParameters[activeBand].freq = value; } else if (component == gainKnob) { setP(activeBand, KIND_GAIN, value); bandParameters[activeBand].gain = value; } else if (component == qKnob) { setP(activeBand, KIND_Q, value); bandParameters[activeBand].q = value; } // Create = find first disabled band, enable it, set defaults, select it if (component == create && value > 0) { create.setValue(0); local freeSlot = findFirstInactiveBand(); if (freeSlot != -1) { setBandEnabled(freeSlot, true); initBandParams(freeSlot, 1000, 0, 1); switchToBand(freeSlot); // Optional: one-time UI nudge during wiring // eqPanel.setContentData(ft_data); } return; } // Delete = disable current band and select next active (if any) if (component == delBtn && value > 0) { delBtn.setValue(0); if (bandParameters[activeBand].Enabled) { setBandEnabled(activeBand, false); local nextBand = findNextActiveBand(activeBand); if (nextBand != -1) switchToBand(nextBand); else { buttons[activeBand].setValue(0); bandParameters[activeBand].active = false; } // Optional: one-time UI nudge during wiring // eqPanel.setContentData(ft_data); } return; }
}
-
@pandaz21 Nice! It would be even nicer if you could post a snippet instead of the code so people can load it without having to recreate the whole beast
-
Here's the full code again, since it got pasted awkwardly the original post.
// ============================================================================ // Parametric EQ UI (9 bands) wired to a Parameter Hub + DraggableFilterPanel // - Hub exposes: Frequency1..9, Gain1..9, Q1..9, Enabled1..9 // - The FloatingTile DraggableFilterPanel binds via IdPatterns // - UI radio buttons select an active band; Create/Delete toggle EnabledN // - Knobs write to hub (Freq/Gain/Q) and the panel follows // ============================================================================ Content.makeFrontInterface(800, 600); // Reference to the Script FX that owns the Parameter Hub attributes const var fx = Synth.getEffect("Script FX1"); // --- UI Components ----------------------------------------------------------- const var buttons = []; for (i = 0; i < 9; i++) buttons[i] = Content.getComponent("Button" + (i + 1)); const var freqKnob = Content.getComponent("Knob1"); const var gainKnob = Content.getComponent("Knob2"); const var qKnob = Content.getComponent("Knob3"); const var create = Content.getComponent("Button10"); const var delBtn = Content.getComponent("Button11"); // Knob ranges must match panel ranges (see setDraggableFilterData below) freqKnob.set("min", 20); freqKnob.set("max", 20000); freqKnob.set("defaultValue", 1000); freqKnob.set("mode", "Frequency"); freqKnob.set("suffix", " Hz"); gainKnob.set("min", -24); gainKnob.set("max", 24); gainKnob.set("defaultValue", 0); gainKnob.set("mode", "Linear"); gainKnob.set("suffix", " dB"); qKnob.set("min", 0.1); qKnob.set("max", 10); qKnob.set("defaultValue", 1); qKnob.set("mode", "Linear"); // --- Local model of band state (for button visuals + selection) ------------- var activeBand = 0; var bandParameters = []; for (i = 0; i < 9; i++) bandParameters[i] = { freq: 1000, gain: 0, q: 1, active: (i == 0), Enabled: false }; // --- Hub attribute resolver (by name) --------------------------------------- const var KIND = ["Frequency","Gain","Q","Enabled"]; const var KIND_FREQ = 0, KIND_GAIN = 1, KIND_Q = 2, KIND_ENABLED = 3; // Cache of resolved attribute indices: paramIdx[band][kind] var paramIdx = []; for (i = 0; i < 9; i++) { paramIdx[i] = []; for (j = 0; j < 4; j++) paramIdx[i][j] = -1; } // Resolve "<Kind><N>" to numeric attribute index by scanning attribute names. // (Robust across HISE versions where getAttributeIndex(name) may differ) inline function resolveIndex(b, k) { reg bandNum, wanted, N, ii; bandNum = b + 1; wanted = KIND[k] + bandNum; // e.g. "Frequency1" N = fx.getNumAttributes(); for (ii = 0; ii < N; ii++) if (fx.getAttributeId(ii) == wanted) { paramIdx[b][k] = ii; return ii; } return -1; // don't cache a miss to allow re-try later } inline function idxOf(b, k) { reg idx; idx = paramIdx[b][k]; if (idx == -1) idx = resolveIndex(b, k); return idx; } inline function setP(b, k, v) { reg id = idxOf(b, k); if (id != -1) fx.setAttribute(id, v); } inline function getP(b, k) { reg id = idxOf(b, k); if (id != -1) return fx.getAttribute(id); return undefined; } // --- DraggableFilterPanel binding ------------------------------------------- // Keep the tile in IdPattern mode but align axes/ranges to the knobs. fx.setDraggableFilterData({ ParameterOrder: ["Gain","Freq","Q","Enabled"], // logical param order DragActions: { "DragX":"Freq", "DragY":"Gain", "ShiftDrag":"Q", "DoubleClick":"", "RightClick":"" }, Range: { // visible grid ranges "Gain": { "min": -24.0, "max": 24.0 }, "Frequency": { "min": 20.0, "max": 20000.0 }, "Q": { "min": 0.1, "max": 10.0 } } }); const var eqPanel = Content.getComponent("FloatingTile2"); const var ft_data = { "Type": "DraggableFilterPanel", "ProcessorId": "Script FX1", // must match the Script FX name "ItemCount": 9, "FrequencyIdPattern": "Frequency$N", "GainIdPattern": "Gain$N", "QIdPattern": "Q$N", "EnabledIdPattern": "Enabled$N", "AllowAdd": false, "AllowRemove": false }; eqPanel.setContentData(ft_data); // --- UI helpers -------------------------------------------------------------- function updateButtonAppearance() { for (i = 0; i < 9; i++) { if (bandParameters[i].Enabled) { buttons[i].set("enabled", 1); buttons[i].setColour(0, 0xFFFFFFFF); buttons[i].setColour(1, 0xFF888888); } else { buttons[i].set("enabled", 0); // ignore clicks on disabled bands buttons[i].setValue(0); buttons[i].setColour(0, 0xFF444444); buttons[i].setColour(1, 0xFF666666); } } } inline function setBandEnabled(idx, on) { setP(idx, KIND_ENABLED, on ? 1 : 0); // EnabledN in the hub bandParameters[idx].Enabled = on; updateButtonAppearance(); } inline function initBandParams(idx, f, g, qv) { setP(idx, KIND_FREQ, f); setP(idx, KIND_GAIN, g); setP(idx, KIND_Q, qv); bandParameters[idx].freq = f; bandParameters[idx].gain = g; bandParameters[idx].q = qv; } inline function findFirstInactiveBand() { for (i = 0; i < 9; i++) if (!bandParameters[i].Enabled) return i; return -1; } inline function findNextActiveBand(fromIdx) { reg k, idx; for (k = 1; k <= 9; k++) { idx = (fromIdx + k) % 9; if (bandParameters[idx].Enabled) return idx; } return -1; } // Read the current hub values into the knobs when switching bands inline function updateKnobs() { reg f, g, qv; f = getP(activeBand, KIND_FREQ); g = getP(activeBand, KIND_GAIN); qv = getP(activeBand, KIND_Q); if (f != undefined) freqKnob.setValue(f); if (g != undefined) gainKnob.setValue(g); if (qv != undefined) qKnob.setValue(qv); bandParameters[activeBand].freq = freqKnob.getValue(); bandParameters[activeBand].gain = gainKnob.getValue(); bandParameters[activeBand].q = qKnob.getValue(); } function storeBandParameters() { bandParameters[activeBand].freq = freqKnob.getValue(); bandParameters[activeBand].gain = gainKnob.getValue(); bandParameters[activeBand].q = qKnob.getValue(); } function switchToBand(bandIndex) { if (bandIndex < 0 || bandIndex >= 9) return; storeBandParameters(); for (i = 0; i < 9; i++) { bandParameters[i].active = (i == bandIndex); buttons[i].setValue(i == bandIndex ? 1 : 0); } activeBand = bandIndex; updateKnobs(); } // --- Initial state: pull hub values and sync buttons/knobs ------------------- for (i = 0; i < 9; i++) { // resolve & read once (ok if some bands don't exist yet) var idF = idxOf(i, KIND_FREQ); var idG = idxOf(i, KIND_GAIN); var idQ = idxOf(i, KIND_Q); var idE = idxOf(i, KIND_ENABLED); if (idF != -1) bandParameters[i].freq = fx.getAttribute(idF); if (idG != -1) bandParameters[i].gain = fx.getAttribute(idG); if (idQ != -1) bandParameters[i].q = fx.getAttribute(idQ); if (idE != -1) bandParameters[i].Enabled = fx.getAttribute(idE) > 0; } updateButtonAppearance(); switchToBand(0); // ======================================================================== // onControl Tab // ======================================================================== function onControl(component, value) { // Radio buttons: only allow selecting enabled bands for (i = 0; i < 9; i++) { if (component == buttons[i]) { if (!bandParameters[i].Enabled) { buttons[i].setValue(0); return; } if (value > 0) { switchToBand(i); } return; } } // Knobs write directly to the Parameter Hub (panel follows) if (component == freqKnob) { setP(activeBand, KIND_FREQ, value); bandParameters[activeBand].freq = value; } else if (component == gainKnob) { setP(activeBand, KIND_GAIN, value); bandParameters[activeBand].gain = value; } else if (component == qKnob) { setP(activeBand, KIND_Q, value); bandParameters[activeBand].q = value; } // Create = find first disabled band, enable it, set defaults, select it if (component == create && value > 0) { create.setValue(0); local freeSlot = findFirstInactiveBand(); if (freeSlot != -1) { setBandEnabled(freeSlot, true); initBandParams(freeSlot, 1000, 0, 1); switchToBand(freeSlot); // Optional: one-time UI nudge during wiring // eqPanel.setContentData(ft_data); } return; } // Delete = disable current band and select next active (if any) if (component == delBtn && value > 0) { delBtn.setValue(0); if (bandParameters[activeBand].Enabled) { setBandEnabled(activeBand, false); local nextBand = findNextActiveBand(activeBand); if (nextBand != -1) switchToBand(nextBand); else { buttons[activeBand].setValue(0); bandParameters[activeBand].active = false; } // Optional: one-time UI nudge during wiring // eqPanel.setContentData(ft_data); } return; } }
-
@ustk For sure! Here's a minimal snippet of the onInit and onControl tabs.
// Minimal reproducible snippet: DraggableFilterPanel bound to Parameter Hub // - Hub exposes attributes: Frequency1..9, Gain1..9, Q1..9, Enabled1..9 // - Panel uses IdPatterns; no default bands created via custom data // - A simple "createBand(0)" shows a dot at 1 kHz / 0 dB / Q=1 Content.makeFrontInterface(640, 360); // 1) Script FX that owns the Parameter Hub attributes const var fx = Synth.getEffect("Script FX1"); // 2) Panel semantics (axes + ranges), WITHOUT creating bands fx.setDraggableFilterData({ ParameterOrder: ["Gain","Freq","Q","Enabled"], DragActions: { "DragX":"Freq", "DragY":"Gain", "ShiftDrag":"Q", "DoubleClick":"", "RightClick":"" }, Range: { "Gain": { "min": -24.0, "max": 24.0 }, "Frequency": { "min": 20.0, "max": 20000.0 }, "Q": { "min": 0.1, "max": 10.0 } } }); // 3) DraggableFilterPanel bound by IdPatterns to the hub const var panel = Content.addFloatingTile("EQ", 10, 10); panel.set("width", 620); panel.set("height", 260); panel.setContentData({ "Type": "DraggableFilterPanel", "ProcessorId": "Script FX1", "ItemCount": 9, "FrequencyIdPattern": "Frequency$N", "GainIdPattern": "Gain$N", "QIdPattern": "Q$N", "EnabledIdPattern": "Enabled$N", "AllowAdd": false, "AllowRemove": false }); // 4) Safe resolver: <Kind><N> -> attribute index by scanning names const var KIND = ["Frequency","Gain","Q","Enabled"]; // 0,1,2,3 var paramIdx = []; for (i=0;i<9;i++) paramIdx[i] = [-1,-1,-1,-1]; inline function idxOf(band, kind) { // cached? if (paramIdx[band][kind] != -1) return paramIdx[band][kind]; reg n = band + 1; reg id = KIND[kind] + n; // "Frequency1", "Gain3", ... reg ii, N = fx.getNumAttributes(); for (ii = 0; ii < N; ii++) if (fx.getAttributeId(ii) == id) { paramIdx[band][kind] = ii; return ii; } return -1; // don't cache miss } inline function setP(band, kind, value) { reg a = idxOf(band, kind); if (a != -1) fx.setAttribute(a, value); } inline function getP(band, kind) { reg a = idxOf(band, kind); return (a != -1 ? fx.getAttribute(a) : undefined); } // 5) Minimal "Create" for one band so the panel shows a point inline function createBand(b) { setP(b, 3, 1); // EnabledN = 1 setP(b, 0, 1000); // FrequencyN setP(b, 1, 0); // GainN setP(b, 2, 1); // QN } // Run once to demonstrate createBand(0); // ======================================================================== // onControl Tab // ======================================================================== const var F = Content.addKnob("Freq", 10, 280); const var G = Content.addKnob("Gain", 220, 280); const var Q = Content.addKnob("Q", 430, 280); F.set("mode","Frequency"); F.set("min",20); F.set("max",20000); F.set("defaultValue",1000); G.set("min",-24); G.set("max",24); G.set("defaultValue",0); Q.set("min",0.1); Q.set("max",10); Q.set("defaultValue",1); function onControl(c, v) { if (c == F) setP(0, 0, v); // Frequency1 if (c == G) setP(0, 1, v); // Gain1 if (c == Q) setP(0, 2, v); // Q1 }
-
@pandaz21 in order to create a snippet, you need to go to
menu->Export->Export Snippet
and paste it here between code backticks