HISE Logo Forum
    • Categories
    • Register
    • Login

    Aliasing when Resampling (C++ Project)

    Scheduled Pinned Locked Moved Solved Scripting
    6 Posts 2 Posters 209 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.
    • griffinboyG
      griffinboy
      last edited by griffinboy

      This is a revival of a post I made last week.
      I thought I had fixed my troubles of Aliasing, but it turns out that I had not. It's definitely some kind of mistake because it's audible, and very visible.

      Ew:
      99029456-5bbd-40eb-b85c-841373874d04-image.png

      I think it's maybe a misunderstanding of the Polyphonic interpolation scripts.

      This sampler has certainly been giving me a headache. but I'm thankful for everyone who has helped me get this far.

      Just posting in case the mistake is obvious to anyone.
      Chat gpt has not been helpful with this problem.

      // The aliasing is apparent when playing loud sine waves. It's not from clipping, I think it would be apparent if that was the issue
      
      #pragma once
      #include <JuceHeader.h>
      #include <array>
      
      namespace project
      {
          using namespace juce;
          using namespace hise;
          using namespace scriptnode;
      
          template <int NV>
          struct Griffin_Sampler : public data::base
          {
              SNEX_NODE(Griffin_Sampler);
      
              struct MetadataClass
              {
                  SN_NODE_ID("Griffin_Sampler");
              };
      
              // ==========================| Node Properties |==========================
              static constexpr bool isModNode() { return true; }
              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; }
      
              static constexpr int NumTables = 0;
              static constexpr int NumSliderPacks = 0;
              static constexpr int NumAudioFiles = 1;
              static constexpr int NumFilters = 0;
              static constexpr int NumDisplayBuffers = 0;
      
              // ==========================| Global Variables |==========================
              using IndexType = index::clamped<0, false>;
              using FloatIndex = index::unscaled<double, IndexType>;
              using InterpolatorType = index::hermite<index::unscaled<double, index::clamped<0, false>>>;
              static const int NUM_CHANNELS = 2;
      
              ExternalData data;
              span<dyn<float>, NUM_CHANNELS> sample;
              double sr = 0.0;
              double file_sr = 0.0;
      
              int64_t loopStart = 0;
              int64_t loopEnd = 1LL << 32;
              bool loopEnabled = false;
              double pitchModulation = 0.0; // Pitch modulation in semitones
              double speedMultiplier = 1.0; // Speed multiplier (1 = normal, -1 = half, 2 = double)
      
              struct VoiceData
              {
                  int64_t uptime = 0;
                  int64_t delta = 1LL << 28;
                  int midiNote = 60;
                  bool isLooping = false;
      
                  void reset() { uptime = 0; }
      
                  int64_t bump(int64_t loopStart, int64_t loopEnd, int64_t numSamples)
                  {
                      int64_t rv = uptime;
                      uptime += delta;
                      if (isLooping && uptime >= loopEnd && loopEnd > loopStart)
                      {
                          int64_t loopLength = loopEnd - loopStart;
                          uptime = loopStart + (uptime - loopStart) % loopLength;
                      }
                      uptime = std::min(uptime, numSamples);
                      return rv;
                  }
              };
      
              PolyData<VoiceData, NV> voiceData;
              ModValue gate;
      
              // Pitch ratio lookup table
              std::array<float, 128> pitchRatios;
      
              // ==========================| Prepare |==========================
              void prepare(PrepareSpecs specs)
              {
                  voiceData.prepare(specs);
                  sr = specs.sampleRate;
                  initPitchRatios();
      
                  // Recalculate delta for all voices
                  if (data.numSamples > 0 && sr != 0.0)
                  {
                      updateAllVoiceDeltas();
                  }
      
                  validateLoopPoints();
              }
      
              void initPitchRatios()
              {
                  constexpr int rootNote = 60; // C4
                  for (int i = 0; i < 128; ++i)
                      pitchRatios[i] = std::pow(2.0f, (i - rootNote) / 12.0f);
              }
      
              void validateLoopPoints()
              {
                  int64_t numSamples = static_cast<int64_t>(data.numSamples) << 32;
                  if (numSamples > 0)
                  {
                      loopStart = juce::jlimit(0LL, numSamples - 1, loopStart);
                      loopEnd = juce::jlimit(loopStart + 1, numSamples, loopEnd);
                  }
                  else
                  {
                      // If there are no samples, reset loop points to default values
                      loopStart = 0;
                      loopEnd = 1LL << 32;
                  }
              }
      
              // ==========================| Reset |==========================
              void reset()
              {
                  for (auto& v : voiceData)
                      v.reset();
              }
      
              // ==========================| Process |==========================
              template <typename ProcessDataType>
              void process(ProcessDataType& data)
              {
                  int numSamples = data.getNumSamples();
      
                  if (numSamples == 0 || this->data.numSamples == 0)
                      return;
      
                  DataReadLock sl(this->data);
                  auto& v = voiceData.get();
      
                  auto& fixData = data.template as<ProcessData<NUM_CHANNELS>>();
                  auto audioBlock = fixData.toAudioBlock();
      
                  processBlock(audioBlock, v, numSamples);
      
                  this->data.setDisplayedValue(static_cast<double>(v.uptime) / (1LL << 32));
                  auto isPlaying = v.uptime < (static_cast<int64_t>(this->data.numSamples) << 32) || v.isLooping;
                  gate.setModValueIfChanged(static_cast<double>(isPlaying));
              }
      
              void processBlock(juce::dsp::AudioBlock<float>& audioBlock, VoiceData& v, int numSamples)
              {
                  auto* leftChannelData = audioBlock.getChannelPointer(0);
                  auto* rightChannelData = audioBlock.getChannelPointer(1);
      
      
                  for (int i = 0; i < numSamples; ++i)
                  {
                      int64_t thisUptime = v.bump(loopStart, loopEnd, static_cast<int64_t>(data.numSamples) << 32);
                      double fractionalSample = static_cast<double>(thisUptime & 0xFFFFFFFF) / (1LL << 32);
                      int wholeSample = static_cast<int>(thisUptime >> 32);
      
                      InterpolatorType idx(fractionalSample);
                      FloatIndex floatIndex(wholeSample + fractionalSample);
      
                      // Use the interpolator through the FloatIndex for both channels 
                      leftChannelData[i] = sample[0][floatIndex] 
                      rightChannelData[i] = sample[1][floatIndex] 
                  }
              }
      
              // Frame-based processing (for potential single-frame calls)
              template <typename FrameDataType>
              void processFrame(FrameDataType& fd)
              {
                  // Implement if needed
              }
      
              // ==========================| Set Parameter |==========================
              template <int P>
              void setParameter(double v)
              {
                  int64_t numSamples = static_cast<int64_t>(data.numSamples) << 32;
                  bool needsValidation = false;
      
                  if (P == 0 && numSamples > 0)
                  {
                      loopStart = static_cast<int64_t>(v * numSamples);
                      needsValidation = true;
                  }
                  else if (P == 1 && numSamples > 0)
                  {
                      loopEnd = static_cast<int64_t>(v * numSamples);
                      needsValidation = true;
                  }
                  else if (P == 2)
                  {
                      loopEnabled = v > 0.5;
                  }
                  else if (P == 3)
                  {
                      pitchModulation = v; // Directly use the value as semitones
                      updateAllVoiceDeltas();
                  }
                  else if (P == 4)
                  {
                      speedMultiplier = v;
                      updateAllVoiceDeltas();
                  }
      
                  if (needsValidation)
                  {
                      validateLoopPoints();
                  }
              }
      
              // ==========================| Create Parameters |==========================
              void createParameters(ParameterDataList& data)
              {
                  {
                      parameter::data p("Loop Start", { 0.0, 1.0, 0.01 });
                      registerCallback<0>(p);
                      p.setDefaultValue(0.0);
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Loop End", { 0.0, 1.0, 0.01 });
                      registerCallback<1>(p);
                      p.setDefaultValue(1.0);
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Loop Enable", { 0.0, 1.0, 1.0 });
                      registerCallback<2>(p);
                      p.setDefaultValue(0.0);
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Pitch", { -12.0, 12.0, 0.001 }); // Range: -12 to +12 semitones
                      registerCallback<3>(p);
                      p.setDefaultValue(0.0); // Default to no pitch change
                      data.add(std::move(p));
                  }
                  {
                      parameter::data p("Speed", { -1.0, 2.0, 0.01 }); // Range: -1 to 2
                      registerCallback<4>(p);
                      p.setDefaultValue(1.0); // Default to normal speed
                      data.add(std::move(p));
                  }
              }
      
              // ==========================| External Data |==========================
              void setExternalData(const ExternalData& ed, int index)
              {
                  int64_t oldNumSamples = static_cast<int64_t>(data.numSamples) << 32;
      
                  data = ed;
                  ed.referBlockTo(sample[0], 0);
                  ed.referBlockTo(sample[1], 1);
      
                  if (data.numSamples > 0 && sr != 0.0)
                  {
                      updateAllVoiceDeltas();
      
                      int64_t newNumSamples = static_cast<int64_t>(data.numSamples) << 32;
                      if (oldNumSamples != newNumSamples)
                          adjustLoopPoints(oldNumSamples, newNumSamples);
                  }
              }
      
              void adjustLoopPoints(int64_t oldNumSamples, int64_t newNumSamples)
              {
                  if (oldNumSamples > 0)
                  {
                      double scaleFactor = static_cast<double>(newNumSamples) / oldNumSamples;
                      loopStart = static_cast<int64_t>(loopStart * scaleFactor);
                      loopEnd = static_cast<int64_t>(loopEnd * scaleFactor);
      
                      validateLoopPoints();
                  }
              }
      
              // ==========================| Handle HISE Event |==========================
              void handleHiseEvent(HiseEvent& e)
              {
                  if (e.isNoteOn())
                  {
                      auto& v = voiceData.get();
                      v.midiNote = e.getNoteNumber();
                      v.delta = calculateDelta(v.midiNote);
                      v.reset();
                      v.isLooping = loopEnabled;
                  }
              }
      
              // ==========================| Modulation Slot |==========================
              bool handleModulation(double& value)
              {
                  return gate.getChangedValue(value);
              }
      
              // ==========================| Helper Functions |==========================
              void updateAllVoiceDeltas()
              {
                  for (auto& v : voiceData)
                  {
                      v.delta = calculateDelta(v.midiNote);
                  }
              }
      
              int64_t calculateDelta(int midiNote)
              {
                  double pitchRatio = std::pow(2.0, pitchModulation / 12.0);
                  double speedFactor = (speedMultiplier >= 1.0) ? speedMultiplier : (1.0 / (2.0 - speedMultiplier));
                  return static_cast<int64_t>((data.sampleRate / sr) * pitchRatios[midiNote] * pitchRatio * speedFactor * (1LL << 32));
              }
          };
      }
      
      Christoph HartC 1 Reply Last reply Reply Quote 0
      • Christoph HartC
        Christoph Hart @griffinboy
        last edited by

                        int64_t thisUptime = v.bump(loopStart, loopEnd, static_cast<int64_t>(data.numSamples) << 32);
                        double fractionalSample = static_cast<double>(thisUptime & 0xFFFFFFFF) / (1LL << 32);
                        int wholeSample = static_cast<int>(thisUptime >> 32);
        
                        InterpolatorType idx(fractionalSample);
                        FloatIndex floatIndex(wholeSample + fractionalSample);
        
                        // Use the interpolator through the FloatIndex for both channels 
                        leftChannelData[i] = sample[0][floatIndex] 
                        rightChannelData[i] = sample[1][floatIndex] 
        

        You're not using the interpolator type, so you actually use nearest neighbour interpolation .

        Also not sure why you're overcomplicating things. Why are you bitshifting and casting doubles to int like a crazy person?

        double thisUptime = v.bump(); // rewrite this to return a double that's wrapped around your loop point
        
        InterpolatorType idx(thisUptime);
        leftChannelData[i] = sample[0][idx] 
        rightChannelData[i] = sample[1][idx] 
        
        griffinboyG 2 Replies Last reply Reply Quote 0
        • griffinboyG
          griffinboy @Christoph Hart
          last edited by griffinboy

          @Christoph-Hart

          😢
          I will admit that I asked an Ai it's opinion.
          I have many the mistake of taking it seriously, only to discover that it's sending me on a wild goose chase.

          Now that I think about it, I'm not sure why I would need sub sample accuracy.

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

            This post is deleted!
            Christoph HartC 1 Reply Last reply Reply Quote 0
            • Christoph HartC
              Christoph Hart @griffinboy
              last edited by

              int64_t uptime = 0;
              

              This is still a problem, usually the uptime and the delta increase per sample are double precision floating point values (and here it's really important to use double precision because the per sample additions need the full accuracy).

              Now that I think about it, I'm not sure why I would need sub sample accuracy.

              You need it because the delta per sample will be a non-integer value for pitch ratios other than 1.0 so you end up with fractional position values hence the entire need for interpolation.

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

                @Christoph-Hart

                Yep, took it down immediately realising the sillyness!

                This is a good solution now, it seems to work fine.
                I've not done resampling before, I was tying myself in knots. Best to make something functional and robust before looking at optimisations.

                This seems to work so I'll leave it here in case anyone needed to see the interpolation method. I need to do more learning myself

                // ==========================| Sampler v0.58 : by Griffinboy |==========================
                
                #pragma once
                #include <JuceHeader.h>
                #include <array>
                
                namespace project
                {
                    using namespace juce;
                    using namespace hise;
                    using namespace scriptnode;
                
                    // Template class for a sampler with a specified number of voices (NV)
                    template <int NV>
                    struct Griffin_Sampler : public data::base
                    {
                        SNEX_NODE(Griffin_Sampler);
                
                        struct MetadataClass
                        {
                            SN_NODE_ID("Griffin_Sampler");
                        };
                
                        // ==========================| Node Properties |==========================
                        // Define properties of the node
                        static constexpr bool isModNode() { return true; } // Indicates this is a modulation node
                        static constexpr bool isPolyphonic() { return NV > 1; } // Determines if the sampler is polyphonic
                        static constexpr bool hasTail() { return false; } // Indicates if the node has a tail (sustain)
                        static constexpr bool isSuspendedOnSilence() { return false; } // Determines if processing is suspended on silence
                        static constexpr int getFixChannelAmount() { return 2; } // Fixed number of audio channels
                
                        // Define the number of various components
                        static constexpr int NumTables = 0;
                        static constexpr int NumSliderPacks = 0;
                        static constexpr int NumAudioFiles = 1;
                        static constexpr int NumFilters = 0;
                        static constexpr int NumDisplayBuffers = 0;
                
                        // ==========================| Global Variables |==========================
                        using InterpolatorType = index::hermite<index::unscaled<double, index::clamped<0, false>>>;
                        static const int NUM_CHANNELS = 2; // Number of audio channels
                
                        ExternalData data; // External data for the sampler
                        span<dyn<float>, NUM_CHANNELS> sample; // Sample data for each channel
                        double sr = 0.0; // Sample rate
                        double file_sr = 0.0; // File sample rate
                
                        // Looping and playback parameters
                        double loopStart = 0.0;
                        double loopEnd = 1.0;
                        bool loopEnabled = false;
                        double pitchModulation = 0.0;
                        double speedMultiplier = 1.0;
                
                        // Structure to hold voice-specific data
                        struct VoiceData
                        {
                            double uptime = 0.0; // Time since the voice started playing
                            double delta = 1.0 / (1 << 28); // Increment per sample
                            int midiNote = 60; // MIDI note number
                            bool isLooping = false; // Looping state
                            bool isPlaying = false; // Playback state
                
                            // Resets the voice data
                            void reset() { uptime = 0.0; isPlaying = true; }
                
                            // Advances the playback position and handles looping
                            double bump(double loopStart, double loopEnd, double numSamples)
                            {
                                if (!isPlaying) return numSamples;
                
                                double rv = uptime;
                                uptime += delta;
                                if (isLooping && uptime >= loopEnd && loopEnd > loopStart)
                                {
                                    double loopLength = loopEnd - loopStart;
                                    uptime = loopStart + std::fmod(uptime - loopStart, loopLength);
                                }
                                else if (uptime >= numSamples)
                                {
                                    uptime = numSamples;
                                    isPlaying = false;
                                }
                                return rv;
                            }
                        };
                
                        PolyData<VoiceData, NV> voiceData; // Container for voice data
                        ModValue gate; // Modulation value for gate
                
                        std::array<float, 128> pitchRatios; // Precomputed pitch ratios for MIDI notes
                
                        // ==========================| Prepare |==========================
                        // Prepares the sampler for playback
                        void prepare(PrepareSpecs specs)
                        {
                            voiceData.prepare(specs);
                            sr = specs.sampleRate;
                            initPitchRatios();
                
                            if (data.numSamples > 0 && sr != 0.0)
                            {
                                updateAllVoiceDeltas();
                            }
                
                            validateLoopPoints();
                        }
                
                        // Initializes pitch ratios for MIDI notes
                        void initPitchRatios()
                        {
                            constexpr int rootNote = 60; // Middle C
                            for (int i = 0; i < 128; ++i)
                                pitchRatios[i] = std::pow(2.0f, (i - rootNote) / 12.0f);
                        }
                
                        // Validates and adjusts loop points
                        void validateLoopPoints()
                        {
                            if (data.numSamples > 0)
                            {
                                loopStart = juce::jlimit(0.0, 1.0, loopStart);
                                loopEnd = juce::jlimit(loopStart, 1.0, loopEnd);
                            }
                            else
                            {
                                loopStart = 0.0;
                                loopEnd = 1.0;
                            }
                        }
                
                        // ==========================| Reset |==========================
                        // Resets all voices
                        void reset()
                        {
                            for (auto& v : voiceData)
                                v.reset();
                        }
                
                        // ==========================| Process |==========================
                        // Processes audio data
                        template <typename ProcessDataType>
                        void process(ProcessDataType& data)
                        {
                            int numSamples = data.getNumSamples();
                
                            if (numSamples == 0 || this->data.numSamples == 0)
                                return;
                
                            DataReadLock sl(this->data);
                            auto& v = voiceData.get();
                
                            auto& fixData = data.template as<ProcessData<NUM_CHANNELS>>();
                            auto audioBlock = fixData.toAudioBlock();
                
                            processBlock(audioBlock, v, numSamples);
                
                            this->data.setDisplayedValue(v.uptime);
                            gate.setModValueIfChanged(static_cast<double>(v.isPlaying));
                        }
                
                        // Processes a block of audio samples
                        void processBlock(juce::dsp::AudioBlock<float>& audioBlock, VoiceData& v, int numSamples)
                        {
                            auto* leftChannelData = audioBlock.getChannelPointer(0);
                            auto* rightChannelData = audioBlock.getChannelPointer(1);
                
                            double numSamplesDouble = static_cast<double>(data.numSamples);
                            double loopStartSamples = loopStart * numSamplesDouble;
                            double loopEndSamples = loopEnd * numSamplesDouble;
                
                            for (int i = 0; i < numSamples; ++i)
                            {
                                double thisUptime = v.bump(loopStartSamples, loopEndSamples, numSamplesDouble);
                
                                if (v.isPlaying)
                                {
                                    InterpolatorType idx(thisUptime);
                
                                    leftChannelData[i] = sample[0][idx];
                                    rightChannelData[i] = sample[1][idx];
                                }
                                else
                                {
                                    leftChannelData[i] = 0.0f;
                                    rightChannelData[i] = 0.0f;
                                }
                            }
                        }
                
                        template <typename FrameDataType>
                        void processFrame(FrameDataType& fd)
                        {
                            // Implement if needed
                        }
                
                        // ==========================| Set Parameter |==========================
                        // Sets a parameter value and validates if necessary
                        template <int P>
                        void setParameter(double v)
                        {
                            bool needsValidation = false;
                
                            if (P == 0)
                            {
                                loopStart = v;
                                needsValidation = true;
                            }
                            else if (P == 1)
                            {
                                loopEnd = v;
                                needsValidation = true;
                            }
                            else if (P == 2)
                            {
                                loopEnabled = v > 0.5;
                            }
                            else if (P == 3)
                            {
                                pitchModulation = v;
                                updateAllVoiceDeltas();
                            }
                            else if (P == 4)
                            {
                                speedMultiplier = v;
                                updateAllVoiceDeltas();
                            }
                
                            if (needsValidation)
                            {
                                validateLoopPoints();
                            }
                        }
                
                        // ==========================| Create Parameters |==========================
                        // Creates and registers parameters for the sampler
                        void createParameters(ParameterDataList& data)
                        {
                            {
                                parameter::data p("Loop Start", { 0.0, 1.0, 0.01 });
                                registerCallback<0>(p);
                                p.setDefaultValue(0.0);
                                data.add(std::move(p));
                            }
                            {
                                parameter::data p("Loop End", { 0.0, 1.0, 0.01 });
                                registerCallback<1>(p);
                                p.setDefaultValue(1.0);
                                data.add(std::move(p));
                            }
                            {
                                parameter::data p("Loop Enable", { 0.0, 1.0, 1.0 });
                                registerCallback<2>(p);
                                p.setDefaultValue(0.0);
                                data.add(std::move(p));
                            }
                            {
                                parameter::data p("Pitch", { -12.0, 12.0, 0.001 });
                                registerCallback<3>(p);
                                p.setDefaultValue(0.0);
                                data.add(std::move(p));
                            }
                            {
                                parameter::data p("Speed", { 0.5, 2.0, 0.01 });
                                registerCallback<4>(p);
                                p.setDefaultValue(1.0);
                                data.add(std::move(p));
                            }
                        }
                
                        // ==========================| External Data |==========================
                        // Sets external data for the sampler
                        void setExternalData(const ExternalData& ed, int index)
                        {
                            data = ed;
                            ed.referBlockTo(sample[0], 0);
                            ed.referBlockTo(sample[1], 1);
                
                            if (data.numSamples > 0 && sr != 0.0)
                            {
                                updateAllVoiceDeltas();
                                validateLoopPoints();
                            }
                        }
                
                        // ==========================| Handle HISE Event |==========================
                        // Handles MIDI events for note on and note off
                        void handleHiseEvent(HiseEvent& e)
                        {
                            if (e.isNoteOn())
                            {
                                auto& v = voiceData.get();
                                v.midiNote = e.getNoteNumber();
                                v.delta = calculateDelta(v.midiNote);
                                v.reset();
                                v.isLooping = loopEnabled;
                            }
                            else if (e.isNoteOff())
                            {
                                auto& v = voiceData.get();
                                if (v.midiNote == e.getNoteNumber())
                                {
                                    v.isPlaying = false;
                                }
                            }
                        }
                
                        // ==========================| Modulation Slot |==========================
                        // Handles modulation changes
                        bool handleModulation(double& value)
                        {
                            return gate.getChangedValue(value);
                        }
                
                        // ==========================| Helper Functions |==========================
                        // Updates the delta for all voices based on current parameters
                        void updateAllVoiceDeltas()
                        {
                            for (auto& v : voiceData)
                            {
                                v.delta = calculateDelta(v.midiNote);
                            }
                        }
                
                        // Calculates the delta for a given MIDI note
                        double calculateDelta(int midiNote)
                        {
                            double pitchRatio = std::pow(2.0, pitchModulation / 12.0);
                            return (data.sampleRate / sr) * pitchRatios[midiNote] * pitchRatio * speedMultiplier;
                        }
                    };
                }
                
                1 Reply Last reply Reply Quote 0
                • griffinboyG griffinboy marked this topic as a question on
                • griffinboyG griffinboy has marked this topic as solved on
                • First post
                  Last post

                23

                Online

                1.8k

                Users

                12.0k

                Topics

                104.1k

                Posts