A little fixed point math for embedded audio
jamesmunns.com
jamesmunns.com
So, in this case (on a tiny microcontroller, not precision equipment), my main optimization factor was speed over everything else. I don't think this is a universal answer, and as other commenters have mentioned, there are other ways to approach this problem!
As to a couple of common questions:
* Why didn't I just use a 1/4 sine LUT? - Because the full sine lut was only 512 bytes, and I had that to spare, which saves me some math per-cycle. I would also hit diminishing returns on a more precise LUT.
* Why didn't I use (some other method)? This one was "good enough", only took 22 CPU cycles per iteration, and linear interpolation was only 5 or so of those 22. Alternatively: I am bad at complex math! But I have worked with fixed point algos before, so I wrote what I knew.
Happy to answer any other questions!Example (1-indexed):
f = 1000;
tPer = 1 / f;
ts = 1 / 48000;
LUT = int16( 2^15 * sinpi( (1:256) / 256 / 2 ) );
t = 0;
while true
t = mod( t + ts, tPer);
i = uint16( floor( t * f * 1024 ) );
if i <= 256
y = LUT(i);
elseif i <= 512
y = LUT(257-mod(i,257));
elseif i <= 768
y = -LUT(mod(i,257));
else
y = -LUT(257-mod(i,257));
end
end
I left out interpolation to keep the concept highlighted.Unsurprisingly: it's faster and more accurate to skip the LUT when using MATLAB. It's faster still to use vectors and batched function calls.
Additionally my error max was already enough, where adding 4x the LUT precision would be diminishing returns for my application.
0.012% error ~= -78dB to put it in respect with other audio stuff, which in most cases should be more than low enough. For reference the dynamic range of 16bit audio is around 90dB
https://hackaday.io/project/28597-the-delta-flyer/log/145736...
If you use a lookup table then you're approximating a sine as a bunch of piecewise linear segments with surprisingly little error, and linear interpolation works well on those. You don't have any harmonics to worry about and the curve is incredibly smooth, so there are no odd effects to take into account.
A while ago I wrote some Python code to demonstrate this effect in calculating frequency from pitch in synthesizers by interpolating a table of semitone steps. The error was negligible up to around C6, which is the top key of most 5-octave keyboards, and only a handful of Hz at C8 which is higher than most folk need. I can't find the code now but when I do I'll post it.
I did my error calculations by brute forcing the whole 16-bit range, basically:
for i in (i16::MIN..=i16::MAX) {
let a = (i as f32).sin() as i16;
let b = lut_sin(i);
let diff = abs_diff(a, b);
}
Which found the largest difference to be 4, and I got 0.012% from (4/32768). t = mod( t + ts, tPeriod );
x = sinpi( t * f * ts );
Maybe it's not as fast as a LUT, but I've never needed high performance and generally work with higher precision data where a LUT would be unreasonable.MATLAB example to be more explicit:
f = double(1000);
tPeriod = 1 / f;
ts = double(1 / 48000);
t = double(0);
while true
t = mod( t + ts, tPeriod );
x = sinpi( t * f * ts );
endAccuracy errors are easy to account for in this scheme; just renormalize the vector so that the length is 1. It's cheaper and less code than doing a sin().
By the way, for linear interpolation (if you wish to keep the table), usually x + (y-x)*t is faster than x*(1-t) + y*t.
However in C++, you might be able to mimic the API more clearly, as templating is able to do integer things that Rust can't yet easily today.
Since the LUT was constant, doing it quickly on my PC made sense, but for a more dynamic approach, that would make sense too.