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

Posted: 7/20/2020 4:59:49 PM
tinkeringdude

From: Germany

Joined: 8/30/2014

People who write things like guitar effects for the PC must have it really hard, as they have to deal with ADC + DAC + I/O buffers + DSP latency < ~10ms.

At least the IO buffers problem has been alleviated by introduction of ASIO in the early 2000's or so, which require special studio-grade audio interfaces, though. There is a driver called "asio4all" which claims to provide an ASIO interface between regular soundcards and pro audio applications. I have not had good experience with it, other than it basically providing this, yeah, but with issues, and not low latency. And how would it, if the original generic soundcard's driver used does not support small buffers.

(Linux has an equivalent for some time of which the name escapes me regularly)

With ASIO, I have seen latency for playback/synthesis-only stuff in the low 1-digit ms range.
I have not tried external "live" signal processing.
In a DAW there is the hypothetical advantage (no idea whether it's used) that you could, for the musican to be able hear the guitar sound while playing, provide a somewhat lower fidelity rendition to keep things real-time.
But as soon as the virgin guitar signal is on disk, nothing matters anymore. For playback (incl. when recording on another track, but listening to already recorded stuff), the system has lots of time to pre-render a generous enough buffer when putting the original guitar signal track through effects, in full fidelity. Not to mention when rendering the mixdown.
So only ever those tracks being recorded at the same time*, and which have real-time effects (like guitar distortion) on them that the musician needs to hear while playing, have this strict requirement.

* tends to be N=1, i.e. each musician recording in his basement and sending stuff to the other band members, these days  Drums will probably not require real-time effects for a drummer being able to play. Can't think of any other many-channel scenario. Maybe 2 for keyboards, but unless someone's playing against an echo time for effect (which is cheap), they too probably don't need real-time effects while recording.

I don't know how much guitar amp software plugins are actually used for high end recording, vs. things like the Kemper amp. One could record both at the same time also, I guess - the original guitar signal, and the distorted one, and wire things up for "re-amping" (which is a funny term as "amplification" is just a 1-cycle mul op on a CPU and not the purpose of that process at all ), should one at some point not be pleased anymore with the amp settings.

Posted: 7/20/2020 6:20:44 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Pitch Axis Calibration

I don't have a video for this yet, and so will try to describe the process via some hand-drawn graphs and written steps.  It needs to end up in the operator's manual anyway, so I might as well get a jump on it.  And Roger is probably dealing with this as well as he just upgraded the FPGA & SW loads in his D-Lev.

There are 5 knobs per axis which influence the response.  For the pitch axis, and in order of application in the axis processing chain:

1. Pcal
2. Lin
3. Ofs-
4. Sens
5. Ofs+


Above are graphs & notes for these knobs.  The graphs show how +/- changes to the knob value affect nominal sensitivity.  The notes describe how +/- changes to the knob values change the pitch.

1. Pcal + changes will lower the far-field the sensitivity (linearity), and will increase the pitch.
2a. Lin + values give a shallow non-linear hump to the near/mid-field sensitivity, increase the overall sensitivity, and increase the pitch.
2b. Lin - values give a shallow non-linear trough to the mid/far-field sensitivity, decrease the overall sensitivity, and decrease the pitch.
3&5. Ofs- and Ofs+ do not change the sensitivity or linearity, but do offset the pitch, with + changes increasing it.
4. Sens + changes increase the overall sensitivity without affecting the linearity, and (depending on Ofs-/+ and the pitch hand position) will increase or decrease the pitch.

Pitch Axis Calibration Procedure
1. Mount the D-Lev on a stand at a comfortable playing height, and with the antennas not too near other objects.
2. Ensure that the D-Lev is properly grounded. 
3. Place your body in a normal playing position, and try not to significantly move your torso during the calibration procedure.
4. Set the following: Pcal = 20; Lin = 0; Ofs- = 32; Sens = 90; Ofs+ = 100.
5. Place both of your hands on the D-Lev case and press the lower right encoder to do an auto calibration (acal).
6. Place your open pitch hand very near the pitch antenna, with fingers pointing at it, and repeatedly close your hand to make a fist and open it again.  Count the number of notes changing on the tuner that this produces.  Adjust Sens to obtain your desired global note spacing (~1 octave for traditional analog Theremins, but I currently prefer a 4 note or 1/3 octave change here).
7. Retract your open pitch hand ~0.4m (16") away from the pitch antenna and toward your body (the far-field) and again perform the closed / open hand gesture described in the step 6.  Adjust Pcal until you get the same number of notes change in response to it.
8. Repeat steps 6 and 7 as many times as it takes to get the same note change response to the hand gesture throughout the pitch field.
9. If you detect minor variations in the pitch field that you cannot eliminate, adjust Lin until these are minimized.  Note that Lin also changes the overall sensitivity as a side effect, so there you will have to adjust Sens after adjusting Lin in order to get back to the same overall sensitivity.
10. Play a tune on the D-Lev and adjust Ofs+ (and/or Ofs-) in order to position the pitch of the voice within the pitch field to your liking.
11. Go to the SYSTEM page, set the Stor encoder to 0, and press it twice to save your Pitch axis parameters.
12. Repeat step 5 (do an acal) and check the far-field linearity.  If it is off, adjust Pcal and repeat step 11 to store your settings.

Whenever you power-up the D-Lev you should perform an acal and then check the far-field linearity.  If the far-field linearity is consistently off in one direction or the other, adjust Pcal and store.  If your hands and body are in pretty much the same position every time when you perform the acal, you will notice that a "good" acal will indicate the same note on the tuner immediately afterward.  Pcal adjustment combined with the acal procedure can compensate for your hands / body being quite close to - or far away from - the antennas during the calibration process, but some intermediate distance is probably ideal as it is the most repeatable in terms of situational capacitance.  You want your whole body to be positioned during acal in such a way that the capacitance it presents to the antennas is consistent, and significant without being overwhelming, so it can then be compensated for in a systematic fashion.  Too close and the capacitance is highly variable.  Too far and the environmental capacitance dominates, which is also variable.

For completeness: You may have noticed that Ofs- was just set to 32 above and wasn't really adjusted after that.  Because they do the same thing, adjusting Ofs- and Ofs+ in opposite directions will give you many essentially identical pitch field responses, which seems redundant and uninteresting.  But in doing so you are adjusting where the Sens control pivots the field when gaining it up.  So if you are inclined to adjust the Sens control a lot for performance reasons, you might try different combinations of Ofs- and Ofs+ to minimize the need to touch up the overall position of the field afterward.  If you commonly employ up to four distinct sensitivities, you migh consider using separate system preset slots for them.

Posted: 7/20/2020 7:16:13 PM
tinkeringdude

From: Germany

Joined: 8/30/2014


As for reading keyboard:

https://stackoverflow.com/questions/421860/capture-characters-from-standard-input-without-waiting-for-enter-to-be-pressed

Look at the post by "anon", providing this snippet:

Code:
char getch() {
        char buf = 0;
        struct termios old = {0};
        if (tcgetattr(0, &old) < 0)
                perror("tcsetattr()");
        old.c_lflag &= ~ICANON;
        old.c_lflag &= ~ECHO;
        old.c_cc[VMIN] = 1;
        old.c_cc[VTIME] = 0;
        if (tcsetattr(0, TCSANOW, &old) < 0)
                perror("tcsetattr ICANON");
        if (read(0, &buf, 1) < 0)
                perror ("read()");
        old.c_lflag |= ICANON;
        old.c_lflag |= ECHO;
        if (tcsetattr(0, TCSADRAIN, &old) < 0)
                perror ("tcsetattr ~ICANON");
        return (buf);
}


Add an include to cstdio next to the other includes provided in the original snippet I linked to.
As this braindead editor can't hancle pointy brackets in "code" sections, I forgot...

Then call this in an endless loop that exits when the obtained character has the decimal value of 27 (Esc key), otherwise prints it.
Works on my Ubuntu VM.

I don't know for sure how you'd make this exit when the program is supposed to exit (unless exiting by a keypress like Esc is ok).
Perhaps instead of calling read() with file-ID of 0 (didn't know that trick before), you might also be able to use epoll with 0? IIRC one can set up timeouts with epoll, so there could be a scheme of having a say 500ms timeout calling epoll in a loop - but when a flag from another thread is set that the program wants to exit, the keyboard thread, upon returning from epoll, does not continue to loop anymore. So there may be a 500ms delay at most until the program really exits. Calling something every 500ms vs. every 1ms should be nicer.

Ah, a timeout with the epoll mechanism is done with epoll_wait().
https://en.wikipedia.org/wiki/Epoll

Posted: 7/20/2020 7:22:11 PM
tinkeringdude

From: Germany

Joined: 8/30/2014

So now this editor also eats part of what you wrote and posts the rest... 2 times in a row, in conjunction with pasting something that will put CODE tags around it. And I did check the code view in the editor. That's new. And I thought this thing was out of surprises.

Posted: 7/21/2020 2:59:05 AM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

Thanks so much tinkeringdude!  I'm testing out the code you pointed to and it seems to work fine, but I need to look a bit closer to be sure.

The editor here is an endless fount of surprise.

[EDIT] Hmm.  It seems to return a series of bytes, but I have to call it repeatedly to get those bytes.  If I type ESC it gives 1b.  If I type F5 it gives 1b, 5b, 31, 35, 7e.  So say I call the function once and it gives me 1b.  The problem is I can't differentiate the two scenarios without more information, and if I call it again and it was ESC, the program is frozen waiting for further input because it's a blocking function (though I suppose one could use a timer here, but that seems like a hack).  I think this was the reason I ended up polling the keyboard, because it lets you examine the buffer without blocking.  In Win32 it's simpler: I do a kbhit() to see if there's anything in the buffer, and if so read out the buffer, and it's all non-blocking because the codes are all 1 byte or 2, and the first in a 2 byte code is always an exclusive escape code (0 or 0xE0) that doesn't moronically double as the ESC key code.  The terminal emulator in the librarian needs a non-blocking keyboard read because the Hive processor does the command line echoing, and the emulator needs to be able to receive at all times.

Posted: 7/21/2020 11:40:02 AM
tinkeringdude

From: Germany

Joined: 8/30/2014

Aaaah, I remember... some "special keys" generate longer codes.

Sometimes it's nice that "all" the online stuff runs Linux.

https://onlinegdb.com/H1rMSLVxw

You can't just compile this, but actually run it and press keys. (if you press the debug button, you can also step through the code)
I took the liberty of adding, below the original getch() from the article, a getchs(), which is used in the main() at the bottom.
The new version of the function makes use of the fact that POSIX read() is defined such that it's allowed that it does not read forever until as much data is present as you want, but as much as is there, and return with a positive value which is the number of actually read bytes.

It seems to be the case that in this scenario of reading keys, all bytes for an escape sequence of special e.g. fuction keys are available at once, when any is available.
So you will know which of the cases it is by the byte count  - see the switch statement in main().

(that gdbonline is relatively new and not all languages you can select on the right work all that well in the editor, at least when I last checked.
If you want to know yourself out trying the basics of some other language, might also look at ideone.com. But it had no debugging, last time I looked)

Posted: 7/21/2020 11:51:34 AM
tinkeringdude

From: Germany

Joined: 8/30/2014

(if you saw this in between my edits, note that I updated the link to the source - I forgot at the end that one has to fetch a new link with the "Share" button there after you edit the code, as a previously fetched link will still point to an old version...)

Posted: 7/21/2020 1:19:23 PM
tinkeringdude

From: Germany

Joined: 8/30/2014

Apparently one can't edit source in a share sandbox like this, only if you own it.
But if you then press the green "Fork it" button at the top, it creates a new one of it all that you then own, and can mess with to your heart's content.

I just discovered that gdbonline supports adding multiple source files and including stuff as if it were all in one folder, by using the white/black paper sheet icon button "new file" (you only see it if you own the whole thing, not the execute-only shared box)

So I felt like playing around with this a bit, slightly de-messified it by splitting it up in files, and made little demo that uses some newer C++ facilities to do some actions when you press certain keys (see the enum at the top of main.cpp)
The std::function value  used in the map below that can not only deal with "global functions" as the ones used in KeyActions.cpp happen to be because that's what I quickly whipped up, you could also connect to functions on objects with bind, or just pass-in a lambda that calls a function of an instantiated object (or an arbitrary code snippet doing something).

https://onlinegdb.com/BktatD4eP

Posted: 7/21/2020 3:53:11 PM
dewster

From: Northern NJ, USA

Joined: 2/17/2012

tinkeringdude, I do appreciate all the time you're putting into this!  The online GDB is quite interesting!

I got the code you pointed to working in toycode in Genie (spent much of the morning trying to figure out why I don't have the debugger in Genie but finally just gave up).  It works absolutely fine in toycode, but when I try to use it in my librarian it misses input now and then, and generally misbehaves.  The cursor in my librarian is in the wrong place on the screen now when typing too.  I think it's perhaps because of the input mode switching between input characters, which I wasn't doing before.

All along, I've actually been doing almost exactly the same mode switching, but I set this:
  old.c_cc[VMIN] = 1;

instead to this:
  old.c_cc[VMIN] = 0;

at the beginning of the program so that it is non-blocking, then I manually poll (with 2ms sleep to keep CPU utilization down) when I want to do blocking, and set it all back when exiting the program, so the tty mode is constant while the program is running, rather than constantly switching.  Also, instead of the read() command I'm using getchar(), which returns bytes, and -1 when empty.  So I concatenate bytes until it is empty, and have a special check for ESC.  I may try switching to read() as it seems cleaner?  I wonder if you can read no bytes just to see how many are in there...

Also, back when I was grasping for solutions, I tried to do nonblocking by setting VTIME=1 (100ms), but the delay was much too long and clunky feeling, which is a shame because that seems like it's really the answer.

[EDIT] There is also the issue of hitting multiple keys and how to deal with that.  The keyboard buffer gives you bytes, but there can be more than one keycode in there!  What I do is read the buffer once, then keep reading it until it returns -1 or ESC, then concatenate and return.  Thus, I sometimes leave bytes in the buffer for when it gets called again.  This has a pretty good chance of separating multiple keycodes in the buffer, and also keeps me from having to buffer them elsewhere (though I buffer the last unused character as a static for use as the first when called again).  Using the keyboard buffer as a fifo works really well, just leave the data in there if you can't immediately use it.

[EDIT2] What sucks about the POSIX keyboard interface is: the OS knows damn well what's going on with the keyboard but can't be arsed to tell you about it.  So you have to put it in a sort of raw mode where it gives you one or more bytes at a time, and it's up to you to figure out the inter-key divisions, with a non-exclusive ESC character thrown into the crazy mix, and a timeout timer that's too large grained to be of any use.  And don't change modes too much.  I believe you would have to consult a table of all possible keycodes (or a rather complex function) in order to decode all inter-key divisions correctly.  But if you're not writing a FPS or whatever, much simpler methods can eliminate most of the cases.

tinkeringdude: Not hollering at you or anything (quite the opposite!), but I do think this sort of functionality (keyboard interface code) must be compiled and run locally, due to the real-time interactive nature of it all.  Adequate response time is often the crux of the issue, and accidental multiple key presses can figure into it too.

Posted: 7/21/2020 4:48:58 PM
tinkeringdude

From: Germany

Joined: 8/30/2014

When I run the pointed to program @ onlinegdb, and press, at the same time, 2 function keys, they will be correctly printed as the 2 function key multibyte values, each on their own output line. If I press an F key and a letter key at the same time, they also get detected separately, even though my code would allow for receiving slightly more than 5 bytes at once.
So it seems the system puts a time delay between putting the key code byte sequences in the internal buffer.

EDIT:
Have you tried running that program on you machine as-is? I don't know what throwing in getchar calls will do, calling read with a bytecount that is more or equal to the number you are expecting at most for a byte sequence seems to work fine, though. I mean my program is not wasting any time other than the printing, there is no waiting, just calls to read() in a loop (with the raw setup around it), so it seems to me to be designed to work that way and not dependent on timing.


Now I don't have a good overview of what else you are doing with the console.
If it gets messy so easily that you're trying to build "the incredible machine" around it,
maybe after all it would make more sense to use ncurses.

https://onlinegdb.com/r1hAq5Vlw

This is just code pasted verbatim (sans line numbers) from this tutorial:
https://www.linuxjournal.com/content/getting-started-ncurses

Just that I added a rand function as the author was expecting his own funny PRNG function that he did not show.

This outputs something that roughly resembles what the page above shows (sometimes only at the 2nd run in a row, for some reason), but it's mangled, probably because of the limited console dimensions onlinegdb offers.
So on that system, ncurses is just installed, I guess, and it works out of the box just by including the header.
Either there is no requirement for extra linker flags, or that system just is set up that this works by default.
But it's one file, I guess there is nothing to lose for you to just try to compile it on your machine. If it just works, ...

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