C++ Third Party node SliderPack, Span & sfloat
-
Making a waveshaper, I get zipper noise when changing the shape on the interface (especially when no signal is present because of the offset caused by the curve not pass by 0).
The curve sets the sliderpack that is connected to C++, it works nicely except for this offset zipper noise, that is probably amplified by the the delay it takes to change the curve and pass all values to the SP, making "steps" in the change instead of being faster (I can see the steps in a node.peak)
No HPF can get rid of this, because if it doesn't allow that static offset to pass through, the step effect causes higher frequency glitches (evidently enough because a zipper/step noise is a square wave)
My idea is to pass the SP values to a span of
sfloat
so I can smoother the change.Question:
Would it make sense/be safe/be optimised enough to have a span of 128/512sfloat
? -
Instead, can't you just Crossfade?
Keep a buffer with the old waveshaper applied, and a buffer with a new waveshaper shape applied, then linear volume mix Crossfade between the old and new buffer over 64 samples.
Basically every time you create a new shape / tweak the shape, you fade out the current buffer while fading in a new buffer that is using the new shape.
This system will give you no clicks no matter how different the waveshaper shapes are.
-
yup, crossfade is the way to go, 512 sfloat calculations is super overkill for this simple task.
-
@Christoph-Hart @griffinboy alright I'll try to do this although I don't know yet how to approach this solution..
@Christoph-Hart BTW, are SliderPacks still limited to 128 sliders? (at least when connecting to C++ external data)
-
@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
-
This is a decent situation to use chat gpt.
Question it about ways to design this efficiently and robustly. It's basically a two voice system, you can think of the buffers as voices. Where you fade between them and then kill the old voice.The thing you must makes sure of when using this xfade method, is getting it to update to the latest version. For instance if you do a bunch of small changes, it needs to finish the first crossfade, and then look to see if the current value doesn't match what it needs to be (since you may have moved it again during it's xfade). At the end of the xfade this check just needs to be inserted, and to retrigger another fade if so.
GPT will be able to outline a good design if you bug it to be really 'ideal efficient and using best practices'
-
@Dan-Korneff @griffinboy Very interesting, thanks guys! I'll try to apply this concept with the sliderpack and local span for old/new curve
@Dan-Korneff Instead of the fractional + jmap, is there a particular reason you're not using an interpolator? Also the counter is not per channel but the idea makes sense even if not optimised
-
@ustk Using jmap was just the quickest way to show how it works. You'll want to use interpolation for sure.
As for the counter, a single instance should work fine... unless you're channels are updating at drastically different times. -
@griffinboy said in C++ Third Party node SliderPack, Span & sfloat:
The thing you must makes sure of when using this xfade method, is getting it to update to the latest version. For instance if you do a bunch of small changes, it needs to finish the first crossfade, and then look to see if the current value doesn't match what it needs to be (since you may have moved it again during it's xfade). At the end of the xfade this check just needs to be inserted, and to retrigger another fade if so.
Effectively this seems to be the trickiest part, even if easy to understand the principle... I'm getting there step by step!
-
@Christoph-Hart What about the slider pack limitation of 128?
Could it be extended?
Or perhaps it would be better to use a cable with complex data to send a more precise curve?