A word of caution. When this article was written the current version of Sonic PI on Raspbian Stretch was 3.0.1. Since then the Pi4 has come out, and the current Raspbian Buster now has a version of Sonic Pi named 3.1 Unfortunately this is not a fully functional version as on the Mac or Windows versions, and it will not support OSC calls necessary for this project to work. Sonic Pi is just about to be upgraded to version 3.2, but currently there is no package for the Raspberry Pi, although the build process has been refined, and it will not be too difficult to build your own. (I currently run it on my Pi4 and performance is very good).
So currently if you want to use this project you must use Raspbian Stretch on a Pi2, or Pi3 with Sonic Pi 3.0.1 (but NOT a Pi4 which will not work with stretch, or build your own version to run on a Pi4, with support for OSC and midi.
Previously I built a theremin based on a modified design from The Magpi magazine, which is now also the subject of a project on the raspberrypi.org site here
Alexandra Stepanoff playing a Theremin in 1930. (from Wikipedia Theremin article)
While this is a great little project, I was not very satisfied with the end result which didn’t sound very similar to a “real” theremin. The original theremin was an analogue instrument in which the audio frequency was produced by the interaction between two radio frequency oscillators at very nearly the same frequency which were controlled by the proximity of a musicians hand to a metal rod aerial. A third oscillator controlled by the proximity to a metal loop of the musicians other hand was used to control the volume. this gave a sound which was continuous when started by the proximity of a hand, whose frequency and volume could be varied smoothly. You can see an example in the picture above (from the Wikipedia article on Theremin)
The first thing I resolved to do was to use two ultrasonic sensors instead of one. I used the same circuit as the one in the raspberrypi.org article but built it twice on the same breadboard. The first circuit used pins 4 and 17 as in the original, and the second one, used pins 23 and 24 for the trigger and echo signals instead, otherwise being identical. I built the circuit on a RasPiO ProHat board which gives convenient access to all the GPIO pins, as shown in the photographs below.
To read the inputs from the two ultrasonic sensors a python script is used. The starting point for the python script is to extract data from the two ultrasonic sensors using the excellent gpiozero library, included by default on the latest Raspbian Stretch image. This makes it very easy to get a series of readings from the two sensors.
In order to send the data to Sonic Pi an OSC message is used, using the python-osc library. You can install this using:
sudo pip3 install python-osc
The complete code for my python3 script is shown below:
#!/usr/bin/env python3 #program sets up two distance sensors and exports readings using OSC to host port 4559 #using gpiozero and python-osc libraries #written by Robin Newman, May 2018 # from gpiozero import DistanceSensor from time import sleep from pythonosc import osc_message_builder from pythonosc import udp_client import argparse import sys def control(spip): sensor = DistanceSensor(echo=17, trigger=4,threshold_distance=0.5) sensor2 = DistanceSensor(echo=23, trigger=24,threshold_distance=0.5) sender = udp_client.SimpleUDPClient(spip,4559) while True: try: r1 = (sensor.distance* 100) #send numbers in range 0->100 r2 = (sensor2.distance* 100) sender.send_message('/play_this',[r1,r2]) #sends list of 2 numbers print("r1:",round(r1,2),"r2:",round(r2,2)) sleep(0.06) except KeyboardInterrupt: print("\nExiting") sys.exit() if __name__=="__main__": parser = argparse.ArgumentParser() parser.add_argument("--sp", default="127.0.0.1", help="The ip onic Pi listens on") args = parser.parse_args() spip=args.sp print("Sonic Pi on ip",spip) sleep(2) control(spip)
You can type this in using text editor from the Pi Gui Accessories Menu, or using the nano editor from a terminal window. Alternatively you can download it from my gist site (link at the end of of the post). To make it easy to start, when you have it installed on your Raspberry Pi set it to be executable by the pi user by typing (in a terminal window):
chmod u+x theremin.py
I have recently received my Kickstarter Pi_Juice board after sdveral years wait :-) and I decided to try it out with this project, as it would mean that the sensor setup: RasPiO board, Pi-Juice board and Raspberry Pi were completely self contained. That is why I added the optional input parameter, so that I could send the OSC messages to an external computer on the local network, instead of just using it on the local Raspberry Pi. Of course if you don;t have a second suitable computer on which to run Sonic Pi, you can do all of this using the built in Sonic Pi on Raspbian Stretch (version 3.0.1), although you will need to have a screen and keyboard and speaker attached to your Pi to do so. (It IS possible to run Sonic Pi on a headless Raspberry Pi using xdrp, which I have done on other projects, but I don’t want to complicate this post by giving details here). If you are going to use a separate computer for Sonic Pi, then first make sure that your Raspberry Pi is set up to connect automatically to your network via WiFi. (I used a Pi3 with built in Wireless). Then you can use the Raspiconfig utility on the gui preferences menu to set your Pi to boot to the command line (logged in). You should also enable SSH using the same utility. When you do so, you should change the Raspberry Pi password for user pi from the default raspberry to something else you can remember. Again the same utility will let you do that. Note the ip address of your Raspberry Pi on your wireless network, so that you can connect to it remotely. If you don’t want to do this, you can of course leave the Pi connected to screen and keyboard and just start the theremin.py script directly from a terminal window.
All being well if you reboot your Raspberry Pi (you can do it with the screen and keyboard attached first time if you like to check that it is working) you should be able to connect to it from your second computer running Sonic Pi. I used a Macbook Pro, with ip address 192.168.1.134 and my Raspberry PI was on 192.168.1.182, so I typed:
on my Mac terminal window, followed by the password for user pi on my Raspberry Pi. This connected me, and since I had saved the theremin.py script in my pi home directory I then just had to type this to get the script running (remember it was set to be executable):
./theremin.py --sp 192.168.1.134
This started the script running, and it reported in the terminal window.
Sonic Pi on 192.168.1.134
Then if I started moving my hands back and foward near the ultrasound sensors I could see streams of numbers beneath, shown that the readings were geing generated. e.g.
r1: 6.69 r2: 6.45 r1: 6.69 r2: 6.45 r1: 9.28 r2: 6.45 r1: 9.28 r2: 6.45 r1: 9.26 r2: 6.49 r1: 9.26 r2: 6.53
That completes the setup for the Raspberry Pi. Moving on to Sonic Pi, this should be started on your second computer (in my case my Macbook Pro). I was using version 3.1 the latest release on this platform. In an empty buffer screen (selected using the tabs at the bottom of the main screen, you should add the following program, either by typing it out or by downloading it from the gist link at the end of this post.
#Theremin plus, controlling pitch and ixi_techno phase by Robin Newman May 2018 #I control a continously playing note (length 2000) use_debug false define :putsPretty do |n,p| num=(n*10**p).round/(10**p).to_f return num end define :scalev do |v,l,h| return (l+v).to_f*(h-l)/100 end with_fx :reverb,room: 0.8,mix: 0.8 do with_fx :ixi_techno,phase: 4,phase_offset: 1,mix: 0.8 do |p| set :p, p use_synth :subpulse k=play octs(0,2),sustain: 10000,amp: 0 set :k,k live_loop :theremin do use_real_time b = sync "/osc/play_this" r1=scalev(b,30,100) r2=scalev(b,0.1,1) puts putsPretty(r1,2),putsPretty(r2,2) if r1 < 60 then #adjust note pitch, and restore volume to 0.7 control get(:k),note: octs(r1+12,2),note_slide: 0.06 ,amp: 0.7,amp_slide: 0.2 else #set output vol to 0 control get(:k),amp: 0,amp_slide: 0.2 end if r2 < 0.8 then #adjust phase modulation rate, and restore mix to 0.8 control get(:p),phase: r2,phase_slide: 0.06,mix: 0.8,mix_slide: 0.2 else #switch off phase modulation by setting mix to zero control get(:p),mix: 0,mix_slide: 0.2 end end end end
Looking at this you will notice that I use the effect :ixi_techno, and further down the program you will see that the :phase value, or the rate at which this effect modulates sounding notes inside the effect are controlled by the value of r2, the second output sent from the two ultrasound sensors. In my initial description of the “real” theremin I said that the two available controls adjusted the pitch or frequency of the notes played and the volume of those notes. Initially I tried adjusting the volume of the notes, and it can be done, but I didn’t find it very effective. Instead I opted to add this optional modulation to the playing note, but the nice thing about Sonic Pi is that it is fairly easy to adjust one of a variety of characteristics using the second number, and I hope to try out some others, and may even find a more satisfying result for volume adjustment in the end.
The main technique I use in the program is to start a very long note playing with zero volume, and then to use Sonic Pi’s ability to control the parameters of the note while it is playing. By adding sliding values to the parameters which change I can get a smoothly varying pitch for the note unlike the more simple theremin in the raspberrypi.org example. There are two functions defined at the start of the program. The first one putsPretty is used to print the numbers to just 2 decimal places. The second scalev is used to scale the number (which arrive in the range 0->100 to whatever I need in the program. r1 is adjusted to range 30->100 and r2 to the range 0.1->1 Some reverb is applied to all of the output, and then the :ixi_techno effect is applied The |p| is a variable which gets a reference to the running effect. I store this in the time_state as :p so that it can be referenced and used to control the parameters of the effect further down the program. Similarly the long note is started and a reference to it is stored in the variable k in the line:
k=play octs(0,2),sustain: 10000,amp: 0
This is then subsequently stored in the time-state as :k so that it can be retrieved and used to alter the pitch of the note according to the value of r1. Notice the use of the octs function to play two notes an octave apart. Initially the pitch is 0 and the volume 0 as well, but this is altered by the control later on.
The meat of the program takes place in the live_loop :theremin This waits for an incoming OSC messages addressed to /play/this in the sending program (Sonic PI adds the initial /osc to show the source. This contains the data of the two numbers r1, and r2 in a list. They are retrieved, printed on the screen using the putsPretty function and then processed to control the note. a cutoff value of 60 is taken for the scaled r1 (equivalent to 60cm). For values above this the volume of the note is set to 0 (with a slide time from any previous value of 0.02), and no note sounds. For values below 60 the pitch of the note is adjusted, and the volume restored to 0.7. Again slide times are used, 0.06 for the note adjustment (the time between successive numbers arriving) and a more sedate 0.2 for the volume adjustment. For(scaled) r2 values < 0.8 the phase rate of the modulation is adjusted, and the mix value is set to 0.8. For values > 0.8 the mix value is set to 0, effectively disabling the effect. Again suitable slide times are chosen.
I you now run the Sonic Pi program, then you should be able to control what you hear by moving your hands in front of the two sensors.