Basic anti-aliasing for non-linear functions

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Hi everyone,

I am teaching myself C++, and I'm trying to make a simple distortion plugin (using JUCE).

However, I am kind of blocked at the antialiasing part - I am having some trouble understanding perfectly what I should do, even with some very interesting topics I found here on KVR (hereand here).

What I understand (correct me if I'm wrong) is that I should do the following steps :
  • Oversample my buffer (let's say 2X to make it simple)
  • Process the oversampled array with my distortion algorithm
  • Low-pass (for instance using a Butterworth filter)
  • Downsample
Okay.

Question 1 :

Regarding the oversampling : for now I'm just adding 0 values between the actual samples values. I noticed links to this article : http://yehar.com/blog/wp-content/upload ... iginal.pdf

I assume it's just a matter of choice, but which of the interpolation algorithms are the most commonly used ?

Since we are using buffers, aren't the oversampling cause a problem at the limits (i.e. first sample and last sample in the buffer) ? I think not but I want to make sure :)

Question 2 :

So far I have a written a very simple code without filtering. Right now I am getting some sound but there is a kind of static noise, so I guess I am doing something wrong. Anyone has an idea ?

(I know its speed can be enhanced, by the way, I just did a very raw thing to understand the concept. Speed will come after :) )

Code: Select all

        float* channelData = buffer.getWritePointer (channel);

		// OVERSAMPLING 2X
		for (int i = 0; i < buffer.getNumSamples()*2; i++) {
			if (i % 2 == 0) { oversampleArray.push_back(channelData[i]); }
			else { oversampleArray.push_back(0); }		
		}

		// DISTORTION
		for (int sample = 0; sample < buffer.getNumSamples()*2; sample++) {
			oversampleArray[sample] = 1 / (atan(5))*atan(5*oversampleArray[sample]);
		}

		// DOWNSAMPLING
		for (int i = 0; i < buffer.getNumSamples(); i++) {
			channelData[i] = oversampleArray[2 * i];
		}

		oversampleArray.clear();
Question 3 :

Would it be absolutely wrong to simply do a LPF at Nyquist frequency, without oversampling ?

--

Sorry, that's a lot of questions and I know those are complex topics. I hope you guys will be able to help me out, because I am reading a lot of things about FIRs and stuffs but in the end I am not sure which direction to go, especially since DSP is not exactly for the faint-hearted in terms of mathematics, and although I understand the formulas I am reading they don't always make crystal-clear sense to me.

Thank you by advance for your help,

V.
Last edited by Valenten on Thu Feb 11, 2016 11:12 pm, edited 1 time in total.

Post

q1: a lot of people have vast experience with this, wait for a good answer here
q2: currently you're actually just processing every even sample (thus not oversampling), storing it quite ineffeciently. is the static noise permanent or scaled by volume?
q3: it wouldn't really do much (if anything). you add the lpf filter only when oversampling, so (ideally) it cuts away half of the spectrum, where the aliasing would most likely be.

Post

Hi Mayae, thanks a lot for your answer.
Mayae wrote:q2: currently you're actually just processing every even sample (thus not oversampling), storing it quite ineffeciently. is the static noise permanent or scaled by volume?
I've just uploaded a sample online, it sounds like this :
https://soundcloud.com/adrienp-mix/static

note : I have lowered the master volume on my DAW to make it "listenable" before exporting. Usually it's clipping.

From what you say, I assume you agree there is no (obvious) mistake in my code. Indeed, I am basically doing "nothing", just wanted to get this correct first and then add the interpolation for the oversampling. So if my code is "correct", I don't understand where this noise comes from.

It *might* come from the fact I am allocating memory in my "oversampleArray" vector... which would create a problem in the DAW.

Post

Yes, the algorithm as is works. It could potentially be the memory allocations, although that would be strange. Obviously you wouldn't do it in production code, but it should be far within the limits of computers can do today.

What does the cpu% say? Did you try skipping the 'oversampling'? Allocating the buffer up front? If yes, the problem is probably in another part of the code.

Post

What about doing that instead :

Code: Select all

 float* channelData = buffer.getWritePointer (channel);

          // OVERSAMPLING 2X
          for (int i = 0; i < buffer.getNumSamples(); i++) 
          {
             oversampleArray.push_back(channelData[i]);
             oversampleArray.push_back(0);
          }

          // DISTORTION
          for (int sample = 0; sample < buffer.getNumSamples()*2; sample++) {
             oversampleArray[sample] = 1 / (atan(5))*atan(5*oversampleArray[sample]);
          }

          // DOWNSAMPLING
          for (int i = 0; i < buffer.getNumSamples(); i++) {
             channelData[i] = oversampleArray[2 * i];
          }

          oversampleArray.clear();
In the original code, I think you are trying to access to channelData with i >= numSamples, which is a very bad idea :wink:

Post

Q1: Just padding with zeros alone is pretty bad because the signal gets mirrored in the frequency domain. When doing zero padding, integer upsampling you need a brick-wall LPF at the original sample rate's nyquist point (e.g. upsampling 44,1kHz with zeros needs a 22.05kHz LPF). Catmul-Rom splines (a variation of Cubic Hermite) are a _way_ better choice if you want to avoid filtering the oversampled signal.

Q2: There's already ppl looking into the code, but: _avoid_ STL vectors and stuff in audio processing callbacks (except you can guarantee that it would not do any malloc/realloc)

Q3: Nope. The highest cutoff a filter can have is nyquist. Also the highest frequency you can reproduce is given by the nyquist theorem. When filtering a 44kHz sample at 22.05kHz (with an ideal, brick-wall LPF) nothing will happen. The aliasing already happened, so you can't get rid of it.

And: avoid IIR filters for downsampling (e.g. biquads or the like), FIR filters are the way to go because they have way better phase responses and a much narrower pass-band with less calculations.
... when time becomes a loop ...
---
Intel i7 3770k @3.5GHz, 16GB RAM, Windows 7 / Ubuntu 16.04, Cubase Artist, Reaktor 6, Superior Drummer 3, M-Audio Audiophile 2496, Akai MPK-249, Roland TD-11KV+

Post

And: avoid IIR filters for downsampling (e.g. biquads or the like), FIR filters are the way to go because they have way better phase responses and a much narrower pass-band with less calculations.
Doing that might increase a lot the latency doesn't it ? For a guitar amp simulation for example, I might avoid like hell FIR filters in the up/downsampling

Post

'Increase a lot' ... it depends on what you do, the overall latency in the end is ~taps/(2*oversample_factor). So, using a 127 tap fir filter with 4 times oversampling results in a latency of 15 samples, which is 0.3ms at 44.1kHz ... you can easily go up to 10ms, maybe even more, depending on how much latency your audio drivers introduce.
... when time becomes a loop ...
---
Intel i7 3770k @3.5GHz, 16GB RAM, Windows 7 / Ubuntu 16.04, Cubase Artist, Reaktor 6, Superior Drummer 3, M-Audio Audiophile 2496, Akai MPK-249, Roland TD-11KV+

Post

Hi everyone,

Wow that's already a lot of answers, thank you very much.
Ivan_C wrote:In the original code, I think you are trying to access to channelData with i >= numSamples, which is a very bad idea :wink:


Oh my god, can't believe my mistake was so obvious. Can't believe the host didn't crash, too.

Thanks a lot, at least now it's working perfectly.

neotec wrote:Q1: Just padding with zeros alone is pretty bad because the signal gets mirrored in the frequency domain. When doing zero padding, integer upsampling you need a brick-wall LPF at the original sample rate's nyquist point (e.g. upsampling 44,1kHz with zeros needs a 22.05kHz LPF). Catmul-Rom splines (a variation of Cubic Hermite) are a _way_ better choice if you want to avoid filtering the oversampled signal.


Thanks a lot for the advice. Yeah, I had a feeling that using zeros was a really raw approach. I will try to follow your suggestion and implement Catmul-Rom splines.

neotec wrote:_avoid_ STL vectors and stuff in audio processing callbacks


Yes, I was doing it just for the sake of writing a simplified algorithm, but I do understand it's essentially wrong. I will have to think a bit of which option is better. I think JUCE is providing some buffer array types which I think must be way more optimised (as long as there is no memory allocation in the processing thread of course)

Ivan_C wrote:Q3: Nope. The highest cutoff a filter can have is nyquist. Also the highest frequency you can reproduce is given by the nyquist theorem. When filtering a 44kHz sample at 22.05kHz (with an ideal, brick-wall LPF) nothing will happen. The aliasing already happened, so you can't get rid of it.


Alright, I think I got it :)

neotec wrote:And: avoid IIR filters for downsampling (e.g. biquads or the like), FIR filters are the way to go because they have way better phase responses and a much narrower pass-band with less calculations.
Ivan_C wrote: Doing that might increase a lot the latency doesn't it ? For a guitar amp simulation for example, I might avoid like hell FIR filters in the up/downsampling


Well on this topic I think I will try both and see what fits best. Here I am doing a simple distortion, not a guitar amp, so I think the algorithm could be rather fast.
I will try to find/use existing classes, it may be a quick way to test. (such as these : https://github.com/vinniefalco/DSPFilters - as soon as I understand how to implement them).

Again thank you everyone. I'll post again below as I progress on this topic.

V.

Post

Yes, I was doing it just for the sake of writing a simplified algorithm, but I do understand it's essentially wrong. I will have to think a bit of which option is better. I think JUCE is providing some buffer array types which I think must be way more optimised (as long as there is no memory allocation in the processing thread of course)
Its not that stl vectors are not optimized (in fact, it should probably be the most optimized array out there), it's all about usage.
So obviously you want to allocate space up front. If you do not care *that* much, the easiest solution is probably to have a vector.resize(numSamples * oversamplingFactor) in the start of the loop. Yes, in the start it may allocate some memory once or twice, but it will never release memory back - so after the first 2/3 calls or so, it will be a no-op, and thus you always have a buffer that is adequately sized, with optimal speed and size for the particular system.

You can also allocate a big enough (TM) array at the start, and hope your host never exceeds that size, comprimising safety, general-purpose memory size and drop-outs for speed (like people used to do in oldschool C programming).

If you want to do it correctly, you're already looking at a threaded solution with a generally passive worker thread, that does the dirty jobs for your audio thread (passing messages, allocating memory). If you want this to work, your buffer needs to be hotswappable, ie. an atomic two-stage pointer. This also reworks your processing model to not always work (ie. if your buffer is less than what is requested, you need to pass on processing, wait till your worker thread has created a big enough array, and continue). Depending on the system, it may happen instantly or be delayed by a couple of blocks.

Solution one is pretty common and actually employed inside the JUCE framework.

Post

neotec wrote:Just padding with zeros alone is pretty bad because the signal gets mirrored in the frequency domain. When doing zero padding, integer upsampling you need a brick-wall LPF at the original sample rate's nyquist point (e.g. upsampling 44,1kHz with zeros needs a 22.05kHz LPF).
Zero stuffing + FIR is still one of the standard ways for upsampling (https://en.wikipedia.org/wiki/Upsampling).

But if you want to avoid latency, interpolation is the way to go.

Optimal interpolation:
http://www.student.oulu.fi/~oniemita/dsp/deip.pdf

Post

Mayae wrote:Its not that stl vectors are not optimized
Yes, I think I didn't explain my vision clearly - but I do understand the interest of allocating the memory upfront.

Regarding the two options you mention, I think I will focus on the first one for now. Definitely the 2nd one is way too complex for my understanding - I guess it would be an interesting upgrade later on though. :)
Chris-S wrote:
neotec wrote:Zero stuffing + FIR is still one of the standard ways for upsampling (https://en.wikipedia.org/wiki/Upsampling).

But if you want to avoid latency, interpolation is the way to go.
Well I would think that using interpolation would give better result ? Not sure to understand where the latency would come from though, but I think I have to delve deeper in the algorithms.

For the exercise, I am currently trying to use a Catmull Rom interpolation to upsample and then downsample the buffer array. So far I can't find a simple way to include an external class, so I think I will program it myself...

However I'll ask two other questions : how am I supposed to manage the border values with the interpolation ? If I am using a Catmull Rom spline algorithm, I am using 4 points to do the calculation. So to evaluate the interpolated values between sample 0 and sample 1, I need a fictive sample -1 value. Should I choose sample-1 = sample 0 ?

Also, (it will come afterwards though) : when downsampling, instead of using an interpolation, isn't it correct to simply select "one sample out of [oversampling rate]" ?

Post

Catmul-Rom (for upsampling, downsampling needs filtering+decimation) needs four samples, and interpolates between the 2. and 3. sample.

Let's call the values v0-v3, then v1 is your _current_ sample, v2 would be the next and also the two that you wish to interpolate between, v0 is the one before the current and v3 is the one after next, so, yes, you have 2 samples latency here, also.

Code: Select all

static inline double cmSpline(const double v0, const double v1, const double v2, const double v3, const double f)
{
    return ((((v3 + 3 * (v1 - v2) - v0) * f + v0 + v0 - 5 * v1 + 4 * v2 - v3) * f + v2 - v0) * f + v1 + v1) * 0.5;
}
And yes, for downsampling, filter first, then just pick every nth sample.
... when time becomes a loop ...
---
Intel i7 3770k @3.5GHz, 16GB RAM, Windows 7 / Ubuntu 16.04, Cubase Artist, Reaktor 6, Superior Drummer 3, M-Audio Audiophile 2496, Akai MPK-249, Roland TD-11KV+

Post

Valenten wrote:
Mayae wrote:Its not that stl vectors are not optimized
Yes, I think I didn't explain my vision clearly - but I do understand the interest of allocating the memory upfront.

Regarding the two options you mention, I think I will focus on the first one for now. Definitely the 2nd one is way too complex for my understanding - I guess it would be an interesting upgrade later on though. :)
Yes, don't bother with that stuff (especially during the prototyping stage). Just to clarify something I forgot: The vector should of course be a member of your processor class, not a local variable :)

Post

Valenten wrote:Well I would think that using interpolation would give better result ? Not sure to understand where the latency would come from though, but I think I have to delve deeper in the algorithms.
FIR interpolation filters have to be very steep, so they are using typically 64 or more taps (using the recent and the 63 previous samples). This gives some delay.

Post Reply

Return to “DSP and Plugin Development”