Let's Design and Build a (mostly) Digital Theremin!

Posted: 6/13/2024 11:00:14 AM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

"Well... I should probably apply it to my calculation of "log(x)/log(2 to the power of (1/12))" in Open Theremin with MIDI."  - Mr_Dham

With log(x) / log(2^(1/12)) the denominator is a constant, so it is probably evaluated at compile time rather than run time.  And division by a constant is multiplication by its inverse, so the otherwise time consuming division is likely just a multiplication.  In the Hive math libraries I've only implemented log2 and exp2 (i.e. nothing based on the constant e) as they are the easiest and fastest to calculate in binary, and are the most useful.  On a 32 bit machine, log2 can actually be accomplished using only shifts and subtracts to produce a result within 0.3%, and on Hive this takes 9 cycles max.  Using a simple change of variable and a second order polynomial gets the error down to +/-0.025% (for larger integer inputs) and uses 13 cycles max.  These are all integer input, producing a 5.27 fixed point output.

That said, I don't see that log2, of ANY precision, is used anywhere in the D-Lev code base.  The field linearization maths also make the pitch and volume numbers linear, so there is just exp2 at the end of it all for our conveniently doubly logarithmic (i.e. pitch and volume) ears.  For the exp2 the code uses low quality (+/-0.0005% error, 4th order poly, 18 cycles) and very low quality (+/-0.33% error, 2nd order poly, 12 cycles).  The desired quality is selected by calling the specific subroutine, I wonder if there is any way to explicitly control this in the OpenTheremin code?  Higher level languages tend to get in the way of one micromanaging the maths and value representations themselves.

There is a rather trivial non-polynomial version of exp2 as well that only requires 7 cycles, but the error is 6.15% for larger inputs.  Clearly, log2 is somewhat more amenable to calculation than exp2.

Floats add a ton of overhead too, so I've very deliberately and laboriously avoided them whenever possible, sticking to ints and fracs (<1) though that can get pretty tricky.  Put enough elbow grease into it up front and you can put a man on the moon with a pocket calculator! :-)

Posted: 6/13/2024 1:40:54 PM
chrisbei

Joined: 11/13/2023

For a good example of elbow grease mathematics and ham radio sdr have a look at (not my project but i use it):

https://github.com/threeme3/usdx

73 de Chris DL6SEZ

Posted: 6/14/2024 1:38:09 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

"https://github.com/threeme3/usdx"  - chrisbei

Thanks, that's an impressive project Chris!  I often wonder how SDR techniques might be applied to the digital Theremin.  The Theremin is sort of both a transmitter and receiver, but not at a fixed frequency, and with no modulation (beyond possible dither / spreading).  "Upside down" Theremin designs utilize a fixed frequency and then measure the phase offset introduced by the hand, which conveniently maximizes the far field response, but higher Q coils might not be entirely amenable to this scheme as the phase change and antenna voltage may poop out prematurely.

Posted: 6/14/2024 7:08:31 PM
Mr_Dham

From: Occitanie

Joined: 3/4/2012

In the Hive math libraries I've only implemented log2 and exp2 (i.e. nothing based on the constant e) as they are the easiest and fastest to calculate in binary, and are the most useful. - Dewster

Yes, I agree, I would have initially searched for a log2. Any log can be achieved with a constant multiplication then I imagine that natural log from math.h is just log2(x)/log2(e).


I don't see that log2, of ANY precision, is used anywhere in the D-Lev code base - Dewster
Yes, if D-lev's working value is directly in note number (linear) then you don't need it, that's all the beauty of it.

I need some precision in the log because I must convert a frequency (exponential) into a note number (linear) with some resolution between two notes. And as I advertise that you can play theremin's sound along an external synth through MIDI, any approximation could push me out of tune. 
That's why I rather speak about a log 2^(1/12) (which is a "log note" if log2 is a "log octave").

Open Theremin V4 has a log2, somewhere in the code (to calculate a pitch CV), with linear interpolation between 1 and 2. But the error makes that a G is taken for a F#. It not a problem as long as you don't want to play a external synth along open theremin sound.

Shifts and subtracts will bring me the range [1; 2] (an octave), 12 more multiplications and subtracts will bring me in [1; 2^1/12], then linear interpolation can do between notes. 24 steps + 1 linear interpolation would give me the precision I need. Notes (C, C#, ...) would have almost no error which is nice. But I imagine it still takes some CPU load.

3rd degree Polynomial approximation of log2 in [1; 2] doesn't distribute precision as expected around 2. I probably need to consider a variable change or find a trick. I need to reopen my math lessons... (27 years after) but I didn't say my last word.

My MIDI data shows that the open theremin sends a message every 4 ms as expected by code construction. Then I have no problem. It ain't broke, don't fix it they say... But the uglyness of calculating in float... and that redundancy with pitch CV inaccurate calculation...


Posted: 6/15/2024 1:00:55 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Vincent, I posted my Hive algorithms spreadsheet if you want to take a look at it.  It explains the change of variable too:

  https://d-lev.com/research/HIVE_algorithms_2018-05-15.xls

Also, for fitting data to polynomials, this site comes in pretty handy:

  http://polynomialregression.drque.net/online.php

Posted: 6/15/2024 4:51:25 PM
Mr_Dham

From: Occitanie

Joined: 3/4/2012

Wow!

"for fitting data to polynomials, this site comes in pretty handy" - dewster
I won't count the number of sheet of paper I wrote down yesterday, all this ending with tons of error accumulation. One problem fixed, Thanks !


Hive algorithms spreadsheet - dewster
Very nice work and very clear explanations. 
Change of variable: got it, I need to bring my calculation where the polynomial function is precise and efficient (it talks to me and my mathematics memories). 

 
Shifts and subtracts will bring me the range [1; 2] (an octave), 12 more multiplications and subtracts will bring me in [1; 2^1/12], then linear interpolation can do between notes. 24 steps + 1 linear interpolation would give me the precision I need. Notes (C, C#, ...) would have almost no error which is nice. But I imagine it still takes some CPU load. - Me...
More "dichotomic" approach requires only 4 steps to separate exponent and mantissa over 11 otaves for log2, and 4 [color=#000000]more steps to separate exponent and mantissa over 12 notes for log2^1/12 ("log note"). More optimistic on cpu load,... but a risk of corner case. [/color]

I have two options now (polynomial and log note), let's try both and compare...

Thanks for the hints.

Posted: 6/16/2024 10:31:21 AM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

"Change of variable: got it, I need to bring my calculation where the polynomial function is precise and efficient (it talks to me and my mathematics memories)."  - Mr_Dham

Many functions don't converge (give decreasing error with increasing polynomial order) without a change of variable.  With it, log2 converges pretty well because it's fairly linear (over the poly interval) in the first place - but 32 bits of precision require 11th order or thereabouts.  Sine and Cosine converge like crazy because they are even / odd (poly factors are applied to x^2n rather than x^n) and they are basically very close to a squared function, though not spectrally pure enough to use just one term - the 16 or so bits of precision as used in the D-Lev oscillators require a 3rd order poly.  I "wasted a lot of time" (i.e. had a lot of fun!) coming up with 32 bit precision everything only to end up not really using any of it, though it helps to know the rules of the game before one goes off breaking them.  If I had to do it over again I'd try to use golang and bessel functions rather than doing things manually and laboriously in a spreadsheet.  Writing the Hive math library was a fascinating intellectual experience, it provided a lot of DSP groundwork and was an excellent proving ground for the Hive opcode set.

For your log2, a possible error target might be 1 cent, which is 2^(1/1200) = 1.0005778 or 0.06%.  A second order polynomial here should give you +/-0.025% (for large inputs).

Posted: 6/17/2024 8:19:33 PM
Mr_Dham

From: Occitanie

Joined: 3/4/2012

I think I am on the good way, thanks !

For your log2, a possible error target might be 1 cent, which is 2^(1/1200) = 1.0005778 or 0.06%.  A second order polynomial here should give you +/-0.025% (for large inputs). Dewster

It sounds more than reasonable, the 16 bit resolution of what stands for frequency makes that theres is 55 steps between A4 and A4#. and it gets even worse as we reduce the frequency... 
Log float is definitely an overkill !
 

Posted: 7/2/2024 7:30:48 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Encoder Encounters Of The Second Kind

Recently encountered some encoders of which I only imagined how they might work from the rather unclear datasheets, and I've lived somewhat in fear of accidentally ordering them.  So here was my chance to better understand them.  The D-Lev uses encoders that have 20 detents per revolution, and produce 20 pulses per revolution.  That is, each detent movement produces a full high-low-high cycle on each of the two pins, with the phase of one leading or lagging the other depending on the direction of rotation.  The phase difference generates a full two bit Gray code per detent.  There exist encoders which in essence generate the Gray code at 1/2 this rate.  So at a detent both pins could be high or low, and rotating one detent inverts them.  I'm not exactly sure why these exist, though I can imagine the 1/2 rate is more processor interrupt / GPIO friendly.  The obvious downside here is that there's a 50/50 chance the encoder is drawing power from both pullup resistors in its quiescent state, which isn't good for very low power hibernation states, and the lack of a benign quiescent state also obviates certain clever multiplexing scenarios.  Though perhaps the small "wetting" current could help keep the contacts cleaner?  Who knows.

The full cycle encoders are pretty easy to decode, and the resulting state machine provides for a lot of debouncing because four states are traversed per detent, and the two state rings (one each for CW & CCW) clearly separate what is going on.  Half cycle encoders are more problematic, and it took me a day or so to get a handle on what to do.  Here is my solution:

1. Debounce each lead with a separate linear hysteresis counter in the FPGA.
2. Convert the Gray code to a binary code via a bit of logic.
3. Feed this to a 4 state state machine:


4. Keep track of which direction we're headed around the loop by registering it when in states 0 or 2 (the detent states).
5. If we do the full 3 state 1/2 loop then inc or dec the output.  This requires knowing the direction from step 4.

Since the inc / dec is separated by two states, we get a good debounce action.  Not shown here are transitions between states 0 and 2, which are necessary for proper initialization.

The things that hung me up were initialization, and the extra direction state tracking needed to decode the output - extra state needs to be added very carefully and it needs to be very well behaved.  I think this same construct with a few minor mods would work well for full cycle encoders too, cutting the states from 7 down to 4.

All of this stuff probably shouldn't be handled by FPGA hardware, where a re-pump is needed to change anything in the field.  Doing it in software at the audio interrupt rate of 48kHz seems like a better and safer fit.

You must be logged in to post a reply. Please log in or register for a new account.