Interpolating between Waveforms

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

I use the mip mapped approach to creating/using bandlimited waveforms. This approach goes all the way back to the Korg DW6000 and Ensoniq ESQ-1 (maybe earlier).

Also, I linear interpolate between adjacent waveforms. That way as you play up the keyboard, you don't notice a sudden drop off in the high harmonics as the oscillators switch between waveforms.

This works, but there's a problem when doing pitch bends over, say, an octave or so. This is a situation you could run into using portamento. The danger when bending up is that you'll get aliasing when the underlying waveform's harmonic content goes above Nyquist. When bending down, the danger is that the waveform will sound dull as the waveforms harmonic content is well below Nyquist.

So ideally you'd remap the waveform on a per-sample basis instead of just mapping it once when a note is triggered. I'm trying to think of the most efficient way to do this.

Here's an approach I'm thinking about.

First, when a note is triggered, you calculate the phase increment the oscillator will be using by converting the note into a frequency and dividing that by the sample rate.

You also calculate an index into your mip mapped waveforms to decide which one will be used. I'm assuming one waveform per octave.

Then for each sample, you add the FM input value to that index, clipping the index when it goes below zero or above or equal to the waveform count.

Finally, use the FM value to modulate the phase increment.

It would look something like this:

Code: Select all

float Process(float fm)
{
    float w = waveIndex + fm;

    if(w < 0)
    {
        w = 0;
    }
    else if(w > waveCount - 1)
    {
        w = waveCount - 1;
    }

    float f = pow(2, fm);

    phaseAccumulator += phaseIncrement * f;

    // Read waveform data and linear interpolate between them...
}
Offsetting the waveIndex with the raw fm value works if we're using one waveform per octave. If the fm variable has a value of 1, it is a equivalent to one octave of modulation (pow(2, 1)). So simply incrementing our wave index by 1 moving it to the next waveform in the map works.

We'd use the fractional value of w to linear interpolate with the next waveform above the current one. Here it's helpful to have an extra waveform in the map at the top end. In other words the actual waveform count is waveCount + 1.

Anyway, it still seems like a lot of work to do on a per-sample basis, so I dunno...

Post

Anyway, it still seems like a lot of work to do on a per-sample basis, so I dunno...
Hey Leslie,

You're right. Have you looked into oversampling and simple filtering/decimation as an alternative to this? You may be oversampling already, increasing your internal sample rate (again) may give better sound and performance compared to working within a small window while interpolating among many small tables (supposing your memory and CPU consumption is identical for the tests). I don't actually know the best solution in an fm application, but that is where I would begin. Have you tried it at 4x or 8x the current sampling rate? Are you already oversampling internally? by how much? Your tables could then have plenty of hf content, and you would have the opportunity to reduce it as necessary, so you could use even fewer tables (and then larger tables in exchange, and use less scanning, interpolation, branching, etc.).

Justin

Post

How much do you actually notice the problem, given that it only happens doing pitch bends and in portamento? I'm mostly curious because I don't hear high pitches anymore. :-)
Swing is the difference between a drum machine and a sex machine.

Post

Given a waveform of length N, you have to lowpass filter with a sharp FIR filter at every M semitones and store those filtered copies in memory. Then, based on your playspeed, you should determine which adjacent filtered copies to use, and interpolate between those tables.

In SynthMaster 2.0, N and M can be specified in a configuration file. Default values are: N=1024, M=3. 3 semitones means a playback speed of 1.19, so at 44 khz, your waveforms should be bandlimited to 22k/1.19 = 18.5 kHz, which is not bad!
Works at KV331 Audio
SynthMaster voted #1 in MusicRadar's "Best Synth of 2019" poll
SynthMaster One voted #4 in MusicRadar's "Best Synth of 2019" poll

Post

I have been working on some interpolation programming recently, so I read this post...and my first reaction was :shock: and then :? At one wavetable per octave, even without mip mapping, you could cover the entire audible range with 10 KB of data! Are you trying to run this on a Commodore 64?
[Leslie - this part is not directed at you. It's just a general rant.]
I'm tired of softsynths that just sound okay because programmers are afraid to use a few cpu cycles to make them sound right. Who cares that you can run twenty copies of a synth at once if they all sound like shit? So I have been devoting my efforts to doing whatever it takes to build a synth that sounds right (like a piece of analog hardware, but with the stupid number of modulation options that you can only get with software).
In order to bring these principles to others, I have decided to begin a Crusade against Ruining Audio Programming, or CRAP for short. This will be my first attempt to recruit crusaders (or CRAPpers, if you prefer) to the cause.

[Here begins the actual on topic response]

Efficiency is good. Trading sound quality for a cheaper algorithm is not, when most people have at least 5-6 GHz of processing power and 2+ GB of RAM.

Keep in mind that I'm better with physics and math than with programming, so I'm sure there are other and/or more efficient ways to do this. The basic idea is to use more wavetables and to use SSE to implement high quality interpolation. We have to pick some numbers here. I don't know the interpolation methods used in synthmaker's sawtooth oscillator, but I do know that it sounds excellent, so I'll copy the wavetable size. Which is 256 samples per single cycle table * 2K tables. That's two thousand tables, but in single precision still only takes up 2 MB of memory. Map them from 20 Hz to 10015 Hz at 5 Hz intervals, with the last one just being a sine wave. (That's MIDI notes 15.4868 to 123.1022). IIRC, the best way to do this is to load the data into a 256*2K array and use pointers to access the array contents rather than using indexes (but don't ask me how to do that) . Let's say Frequency current equals Frequency initial plus Frequency modulation, i.e., Fc=Fi+Fm. So Fc is the actual freq we want for this particular sample, in Hz. Subtract 20 from Fc and multiply by 0.2 (divide by 5). That's the wavetable index plus fraction.

Here comes the fun part (just kidding, this is not fun at all!). Let's call the tables around Fc W0, W1, W2, W3, with Fc being above W1 and below W2. Say we already know the current phase. Out of our infinite choices, I will pick two interpolation methods that can be implemented with SSE. Four point and six point splines can be calculated with straightforward polynomials - search for "spline interpolation" on musicdsp.org. The 4 point uses the two data points before and two points after your target. The 6 point uses 3 on each side. As you might guess, the 4 point is faster and the 6 point is more accurate.
SSE data are 128 bits, so they can be 4 single precision numbers or 2 double precision. The fastest, but still quite accurate, method would be to use double precision data from W1 and W2 to calculate a 4 point spline on each curve, then do a linear interpolation between the two. Probably the best balance between speed and accuracy would be to do this with a 6 point spline instead of 4 point. This is good, but we can do even better. We can use single precision data from all four curves, W0, W1, W2, and W3, to calculate six point splines on all four in the time it would take to do just one without SSE. Now we have four points at our current phase, two above and two below our current freq, and we can do a four point spline on that, too! That may be a lot of cpu cycles, but I bet it would be the best damn sounding oscillator ever. Now that I've written it down, it doesn't sound so hard. I might try it myself.

I didn't intend to write a book, so just a couple more quickies: I've implemented the six point spline (with 4x oversampling via SSE) in a chorus, and it is sweet. It sounds so natural that it doesn't even sound like what I've come to expect from a chorus. After you've determined your data points, the algorithm itself consists of 25 adds and 25 multiplies; it produces about 100 lines of assembler code.
Other thing: the pow function in the phase updater is fairly expensive (something like 80 cycles?). Would it work if you got rid of pow, forgot about fm, and just did (phase accumulator)+(current freq)/(samplerate) ? And would you want the frequency of the last sample or the next one?

Post

I did waveforms of 4096 samples and one waveform per semitone, and interpolated between those in my unreleased synth. I needed 4096 to get the sine "perfect," meaning nothing showing up but that one peak.

But I wasn't too thrilled with that approach. You really only want to have sine, saw, and triangle if you do that (and derive the pulse/square from the saw).

My new synth is 8x (or 16x--we'll see) oversampled, and it's pure hard-edged saw with an oversample filter. No table--just one line of osc code. This lets me do any kind of weird hard-sync, audiorate manipulations, fm, am, ring, PD, PWM, ANYTHING. I like this approach better. It's all trade-offs. There are many, many ways to make an oscillator. As long as the osc doesn't have a large section of missing high harmonics or nasty aliasing, I'm happy.
Swing is the difference between a drum machine and a sex machine.

Post

hey guys
in the synth which i'm working on, my oscillator uses the mip-map technique too
but i think i didn't make it the way you explain it here

the mipmap looks like this:
1. a "fundamental" wavetable <- the biggest one
2. the small "mip-maps"
i always use a power of 2 for the size of the fundamental table

if i use 1024 sampes for the fundamental table - the algorithm allocates 2048 samples (since all the mip-maps fit in the other 1024 samples, so nice)
but no, my table is not 1024 samples
it's size is dynamic, and automaticaly determined on init, depending on the sampling rate!

Code: Select all

#define LUT_SZ_MIN 256 // powers of 2 only!
#define LUT_SR_MIN 11025
// on init:
	int sr = getSampleRate(); // from host (erm, that's synthedit code right here, but doesn't matter)
	int ftbl_size = (sr / LUT_SR_MIN) * LUT_SZ_MIN;
so, in this case, if the sampling rate was 11025, my fundamental table will be 256 samples
Fs = 44100, ftbl_size = 1024
Fs = 88200, ftbl_size = 2048...

each table has a "perfect" frequency, where the osc reads 1 table sample per output sample, i call that the "table omega frequency"
that's where the waveform sounds perfect, and has all the harmonics up to nyquist
if the osc-Fc is above that point, it should switch (or interpolate) with the next smaller table
becase the fundamental table is the "biggest" of them all, you can calculate (depending on the sampling rate & the size of it) what is the minimum frequency at which your oscillator will have "rich" sound (since smaller frequencies will still use the fundamental table, but the harmonics will be lower than nyquist)
so the formula is:
F_low = Fs / ftbl_size;
this gives the frequency in Hz.. with that mipmap technique, i got "rich" oscillator output anywhere above that frequency
here's what happens with LUT_SZ_MIN=256:

Code: Select all

Fs =  44100, ftbl_size = 1024, F_low = 43.066Hz
Fs =  48000, ftbl_size = 1024, F_low = 46.875Hz
Fs =  88200, ftbl_size = 2048, F_low = 43.066Hz
Fs = 192000, ftbl_size = 4352, F_low = 44.117Hz
this looks nice to me ;]

on the other hand, with a fixed table size, you will probably get "cheaper" wavetable sound for higher sampling rates (fundamental table too small, F_low too high)

hope you like this ;]
It doesn't matter how it sounds..
..as long as it has BASS and it's LOUD!

irc.libera.chat >>> #kvr

Post

256 samples for a single cycle wavetable is plenty for the high frequency content, but not enough for the low frequency content; you will be missing a few octaves at the top end which would be noticeable to some people when using harmonically rich waveforms (i.e. saw), even 4096 is not enough for many uses in that example. In my implementation, I quickly realized it made the most sense to use tables which varied in length across the keyboard, because the amount of 'wasted' memory became very high when using large tables which reproduced the hf content well. Depending on the storage format of the table and the number or tables per octave, this can consume a fair amount of memory - if you only need Saw+Tri+Sine then it will be no real problem. If you are oversampling, then you can get away with using fewer tables/octave (though they may be longer). (I've found) Interpolation will only take you so far in a realtime polyphonic contexts. Interpolation will approximate the intermediate samples, but it shouldn't be relied on to remove or construct octaves of content in this manner (at least, to my ears).

J

Post

eigentone wrote:256 samples for a single cycle wavetable is plenty for the high frequency content, but not enough for the low frequency content; you will be missing a few octaves at the top end which would be noticeable to some people when using harmonically rich waveforms (i.e. saw), even 4096 is not enough for many uses in that example. In my implementation, I quickly realized it made the most sense to use tables which varied in length across the keyboard, because the amount of 'wasted' memory became very high when using large tables which reproduced the hf content well. Depending on the storage format of the table and the number or tables per octave, this can consume a fair amount of memory - if you only need Saw+Tri+Sine then it will be no real problem. If you are oversampling, then you can get away with using fewer tables/octave (though they may be longer). (I've found) Interpolation will only take you so far in a realtime polyphonic contexts. Interpolation will approximate the intermediate samples, but it shouldn't be relied on to remove or construct octaves of content in this manner (at least, to my ears).

J
Mostly I agree. Interpolation does smooth out the transition, but if you look at the frequencies, you might be bringing up multiple partials, rather than the ideal one at a time. But practically, a lot of what happens at the top of an oscillator will be masked by a low pass filter in many cases. So it depends on the whole path. I rarely hear a naked osc in music.
Swing is the difference between a drum machine and a sex machine.

Post

mistertoast wrote:How much do you actually notice the problem, given that it only happens doing pitch bends and in portamento? I'm mostly curious because I don't hear high pitches anymore. :-)
I definitely notice it when using portamento over a large range. Hit a high note and hold it. Then hit a note three or four octaves lower. That bright, buzzy sawtooth suddenly becomes dull.

Post

mistertoast wrote:My new synth is 8x (or 16x--we'll see) oversampled, and it's pure hard-edged saw with an oversample filter. No table--just one line of osc code. This lets me do any kind of weird hard-sync, audiorate manipulations, fm, am, ring, PD, PWM, ANYTHING. I like this approach better. It's all trade-offs. There are many, many ways to make an oscillator. As long as the osc doesn't have a large section of missing high harmonics or nasty aliasing, I'm happy.
What kind of filter are you using?

Post

Leslie Sanford wrote:
mistertoast wrote:My new synth is 8x (or 16x--we'll see) oversampled, and it's pure hard-edged saw with an oversample filter. No table--just one line of osc code. This lets me do any kind of weird hard-sync, audiorate manipulations, fm, am, ring, PD, PWM, ANYTHING. I like this approach better. It's all trade-offs. There are many, many ways to make an oscillator. As long as the osc doesn't have a large section of missing high harmonics or nasty aliasing, I'm happy.
What kind of filter are you using?
Right now just a 4-pole lowpass. But I'm reading about and experimenting with others. All my other code got so simple (trivial) that I can afford to put some effort into the filter.

Also looking at a couple 2-pass solutions. Take it down from 8x to 2x (88200) with the oversample filter, do some more work (including the resonant filter, then take it down to 44100.

Still a work in progress, but a very enjoyable solution for me.
Swing is the difference between a drum machine and a sex machine.

Post

mistertoast wrote:
Leslie Sanford wrote:What kind of filter are you using?
Right now just a 4-pole lowpass. But I'm reading about and experimenting with others. All my other code got so simple (trivial) that I can afford to put some effort into the filter.
I've been reading some of the recent threads here on filters, particularly the ladder filter. After playing around with it myself, I came up with a working ladder filter using 4x oversampling and scaling the resonance back a bit depending on the cutoff frequency. It sounded good, at least it didn't blow up.

But having to oversample the filter made me wonder if I couldn't just use it instead of having to use a seperate filter for oversampling. That is to say, since my ladder filter is already running at 4x, why not run the input at 4x? So instead of running the same input 4x through the filter, I'd just run the 4x input once and take every fourth output.

This assumes that nothing that comes after the filter will need oversampling, which is a fairly safe bet in most situations, I think. Most of the stuff that's going to need oversampling will occur at the oscillator stage.

Post

For my DarkSide websynth I used a naive everything (super easy sync/ring/fm) 16 times over-sampled and a 32 tap polyphase brick wall FIR filter using Kaiser window.

Post

GameSmith wrote:For my DarkSide websynth I used a naive everything (super easy sync/ring/fm) 16 times over-sampled and a 32 tap polyphase brick wall FIR filter using Kaiser window.
Can you share that filter code?
Swing is the difference between a drum machine and a sex machine.

Post Reply

Return to “DSP and Plugin Development”