Basic Wavetable Sine Table Generation & Usage Questions

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

I'm currently using the Wavetable Sin from Maximilian. As I stated in other threads, I'm doing additive synthesis which means hundreds of sine waves and it's really important to keep CPU usage to the minimum per.

Maximilian has a sinewave 512 point wavetable with linear interpolation like this:
https://github.com/micknoise/Maximilian ... ximilian.h

Code: Select all

double sineBuffer[514]={0,0.012268,0.024536,0.036804,0.049042,0.06131,0.073547,0.085785,0.097992,0.1102,0.12241,0.13455,0.1467,0.15884,0.17093,0.18301,0.19507,0.20709,0.21909,0.23105,0.24295,0.25485,0.26669,0.2785,0.29025,0.30197,0.31366,0.32529,0.33685,0.34839,0.35986,0.37128,0.38266,0.39395,0.40521,0.41641,0.42752,0.4386,0.44958,0.46051,0.47137,0.48215,0.49286,0.50351,0.51407,0.52457,0.53497,0.54529,0.55554,0.5657,0.57578,0.58575,0.59567,0.60547,0.6152,0.62482,0.63437,0.6438,0.65314,0.66238,0.67151,0.68057,0.68951,0.69833,0.70706,0.7157,0.72421,0.7326,0.74091,0.74908,0.75717,0.76514,0.77298,0.7807,0.7883,0.79581,0.80316,0.81042,0.81754,0.82455,0.83142,0.8382,0.84482,0.85132,0.8577,0.86392,0.87006,0.87604,0.88187,0.8876,0.89319,0.89862,0.90396,0.90912,0.91415,0.91907,0.92383,0.92847,0.93295,0.93729,0.9415,0.94556,0.94949,0.95325,0.95691,0.96039,0.96375,0.96692,0.97,0.9729,0.97565,0.97827,0.98074,0.98306,0.98523,0.98724,0.98914,0.99084,0.99243,0.99387,0.99515,0.99628,0.99725,0.99808,0.99875,0.99927,0.99966,0.99988,0.99997,0.99988,0.99966,0.99927,0.99875,0.99808,0.99725,0.99628,0.99515,0.99387,0.99243,0.99084,0.98914,0.98724,0.98523,0.98306,0.98074,0.97827,0.97565,0.9729,0.97,0.96692,0.96375,0.96039,0.95691,0.95325,0.94949,0.94556,0.9415,0.93729,0.93295,0.92847,0.92383,0.91907,0.91415,0.90912,0.90396,0.89862,0.89319,0.8876,0.88187,0.87604,0.87006,0.86392,0.8577,0.85132,0.84482,0.8382,0.83142,0.82455,0.81754,0.81042,0.80316,0.79581,0.7883,0.7807,0.77298,0.76514,0.75717,0.74908,0.74091,0.7326,0.72421,0.7157,0.70706,0.69833,0.68951,0.68057,0.67151,0.66238,0.65314,0.6438,0.63437,0.62482,0.6152,0.60547,0.59567,0.58575,0.57578,0.5657,0.55554,0.54529,0.53497,0.52457,0.51407,0.50351,0.49286,0.48215,0.47137,0.46051,0.44958,0.4386,0.42752,0.41641,0.40521,0.39395,0.38266,0.37128,0.35986,0.34839,0.33685,0.32529,0.31366,0.30197,0.29025,0.2785,0.26669,0.25485,0.24295,0.23105,0.21909,0.20709,0.19507,0.18301,0.17093,0.15884,0.1467,0.13455,0.12241,0.1102,0.097992,0.085785,0.073547,0.06131,0.049042,0.036804,0.024536,0.012268,0,-0.012268,-0.024536,-0.036804,-0.049042,-0.06131,-0.073547,-0.085785,-0.097992,-0.1102,-0.12241,-0.13455,-0.1467,-0.15884,-0.17093,-0.18301,-0.19507,-0.20709,-0.21909,-0.23105,-0.24295,-0.25485,-0.26669,-0.2785,-0.29025,-0.30197,-0.31366,-0.32529,-0.33685,-0.34839,-0.35986,-0.37128,-0.38266,-0.39395,-0.40521,-0.41641,-0.42752,-0.4386,-0.44958,-0.46051,-0.47137,-0.48215,-0.49286,-0.50351,-0.51407,-0.52457,-0.53497,-0.54529,-0.55554,-0.5657,-0.57578,-0.58575,-0.59567,-0.60547,-0.6152,-0.62482,-0.63437,-0.6438,-0.65314,-0.66238,-0.67151,-0.68057,-0.68951,-0.69833,-0.70706,-0.7157,-0.72421,-0.7326,-0.74091,-0.74908,-0.75717,-0.76514,-0.77298,-0.7807,-0.7883,-0.79581,-0.80316,-0.81042,-0.81754,-0.82455,-0.83142,-0.8382,-0.84482,-0.85132,-0.8577,-0.86392,-0.87006,-0.87604,-0.88187,-0.8876,-0.89319,-0.89862,-0.90396,-0.90912,-0.91415,-0.91907,-0.92383,-0.92847,-0.93295,-0.93729,-0.9415,-0.94556,-0.94949,-0.95325,-0.95691,-0.96039,-0.96375,-0.96692,-0.97,-0.9729,-0.97565,-0.97827,-0.98074,-0.98306,-0.98523,-0.98724,-0.98914,-0.99084,-0.99243,-0.99387,-0.99515,-0.99628,-0.99725,-0.99808,-0.99875,-0.99927,-0.99966,-0.99988,-0.99997,-0.99988,-0.99966,-0.99927,-0.99875,-0.99808,-0.99725,-0.99628,-0.99515,-0.99387,-0.99243,-0.99084,-0.98914,-0.98724,-0.98523,-0.98306,-0.98074,-0.97827,-0.97565,-0.9729,-0.97,-0.96692,-0.96375,-0.96039,-0.95691,-0.95325,-0.94949,-0.94556,-0.9415,-0.93729,-0.93295,-0.92847,-0.92383,-0.91907,-0.91415,-0.90912,-0.90396,-0.89862,-0.89319,-0.8876,-0.88187,-0.87604,-0.87006,-0.86392,-0.8577,-0.85132,-0.84482,-0.8382,-0.83142,-0.82455,-0.81754,-0.81042,-0.80316,-0.79581,-0.7883,-0.7807,-0.77298,-0.76514,-0.75717,-0.74908,-0.74091,-0.7326,-0.72421,-0.7157,-0.70706,-0.69833,-0.68951,-0.68057,-0.67151,-0.66238,-0.65314,-0.6438,-0.63437,-0.62482,-0.6152,-0.60547,-0.59567,-0.58575,-0.57578,-0.5657,-0.55554,-0.54529,-0.53497,-0.52457,-0.51407,-0.50351,-0.49286,-0.48215,-0.47137,-0.46051,-0.44958,-0.4386,-0.42752,-0.41641,-0.40521,-0.39395,-0.38266,-0.37128,-0.35986,-0.34839,-0.33685,-0.32529,-0.31366,-0.30197,-0.29025,-0.2785,-0.26669,-0.25485,-0.24295,-0.23105,-0.21909,-0.20709,-0.19507,-0.18301,-0.17093,-0.15884,-0.1467,-0.13455,-0.12241,-0.1102,-0.097992,-0.085785,-0.073547,-0.06131,-0.049042,-0.036804,-0.024536,-0.012268,0,0.012268
};

double maxiOsc::sinebuf(double frequency) { //specify the frequency of the oscillator in Hz / cps etc.
    //This is a sinewave oscillator that uses linear interpolation on a 514 point buffer

	double remainder;
 	phase += 512./(maxiSettings::sampleRate/(frequency));
	if ( phase >= 511 ) phase -=512;
	remainder = phase - floor(phase);
	output = (double) ((1-remainder) * sineBuffer[1+ (long) phase] + remainder * sineBuffer[2+(long) phase]);
	return(output);
}
I'm trying to understand this code first of all. The oscillator is initialized at phase 0.0 by default. Let's say hypothetically the unit is running at exactly the right sampleRate & frequency so there are no remainders (sampleRate/frequency = 512):

1st sample: output = sineBuffer[1]
2nd sample: output = sineBuffer[2]
...

Isn't this wrong though? Shouldn't the first output be sinBuffer[0]? ie. Shouldn't the first output from the wavetable be a 0 (phase =0, output =0)?

Or am I thinking about that wrong?

I also adapted this to make a noninterpolated version:

Code: Select all

double MaxiOsc::sine512noint(double frequency) { //specify the frequency of the oscillator in Hz / cps etc.
			
	output = sineBuffer[(long)phase];
	phase += 512. / (sampleRate / (frequency));
	if (phase >= 511) phase -= 512;
	return(output);
}
I reordered the lines so that the first output will truly be sineBuffer[0] = 0, which I think is the right way.

What is the correct first output from the wave table in these cases?

Since CPU is critical for me but not memory, I want to try a 1024 non Interpolated function. I'd like to just print out a new 1024 value table but I'm not sure how to get one.

Is there any simple code that I can use to print out the values for a 1024 value sine wave separated by commas so I can plug them into a new array?

Thanks

Post

mikejm wrote: Mon Dec 03, 2018 4:17 am Is there any simple code that I can use to print out the values for a 1024 value sine wave separated by commas so I can plug them into a new array?

Code: Select all

double pi = acos(-1.);
for(int i = 0; i < 1024; ++i)
{
  printf("%.16e\n", sin(2*pi*i / 1024.0));
}
You can reduce the number of digits at the cost of precision... but why not just initialise the array at load time with a loop like that and avoid storing a lot of pointless data in your source code?

That said, for additive synthesis, if you truly care about CPU your best bet is to forget about table lookup oscillators completely, because they are just (far) too slow. If you just want to experiment, then go ahead, but keep in mind that for competitive performance, your best bet is likely to be some variation of SIMD-optimised trigonometric recurrences. Sadly (judging by your original question) that probably means you probably have a lot of math to learn.

Post

mystran wrote: Mon Dec 03, 2018 7:18 pm
mikejm wrote: Mon Dec 03, 2018 4:17 am Is there any simple code that I can use to print out the values for a 1024 value sine wave separated by commas so I can plug them into a new array?

Code: Select all

double pi = acos(-1.);
for(int i = 0; i < 1024; ++i)
{
  printf("%.16e\n", sin(2*pi*i / 1024.0));
}
You can reduce the number of digits at the cost of precision... but why not just initialise the array at load time with a loop like that and avoid storing a lot of pointless data in your source code?

That said, for additive synthesis, if you truly care about CPU your best bet is to forget about table lookup oscillators completely, because they are just (far) too slow. If you just want to experiment, then go ahead, but keep in mind that for competitive performance, your best bet is likely to be some variation of SIMD-optimised trigonometric recurrences. Sadly (judging by your original question) that probably means you probably have a lot of math to learn.
Thanks mystran. Yeah I realized I may as well just initialize an array that is globally accessible at load time. I didn't want every object of my sine wave oscillator to initialize it's own array because if I have 300 sine oscillators I don't want 300 arrays. Thanks for the print function though.

Juce has a simple tutorial on these: https://docs.juce.com/master/tutorial_w ... synth.html

I will try that. As you can figure I still kind of suck at this type of C++/DSP as in Reaktor I never got to this low level of planning/design/organization. I tried reading about "SIMD-optimised trigonometric recurrences" but the things I'm finding look pretty abstract and difficult to understand as to how they would be practically relevant.

I definitely want the lowest CPU use long term as less CPU per sine means more sines I can run at a time. I don't expect you to hold my hand, but can you clarify - is this something that any good C++ coder would know how to do? I don't mind paying someone for tutoring to explain or learn it. Or is it a pretty abstract/specific/rare skill?

I certainly can't find any tutorials on SIMD trigonometric sine waves, so it seems pretty esoteric ...

If you wouldn't mind I'd appreciate your comment also on whether the Maximilian sine wave I noted above is designed in error at present given that it doesn't output a 0 at first sample...

Post

There's two different things to consider:

1. Trigonometric recurrences: These are basically incremental rotations. If you rotate a 2D point (x,y) incrementally in small steps then x traces a cosine and y traces a sine. If you set the incremental angle to 2*pi*frequency/samplerate, you get a "quadrature" oscillator (ie. one that produces both cosines and sine; you can obviously only use one). The trivial rotation takes 4 multiplies and 2 adds, although there are other more numerically small stable options (simple rotations will accumulate error pretty fast).

2. SIMD processing: using the SSE or AVX or whatever you have to compute multiple sines in parallel. This particular application is pretty much ideal for SIMD, so if you use SSE to compute 4 sines at the same time, your code runs more or less 4 times as fast... well until you have to shuffle control data around, but the gains are still huge.

Actually writing high performance additive synthesis requires fairly intimate understanding of low-level programming. There's a lot more to it than just the oscillators since you have a lot of data you need to shuffle around, so cache considerations also become very important. Most of this is fairly general as far as high-performance low-level programming goes, but additive synthesis is definitely one of those things where good performance doesn't exactly come for free.

That said, there's nothing wrong with writing an additive synthesiser using table driven oscillators. It's just that with all the emphasis on CPU in the original post, I wanted to warn you that you shouldn't feel surprised if it uses 10 or 100 times more CPU than one of the commercial additive synths on the market.
;)

Post

There were a couple of threads here related to fast sine approximations:
viewtopic.php?f=33&t=412674
viewtopic.php?t=394360
viewtopic.php?t=186381
viewtopic.php?t=274398
viewtopic.php?t=474872
We are the KVR collective. Resistance is futile. You will be assimilated. Image
My MusicCalc is served over https!!

Post

BertKoor wrote: Tue Dec 04, 2018 8:55 am There were a couple of threads here related to fast sine approximations:
viewtopic.php?f=33&t=412674
viewtopic.php?t=394360
viewtopic.php?t=186381
viewtopic.php?t=274398
viewtopic.php?t=474872
Thanks Bert. This one seemed particularly of interest:
viewtopic.php?f=33&t=394360&start=30

Primarily people there seemed to be going to lookup tables or suggesting parabolic square approximations. I know how to do lookup tables. The parabolic square approximation suggested was:

Code: Select all

uble fastSine (double phase)
{
    phase = 0.5 - phase;
    double parabola = phase * (8.0 - 16.0 * ::std::abs(phase));
    // lerp with squared parabola
    return parabola * (0.776 + 0.224 * ::std::abs(parabola));
}
Perhaps I will try that too.

Post

mikejm wrote: Mon Dec 03, 2018 4:17 am Shouldn't the first output be sinBuffer[0]? ie. Shouldn't the first output from the wavetable be a 0 (phase =0, output =0)?
That's what I would expect as well. Apparently the developer doesn't need the oscillator to start at zero. A leading zero might be viewed as redundant, depending on your requirements. I wonder if this is a common implementation?

Post

Just wanted to say for anyone who can't handle the SSE type processing, the FT and FTA processes here are fantastic:

https://github.com/divideconcept/FastTrigo

Simple quicker sin/cos functions - really helps quite a bit actually. Doesn't require any particular skill to use either.

Post Reply

Return to “DSP and Plugin Development”