Building a Soda Can Theremin with the Analog Discovery Studio

Introduction

Remember those old-school sci-fi movies with squeaky and eerie background music? Those unique sounds were made using a Theremin, an electronic instrument that can be played without physical contact. When playing a Theremin, the operator can control volume and pitch with his or her hand position relative to two antennas.

This project is going to demonstrate how to build and debug a simple Theremin on the Analog Discovery Studio using a soda can as the antenna! With simplicity in mind, the Theremin in this project will only demonstrate pitch modulation. The design that is implemented is shown in the block diagram to the right. It doesn't take too much extra work to add volume control, so I encourage you to try it if you're looking for an extra challenge.

To keep it short and sweet we won't get too deep into the theory of how a Theremin works. Briefly though, the antenna (the soda can) acts as one plate of a capacitor. Your hand will act as the other plate of the capacitor. Thus, by moving your hand nearer and further from the can, you are changing the capacitance of the circuit. This takes place within the variable-frequency oscillator portion of our circuit. As the capacitance in the oscillator changes, the oscillator's output frequency changes as well. This change in frequency can be heard at the speaker at the end of the circuit.

In this project, two approaches will be discussed: first we will build the Theremin on the breadboard, debugging the circuit with the Analog Discovery Studio (this will be a fully analog circuit), then we will remove the signal processing part from the circuit and use the Analog Discovery Studio and WaveForms SDK to record, process and output the signal to the speaker (this will be a mixed signal circuit).


Inventory

For the fully analog theremin:

    • speaker
    • audio connector
    • OP27 operational amplifier (x2)
    • OP37 operational amplifier (x2)
    • 2N3904 NPN transistor
    • 2N3906 PNP transistor
    • 1N3064 small-signal diode
    • 1N914 diode (x2)
    • 10kΩ potentiometer
    • 100pF ceramic capacitor
    • 1nF ceramic capacitor (x2)
    • 10nF ceramic capacitor (x2)
    • 47nF ceramic capacitor
    • 100nF ceramic capacitor
    • 47Ω resistor (x2)
    • 470Ω resistor
    • 4.7kΩ resistor
    • 10kΩ resistor (x5)
    • 20kΩ resistor (x2)
    • 47kΩ resistor (x2)
    • 100kΩ resistor (x3)
  • a soda can

For the mixed-signal theremin:


Building the Circuit

1. Variable-Frequency Oscillator

For the variable-frequency oscillator, we will be using the OP27 operational amplifier. The pin out and specifications of the OP27 can be seen in its data sheet.

Place the op-amp such that it straddles the valley of the breadboard and the notch is towards the top of the breadboard. Op-amps require +5V at one rail and -5V at the other rail. That being said, one power rail of the breadboard is designated for +5V and the other power rail is designated for -5V. In the breadboard image below, red wires are used for connections to the +5V power rail and white wires are used for connections to the -5V power rail. Connect pin 7 of the op-amp to the positive power rail (+5V). Connect pin 4 of the op-amp to the negative power rail (-5V). Connect a black wire between the ground rail and one of the breadboard rows above the op-amp.

Place the 100pF capacitor between the grounded row of the breadboard and pin 2 of the op-amp. Now we'll place the resistors. All the resistors used in the oscillator are 100kΩ. Place one resistor across the op-amp from pin 2 to pin 6. Place another resistor across the op-amp from pin 3 to pin 6. Place the last resistor between the grounded row of the breadboard and pin 3 of the op-amp.

The soda can is to be placed in parallel with the capacitor. Connect one end of a black jumper wire to the grounded rail of the breadboard. You will hold the other end of this wire in your right hand when operating the Theremin. Connect one end of a green jumper wire to pin 2 of the op-amp. Connect the other end of this wire to the soda can. A slightly longer jumper wire is easier here. I place the wire such that it is held on by the tab on the top of the can. Taping the wire to the can would work as well. Just make sure there is metal-to-metal contact. By holding the black wire in your right hand, your body is now part of the circuit. This allows you to use your left hand to act as a plate of a capacitor with the soda can.

Connect an orange wire to pin 6 of the op-amp. This will carry the output voltage from this oscillator to the next section of the circuit, the weighted summer.

To see the output of this circuit block, open WaveForms, then, in the Supplies instrument, set +5V, respectively -5V for the variable power supplies. Enable both power supplies, then turn on the switch on the Breadboard Canvas, to power on the circuit.

Connect the Oscilloscope Channel 1+ (orange wire) to the output of the block and the 1- (orange-white wire) to ground, then open and start the Scope instrument. As you move your hand towards the soda can, you should see the oscillation frequency changing.


2. Fixed-Frequency Oscillator

The output of the fixed-frequency oscillator should be a sinusoidal wave with a frequency close to that of the variable-frequency oscillator. In this project, that is about 32 kHz. To implement this, we can use the AD654 voltage to frequency converter from the Analog Parts Kit and a low-pass filter network to filter out higher frequency harmonics from the generated square wave.

For the fixed-frequency oscillator, we will be using the OP37 operational amplifier. The pin out and specifications of the OP37 can be seen in the data sheet.

Place the op-amp such that it straddles the valley of the breadboard and the notch is towards the top of the breadboard. Op-amps require +5V at one rail and -5V at the other rail. That being said, one power rail of the breadboard is designated for +5V and the other power rail is designated for -5V. In the breadboard image below, red wires are used for connections to the +5V power rail and white wires are used for connections to the -5V power rail. Connect pin 7 of the op-amp to the positive power rail (+5V). Connect pin 4 of the op-amp to the negative power rail (-5V).

Place the 1nF capacitor between the grounded row of the breadboard and pin 2 of the op-amp. Place one 10kΩ resistor across the op-amp from pin 2 to pin 6. Place another 10kΩ resistor across the op-amp from pin 3 to pin 6. Place the 20kΩ resistor between the grounded row of the breadboard and pin 3 of the op-amp.

Connect an orange wire to pin 6 of the op-amp. This will carry the output voltage from this oscillator to the low-pass filter network. This network consists of three RC low-pass filters, connected in series, the first being connected to the output of the op-amp (pin 6). The output of this block is taken from the ungrounded pin of the last capacitor.

To view the signals on different parts of this block, power on the circuit, as previously described, then connect the Oscilloscope Channel 1+ (orange wire) to the output of the op-amp (pin 6) and Channel 2+ (blue wire) to the output of the low-pass filter network. Don't forget to ground Channels 1- (orange-white wire) and 2- (blue-white wire).

As seen in the image to the right, the output signal is more a triangular wave, than a sinus, but for this application, this will be a good approximation.


3. Mixer

This next portion of the circuit, the weighted summer, mixes the signals from our fixed and variable-frequency oscillators. We will be using another OP27.

Move a few holes down the breadboard from the oscillator and place the OP27 such that it straddles the valley of the board and the notched side is towards the left side of the board. Just as we did when building the oscillator, connect pin 7 of the op-amp to the positive power rail and pin 4 of the op-amp to the negative power rail. Connect pin 3 of the op-amp to one of the ground rails on the breadboard.

Connect two 47kΩ resistors in parallel as shown in the breadboard image below. The bottom side of both resistors should be connected to pin 2 of the op-amp. The left sides of the resistors should be off by one row as shown. Connect the other end of the orange wire from the oscillator to one of these 47kΩ resistors. Use another wire to connect the output of the low-pass filter network after the fixed-frequency oscillator to the other 47kΩ resistor. Place a 20kΩ resistor across the op-amp from pin 2 to pin 6. Lastly, place an orange wire at pin 6 of the op-amp, just like in step 1. This will connect the output of the weighted summer to the next section of the circuit, the envelope detector.

As the frequencies of the two input signals are very close, when they are added, they interfere, producing a beat (a specific interference pattern). At this point the output signal is still outside the audible frequency domain, but it can be visualized with the oscilloscope.


4. Envelope Detector

The new signal output from the weighted summer will be passed through an envelope detector which traces the beating pattern.

Note: The orientation of the diode is very important. The diode in the Analog Parts Kit is orange with a black band on one side. The side with the black band is “negative”.

The input of the envelope detector is the output of the weighted summer. A few holes down from the weighted summer, connect the other end of the orange wire (from the weighted summer) to the orange (“positive”) side of the diode. At the negative side of the diode, place the 10kΩ resistor and 47nF capacitor in parallel. Connect the other side of the resistor and capacitor to a ground rail.

The output from this circuit is at the junction of the diode, resistor, and capacitor. Connect one side of an orange wire here.

The output signal falls in the audible domain. However, it is “weak”, having a small amplitude, but a relatively large DC offset, which can damage the speaker, so further processing is necessary.


5. Amplifier

The amplifier used here is an inverting amplifier built with an OP37 op-amp. The OP37 has an identical pin out compared to the OP27. It behaves similarly as well. If you're interested though, here is the data sheet specific to the OP37.

Moving moving further down the board, place the OP37 the same way the OP27's were placed, straddling the valley of the board with the notch to towards the top of the breadboard. As with the previous two op-amps, connect pin 7 of the op-amp to the +5V rail of the breadboard and connect pin 4 of the op-amp to the -5V rail of the breadboard.

Place a 10kΩ potentiometer near the op-amp. This potentiometer will be used to set the amplitude (volume) of the signal. Connect the middle pin to pin 2 of the op-amp and another pin to pin 6 of the op-amp. Place a 4.7kΩ resistor such that one side of it is connected at pin 2 of the op-amp. The other side of the resistor should be connected a few holes to above the op-amp. Connect the other side of the orange wire from the envelope detector's output to this side of the resistor.

Pin 3 of the op-amp would normally be connected to ground through a resistor. In this scenario, it is ideal to use as large of a resistor as possible. To simulate an infinite resistance, we will leave pin 3 unconnected such that it sees an “open circuit”. No current can flow through an open circuit it so it acts as an infinite resistance.

Connect the 100nF capacitor to the output of the op-amp. Connect an orange wire to the other pin of the capacitor. This will be used to send the amplifier to the speaker, while the capacitor in series with the output will eliminate the DC offset of the signal (decoupling capacitor).

To visualize the signals, connect the Oscilloscope Channel 1+ (orange wire) to the input of the block and Channel 2+ (blue wire) to the output, after the decoupling capacitor. Don't forget to ground the 1- (orange-white) and 2- (blue-white) wires.


6. Power Amplifier

At this point you can connect the speaker, or the audio connector between the output of the last block and ground, or you can follow the steps further to add a power amplifier circuit to before the speaker, which will make your “music” much louder. If you intend to use a separate audio amplifier (maybe built in the speaker), you can leave this part out.

As the output current of the op-amp should be kept as low as possible (below ±10mA according to the datasheet), but a 8Ω speaker would draw more than 100mA current at a 1V amplitude signal (our signal can have an amplitude even higher than 1V), to reach maximum loudness, a class AB power amplifier can be used to supply the necessary current.

Connect together the emitters of an NPN and a PNP transistor. Connect the collector of the NPN transistor to +5V, the collector of the PNP transistor to -5V. Connect one 10KΩ resistor between the base and the collector of each transistor.

Connect the anode (“positive” pin) of a diode to the base of the NPN transistor. Connect the anode of a second diode to the cathode of the first, and the cathode of the second diode to the base of the PNP transistor.

The input of the power amplifier is the cathode of the first diode and the output is the emitter of the transistors.


Review of the Whole Circuit

A schematic and a breadboard diagram of the whole circuit can be found below. The Fritzing diagram can be downloaded here.

To use the theremin, open WaveForms, open the Supplies instrument, enable the positive and negative variable supplies and set their voltage to +5V, respectively -5V. Turn on the switch on the Breadboard Canvas. You can use a small screwdriver to set the loudness of the theremin with the trimmer potentiometer.


Reducing the Circuit and Writing the Software

This time the theremin will be implemented mostly from software: we will use Python 3 and WaveForms SDK to record the output of the variable-frequency oscillator, generate the output of the fixed-frequency oscillator (virtually), and mix the two signals. The envelope detector and the amplifier will also be implemented in software.

Note: If you are not familiar with using WaveForms SDK in Python, check this guide: Using WaveForms SDK.

1. Modifying the Circuit

As the Analog Discovery Studio can't measure capacitance directly (the impedance analyzer can measure capacitance but it needs a waveform generator channel and is too slow for this application), we still need the first block of the previous circuit (see the Variable-Frequency Oscillator section). You won't need the other parts of the circuit.

Connect the Oscilloscope Channel 1+ (orange wire) to the output of the oscillator and ground Channel 1- (orange-white wire).

Connect the Oscilloscope Channel 2+ (blue wire) to the middle pin of the 10kΩ potentiometer and ground Channel 2- (blue-white wire). Use jumper wires to connect one unconnected leg of the potentiometer to the positive supply voltage and the other to the ground. This potentiometer will set the volume of the theremin.


2. Modules

In the following, separate python modules will be created for controlling the Test & Measurement device and different instruments. The full source code of the project can be downloaded here.

Device

This module contains functions which load the WaveForms SDK and connect and disconnect the Analog Discovery Studio.

device.py
"""
    Module containing functions for controlling the device connection and status
"""
 
# import necessary modules
import ctypes as ctp
import sys
 
 
class state:
    # device parameters
    dwf = None
    hdwf = None
    error = False
    error_msg = None
 
 
def load():
    # load the WaveForms SDK
    if sys.platform.startswith("win"):
        state.dwf = ctp.cdll.dwf
    elif sys.platform.startswith("darwin"):
        state.dwf = ctp.cdll.LoadLibrary(
            "/Library/Frameworks/dwf.framework/dwf")
    else:
        state.dwf = ctp.cdll.LoadLibrary("libdwf.so")
 
    # check for errors
    check_error()
    if state.error:
        print(state.error_msg)
        quit()
    return state.dwf
 
 
def check_error():
    # set error flag and error message if necessary
    szerr = ctp.create_string_buffer(512)
    state.dwf.FDwfGetLastErrorMsg(szerr)
    if szerr[0] != b'\0':
        state.error = True
        state.error_msg = str(szerr.value)
    return
 
 
def open():
    # count devices
    cDevice = ctp.c_int()
    state.dwf.FDwfEnum(ctp.c_int(0), ctp.byref(cDevice))
    if cDevice.value <= 0:
        state.error = True
        state.error_msg = "No device is connected"
        print(state.error_msg)
        quit()
 
    # search for Analog Discovery Studio
    devicename = ctp.create_string_buffer(64)
    index = -1
    for iDevice in range(0, cDevice.value):
        state.dwf.FDwfEnumDeviceName(ctp.c_int(iDevice), devicename)
        if str(devicename.value) == "b'Analog Discovery Studio'":
            index = iDevice
            break
    if index < 0:
        state.error = True
        state.error_msg = "Analog Discovery Studio isn't connected"
        print(state.error_msg)
        quit()
 
    # open device
    hdwf = ctp.c_int()
    state.dwf.FDwfDeviceOpen(ctp.c_int(index), ctp.byref(hdwf))
    state.hdwf = hdwf
    check_error()
    if state.error:
        print(state.error_msg)
        quit()
 
    # set dynamic auto configuration
    state.dwf.FDwfDeviceAutoConfigureSet(state.hdwf, ctp.c_int(3))
    return state.hdwf
 
 
def close():
    # reset every instrument
    state.dwf.FDwfAnalogInReset(state.hdwf)
    state.dwf.FDwfAnalogOutReset(state.hdwf, ctp.c_int(-1))
    state.dwf.FDwfAnalogIOReset(state.hdwf)
    state.dwf.FDwfDigitalInReset(state.hdwf)
    state.dwf.FDwfDigitalOutReset(state.hdwf)
    state.dwf.FDwfDigitalIOReset(state.hdwf)
 
    # close the device
    state.dwf.FDwfDeviceClose(state.hdwf)
    return

Supplies

This module contains functions which set the positive and negative supply voltages and turn on and off the supplies.

supply.py
"""
    Module containing functions for controlling the power supplies
"""
 
# import the necessary module
import ctypes as ctp
 
# these will be initialized in the main program
dwf = None
hdwf = None
 
 
def enable(state):
    # power supply master switch
    dwf.FDwfAnalogIOEnableSet(hdwf, ctp.c_int(state))
    return
 
 
def initialize(positive_voltage, negative_voltage):
    # enable variable supplies and set voltages
    dwf.FDwfAnalogIOChannelNodeSet(
        hdwf, ctp.c_int(0), ctp.c_int(0), ctp.c_double(True))
    dwf.FDwfAnalogIOChannelNodeSet(hdwf, ctp.c_int(
        0), ctp.c_int(1), ctp.c_double(positive_voltage))
    dwf.FDwfAnalogIOChannelNodeSet(
        hdwf, ctp.c_int(1), ctp.c_int(0), ctp.c_double(True))
    dwf.FDwfAnalogIOChannelNodeSet(hdwf, ctp.c_int(
        1), ctp.c_int(1), ctp.c_double(negative_voltage))
    return

Oscilloscope

This module contains functions which initialize the oscilloscope and record data.

scope.py
"""
    Module containing functions for controlling the Oscilloscope
"""
 
# import modules, constants and types
import dwfconstants as dwfct
import ctypes as ctp
import time
 
# these will be initialized in the main program
dwf = None
hdwf = None
 
 
class state:
    # instrument parameters
    sampling_frequency = 1000000.0
    buffer_size = 8192
 
 
def initialize():
    # initialize the oscilloscope
    # set sampling frequency
    dwf.FDwfAnalogInFrequencySet(hdwf, ctp.c_double(state.sampling_frequency))
    # set buffer size
    dwf.FDwfAnalogInBufferSizeSet(hdwf, ctp.c_int(state.buffer_size))
    # enable both channels
    dwf.FDwfAnalogInChannelEnableSet(hdwf, ctp.c_int(-1), ctp.c_bool(True))
    # set range from -5 to +5 V
    dwf.FDwfAnalogInChannelRangeSet(hdwf, ctp.c_int(-1), ctp.c_double(5))
    # store every n-th conversion (don't average)
    dwf.FDwfAnalogInChannelFilterSet(hdwf, ctp.c_int(-1), dwfct.filterDecimate)
    # turn off the trigger
    dwf.FDwfAnalogInTriggerSourceSet(hdwf, dwfct.trigsrcNone)
    time.sleep(2)
    return
 
 
def acquisition(channel):
    # data acquisition
    sts = ctp.c_byte()
    rgdSamples = (ctp.c_double*state.buffer_size)()
    # start acquisition
    dwf.FDwfAnalogInConfigure(hdwf, ctp.c_int(1), ctp.c_int(1))
    while True:
        # wait for it to finish
        dwf.FDwfAnalogInStatus(hdwf, ctp.c_int(1), ctp.byref(sts))
        if sts.value == dwfct.DwfStateDone.value:
            break
    # get data
    dwf.FDwfAnalogInStatusData(
        hdwf, channel - 1, rgdSamples, state.buffer_size)
    return rgdSamples

Waveform Generator

This module contains functions which initialize the waveform generator and generate custom signals.

wavegen.py
"""
    Module containing functions for controlling the Waveform Generator
"""
 
# import modules, constants and types
import dwfconstants as dwfct
import ctypes as ctp
import time
 
# these will be initialized in the main program
dwf = None
hdwf = None
 
 
class state:
    # instrument parameters
    sampling_frequency = 1000000.0
    buffer_size = 8192
    frame_repeat = 1
 
 
def initialize():
    # initialize the instrument
    frequency = state.sampling_frequency / (1.0 * state.buffer_size)
    # enable wavegen
    dwf.FDwfAnalogOutNodeEnableSet(
        hdwf, ctp.c_int(-1), dwfct.AnalogOutNodeCarrier, ctp.c_bool(True))
    dwf.FDwfAnalogOutNodeFunctionSet(
        hdwf, ctp.c_int(-1), dwfct.AnalogOutNodeCarrier, dwfct.funcCustom)
    # set frequency
    dwf.FDwfAnalogOutNodeFrequencySet(
        hdwf, ctp.c_int(-1), dwfct.AnalogOutNodeCarrier, ctp.c_double(frequency))
    # set run time
    dwf.FDwfAnalogOutRunSet(hdwf, ctp.c_int(-1), ctp.c_double(1.0 / frequency))
    # turn off wait
    dwf.FDwfAnalogOutWaitSet(hdwf, ctp.c_int(-1), ctp.c_double(0))
    # set repeat times
    dwf.FDwfAnalogOutRepeatSet(
        hdwf, ctp.c_int(-1), ctp.c_int(state.frame_repeat))
    return
 
 
def generate(signal, amplitude):
    # generate a signal
    # load the buffer
    dwf.FDwfAnalogOutNodeDataSet(hdwf, ctp.c_int(-1), dwfct.AnalogOutNodeCarrier,
                                 (ctp.c_double * len(signal))(*signal), ctp.c_int(len(signal)))
    # set the amplitude
    dwf.FDwfAnalogOutNodeAmplitudeSet(
        hdwf, ctp.c_int(-1), dwfct.AnalogOutNodeCarrier, ctp.c_double(amplitude))
    # start generation
    dwf.FDwfAnalogOutConfigure(hdwf, ctp.c_int(-1), ctp.c_int(1))
    return

3. Main Script

The main part of the project starts with importing the necessary modules, then defining the most important parameters of the project:

  • sampling_frequency - the frequency at which analog data is sampled
  • buffer_size - the number of data point in a data set (maximum is 8192)
  • amplitude - this sets the amplitude of the output signal, set it in a way to get around 1V amplitude at the output
  • frame_repeat - how many times should a data set be repeated
  • fixed_frequency - the frequency of the fixed-frequency oscillator

These parameters, the device handle and the library of the SDK are also initialized in the modules.

# import necessary modules
import sys
import math
import numpy
import ctypes as ctp
import dwfconstants as dwfct
from scipy.signal import hilbert
 
# import own modules
import device
import supply
import scope
import wavegen
 
 
class state:
    # application parameters
    sampling_frequency = 1000000.0
    buffer_size = 8192
    amplitude = 2.0
    frame_repeat = 5
    fixed_frequency = 30500.0
 
 
# set parameters in modules
scope.state.sampling_frequency = state.sampling_frequency
wavegen.state.sampling_frequency = state.sampling_frequency
scope.state.buffer_size = state.buffer_size
wavegen.state.buffer_size = state.buffer_size
wavegen.state.frame_repeat = state.frame_repeat
 
# load dynamic the SDK
dwf = device.load()
supply.dwf = dwf
scope.dwf = dwf
wavegen.dwf = dwf
 
# open device, send handle to modules
hdwf = device.open()
supply.hdwf = hdwf
scope.hdwf = hdwf
wavegen.hdwf = hdwf

After this, the instruments are initialized.

The output of the fixed-frequency oscillator is generated. This is always the same, based on the following formula: $signal_{f_{fixed}}=sin(2*{\pi}*\frac{f_{fundamental}}{f_{sampling}}*(0:nr_{sample}))$

try:
    # setup instruments
    supply.initialize(5.0, -5.0)
    supply.enable(True)
    scope.initialize()
    wavegen.initialize()
 
    # generate fixed-frequency sine wave
    samples = numpy.arange(0, state.buffer_size - 1)
    angular_velocity = 2.0 * math.pi * state.fixed_frequency / state.sampling_frequency
    fixed_signal = samples * 1.0
    for index in range(len(samples)):
        fixed_signal[index] = math.sin(angular_velocity * samples[index])
    mixed_signal = samples * 1.0

In the main loop the data acquired on Scope Channel 1 is normalized, then added (mixed) with the generated fixed-frequency signal. The envelope of the resulting signal is extracted by calculating the absolute value of the signal's Hilbert transform. The DC offset of the envelope is removed by substracting from the samples the average of the samples, then the resulting signal is normalized again.

The second Oscilloscope Channel is read and averaged to get the potentiometer state, which determines the volume (amplitude) of the resulting signal.

As a final step, the signal is generated with the desired amplitude on both Wavegen Channels.

You can use the audio output (3.5 mm jack plug) on the Analog Discovery Studio as output. It also has an in-built amplifier, able to supply ±250 mA current.

# loop
    while True:
        # mix signals
        variable_signal = scope.acquisition(1)
        variable_maximum = max(variable_signal)
        for index in range(len(fixed_signal)):
            mixed_signal[index] = fixed_signal[index] + \
                (variable_signal[index] / variable_maximum)
 
        # detect the envelope
        analytic_signal = hilbert(mixed_signal)
        amplitude_envelope = numpy.abs(analytic_signal)
 
        # remove offset and limit the resulting values between +1 and -1
        envelope_average = sum(amplitude_envelope) / len(amplitude_envelope)
        ac_envelope = amplitude_envelope - envelope_average
        envelope_maximum = max(ac_envelope)
        ac_envelope = ac_envelope / envelope_maximum
 
        # get potentiometer information and calculate volume
        voltages = scope.acquisition(2)
        voltage_average = sum(voltages) / len(voltages)
        volume = voltage_average * 1.0 / 5.0
 
        # output the result on the waveform generator
        wavegen.generate(ac_envelope, volume * state.amplitude)

You can stop the theremin by pressing Ctrl+C on the keyboard. The program will stop the supplies, reset all instruments and close the device, to make it available for other software (like WaveForms).

except KeyboardInterrupt:
    # exit on ctrl+c
    pass
 
finally:
    # stop the supplies and close the device
    supply.enable(False)
    device.close()

4. Results

If you want to visualize the signals outputted on the Wavegen Channels, connect an oscilloscope (for example another Test & Measurement device) to any output channel, or use the matplotlib.pyplot python module and the plot() function; however, plotting will introduce significant delays, thus the results may not be correct.

You can output intermediate signals on the Wavegen Channels by sending the respective lists to the wavegen.generate() function.

It can be seen, that if the variable frame_repeat is set to 1, huge delays appear in the output signal. This happens due to the processing speed of our script: mixing and demodulating/filtering the signals requires more time than the runtime of the waveform generator for a data set.

This effect can be reduced by a small trick: we will repeat every data set a few times (around 5 is fine). As the signals are mostly periodic and because of the large sampling frequency and the reduced buffer size of the instrument, this trick isn't even audible in the resulting audio signal.

Small (around 3ms) delays still are present. These are the moments when new data sets are loaded in the Wavegen's buffer, but these interruptions add a really cool effect to the resulting sound.


Comparing the Two Implementations

Both implementations have their advantages and draw-backs, so here comes a side-by-side comparison:

Advantages

Analog Theremin

  • continuous output signal
  • no delays present
  • fun to build

Mixed-Signal Theremin

  • much less components needed
  • more flexible (for example the fixed frequency is changeable)
  • easier to debug
  • fun to build

Disadvantages

Analog Theremin

  • many components needed
  • hard to debug
  • not flexible at all
  • sensible to noise (for example if you put your hand on one of the wires)

Mixed-Signal Theremin

  • small delays and drop-outs appear in the output (this might be wanted as sounds interesting)

Further Improvements

If you would like to play around more, adding another antenna to set the loudness and adding sliders/rotary encoders to tweak the sound of the mixed-signal theremin (you could dynamically set the sample rate, the frame repeat counter, and/or the fixed-frequency) are just a few ideas to set you off.


Next Steps

For more information on WaveForms SDK, see its Resource Center.

For technical support, please visit the Test and Measurement section of the Digilent Forums.