@ustk @Christoph-Hart @griffinboy I think something like this? Beware, I didn't listen to the example waveshaper and the constant DC offset will probably not be nice to your speakers:
// ==================================| Third Party Node with Crossfaded Waveshaper |==================================
// 1. Maintain two tables: `oldCurve` contains the currently active shape; `newCurve` is
// regenerated when the user tweaks the curve parameter.
// 2. On parameter change, rebuild `newCurve` and reset `fadeCounter` to 0 to start a crossfade.
// 3. During the next `FadeSamples` (64) samples, blend outputs from both tables using a
// linearly increasing alpha. This avoids abrupt jumps and eliminates zipper noise.
// 4. After the crossfade completes, copy `newCurve` into `oldCurve` and continue normal
// processing until the next tweak.
//
// Configuration:
// • `TableSize` controls the resolution of each lookup table (1024 samples).
// • `FadeSamples` defines the crossfade length (default: 64 samples).
// • `mapSampleToCurve()` can be replaced with any shaping function (tanh, polynomial, etc.)
//
// Usage:
// • Adjust the “Curve” parameter at runtime.
// • Increase `FadeSamples` for a longer, smoother transition or decrease for faster response.
#pragma once
#include <JuceHeader.h>
namespace project
{
using namespace juce;
using namespace hise;
using namespace scriptnode;
// ==========================| The node class with all required callbacks |==========================
template <int NV> struct CrossfadeBufferExample : public data::base
{
// Metadata Definitions ------------------------------------------------------------------------
SNEX_NODE(CrossfadeBufferExample);
struct MetadataClass { SN_NODE_ID("CrossfadeBufferExample"); };
// Polyphony / tail / silence handling
static constexpr bool isModNode() { return false; }
static constexpr bool isPolyphonic() { return NV > 1; }
static constexpr bool hasTail() { return false; }
static constexpr bool isSuspendedOnSilence() { return false; }
static constexpr int getFixChannelAmount() { return 2; }
// Define the amount and types of external data slots you want to use
static constexpr int NumTables = 0;
static constexpr int NumSliderPacks = 0;
static constexpr int NumAudioFiles = 0;
static constexpr int NumFilters = 0;
static constexpr int NumDisplayBuffers = 0;
// DSP tables and crossfade state
static constexpr int TableSize = 1024;
static constexpr int FadeSamples = 64;
float oldCurve[TableSize];
float newCurve[TableSize];
int fadeCounter = FadeSamples;
float fadeInc = 1.0f / FadeSamples;
// Prepare: initialize lookup tables
void prepare(PrepareSpecs specs)
{
float initShape = 0.5f;
for (int i = 0; i < TableSize; ++i)
{
float x = i / float(TableSize - 1);
oldCurve[i] = newCurve[i] = mapSampleToCurve(x, initShape);
}
fadeCounter = FadeSamples;
}
void reset() {}
void handleHiseEvent(HiseEvent& e) {}
// Frame 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 SpanType> void processFrame(SpanType& frame)
{
for (int ch = 0; ch < getFixChannelAmount(); ++ch)
{
// normalize input [ -1,1 ] -> [0,1]
float in = (frame[ch] * 0.5f) + 0.5f;
in = jlimit(0.0f, 1.0f, in);
float out;
if (fadeCounter < FadeSamples)
{
float idx = in * (TableSize - 1);
int i0 = int(idx);
float frac = idx - i0;
float vOld = jmap(frac, oldCurve[i0], oldCurve[i0 + 1]);
float vNew = jmap(frac, newCurve[i0], newCurve[i0 + 1]);
float alpha = fadeCounter * fadeInc;
out = jmap(alpha, vOld, vNew);
if (++fadeCounter == FadeSamples)
memcpy(oldCurve, newCurve, sizeof(newCurve));
}
else
{
float idx = in * (TableSize - 1);
int i0 = int(idx);
float frac = idx - i0;
out = jmap(frac, oldCurve[i0], oldCurve[i0 + 1]);
}
// back to [-1,1]
frame[ch] = (out * 2.0f) - 1.0f;
}
}
// Curve mapping function
float mapSampleToCurve(float x, float curveShape)
{
float drive = jmap(curveShape, 1.0f, 10.0f);
return std::tanh((x * 2.0f - 1.0f) * drive);
}
// Handle parameter changes
template <int P> void setParameter(double v)
{
if constexpr (P == 0)
{
for (int i = 0; i < TableSize; ++i)
{
float x = i / float(TableSize - 1);
newCurve[i] = mapSampleToCurve(x, float(v));
}
fadeCounter = 0;
}
}
void createParameters(ParameterDataList& data)
{
// Curve shape parameter
parameter::data p("Curve", { 0.0, 1.0 });
registerCallback<0>(p);
p.setDefaultValue(0.5);
data.add(std::move(p));
}
};
} // namespace project