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

Posted: 7/26/2018 9:34:31 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Still beavering away, trying different configurations for velocity.  Rewatching Dominik's most excellent video (link) again and again for inspiration - thank you for taking the time to make and post that video Dominik!  Getting closer and have a strong feeling it will end up more or less like this:


Linear left hand position goes through the knee function, and forks to feed EXP2 and a high pass filter (HPF).  This is fed to a first order low pass filter to gain it up with some averaging of the stepping noise (LP1_GAIN), then the two are combined to form the system exponential control signal (~CV), which is fed to LOG2 to form the system linear control signal.  It doesn't look like much but, ye gods, the mixing of linear and exponential signals isn't something I find at all obvious as a solution to anything.

The knee probably provides enough control over the sharpness of the attack that I don't need a dedicated control for this in a formal function generator, though this perhaps precludes things like reverse envelope type effects.  The HPF frequency knob controls the exponential slew, or decay.  I may add an optional peak hold, separate rise / fall decay rates, a dedicated damping point, etc. once I'm happy with the basic functionality.

I have most of the elements of this up and running and it's quite sensitive to C changes.  I can sit ~1m away and tap my bare foot on the metal leg of the table my PC is sitting on, which causes the volume LED bar graph to jump each time.

Pissed away yesterday trying to make an exponential envelope generator based on a 1st order filter.  It mostly works but the attack phase is problematic (too dependent on accumulator initial and dynamic values). Linear slewing run through EXP2 is simpler and airtight.  It seems just about everything is easier to think about and do linearly (so it's unsurprising that the dominant patch cord domain for analog synths is linear).

Posted: 7/27/2018 7:38:42 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

My (Least) Favorite Mistake

Still at it, but one thing is becoming quite clear: measuring and using true velocity (as the difference of two consecutive left hand position values) is a dead end for synthesis use.  The difference over a small time period (1/48kHz or ~20 us) is itself small, so gaining it up gives nasty stepping noise.  You can average the differences, but do it too much and it dulls the attack by slowing it down.  You get a much larger signal with conventional first order high pass filtering, and the high pass filter time constant can then be used to generate an exponential decay if you like.  I'm currently doing this, but running into internal limiting issues that I need to address (gained-up attack overflow is causing the envelope to peg at max for a while rather than simply peak and decay).

Sometimes you take what seems to be the most obvious and direct road, and months later you're actually farther away from the destination.

[EDIT] I suppose a lot of the directional incorrectness can be chalked up to semantics.  You go into it thinking "velocity" and so build a velocity sensor, when what you really want is the log2 of the high pass filtered position - the former is highly similar but different enough to be not the droid you're looking for (Theremin mind trick you play on yourself).

[EDIT2] Having played with this for a while it of course isn't working as well as I'd like either.  For quieter notes the attack isn't sharp enough sounding, and there isn't sufficient control over dynamics, so I think explicit generation of the attack envelope is called for.

Posted: 7/30/2018 4:18:13 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Not Quite...

Got this running an hour ago:

Subtracting post LOG2 is mathematically equivalent to dividing pre LOG2, but in actual use with integers it's generally better from a resolution standpoint to subtract after, and I'm using a flooring subtract here to limit the effect.  What to do with signed values pre LOG2 is a problem, do you offset the input or lop off the bottom half?  Here I'm lopping with a math diode (zero if < 0).  The HP1 corner frequency rate gives a convenient exponential decay that gets linearized via the LOG2.  So the final sum is linear and unsigned.

Does it work?  Yes, with it I can replicate what DOMINIK is doing in his video.  What I don't like about it is when the attack is made steep enough to be sharp sounding the attack amplitude then becomes somewhat more difficult to control.  And the decay gets lost if the sensitivity is set too high (if the subtracted number is too small).  But it seems to be a step in the right direction.  I believe that having the envelope generation in the velocity path rather than in the final combined path may give more opportunity for expressive control?

One behavior that seems useful is the rectification, or the use of only positive velocity.  When the hand goes through the knee zone in a positive direction the velocity is naturally higher, generating an attack.  Hand movement after this doesn't influence the decay much because the velocity gain is lower when not in the knee transition.  Moving the hand in the opposite direction will cancel or damp the decay, but not as profoundly or abruptly as the attack.  It's this kind of subtle but very useful (from a performance standpoint) behavior that I'm searching for.

I think the next obvious experiment is to move the exponential decay to a slew limiter on the linear side, and concentrate on making the attack amplitude more controllable.  Having the velocity sensing and decay combined is rather neat, but it's probably asking too much of a simple algorithm.

Posted: 8/1/2018 2:50:33 AM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

The (90) 7% Solution - Volume Side

The above made me realize I should place the envelope generation in the velocity sensing leg, rather than in the final combined volume processing leg:

"...one thing is becoming quite clear: measuring and using true velocity (as the difference of two consecutive left hand position values) is a dead end for synthesis use."  - dewster

Yeah, don't listen to that guy!  What an idiot...

Knee, then lower branch is a rectified single sample difference, fed to LOG2, floor subtracted for variable gain, then the usual peak hold and envelope generator. As before, this gives a very nicely controllable (in terms of variable peak amplitude) fixed attack and decay.  If simply combined with the knee, the player must keep their hand fairly still after the envelope is triggered if either the attack or decay is set to a rather long time, otherwise the envelope can be instantly damped (particularly so if pulled back through the knee) or, to a lesser extent, accentuated.  For some things this might be exactly what you want, but if you desire retriggering without significant damping, then the "SLIM_FALL" linear decay slew limiter (which instantly tracks when the input is larger than the output, but decays at a programmable linear rate when the input is less than the output) in the upper leg can be set to a time constant somewhere near the envelope fall time to allow decent sounding uninterrupted retriggering, as well as less abrupt sounding damping.  And the lower velocity leg can be bypassed (by setting the vel subtraction to a large value) to give fixed decay envelopes off the knee via slim_fall if that's what you want.  Overall, it seems sufficiently versatile without too many knobs, and attacks can be quite sharp sounding if the envelope rise time is set to the quicker side.  It's easy to adjust and easy to play.

So that's it!  I think I'm pretty much done with the volume side.  Hope to make a video of it, or at least an MP3 or two, before the family camping trip hits this weekend.  This was a long time coming, and now it's major hump-overage for me, whew!  I suppose it doesn't seem like much, or all that different from what I had before, but finding basic combinations that work, getting them in the proper sequence, and then fine tuning the parameters, can make all the difference.  The lower branch elements had been previously fine-tuned, so it all went together fairly quickly once I finally got it though my thick skull where to put stuff.

Bit of work on the UI as well, the main power-up page now has controls for preset load/store, auto calibration, volume and pitch side null, and output volume.  Made the auto cal a knob twist rather than a knob push, as that's easier to do.  Unlike the Theremini, auto cal is instantaneous.

Posted: 8/1/2018 4:08:27 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Great site you can get completely ambiently lost in, not sure if I've pointed to it before: https://mynoise.net/noiseMachines.php

Under "Synthetic Noises" the "Sweep Noise" set to "Brown" sounds amazingly angry surf-like.

Posted: 8/2/2018 2:41:10 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Next up I'd like to nail down the filtering, which amounts to: 

1. Articulating (volume & pitch axis modulating of) the center frequencies of the formant filters (all 2nd order bandpass).
2. A separate filter for the oscillator source.
3. Optional 4th order filtering (maybe just lowpass?) for the noise and oscillator sources.
4. UI support (knobs/screens) for the above.

I notice I'm using a lot of saturating math (add, subtract, multiply) which can be rather expensive (depending on the operation and the signed/unsigned assignments of the operands: 11/4 cycles max/min, including the subroutine call) and which could conceivably be done in one cycle in the processor hardware.  I don't believe multiplication overflow can be detected early enough in the cycle to conditionally jump, and the jump approach probably wouldn't save that many cycles anyway.  But I would need to shoehorn in a lot of extra opcodes, and employ the final multiplication multiplexer to cover saturation values (0, 1, -1/2, +1/2) as it only covers zero at the moment (for shifts that slide into the abyss) so that could be a timing bottleneck.  Something to think about on vacation.  If I end up doing it that would likely be the final major change to the processor as well.

Posted: 8/3/2018 10:03:54 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Off on the family reunion camping trip for a week (Stokes State Forest, NJ - rent an enclosed lean-to in winter and visit High Point next door!).

Y'all take care of each other!  Cheers!

Posted: 8/11/2018 11:10:15 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Sisterhood of the Traveling Flags

I'm back!  In the lean-to, with notebook in hand, I had a chance to really think about the limiting [0:2^32) of unsigned results and the saturation [-2^31:2^31) of signed results.  The multiply is the pipeline bottleneck, just not enough time to both evaluate and set the output to the limits (one more stage and I could do it).  And jump testing is not the way to do this quickly.

I believe one could use the 4 extra unused stack memory bits (each byte of width has an extra bit for parity, error correction, general use, etc.) to hold information about the extended 64 bit result.  So after each (lower, or unextended) add, subtract, or multiply these bits would be assigned various overflow results, and those results may then be used by two special opcodes (LIM, SAT) on the next cycle (or later) to test for out-of-range and do the limiting.  I have to extend the multiply result to 65 bits (+1) to cover the full unsigned and signed ranges and see if that impacts top logic clock speed too much.  

With this scheme one can't have the stacks "spill to / fill from memory" as that is 32 bits wide, but I don't find myself ever doing that as the stacks are way deeper than I really need (32 entries deep each).  Rather like the simple "zero, sign, etc." flags that get tested in a conventional register-based processor, these would be copies that "stick" or "ride along" with the stack data, neatly avoiding the addition of shared core state.  This way the thread can take an interrupt and return, with the flags for that particular stack datum intact.  I only need three bits for this, the fourth could be used for other things I suppose.  Unlike with conventional processors, I can't imagine needing instructions that explicitly manipulate (set, clear) these traveling flags.  If it works out it should be quite clean, and may resolve a couple of other long-standing awkwardnesses.

It's interesting finding two cycle solutions to opcode functionality, and the individual one cycle steps often end up being handy for other uses.

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