From the original SynthMaker forum (2004–2014), with its members’ names and dates.
mindplay ·
I'm missing a few maths functions, which are fairly essential to all kinds of DSP applications... some of these I could implement as components - some of them I did, but since calculations are done in 32-bit (?) floats, the results are too inaccurate to be of any use, and as far as I can figure, there's no real option except to implement these components as built-in components, which can do the calculations accurately...
The first is dB to Amplitude Factor conversion, e.g.:
AF = 10 ^ dB/20
I built a component to do this, but it's extremely inaccurate - for dB=-6, it gives AF=0.501187 which is off by 0.001187 from the result it should give. For dB=6, it gives AF=1.99526, which is off by 0.00474 ... so it's way off, already on the third decimal, which is pretty much useless.
Also missing is Log2(x), Log10(x), LogE(x) and Exp(x) (the value of e raised to the power of X, where e is the base of the natural logarithms, Euler's constant = 0.577215664901532860606512~) ... note that although LogN(x,y) could be used to calculate the other Log functions, this would probably not be the best solution, as for example, Euler's constant can only be inserted into a float-component in SM with six decimals, which is not by any means accurate enough for most applications.
I also often need to calculate a decay coefficient, e.g.:
silence = dB2AF(-90)
c = Exp(LogN(silence)/(SampleRate*Time))
the number "c", can be used in various iterations, decay envelopes among others, for example:
i=1
loop {
i = i * c
}
now after iterations corresponding to SampleRate*Time, the level of i will be equal to "Silence".
you need this component to do for example custom envelopes, envelope followers etc.
so the component should have three inputs: silence level in dB, SampleRate, time in seconds - and one output, the coefficient.
the calculations of these functions must be highly accurate, and CPU usage is probably less of a consideration, as these function are never used on a per-sample basis, but usually only to initialize a variable.
mindplay ·
any comments on this? I can't be the only person who's going to miss these functions - unless everyone's planning on just doing FM-synths for the rest of their lives? 😉
anyway, I was thinking - maybe a better solution might be, if it were somehow possible to do modules with accurate maths? Because a generic Log(N,X) component would be preferable then - I would implement (and donate) these components myself then... as far as I can figure, there would be three possible solutions:
1. a special component container, which if you use that, all the maths you perform inside it are done in high precision ... I don't know if that's possible under the SM architecture.
2. a separate set of high-precision maths components. I don't know if that would be practical though, as this would probably mean another datatype (and connection type, conversions between types etc.) would be necessary.
3. all non-realtime calculations (e.g. everything that doesn't compile into the inner DSP loop of the schematic; all calculations from GUI-control outputs etc.) could be done in high precision.
I don't know if these are the only possible solution, or if any of them would really be good in practice, so I'd like to hear from the developers and other user's thoughts on this?
I'm sure for one thing, we can all agree that high-precisions maths is a feature that's currently missing?
Shifrin ·
You're not the only one who'd like more maths modules, I'm sure they'll come soon 🙂
Shif.
Tom7777 ·
Hi, these sound like decent maths modules which would improve things but you should remember that the apps still in beta. Stuff like this is not as important right now as the main things that need to be done but im sure Jorge and Malc will add a load of stuff like this later on 🙂
jorge ·
mindplay wrote: I'm missing a few maths functions, which are fairly essential to all kinds of DSP applications...
Mindplay, did you try implementing them using taylor expansion series? The hard coded poly maths components will probably be based on algorithms first created with the Code component.
To create a primitive from high-level code is really easy although you can do the same as a module. If you have any high-level code you'd like to see as a primitive in the toolbox please let us know.
I think maybe you're more interesting in the algorithms themselves rather than having them as primitives or not.
mindplay ·
Jorge,
I have no idea what "taylor expansion series" are 😉
I would like to doodle with the Code component though - but I miss a reference of it's language ... I can tell it's similar to C, but I'm still missing at least a short list of available types and keywords.
And how wide is the float type in the code component? Because if it too is only 32 bits, then coding it won't bring me any closer to a solution...
mindplay ·
okay, I looked around ... Taylor series seems to be a pretty CPU intensive approximation of logarithms, probably not the best way for DSP applications?
A different technique called shift-add .. it's supposed to be fairly accurate and very fast - from what I could gather, it's more commonly used in DSP applications... It uses clever table lookups of some sort. It's a self-contained example with source code, apparently, so it shouldn't be that difficult to integrate into your project? 🙂
jorge ·
Hi mindplay, thanks for your post and link, this is an interesting and important subject.
mindplay wrote: Taylor series seems to be a pretty CPU intensive approximation of logarithms, probably not the best way for DSP applications?
At the moment I think Taylor is the best. SSE is very good at + and * which are the main operations used by Taylor series.
mindplay wrote: Jorge, if you have time, take a quick look here:
A different technique called shift-add .. it's supposed to be fairly accurate and very fast - from what I could gather, it's more commonly used in DSP applications... It uses clever table lookups of some sort. It's a self-contained example with source code, apparently, so it shouldn't be that difficult to integrate into your project? 🙂
I had a look at your link, I think it wouldnt be too hard to implement it but I still prefer Taylor series. Lookup tables affect the memory cache performance.
SSE instructions operate on 4 float values often taking one clock cycle. There is no way to do 4 simultaneous table lookups so on top of the memory cache performance problem there's the fact that you have to repeat the code 4 times, one for each of the 4 sound channels.
Did you see the Moog ladder filter that rl posted?
Fun with SM (Moog ladder filter)
It uses taylor expansion series for tanh and exp.
IMO you can do a lot of SSE processing with + and * with very good performance. It would be interesting to compare different implementations using the analyser component.
mindplay ·
that makes sense .. I didn't consider the SSE aspects 🙂
malc ·
Hi mindplay,
mindplay wrote: I would like to doodle with the Code component though - but I miss a reference of it's language ... I can tell it's similar to C, but I'm still missing at least a short list of available types and keywords.
Have you had a look at the online documentation? We now have a complete list of the commands and some info on how to use them.
[old link]
mindplay ·
I see ... well, that's an extremely simple language for now - hopefully you'll be adding a while construct at some point?
anyways, it should be possible then, using a taylor series ... so I tried that.
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
denom = denom + 2;
series = series * fract * fract;
result = result + (1/denom) * series;
result = result * 2;
I tried the code in another language first (JavaScript in a browser), and it gets the correct result, accurate to the fifth decimal or so - not bad 🙂
However, I can't get it to work in the code component ... all I get at the two outputs, is a string containing the code I wrote - where do I get the polyout result, shouldn't an output appear for it? an output does appear, and I tried connecting a float to it, but it gets no result - when I connect a string to the output, it gets the code...
jorge ·
mindplay, it's very useful too follow your thought process.
At the moment there is no conversion from Poly to Float. Poly data works at sampling rate like 44100 times per second while the green Float data works on an event basis. You cant really update a float edit 44100 per second. And the poly data can have between 0 and N voices being processed, which of them would you like to see as float data?
You should try your module as part of a Poly schematic, you can also test it with an analyser component, if you want I'll post a schematic to show you how.
mindplay ·
But there is no floatin/floatout type - so I can only use the code component to do DSP calculations, e.g. at samplerate? ... so if I want to do the taylor series for floats, I'll have to actually build that whole expression using components instead??
jorge ·
mindplay wrote: But there is no floatin/floatout type - so I can only use the code component to do DSP calculations, e.g. at samplerate? ... so if I want to do the taylor series for floats, I'll have to actually build that whole expression using components instead??
yeah, that's the way it is at the moment, it doesnt mean its the way it should be. The whole point of the code component was to compile sound processing as it needs to be fast. Float data works with events and doesnt need so much power. But I agree that it would be useful to use the same code with poly and float data, at least to test it by inspecting the float results. I'll try to think of some way of doing that.
mindplay ·
Cool.
In the meantime, I've unrolled the functions, so they should compute as fast as possible - here are the optimized versions of both routines...
result = result + 0.33333333333333333333333333333333 * series;
series = series * fract * fract;
result = result + 0.2 * series;
series = series * fract * fract;
result = result + 0.14285714285714285714285714285714 * series;
series = series * fract * fract;
result = result + 0.11111111111111111111111111111111 * series;
series = series * fract * fract;
result = result + 0.090909090909090909090909090909091 * series;
series = series * fract * fract;
result = result + 0.076923076923076923076923076923077 * series;
series = series * fract * fract;
result = result + 0.066666666666666666666666666666667 * series;
series = series * fract * fract;
result = result + 0.058823529411764705882352941176471 * series;
series = series * fract * fract;
result = result + 0.052631578947368421052631578947368 * series;
series = series * fract * fract;
result = result + 0.047619047619047619047619047619048 * series;
result = result * 2;
For ExpN(x): the inverse function of LogN(x)
polyin x;
polyout result;
float numerator;
result = 1 + x;
numerator = x * x;
result = result + numerator/2;
numerator = numerator * x;
result = result + numerator/6;
numerator = numerator * x;
result = result + numerator/24;
numerator = numerator * x;
result = result + numerator/120;
numerator = numerator * x;
result = result + numerator/720;
numerator = numerator * x;
result = result + numerator/5040;
numerator = numerator * x;
result = result + numerator/40320;
numerator = numerator * x;
result = result + numerator/362880;
numerator = numerator * x;
result = result + numerator/3628800;
numerator = numerator * x;
result = result + numerator/39916800;
both functions have been tested in JavaScript, and produce the correct results ... as said though, accurate only to the fifth decimal. Which may be enough for DSP, if you need these functions for that, which is why I'm providing them here - personally I don't have any real use for them, I need a much more accurate calculation than this, so I'm pretty much stuck for now, although it was interesting to do the functions 😉 ...
stefan ·
about the "while construct", no, that won't be introduced. if it is, it won't work with the regular poly or mono data because you have to use the same "instruction path" for all channels.
mindplay ·
stefancrs wrote: about the "while construct", no, that won't be introduced. if it is, it won't work with the regular poly or mono data because you have to use the same "instruction path" for all channels.
I see - so what you're saying is, any kind of loop is basically out of the question?
Would it at least be possible to implement a simple "for-next" compiler directive then? you should be able to handle that in a small preprocessor ... like for example:
a = 1;
#FOR $i = 1 TO 4
a = a * $i;
#NEXT
Which you would unroll to:
a = 1;
a = a * 1;
a = a * 2;
a = a * 3;
a = a * 4;
This would work for loops with a fixed number of iterations only, of course - but that way, it should at least be possible to implement stuff like the log/exp functions without copy-pasting the code ten or twenty times? 🙂
jorge ·
mindplay wrote: I see - so what you're saying is, any kind of loop is basically out of the question?
Would it at least be possible to implement a simple "for-next" compiler directive then? you should be able to handle that in a small preprocessor ... like for example:
a = 1;
#FOR $i = 1 TO 4
a = a * $i;
#NEXT
Which you would unroll to:
a = 1;
a = a * 1;
a = a * 2;
a = a * 3;
a = a * 4;
This would work for loops with a fixed number of iterations only, of course - but that way, it should at least be possible to implement stuff like the log/exp functions without copy-pasting the code ten or twenty times? 🙂
Loops that affect the 4 sound channels are no problem, it's easy enough to implement. What you cant have is conditional statements as in general they'd cause sound channels to follow different code paths. There's already ways of achieving the same as conditional statements using boolean operations and masks.
Also, in the example you give, you have many assignements which is not the most efficient way. Each assignement uses a memory location to store the result. Better is to use a long expression ( but not too long as you might run out of SSE internal registers ).
For example
a=b+c+d+e; writes to memory only once, the other additions are done using very fast internal registers.
but,
a=b;
a=a+c;
a=a+d;
a=a+e;
now you have 4 assignments and 4 memory writting operations.
Having said that, you'll probably find that writting to memory is fast enough, but using internal register is a lot faster. You'll probably only notice it for very big synths/effects.
mindplay ·
jorge wrote: Loops that affect the 4 sound channels are no problem, it's easy enough to implement. What you cant have is conditional statements as in general they'd cause sound channels to follow different code paths.
I understand that - that's why I suggested a compiler directive instead of a language construct .. since the ordinary "for" language construct from C would imply a conditional statement inside the loop...
jorge wrote: Also, in the example you give, you have many assignements which is not the most efficient way.
it was just an example 😉
the log and exp functions, for example, do not require a conditional statement in the loop - as long as you have a fixed number of iterations, and no conditional statement, a for-loop language construct or compiler directive should be possible in the future then?
mindplay ·
by the way, it turns out my ExpN(x) function was not optimal - you can multiply by a fraction instead of dividing, should be faster.
here's a code generator:
<HTML>
<HEAD>
<SCRIPT LANGUAGE="JavaScript">
function generate_code() {
var numDecimals = 15;
var iterations = parseInt(myform.iterations.value);
var lf = String.fromCharCode(13) + String.fromCharCode(10);