HISE Logo Forum
    • Categories
    • Register
    • Login

    C++ Third Party node SliderPack, Span & sfloat

    Scheduled Pinned Locked Moved C++ Development
    10 Posts 4 Posters 675 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.
    • ustkU
      ustk
      last edited by

      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/512 sfloat?

      Can't help pressing F5 in the forum...

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

        @ustk

        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.

        Christoph HartC 1 Reply Last reply Reply Quote 3
        • Christoph HartC
          Christoph Hart @griffinboy
          last edited by

          yup, crossfade is the way to go, 512 sfloat calculations is super overkill for this simple task.

          ustkU 2 Replies Last reply Reply Quote 2
          • ustkU
            ustk @Christoph Hart
            last edited by ustk

            @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)

            Can't help pressing F5 in the forum...

            Dan KorneffD griffinboyG 2 Replies Last reply Reply Quote 0
            • Dan KorneffD
              Dan Korneff @ustk
              last edited by Dan Korneff

              @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
              
              

              Dan Korneff - Producer / Mixer / Audio Nerd

              ustkU 1 Reply Last reply Reply Quote 1
              • griffinboyG
                griffinboy @ustk
                last edited by

                @ustk

                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'

                ustkU 1 Reply Last reply Reply Quote 1
                • ustkU
                  ustk @Dan Korneff
                  last edited by ustk

                  @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

                  Can't help pressing F5 in the forum...

                  Dan KorneffD 1 Reply Last reply Reply Quote 0
                  • Dan KorneffD
                    Dan Korneff @ustk
                    last edited by

                    @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.

                    Dan Korneff - Producer / Mixer / Audio Nerd

                    1 Reply Last reply Reply Quote 1
                    • ustkU
                      ustk @griffinboy
                      last edited by

                      @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! ☺

                      Can't help pressing F5 in the forum...

                      1 Reply Last reply Reply Quote 0
                      • ustkU
                        ustk @Christoph Hart
                        last edited by

                        @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?

                        Can't help pressing F5 in the forum...

                        1 Reply Last reply Reply Quote 0
                        • First post
                          Last post

                        23

                        Online

                        1.8k

                        Users

                        11.9k

                        Topics

                        104.0k

                        Posts