Project 6: Input devices. Make a
theremin. More realistically, make something that produces sound that
changes when you wave your hands.
-- hardware setup --
A traditional theremin uses a solid copper antenna. I approixmated this by
wrapping the adhesive copper we have around an acrylic rod.
The biggest problem I had was that it was hard to get a range on the
capacitance sensing. I thus changed the hello3 board to have a smaller
resistor and thus have a larger current.
However, I am limited by the fact that the microcontroller requires 5V.
This took quite a bit of trial and error, soldering an desoldering. In the end, it worked but I was not able
to get a very good range, maybe only 1 inch or so.
I hooked up a 9V battery to my board because when I was using a
USB-->serial hookup to try to run the GUI on my laptop I saw that the
actual voltage drop was only 4.4V, and that the adapter was sapping some
of the voltage away.
-- my quasi-theremin --
Here is what the final set up looked like.
Here it is in action:
...
Theremin Movie ...
-- making a sound in python --
I downloaded a soundlibrary called sndobj that allows you to play a sound in python.
It was frustrating on the linux computer because it required installing libjack.
Also frustrating was the fact that there were no speakers on the computer, so I had to find a friend with
speakers at the Media Lab and hook them up to the computer.
Unfortunately, even though I downloaded all the required things to run my board from my laptop, the response was very noisy
and strange and I decided to just try to work on the linux computer in the 016 lab anyway.
My test sound code looks like this:
import sndobj
import time
tab = sndobj.HarmTable()
tab.SetHarm(2,2)
osc = sndobj.Oscili(tab, 100, 10000)
mod = sndobj.Oscili(tab, 200, 200)
outp = sndobj.SndRTIO(1, sndobj.SND_OUTPUT)
mod.SetFreq(200)
osc.SetFreq(300, mod)
outp.SetOutput(1, osc)
thread = sndobj.SndThread()
thread.AddObj(mod)
thread.AddObj(osc)
thread.AddObj(outp, sndobj.SNDIO_OUT)
thread.ProcOn()
time.sleep(5)
osc.SetFreq(600, mod)
time.sleep(5)
thread.ProcOff()
which basically made a tone that is an oscillation modulated by a second oscillation, and then the same tone one octave
higher.
It was fun to play with sound modulation and harmonics and different waveforms. I decided that a saw-shaped waveform with 5
harmonics sounded the coolest.
-- modifying the assembly code --
I changed the assembly code from the hello3.step.asm to only sample the
capacitance at the charging part of the curve. I made several changes:
-only charge the capacitor until it samples, then let it discharge
-only sample on the charging part of the curve
-sample early in the curve for higher sensitivity
-sample at the same place each time through
-only send two bytes of the data--i.e., one reading from the ADC
The resulting code is much simpler, and as I hoped, faster.
The assembly code I used is at the bottom.
-- modifying the GUI --
I used my voltmeter GUI from Project 4 as a base.
I then added the sound code and replaced the frequency argument (i.e., the pitch) with a variable dependent on the input
coming from the serial port.
There was a problem synching the board with the computer. David helped me make the GUI check the framing each time to make
sure it responds correctly.
There was obviously also some noise issues. When we amplified the signal to make a bigger change in the response, we
amplified the noise as well. We got rid of some of this problem by doing a rolling average.
Here is a screenshot of the visual display.
The code is displayed at the end.
******************************************************************************
The assembly code:
;
; suelin.asm
; capacitance measurement
; based on Neil Gershenfeld CBA MIT 10/29/05
;
; definitions
;
.include "tn13def.inc"
.equ chargepin = PB1 ; charging pin
.equ txpin = PB4 ; serial transmit pin
.def bitcnt = R16 ; bit counter
.def temp = R17 ; temporary storage
.def txbyte = R18 ; data byte
.def delaycnt = R19 ; delay counter
.def uplo = R20 ; up low byte
.def uphi = R21 ; up hi byte
;
; start of code
;
.cseg
;puts program at beginning of memory
.org 0
rjmp reset ; jump to reset routine
;
; putchar routine
; assumes no line driver (doesn't invert bits)
;
.equ sb = 1 ; number of stop bits
putchar:
ldi bitcnt, 9+sb ; 1+8+sb
com txbyte ; invert everything
sec ; set start bit
putchar0:
brcc putchar1 ; if carry set
sbi PORTB, txpin ; send a '0'
rjmp putchar2 ; else
putchar1:
cbi PORTB, txpin ; send a '1'
nop
putchar2:
rcall bitdelay ; one bit delay
rcall bitdelay
lsr txbyte ; get next bit
dec bitcnt ; if not all bits sent
brne putchar0 ; send next
ret ; else return
;
; serial bit delay routine
;
.equ b = 17 ; 9600 bps
bitdelay:
ldi temp, b
bitloop:
dec temp
brne bitloop
ret
;
; routine to wait for sample to settle
;
.equ delay = 255
settle:
ldi temp, delay
settleloop:
dec temp
brne settleloop
ret
;
; main program
;
reset:
ldi temp, low(RAMEND) ; set stack pointer to top of RAM
out SPL, temp ;
;
; init output pins
;
sbi PORTB, txpin ; comm
sbi DDRB, txpin ; "
sbi PORTB, chargepin ; charging
sbi DDRB, chargepin ; "
;
; init A/D
;
cbi ADMUX, REFS0 ; use Vcc as reference
cbi ADMUX, ADLAR ; right-adjust result
sbi ADCSRA, ADEN ; enable A/D
cbi ADCSRA, ADATE ; disable auto-trigger
cbi ADCSRA, ADPS2 ; set prescaler for /2
cbi ADCSRA, ADPS1 ; "
cbi ADCSRA, ADPS0 ; "
cbi ADMUX, MUX1 ; input on ADC1
sbi ADMUX, MUX0 ; "
;
; infinite main loop
;
loop:
;
; loop over delays
;
;changed from 254 to sample earlier
ldi delaycnt, 5
delayloop:
;
; settle sample and start upward step response
;
cbi PORTB, chargepin
rcall settle
mov temp, delaycnt
sbi PORTB, chargepin
;
; wait for delay--maybe change amount of time depending on
charge curve
;
addelayup:
dec temp
brne addelayup
;
; read response
;
sbi ADCSRA, ADSC ; start conversion
adloopup:
sbic ADCSRA, ADSC ; loop until complete
rjmp adloopup
;
; save conversion
;
;in uplo, ADCL ; get low byte
;in uphi, ADCH ; get high byte
;
;
; send conversions
;
in txbyte, ADCL ; low down byte
rcall putchar
in txbyte, ADCH ; hi down byte
rcall putchar
;mov txbyte, uplo ; low up byte
;rcall putchar
;mov txbyte, uphi ; hi up byte
;rcall putchar
;dec delaycnt
;dec delaycnt
;brne delayloop
;
; send 1 2 3 4 for framing
;
ldi txbyte, 1
rcall putchar
ldi txbyte, 2
rcall putchar
ldi txbyte, 3
rcall putchar
ldi txbyte, 4
rcall putchar
rjmp loop
******************************************************************************
Here is the GUI for the theremin:
#
#
# theremin GUI
# 11.05.06
#
from Tkinter import *
import serial
import sndobj
import time
tab = sndobj.HarmTable()
tab.SetHarm(5,4)
osc = sndobj.Oscili(tab, 100, 500)
mod = sndobj.Oscili(tab, 440, 2)
outp = sndobj.SndRTIO(1, sndobj.SND_OUTPUT)
mod.SetFreq(200)
osc.SetFreq(5, mod)
outp.SetOutput(1, osc)
thread = sndobj.SndThread()
thread.AddObj(mod)
thread.AddObj(osc)
thread.AddObj(outp, sndobj.SNDIO_OUT)
thread.ProcOn()
#thread.ProcOff()
WINDOW = 800
NSAMPLES = 254
MAX = 1040
MAX_VOLTAGE = 5.0
eps = .9
saveflag = 0
index = 0
path = []
step = []
path_filt = []
step_filt = []
baseline = []
xpos = .05*WINDOW
def idle(parent,canvas):
global index, channel, baseline, path, path_filt, step, step_filt, saveflag, xpos
#
# idle routine
#
eps = float(sfilter.get())
# lo_dn = ord(ser.read())
# hi_dn = ord(ser.read())
#uplo = ord(ser.read())
#uphi = ord(ser.read())
voltage_value1 = 90
voltage_value2 = 90
voltage_value3 = 90
voltage_value4 = 90
voltage_value5 = 90
voltage_value6 = 90
voltage_value7 = 90
voltage_value8 = 90
voltage_value9 = 90
voltage_value10 = 90
voltage_value11 = 90
voltage_value12 = 90
i = 0
x0 = 0
x1 = 0
x2 = 0
x3 = 0
x4 = 0
x5 = 0
keepLooping = 1
ser.flushInput()
while keepLooping:
#i += 1
x5 = x4
x4 = x3
x3 = x2
x2 = x1
x1 = x0
x0 = ord(ser.read())
if ((x5 == 1) & (x4 == 2) & (x3 == 3) & (x2 == 4)):
voltage_value1 = voltage_value2
voltage_value2 = voltage_value3
voltage_value3 = voltage_value4
voltage_value4 = voltage_value5
voltage_value5 = voltage_value6
voltage_value6 = voltage_value7
voltage_value7 = voltage_value8
voltage_value8 = voltage_value9
voltage_value9 = voltage_value10
voltage_value10 = voltage_value11
voltage_value12 = float(x1 + (x0*256))
voltage_value = (voltage_value1 +voltage_value2 +voltage_value3 +voltage_value4 +voltage_value5
+voltage_value6 +voltage_value7 +voltage_value8 +voltage_value9 +voltage_value10 +voltage_value11 +voltage_value12)/12
voltage_value = (voltage_value-100)*50
keepLooping = 0
print '%.3f'%voltage_value
keepLooping = 1
#to make the window start over
if xpos == WINDOW:
xpos = .05*WINDOW
canvas.delete("dot")
#want voltage_value to be the slope of the line
#convert these two to one number
#voltage_value = 256*uphi + uplo # this number is between 0-1023
#convert to 0-5 volt values
#taking care of invalid voltage readings from chip
if voltage_value < 0:
voltage_value = 0
if voltage_value > 1023:
voltage_value = 1023
voltage_value_normalized = float( voltage_value / 1023.0 ) # range [0,1]
voltage_value_volts = voltage_value_normalized * MAX_VOLTAGE # range [0,5]
vvsn = int ( voltage_value / 50 ) # correlate voltage value to text size
# vvsn stands for voltage value size normalized
r=int (voltage_value_volts * 51)
b=int (255 - r)
g=0
vvcn = "#%02x%02x%02x"% (r,g,b)
osc.SetFreq(20+(5**voltage_value_normalized)*50, mod)
# vvsn stands for voltage value color normalized
dotcolor = "#%02x%02x%02x"% (r,g,b)
#time counter==xposition
#read 1 2 3 4 again and ignore
print "-------"
print voltage_value, voltage_value_normalized
#show convetred value on screen
canvas.delete("voltage")
canvas.delete("voltagestatic")
canvas.create_text(xpos, (1-voltage_value_normalized)*WINDOW,font=("Times New Roman", vvsn),tags="voltage",fill=vvcn)
canvas.create_text(.05*WINDOW,.05*WINDOW, font=("Times New Roman", vvsn),tags="voltagestatic", fill=vvcn)
canvas.itemconfigure("voltage",text="%.2f [V]"%voltage_value_volts)
canvas.itemconfigure("voltagestatic",text="%.2f [V]"%voltage_value_volts)
# canvas.delete("oval")
xpos = xpos + 1
canvas.create_oval(xpos, (1-voltage_value_normalized)*WINDOW, xpos, (1-voltage_value_normalized)*WINDOW, width=3,
tags="dot", fill=vvcn, outline=vvcn)
parent.after_idle(idle,parent,canvas)
"""
lo_dn =0
hi_dn =0
lo_up =0
hi_up =0
if ((lo_dn == 1) & (hi_dn == 2) & (lo_up == 3) & (hi_up == 4)):
if (path_filt == []):
path_filt = path
step_filt = step
else:
for i in range(len(path_filt)):
path_filt[i] = (1-eps)*path_filt[i] + eps*path[i]
for i in range(len(step_filt)):
step_filt[i] = (1-eps)*step_filt[i] + eps*step[i]
canvas.delete("path")
canvas.create_line(path_filt,tag="path",width=3,fill="#00b000")
if (baseline != []):
canvas.delete("baseline_path")
canvas.create_line(baseline,tag="baseline_path",width=3,fill="#b00000")
canvas.itemconfigure("y0",text="y[0]: %.2f"%step_filt[0])
canvas.itemconfigure("y1",text="y[1]: %.2f"%step_filt[1])
canvas.itemconfigure("y-1",text="y[-1]: %.2f"%step_filt[-1])
canvas.coords('x0',0,.95*WINDOW,step_filt[0],WINDOW)
if (saveflag == 1):
file = open(soutfile.get(),"w")
for i in range(len(step_filt)):
file.write("%f\n"%(step_filt[i]))
file.close()
print 'saved to '+soutfile.get()
saveflag = 0
index = 0
path = []
step = []
else:
value_up = 256*hi_up + lo_up
value_dn = 256*hi_dn + lo_dn
value = (value_up + (1023-value_dn))/2.0
index += 2
step.insert(0,value)
path.insert(0,WINDOW-value*WINDOW/float(MAX))
path.insert(0,WINDOW-index*WINDOW/float(NSAMPLES))
"""
#parent.after_idle(idle,parent,canvas)
def save_data(parent):
global saveflag
saveflag = 1
def store_baseline(parent):
global path_filt, baseline
baseline = []
for i in range(len(path_filt)):
baseline.append(path_filt[i])
#
# open serial port
#
#ser = serial.Serial('/dev/ttyUSB0',9600)
ser = serial.Serial('/dev/ttyS0',9600)
#ser = serial.Serial('COM6',9600)
ser.setDTR()
#
# find framing
#
print "finding framing ..."
byte2 = 0
byte3 = 0
byte4 = 0
while 1:
byte1 = byte2
byte2 = byte3
byte3 = byte4
byte4 = ord(ser.read())
if ((byte1 == 1) & (byte2 == 2) & (byte3 == 3) & (byte4 == 4)):
print "start plotting"
break
#
# start plotting
#
root = Tk()
root.title('hello3.step.py')
root.bind('q','exit')
canvas = Canvas(root, width=WINDOW, height=WINDOW, background='white')
canvas.create_text(.5*WINDOW,.5*WINDOW,font=("Helvetica", 24),tags="voltage",fill="#000000")
canvas.create_text(.2*WINDOW,.9*WINDOW,font=("Helvetica", 24),tags="y0",fill="#0000b0")
canvas.create_text(.5*WINDOW,.9*WINDOW,font=("Helvetica", 24),tags="y1",fill="#0000b0")
canvas.create_text(.8*WINDOW,.9*WINDOW,font=("Helvetica", 24),tags="y-1",fill="#0000b0")
canvas.create_rectangle(0,.95*WINDOW,0,WINDOW, tags='x0', fill='#b00000')
canvas.pack()
ioframe = Frame(root)
Label(ioframe, text=" filter (0-1):").pack(side="left")
sfilter = StringVar()
sfilter.set(str(eps))
Entry(ioframe, width=5, textvariable=sfilter).pack(side="left")
Label(ioframe, text=" ").pack(side="left")
basebtn = Button(ioframe, text="store baseline")
basebtn.bind('',store_baseline)
basebtn.pack(side="left")
Label(ioframe, text=" ").pack(side="left")
savebtn = Button(ioframe, text="save data")
savebtn.bind('',save_data)
savebtn.pack(side="left")
Label(ioframe, text=" output file:").pack(side="left")
soutfile = StringVar()
soutfile.set("out.dat")
Entry(ioframe, width=10, textvariable=soutfile).pack(side="left")
Label(ioframe, text=" ").pack(side="left")
quitbtn = Button(ioframe, text="quit")
quitbtn.bind('','exit')
quitbtn.pack(side="left")
ioframe.pack()
root.after(1000,idle,root,canvas)
root.mainloop()