The theremin is an experimental musical instrument invented by the russian physicist Lev Sergeevich Termen in the 20's. Without physical contact, two antennas can sense the position of the theremin player's hands in space and use it to control the frequency and amplitude of the sound. The theremin has been used in classic movie soundtracks such as The Day the Earth Stood Still and The Thing from another world.
In this tutorial, we'll see how to make a simplified theremin-like instrument that changes the pitch played as you wave your hand over an Infrared Proximity Sensor. In addition you can easily vary the length of the "beat" and drive various blinking LEDs (seemingly) all at the same time... it's the magic of multi-threading!
Despite its apparent complexity, this project requires very simple electronics, as most of the dirty jobs is done for you by Zerynth, the software used for programming the theremin.
Zerynth is an easy to use development suite for the high level design of interactive objects ready for the cloud and the IoT. With Zerynth creatives, designers and professionals can develop in Python for Arduino DUE, ST Nucleo and most of Particle (formerly Spark) products, and similar boards using paradigms and features typical of PC and mobile programming.
Step 1: DISCLAIMER
Even though Zerynth is a professional and performant development suite, this tutorial shows how to make a non-professional theremin, played by a non-professional thereminist, as shown in the "Intro" video.
However, we are quite confident that you will get better outcomes than Sheldon :)
So, let's go!
Step 2: Required Material
- 1 x Sharp infrared proximity sensor (GP2Y0A21YK) or other analog distance sensors
- 2 x 1 kΩ resistor
- 2 x low-power LEDs (red and green)
- 1 x piezo buzzer
- 1 x rotary potentiometer
- 1 x Breadboard
- Some jumper wires
- Zerynth Studio. No matter which OS you use, Zerynth runs! Just download the package and install it (see "Step 4" for the download link)
- Arduino DUE or ST Nucleo F401RE or Particle (formerly Spark) Core. No matter which board you use, Zerynth is multi-board compatible! You can find all the Supported Boards details in the dedicated section of the Zerynth documentation.
Step 3: Assembling the Circuits
- Each Sharp IR proximity sensor has three pins. One is the power input, which we connect to 3.3V. Another is the ground that we will connect to one GND pin. Lastly, there is the analog output that varies from 3.1V at 10cm to 0.4V at 80cm. The analog output pin needs to be connected to an analog input. Here, we used pin A5.
- Piezo buzzer uses a special crystal that expands and contracts as an electrical signal passes through it. This will generate a tone that we can hear. One pin of the piezo buzzer goes to GND connection and the other to digital pin D7 (or another pin with PWM feature if you use Spark Core, such as A4).
- A rotary adjustable potentiometer has three pins. Connect 3.3V to an outer pin, GND to the other, and the center pin will have a voltage that varies from 0 to 3.3V depending on the rotation of the pot. Hook the center pin to an ADC on a microcontroller and get a variable input from the user! Here, we used pin A2.
- To build the red LED circuit, connect one end of the resistor to pin D8 (or D7 on Spark Core). Connect the long leg of the LED (the positive leg, called the anode) to the other end of the resistor. Connect the short leg of the LED (the negative leg, called the cathode) to the GND.
- To build the green LED circuit, do as for the red LED circuit but connect the resistor to pin D6.
Step 4: Programming the Board With Zerynth
Using Zerynth is very easy! Let’s see how step by step:
- Download the Windows, Linux or iOS installers from Zerynth Download page.
- Install it (here’s a tutorial on how to install Zerynth Studio) and launch it.
- Create a Zerynth user through the dedicated button. Check your email and verify your new account by clicking on the provided link. Why registering? With a Zerynth account you can: join the Zerynth community; save your projects on the cloud and access them from different devices; automatically receive updates of the ZerynthStudio, Zerynth VM, Libraries, and new examples.
- Once the account has been verified the Zerynth Studio automatically logs you into the Zerynthcloud (the first time you create a user account an IDE restart can be required. If you have sign-in issue please restart the IDE).
- Connect your board and rename it as you prefer in order to easily recognize it in future.
- To make the board usable, you need to virtualize it. Virtualization is the process of installing the Zerynth VM (Virtual Machine) on a board. The process can be launched by clicking on the dedicated button available on the Zerynth Studio top bar.
- Create a new project, copy the attached code (see "Step 5"), compile your script and upload it in the board. Follow the IDE messages, some boards require a manual reset during the upload process.
- Enjoy your running Zerynth script!
Step 5: Explaining the Code
Why using Zerynth
One of the concepts many people find challenging when beginning to write code for microcontrollers is how to manage multiple hardware-related tasks, seemingly all running at the same time. Designers are consequently frustrated by the difficulties in implementing such functionalities in microcontrollers.
In order to solve these pains, Zerynth supports all the most used high-level features of Python like modules, classes, multithreading, callbacks, timers and exceptions, plus some custom hardware-related features like interrupts, PWM, digital I/O, etc.
Each thread in Zerynth is a sort of separated and parallel process that runs autonomously on your board. A thread requires a function to be executed as input for the definition. The same function can be instanced by various thread giving you the possibility to write very concise and readable code. With threads you can design your algorithm architecture assuming parallelism that is typical of high level.
Inside the code
The script is implemented using 4 threads that run in parallel. One thread is used for acquiring and normalize the analog signals acquired through a potentiometer and a IR proximity sensor. The other three threads are used to instantiate a generic blink() function that drives two LEDs at different frequencies and a generic buzz() function that drives a buzzer at different frequency e length of the sleep (to create a "beat" effect), calculated on the basis of the acquired analog signals.
Get the script from github. The code has a ton of comments. Just a couple of notes.
- delay() vs. sleep()
In Arduino/Wiring using delay() has a side effect - the Arduino does nothing for that while. To get two or more "actions" to run independent of each other, you cannot use delay().
In Zerynth the sleep() function suspends the current thread for time expressed in time_units BUT all the other threads are free to continue their execution!
Zerynth built-in functions
Zerynth VM extends Python with built-in functions to handle the General Purpose Input Output pins of the embedded device. These functions resemble the ones used by Arduino, but are more flexible.
analogRead() vs. adc.read()
The analogRead() function is provided as a built-in to ease the passage from the Arduino/Wiring to Zerynth.
However the preferred way to read an analog pin in Zerynth is:
# import the adc driver
x = adc.read(pin, samples=1)
Reads analog values from pin that must be one of the Ax pins. If samples is 1 or not given, returns the integer value read from pin. If samples is greater than 1, returns a tuple of integers of size samples.
analogWrite() vs. pwm.write()
The Arduino's analogWrite() function provides a simple interface to the hardware PWM, but doesn't provide any control over frequency. The analogWrite() function is provided as a built-in to ease the passage from the Arduino/Wiring to Zerynth. However the preferred way to use pwm in Zerynth is:
# import the pwm driver
pwm.write(pin, period, pulse, time_unit=MILLIS)
The state of pin is periodically switched between LOW and HIGH according to parameters:
- period is the duration of a pwm square wave
- pulse is the time the pwm square wave stays in the HIGH state
- time_unit is the unit of time period and pulse are expressed in time_unit
Step 6: Have Fun!
You can start from this very simple example code to develop the behavior you prefer. Of course there is plenty of room for improvement, such as:
- adding a volume control
- taking advantage of the "SmartSensors" library instead of mapping the analog input values range by hand
Feel free to suggest your own ideas, and have fun annoying people with your brand new multithreaded blinking theremin powered by Zerynth!
Luigi F. Cerfeda (@L_F_Cerfeda) - Zerynth team