Differences
This shows you the differences between two versions of the page.
| test-and-measurement:analog-discovery-studio:soda-can-theremin [2022/09/12 15:40] – changed forum.digilentinc.com to forum.digilent.com jeff.greenberg_ni.com | test-and-measurement:analog-discovery-studio:soda-can-theremin [2022/10/04 11:38] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== Building a Soda Can Theremin with the Analog Discovery Studio ====== | ||
| + | |||
| + | ~~TechArticle~~ | ||
| + | |||
| + | ===== Introduction ===== | ||
| + | <WRAP group>< | ||
| + | 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' | ||
| + | </ | ||
| + | {{ : | ||
| + | </ | ||
| + | |||
| + | <WRAP group> | ||
| + | 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' | ||
| + | |||
| + | 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 ===== | ||
| + | <WRAP group>< | ||
| + | For the fully analog theremin: | ||
| + | * [[test-and-measurement: | ||
| + | * [[test-and-measurement: | ||
| + | * [[https:// | ||
| + | * 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 | ||
| + | * [[software: | ||
| + | </ | ||
| + | For the mixed-signal theremin: | ||
| + | * [[test-and-measurement: | ||
| + | * [[test-and-measurement: | ||
| + | * [[https:// | ||
| + | * OP27 operational amplifier | ||
| + | * 10kΩ potentiometer | ||
| + | * 100pF ceramic capacitor | ||
| + | * 100kΩ resistor (x3) | ||
| + | * a soda can | ||
| + | * [[software: | ||
| + | * [[https:// | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ===== Building the Circuit ===== | ||
| + | ==== 1. Variable-Frequency Oscillator ==== | ||
| + | <WRAP group> | ||
| + | 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 [[http:// | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | To see the output of this circuit block, open WaveForms, then, in the // | ||
| + | |||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ==== 2. Fixed-Frequency Oscillator ==== | ||
| + | <WRAP group> | ||
| + | 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 [[https:// | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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 [[https:// | ||
| + | |||
| + | 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). | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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, | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ==== 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. | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ==== 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”.// | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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 (" | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | The output from this circuit is at the junction of the diode, resistor, and capacitor. Connect one side of an orange wire here. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | The output signal falls in the audible domain. However, it is " | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ==== 5. Amplifier ==== | ||
| + | <WRAP group> | ||
| + | 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, [[https:// | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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' | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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). | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ==== 6. Power Amplifier ==== | ||
| + | <WRAP group> | ||
| + | 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 " | ||
| + | |||
| + | 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. | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | Connect the anode (" | ||
| + | |||
| + | The input of the power amplifier is the cathode of the first diode and the output is the emitter of the transistors. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ==== Review of the Whole Circuit ==== | ||
| + | <WRAP group> | ||
| + | A schematic and a breadboard diagram of the whole circuit can be found below. The Fritzing diagram can be downloaded {{ reference: | ||
| + | |||
| + | {{ reference: | ||
| + | |||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group> | ||
| + | To use the theremin, open WaveForms, open the // | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ===== Reducing the Circuit and Writing the Software ===== | ||
| + | <WRAP group> | ||
| + | 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), | ||
| + | |||
| + | **Note:** //If you are not familiar with using WaveForms SDK in Python, check this guide: [[test-and-measurement: | ||
| + | </ | ||
| + | |||
| + | ==== 1. Modifying the Circuit ==== | ||
| + | <WRAP group> | ||
| + | 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), | ||
| + | |||
| + | Connect the Oscilloscope Channel 1+ (orange wire) to the output of the oscillator and ground Channel 1- (orange-white wire). | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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. | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ==== 2. Modules ==== | ||
| + | <WRAP group> | ||
| + | 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 {{ reference: | ||
| + | </ | ||
| + | |||
| + | === Device === | ||
| + | <WRAP group> | ||
| + | This module contains functions which load the WaveForms SDK and connect and disconnect the Analog Discovery Studio. | ||
| + | |||
| + | --> device.py # | ||
| + | <code python> | ||
| + | """ | ||
| + | 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(" | ||
| + | state.dwf = ctp.cdll.dwf | ||
| + | elif sys.platform.startswith(" | ||
| + | state.dwf = ctp.cdll.LoadLibrary( | ||
| + | "/ | ||
| + | else: | ||
| + | state.dwf = ctp.cdll.LoadLibrary(" | ||
| + | |||
| + | # 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' | ||
| + | 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), | ||
| + | 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), | ||
| + | if str(devicename.value) == " | ||
| + | index = iDevice | ||
| + | break | ||
| + | if index < 0: | ||
| + | state.error = True | ||
| + | state.error_msg = " | ||
| + | print(state.error_msg) | ||
| + | quit() | ||
| + | |||
| + | # open device | ||
| + | hdwf = ctp.c_int() | ||
| + | state.dwf.FDwfDeviceOpen(ctp.c_int(index), | ||
| + | state.hdwf = hdwf | ||
| + | check_error() | ||
| + | if state.error: | ||
| + | print(state.error_msg) | ||
| + | quit() | ||
| + | |||
| + | # set dynamic auto configuration | ||
| + | state.dwf.FDwfDeviceAutoConfigureSet(state.hdwf, | ||
| + | return state.hdwf | ||
| + | |||
| + | |||
| + | def close(): | ||
| + | # reset every instrument | ||
| + | state.dwf.FDwfAnalogInReset(state.hdwf) | ||
| + | state.dwf.FDwfAnalogOutReset(state.hdwf, | ||
| + | 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 === | ||
| + | <WRAP group> | ||
| + | This module contains functions which set the positive and negative supply voltages and turn on and off the supplies. | ||
| + | |||
| + | --> supply.py # | ||
| + | <code python> | ||
| + | """ | ||
| + | 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, | ||
| + | return | ||
| + | |||
| + | |||
| + | def initialize(positive_voltage, | ||
| + | # enable variable supplies and set voltages | ||
| + | dwf.FDwfAnalogIOChannelNodeSet( | ||
| + | hdwf, ctp.c_int(0), | ||
| + | dwf.FDwfAnalogIOChannelNodeSet(hdwf, | ||
| + | 0), ctp.c_int(1), | ||
| + | dwf.FDwfAnalogIOChannelNodeSet( | ||
| + | hdwf, ctp.c_int(1), | ||
| + | dwf.FDwfAnalogIOChannelNodeSet(hdwf, | ||
| + | 1), ctp.c_int(1), | ||
| + | return | ||
| + | |||
| + | </ | ||
| + | <-- | ||
| + | </ | ||
| + | |||
| + | === Oscilloscope === | ||
| + | <WRAP group> | ||
| + | This module contains functions which initialize the oscilloscope and record data. | ||
| + | |||
| + | --> scope.py # | ||
| + | <code python> | ||
| + | """ | ||
| + | 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, | ||
| + | # set buffer size | ||
| + | dwf.FDwfAnalogInBufferSizeSet(hdwf, | ||
| + | # enable both channels | ||
| + | dwf.FDwfAnalogInChannelEnableSet(hdwf, | ||
| + | # set range from -5 to +5 V | ||
| + | dwf.FDwfAnalogInChannelRangeSet(hdwf, | ||
| + | # store every n-th conversion (don't average) | ||
| + | dwf.FDwfAnalogInChannelFilterSet(hdwf, | ||
| + | # turn off the trigger | ||
| + | dwf.FDwfAnalogInTriggerSourceSet(hdwf, | ||
| + | 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, | ||
| + | while True: | ||
| + | # wait for it to finish | ||
| + | dwf.FDwfAnalogInStatus(hdwf, | ||
| + | if sts.value == dwfct.DwfStateDone.value: | ||
| + | break | ||
| + | # get data | ||
| + | dwf.FDwfAnalogInStatusData( | ||
| + | hdwf, channel - 1, rgdSamples, state.buffer_size) | ||
| + | return rgdSamples | ||
| + | |||
| + | </ | ||
| + | <-- | ||
| + | </ | ||
| + | |||
| + | === Waveform Generator === | ||
| + | <WRAP group> | ||
| + | This module contains functions which initialize the waveform generator and generate custom signals. | ||
| + | |||
| + | --> wavegen.py # | ||
| + | <code python> | ||
| + | """ | ||
| + | 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), | ||
| + | dwf.FDwfAnalogOutNodeFunctionSet( | ||
| + | hdwf, ctp.c_int(-1), | ||
| + | # set frequency | ||
| + | dwf.FDwfAnalogOutNodeFrequencySet( | ||
| + | hdwf, ctp.c_int(-1), | ||
| + | # set run time | ||
| + | dwf.FDwfAnalogOutRunSet(hdwf, | ||
| + | # turn off wait | ||
| + | dwf.FDwfAnalogOutWaitSet(hdwf, | ||
| + | # set repeat times | ||
| + | dwf.FDwfAnalogOutRepeatSet( | ||
| + | hdwf, ctp.c_int(-1), | ||
| + | return | ||
| + | |||
| + | |||
| + | def generate(signal, | ||
| + | # generate a signal | ||
| + | # load the buffer | ||
| + | dwf.FDwfAnalogOutNodeDataSet(hdwf, | ||
| + | | ||
| + | # set the amplitude | ||
| + | dwf.FDwfAnalogOutNodeAmplitudeSet( | ||
| + | hdwf, ctp.c_int(-1), | ||
| + | # start generation | ||
| + | dwf.FDwfAnalogOutConfigure(hdwf, | ||
| + | return | ||
| + | |||
| + | </ | ||
| + | <-- | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ==== 3. Main Script ==== | ||
| + | <WRAP group>< | ||
| + | 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 | ||
| + | |||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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: | ||
| + | </ | ||
| + | try: | ||
| + | # setup instruments | ||
| + | supply.initialize(5.0, | ||
| + | supply.enable(True) | ||
| + | scope.initialize() | ||
| + | wavegen.initialize() | ||
| + | |||
| + | # generate fixed-frequency sine wave | ||
| + | samples = numpy.arange(0, | ||
| + | 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 | ||
| + | |||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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' | ||
| + | |||
| + | 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. | ||
| + | |||
| + | <WRAP round tip> | ||
| + | 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, | ||
| + | |||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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 ==== | ||
| + | <WRAP group>< | ||
| + | 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 // | ||
| + | |||
| + | <WRAP round tip> | ||
| + | You can output intermediate signals on the Wavegen Channels by sending the respective lists to the // | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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/ | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | |||
| + | <WRAP group>< | ||
| + | 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' | ||
| + | </ | ||
| + | {{ reference: | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ===== Comparing the Two Implementations ===== | ||
| + | <WRAP group> | ||
| + | Both implementations have their advantages and draw-backs, so here comes a side-by-side comparison: | ||
| + | |||
| + | === Advantages === | ||
| + | <WRAP group>< | ||
| + | **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 === | ||
| + | <WRAP group>< | ||
| + | **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 ===== | ||
| + | <WRAP group> | ||
| + | If you would like to play around more, adding another antenna to set the loudness and adding sliders/ | ||
| + | </ | ||
| + | ---- | ||
| + | |||
| + | ===== Next Steps ===== | ||
| + | <WRAP group> | ||
| + | For more information on WaveForms SDK, see its [[software: | ||
| + | |||
| + | For technical support, please visit the [[https:// | ||
| + | </ | ||

