Solved Aliasing when Resampling (C++ Project)
-
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:
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)); } }; }
-
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]
-
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.
-
This post is deleted! -
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.
-
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; } }; }
-
-