Navigation

The Light Theremin

The Theremin was invented by Léon Theremin (real name Lev Sergeevich Termen) in October 1920. It was presented to Lenin who was definitely pretty taken up by the instrument as he granted Termen the relative freedom to tour the world with it, and he was a hit seemingly everywhere! The neat thing about the theremin is that it is played without any contact with the instrument. The musician stands in front of it and waves her hands in front of two antennas. A real theremin works using radio frequency oscillators with the performer's hands acting as the grounded plates of a capacitor and therefore altering the frequency of the oscillators. If you haven't heard one in action have a look on Youtube.

We will not be using radio frequency feedback circuits, but will instead alter the volume and pitch of our theremin using light. Hence the name Light Theremin. The idea here is to use a phototransistor to sense the proximity of our hands and so achieve control. There are limitations to this approach, but, by in large, it will work and we will have our theremin. So here goes.

Circuit Diagram

dac_8bit_6_buffer_rc_amp_theremin_schem.png

And here's the schematic. Keep to this layout as far as you can. dac_8bit_6_buffer_rc_amp_theremin_bb.png

Finally, here's the Fritzing file for the theremin circuit.

The phototransistors may be replaced photodiodes and perhaps even phototransistors. All will allow light sensitivity and hence control, but the frequency response of these devices are different and hence the behaviour of the light theremin will alter.

Volume control

We already use a knob potentiometer to control the volume of the theremin. It works by acting as a potential divider to control the potential of the signal fed into the current amplifier. To make this divider light sensitive we will use a phototransistor in series with the knob potentiometer, the latter now acting to control the sensitivity of the phototransistor. Note that this is a hardware control: There is no change needed to the program running on the Arduino. So the performance of the circuit remains as it was before this alteration.

Pitch control

To control the pitch we need to alter the delay in the sine wave program running on the Arduino. Once again, we will use a phototransistor coupled with a control know potentiometer, but this time the phototransistor is fed into one of the analogue to digital (ADC) ports of the Arduino (pins A0 to A5) where it is read, digitized and available to our program to control the delay value.

This is a software probe. The ADC process and processing needed to convert the signal into a delay value will slow down the program and hence will alter the behaviour of the program (alter the frequency of the waveform). We will use the potentiometer to adjust the behaviour of the potential divider as light levels change.

Experiment

Important

Do not proceed with this experiment unless you have completely completed the prior steps in this experiment.

Build the light theremin circuit. All you will need to do is include the phototransistors and the second knob-potentiometer tot he circuit. Make sure the pitch phototransistor is read by the Arduino at pin A2 (any of the analogue pins will do, but the program we will use assumes it is connected to A2.

The hardware-level volume control phototransistor will work without any change to the program running on the Arduino. You should see that the sine wave program works with this modified circuit and that the volume of the sound can be controlled using the phototransistor. Try shielding it from the light to increase its resistance. Does the volume go up or down when it is shielded? Why?

To control the pitch of the sine wave we need to sample the output of the second phototransistor. This is done at pin A2 which converts the analogue output of the pitch-potential divider to a digital signal using the built-in 10bit analogue to digital convertor at pin A2 of the Arduino. This signal needs to be converted to a pitch delay, with which we will be able to control the pitch of the signal.

Task:

Before we can proceed, we need to know what is an acceptable range of delays (in $\mu$s for producing reasonable sound waves with our DAC. You should have determined this in the last part of the experiment. If you have not already done so, please stop and go back to that part.

Our goal now is to map the pitch phototransistor reading, let's call this $p$, to a delay, $d$ that lies between the minimum and maximum delays you have determined. Call these $d_{\rm min}$ and $d_{\rm max}$. To make such a mapping possible we need to know the range of possible phototransistor readings. Unfortunately, this range will vary significantly depending on the light level in the room and also with the setting of the knob potentiometer. But let us suppose that we have somehow determine that $p$ will lie between $p_{\rm min}$ and $p_{\rm max}$. We will later see how this can be determined; for now accept that it can be done. Now the mapping can be achieved using a linear function: \begin{equation}

\end{equation} Check to see that this actually does work.

This type of mapping is needed quite frequently and the Arduino function set has a special function to do it: map(). map() takes the form:

long map(long x, long in_min, long in_max, long out_min, long out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Here $x$ is the input (= $p$) and the output is the required mapping $d$:

  d = map(p, p_min, p_max, d_min, d_max) 

is equivalent to the mapping function we used above. Note that the map() function uses integer math so will not generate fractions, when the math might indicate that it should do so. Fractional remainders are truncated, and are not rounded or averaged. This can be a problem.

The full Theremin program listing can be found here. Here we will discuss the important parts of the sketch

Calibration

From the above discussion, it should be clear that to make the mapping possible we need to know the range of phototransistor readings we expect. That is, we need to know $p_{\rm max}$ and $p_{\rm min}$. This can be done by trial and error but this is unsatisfactory. Instead, we use a calibration step that goes in the //setup()// function:

Calibration step

void setup(){
  ...
  // The pitch phototransistor will be read at analogue pin A2:
  pinMode(A2,INPUT);
  ...
  //Start the serial monitor:
  Serial.begin(115200);
  
  /*
    Bright calibration.
    Remove your hands from the phototransistors. 
  
    The calibration uses the average of 10 readings 
    spread over around 5 secs.
    A beep will sound when the callibration is over.
  */
  Serial.println("Calibration: BRIGHT : No hands!!!");
  beep(12,500);
  delay(2000);
  bright = 0;
  for (int i = 1; i<10; i++){
    bright = bright + analogRead(A2);
    delay(500);
  }
  bright = bright/10;
  Serial.print("Bright level = ");
  Serial.println(bright);
  ...
  ...similar lines for a "dark" calibration step

The calibration needs to be done once, hence it is done in the setup function. In the **bright** calibration we keep the pitch phototransistor uncovered (by our hand) and in full view of ambient light. The phototransistor is read 10 times at pin A2 using //analogRead(A2)// and the bright value ($p_{\rm max}$) is taken to be the average of these values. A beep signals the start and end of this step.

Note

Why 10 readings?

Like any measurement, we need to take many before we get a reliable reading. While the ADC on pin A2 may not be very error prone, light levels will fluctuate and a single reading frequently isn't enough. So we take 10 and average them. Also, we have made sure these readings are suitably spaced out by 0.5sec using the //delay(500)// command.

A similar calibration step is performed with your hand covering the phototransistor. This gives the the **dark** reading ($p_{\rm min}$).

Task:

View these readings using the serial monitor. Make sure your **bright** reading is sufficiently large (around 30 or more). If it is smaller than 30, try adjusting the knob potentiometer at the pitch phototransistor to get a larger value. You will need to reset the Arduino (press the reset button) each time you adjust the potentiometer.

Mapping

The mapping function has been discussed above. The functions are implemented in the following lines of code.

Mapping

void loop(){
  //Read in the phototransistor output:
  int photores = analogRead(A2);
  /* Calculate the pitch-delay.
     A simple linear interpolation is used to map the range of phototransistor
     readings to the range of expected values of the pitch-delay.
     The callibration step doesn't ensure we won't see a negative
     pitch-delay. This could happen if the light levels changed after the
     callibration was performed. So we use the abs() function.
  
     This calculation needs to be make faster. Divisions are expensive, so
     we should find a way to get rid of the division.
  */
  int pitch_delay = (range_of_delays*(photores-dark))/range_of_readings;
  pitch_delay = abs(pitch_delay) + min_delay;
  /*
    Another way of performing the mapping is to use the map()
    function. This is faster than the above and should be attempted.
    It will not check for negative values so some kind of offset 
    may be needed.
    
    The map() function maps the value of the first entry (variable photores)
    from the range [light,dark] to the range [max_delay,min_delay]
    With the exception of the abs() step, this is exactly what the above
    statements perform. But, this function is much better optimized so 
    is substatntially faster than the above.  
  */
  //int pitch_delay = map(photores,light,dark,max_delay,min_delay);
  
  // Uncomment the following lines if you wish to see the readings on the 
  // Serial monitor:
  /*
  Serial.print(photores);
  Serial.print("  ");
  Serial.println(pitch_delay);
  delay(500);
  */
  ...

Try out both kinds of mappings. You will find that the explicit mapping function is slow and introduces a notable step-like quality to the sound. The Arduino //map()// function should be faster. Un-comment the serial monitor lines to see the values of the phototransistor readings and the pitch delays. Do they go negative when //map()// is used? If so, an offset will be needed. That is, you will need to add a small integer to pitch_delay to prevent negative values.

Waveform generation

We generate the waveform in the usual way with one alteration: we loop through multiple sine wave periods at a time. This done with the outer loop in the code:

Multiple wave periods

  //Generate a specified number of sine waves. 
  for (int n=0; n<num_waves; n++){
    for (int t=0; t<samples; t++){
      PORTD = sine[t];
      delayMicroseconds(pitch_delay);
    }
  }

The variable //num_waves// controls the number of periods we loop through. The reason for doing this is to stop annoying squeaks that occur when the //pitch_delay// changes too quickly. You may want to play with the value of //num_waves// to get the sound you prefer.

The beep() function

We have used the following function to generate beeps:

Beep

void beep(int pitch, int length){
  /* This is a beep function. It is similar to the tone()
     function in the Arduino library, but this one uses the
     DAC to generate the beep. 
     
     It accepts two arguments: the pitch delay (use values between
     6 and 12) and pitch length (use values around 500).
  */
  for (int n = 0; n < length; n++){
    for (int t = 0; t < samples; t++){
      PORTD = sine[t];
      delayMicroseconds(pitch);
    }
  }
}

It's actually a small bit of sine wave code that has been used to generate beeps. The Arduino has a built-in command tone() that can do this, but it requires the addition of a small piezo beeper to the circuit. Why complicate matters when we already have a sound generation circuit!

Beeps can be produced by making calls to this function as follows:

Call to beep()

  beep(12,500);

The first argument, 12, sets the pitch delay. A value of 12 produces a nice beep, but you are free to choose other values. The second argument, 500, sets the number of periods in the wave. 500 is a good value for a short beep.

Have fun

That's it! Use the Theremin Code to drive your light theremin. Make sure you don't have any calls to the serial monitor active as these will significantly slow down the theremin. Try as many light sources on it as you can (flashing bicycle lights result in interesting sound effects).

Challenge!

We will see who can make the most pleasing light theremin. This will require that you have really understood the many many points made in this tutorial. Have fun!

AJMPublic/teaching/arduino-pi/projects/arduino/dac-audio/light-theremin (last edited 2021-04-14 13:15:42 by apw109)