#!/usr/bin/env python
# This is a small program to demonstrate use of wxPython
# Generate sine, square, or triangle waves to Soundcard 0 or 1.
# Dual tone capability
# tone.py - Single / Dual Tone Generator
# Copyright (C) 2006-2009 Martin S. Ewing, AA6E
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import ossaudiodev
import math
import threading
import time
import wx
VERSION="0.60"
# Changes since 0.52
# update to "wx" package from old "wxPython"
# Tested on Ubuntu 9.10 (64 bit), Python 2.6.4
# Changes since version 0.5
# Switch to /dev/dspX and /dev/mixerX devices
# Improve the comments
# Insert version numbers in GUI
# Touch up for GUI layout for wxWorks 2.6.3
ID_EXIT=101
ID_ABOUT=201
ID_DUR=301
ID_START=401
ID_STOP=402
ID_TIMER=501
ID_F2ENA=601
ID_SECENA=602
ID_MASTERSLIDE=620
ID_PCMSLIDE=621
ID_RBOX=700
ID_SQR=701
ID_SIN=702
ID_TRI=703
ID_DEVBOX=710
ID_DEV0=711
ID_DEV1=712
ID_SECSCTRL=750
ID_F1CTRL=751
ID_F2CTRL=752
FMAX=4000 # arbitrary min/max freqs, consider we
FMIN=10 # use 8 kHz sampling.
F1DEFAULT=700 # "standard" 2 tone values
F2DEFAULT=1900
SMIN=1
SMAX=1000
SDEFAULT=1 # 1 pseudo-second
# Globals
flag_dual = False
flag_stop = False
flag_continuous = False
f1 = 700.
f2 = 1900.
secs = 1
idle = True
# Setup up the audio and dsp devices. The settings as given are
# OK for my 2-card system under Fedora Core 5. Other systems may use
# /dev/dsp0 and /dev/mixer0. You need to specify the second card
# devices here, even if you don't have a second card.
#
# We use /dev/dsp for audio in/out, not /dev/audio. (No mu-law)
dsp = ''
mix = ''
dspDict = { 0:'/dev/dsp' , 1:'/dev/dsp1' }
mixDict = { 0:'/dev/mixer', 1:'/dev/mixer1' }
dspList = [ ]
nDev = len(dspList)
wave = -1
waveDict = { 0:ID_SIN, 1:ID_SQR, 2:ID_TRI }
waveList = [ 'sine ', 'square', 'triangle' ]
nWaves = len(waveList)
panelcolor = "#C0C0F0"
runmessage = ""
# Condition to be used as interlock with audio thread
afCond = threading.Condition()
class MyFrame(wx.Frame):
def __init__(self, parent, ID, title):
global flag_dual, flag_continuous, f1, f2, secs, idle
global wave, waveDict, dev, dsp, mix, mixDict, dspDict
wx.Frame.__init__(self, parent, ID, title,
wx.DefaultPosition, wx.Size(420,325))
self.CreateStatusBar()
self.SetStatusText("Idle")
self.SetSizeHints(420,325,420,325) # no resizing?
# This is a basic set of menus.
menuFile = wx.Menu()
menuFile.Append(ID_EXIT, "E&xit", "Terminate Program")
menuHelp = wx.Menu()
menuHelp.Append(ID_ABOUT, "&About", "About the program")
menuBar = wx.MenuBar()
menuBar.Append(menuFile, "&File")
menuBar.Append(menuHelp, "&Help")
self.SetMenuBar(menuBar)
#Parameter Panel (radioboxes)
self.ppanel = wx.Panel(self,-1,wx.Point(10,10),wx.Size(170,110),
wx.SUNKEN_BORDER)
self.ppanel.SetBackgroundColour(panelcolor)
# Check to see if we have 2 or 1 soundcards
dspList = [ 'card 0', 'card 1' ]
try:
ossaudiodev.openmixer(mixDict[1]).close() # open & close!
except IOError:
dspList.pop() # Second dsp does not exist, show button for first only.
#Soundcard selection
wx.StaticText(self.ppanel,-1,"Soundcard",wx.Point(10,0))
self.rbdev = wx.RadioBox(self.ppanel,ID_DEVBOX,"",wx.Point(5,10),wx.DefaultSize,
dspList,1,wx.RA_SPECIFY_COLS|wx.NO_BORDER)
self.rbdev.SetSelection(0)
#Prepare for the default soundcard (0) device and mixer
dsp = dspDict[0]
mix = mixDict[0]
self.mix = ossaudiodev.openmixer(mix) # Open our mixer for gain controls
#Choose sine, square, or triangular wave function
wx.StaticText(self.ppanel,-1,"Function",wx.Point(90,0))
self.rbwave = wx.RadioBox(self.ppanel,ID_RBOX,"",wx.Point(80,10),wx.DefaultSize,
waveList,1,wx.RA_SPECIFY_COLS|wx.NO_BORDER)
self.rbwave.SetSelection(0)
wave = waveDict[0]
#Operating panel - frequencies, etc.
self.oppanel = wx.Panel(self,-1,wx.Point(190,10),wx.Size(220,110),
wx.SUNKEN_BORDER)
self.oppanel.SetBackgroundColour(panelcolor)
#Continuous tone requested? checkbox. If no, use seconds parameter.
self.secenab = wx.CheckBox(self.oppanel,ID_SECENA, "Cont.",
wx.Point(5,2),wx.Size(80,40),wx.NO_BORDER)
self.secenab.SetToolTip(wx.ToolTip("Continuous tone"))
flag_continuous = False # Default = not continuous
#Spin control for Seconds
self.secsctrl = wx.SpinCtrl(self.oppanel,ID_SECSCTRL, "", wx.Point(70, 10),
wx.Size(60, -1))
self.secsctrl.SetRange(1,100)
self.secsctrl.SetValue(1)
self.secsctrl.SetToolTip(wx.ToolTip("Tone duration, pseudo-seconds"))
wx.StaticText(self.oppanel,-1,"Seconds", wx.Point(150,10))
#Spin control for freq F1
self.f1ctrl = wx.SpinCtrl(self.oppanel,ID_F1CTRL,"",pos=wx.Point(70,40),
size=wx.Size(60,-1))
self.f1ctrl.SetRange(FMIN,FMAX)
self.f1ctrl.SetValue(F1DEFAULT)
self.f1ctrl.SetToolTip(wx.ToolTip("Freq. 1, Hz"))
wx.StaticText(self.oppanel,-1,"F1, Hz", wx.Point(150,40))
#Dual tone requested? checkbox. If yes, enable freq F2 control.
self.f2enabox = wx.CheckBox(self.oppanel,ID_F2ENA, "Dual\nTone",
wx.Point(5,62),wx.Size(80,40),wx.NO_BORDER)
self.f2enabox.SetToolTip(wx.ToolTip("Enable Tone 2"))
#Spin control for freq F2
self.f2ctrl = wx.SpinCtrl(self.oppanel,ID_F2CTRL,pos=wx.Point(70,70),
size=wx.Size(60,-1))
self.f2ctrl.SetRange(FMIN,FMAX)
self.f2ctrl.SetValue(F2DEFAULT)
self.f2ctrl.SetToolTip(wx.ToolTip("Freq. 2, Hz"))
flag_dual = False
self.f2ctrl.Enable(flag_dual) # Default to disabled F2
wx.StaticText(self.oppanel,-1,"F2, Hz", wx.Point(150,70))
#Panel for gain controls
self.gainpanel = wx.Panel(self,-1,wx.Point(10,130),wx.Size(400,90),
wx.SUNKEN_BORDER)
self.gainpanel.SetBackgroundColour(panelcolor)
#Set up VOLUME slider, if supported.
# Do we have a VOLUME control on this card? (Master gain)
flagV = self.mix.controls() & (1 << ossaudiodev.SOUND_MIXER_VOLUME)
initmaster = 0
if flagV:
initmaster = self.mix.get(ossaudiodev.SOUND_MIXER_VOLUME)[0]
self.mastergain = wx.Slider(self.gainpanel,ID_MASTERSLIDE,initmaster,0,100,
wx.Point(100,0), wx.Size(280,-1),
wx.SL_LABELS | wx.SL_HORIZONTAL | wx.SL_AUTOTICKS)
txt1 = wx.StaticText(self.gainpanel,-1,"Master Gain",wx.Point(10,20))
self.mastergain.Enable(flagV)
txt1.Enable(flagV)
#Set up PCM slider, if supported.
# Do we have a PCM control on this card?
flagP = self.mix.controls() & (1 << ossaudiodev.SOUND_MIXER_PCM)
initpcm = 0
if flagP:
initpcm = self.mix.get(ossaudiodev.SOUND_MIXER_PCM)[0]
self.pcmgain = wx.Slider(self.gainpanel,ID_PCMSLIDE,initpcm,0,100,
wx.Point(100,40), wx.Size(280,-1),
wx.SL_LABELS | wx.SL_HORIZONTAL | wx.SL_AUTOTICKS)
txt1 = wx.StaticText(self.gainpanel,-1,"PCM Gain",wx.Point(10,60))
self.pcmgain.Enable(flagP)
txt1.Enable(flagP)
#Decoration
txt1=wx.StaticText(self,-1, "wxPython", wx.Point(30, 230))
txt1.SetFont(wx.Font(18, wx.MODERN, wx.ITALIC, wx.NORMAL))
txt1.SetForegroundColour("#C0C0C0")
#Start, Stop, and Exit buttons
self.start = wx.Button(self, ID_START, "START", wx.Point(420-240,230), wx.Size(70,30))
self.start.SetBackgroundColour(panelcolor)
self.stop = wx.Button(self, ID_STOP, "STOP", wx.Point(420-160,230), wx.Size(70,30))
self.stop.SetBackgroundColour(panelcolor)
self.exit = wx.Button(self, ID_EXIT, "EXIT" , wx.Point(420-80,230), wx.Size(70,30))
self.exit.SetBackgroundColour(panelcolor)
#Start time (only for run/idle indicator)
self.timer = wx.Timer(self, ID_TIMER)
#Start thread for audio generation.
self.afThread = threading.Thread(name="Audio", target=DoAudio)
self.afThread.setDaemon(True)
self.afThread.start()
# Bind events
self.Bind(wx.EVT_SCROLL_THUMBTRACK, self.OnMasterGain, id=ID_MASTERSLIDE)
self.Bind(wx.EVT_SCROLL_THUMBTRACK, self.OnPCMGain, id=ID_PCMSLIDE)
self.Bind(wx.EVT_MENU, self.OnExit, id=ID_EXIT)
self.Bind(wx.EVT_MENU, self.OnAbout, id=ID_ABOUT)
self.Bind(wx.EVT_RADIOBOX, self.OnRbox, id=ID_RBOX)
self.Bind(wx.EVT_RADIOBOX, self.OnDevbox, id=ID_DEVBOX)
self.Bind(wx.EVT_BUTTON, self.OnStart, id=ID_START)
self.Bind(wx.EVT_BUTTON, self.OnStop, id=ID_STOP)
self.Bind(wx.EVT_BUTTON, self.OnExit, id=ID_EXIT)
self.Bind(wx.EVT_TIMER, self.OnTimer, id=ID_TIMER)
self.Bind(wx.EVT_CHECKBOX, self.OnContCheck, id=ID_SECENA)
self.Bind(wx.EVT_CHECKBOX, self.OnDualCheck, id=ID_F2ENA)
# Event handler functions
def OnMasterGain(self, event):
i = self.mastergain.GetValue()
self.mix.set(ossaudiodev.SOUND_MIXER_VOLUME, (i,i))
def OnPCMGain(self, event):
i = self.pcmgain.GetValue()
self.mix.set(ossaudiodev.SOUND_MIXER_PCM, (i,i))
def OnDevbox(self, event):
global dsp, dspDict, mix, mixDict
sel = self.rbdev.GetSelection()
dsp = dspDict[sel]
mix = mixDict[sel]
self.mix.close() # open new mixer device & get gains
self.mix = ossaudiodev.openmixer(mix)
self.mastergain.SetValue(self.mix.get(ossaudiodev.SOUND_MIXER_VOLUME)[0])
self.pcmgain.SetValue(self.mix.get(ossaudiodev.SOUND_MIXER_PCM)[0])
def OnRbox(self, event):
global wave, waveDict
wave = waveDict[self.rbwave.GetSelection()]
def OnContCheck(self, event):
global flag_continuous
if self.secenab.GetValue():
self.secsctrl.Disable()
flag_continuous = True
else:
self.secsctrl.Enable(True)
flag_continuous = False
def OnDualCheck(self, event):
global flag_dual, f1, f2, secs, idle, wave, waveDict
if self.f2enabox.GetValue():
self.f2ctrl.Enable(True)
flag_dual = True
else:
self.f2ctrl.Disable()
flag_dual = False
def OnAbout(self, event):
dlg = wx.MessageDialog(self,
"Tone Generator "+VERSION+"\nAA6E, 11/2009\nwww.aa6e.net/aa6e",
"About Tone Generator", wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
def OnExit(self, event):
self.Close()
def OnStart(self, event):
global flag_dual, flag_stop, f1, f2, secs, idle, wave, waveDict
afCond.acquire() # Sync with af thread
flag_stop = False
secs = int(self.secsctrl.GetValue())
f1 = float(self.f1ctrl.GetValue())
f2 = float(self.f2ctrl.GetValue())
self.SetStatusText("")
afCond.notify()
afCond.release() # Sync finished
self.timer.Start(200) # Launch timer at 0.2 s interval
def OnStop(self, event):
global flag_stop
flag_stop = True
def OnTimer(self, event):
if idle: self.SetStatusText("Idle")
else: self.SetStatusText(runmessage)
# Application class
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame(None, -1, "AA6E Tone Generator "+VERSION)
frame.Show(True)
self.SetTopWindow(frame)
return True
# Waveform generation functions
def f_sin(i, a, f1, f2, bl, dual):
aa = a * 32.767 # 0-100, 2**15
o = 2.0 * math.pi / float(bl)
v = aa * math.sin(f1*i*o)
if dual :
v += aa * math.sin(f2*i*o)
else:
v *= 2
return v
def f_sqr0(i, a, f, bl, dual):
aa = a* 32.767
r = (f*i/float(bl)) % 1.0
if r < 0.5: vv = aa
else: vv = -aa
return vv
def f_sqr(i, a, f1, f2, bl, dual):
v = f_sqr0(i, a, f1, bl, dual)
if dual :
v += f_sqr0(i, a, f2, bl, dual)
else:
v *= 2
return v
def f_tri0(i, a, f, bl, dual):
aa = a * 32.767
r = (f*i/float(bl)) % 1.0
if r <= 0.25: u = 4.0 * r
elif r <= 0.75: u = 1.0 - 4.0 * (r - 0.25)
else : u = -1.0 + 4.0 * (r - 0.75)
return aa * u
def f_tri(i, a, f1, f2, bl, dual):
v = f_tri0(i, a, f1, bl, dual)
if dual:
v += f_tri0(i, a, f2, bl, dual)
else:
v *= 2
return v
##############################################################
# Main Audio generation thread
# Our strategy is to force an integral number of cycles per
# buffer (32768 samples). We compute the buffer once and send
# it as many times as necessary. It might have been nicer to
# compute each buffer independently, allowing wider choice of
# frequencies.
def DoAudio():
global flag_dual, flag_stop, flag_continuous, f1, f2, secs, idle, wave, waveDict, dsp
global runmessage
speeds = { 1:8000, 2:11025, 3:22050, 4:44100, 5:96000 }
# In this implementation, speed is fixed at 44.1 kHz
# Note that high sample rate minimizes distortion, but low
# sample rate minimizes frequency quantization (step size).
speed= speeds[4]
while True: # MAIN LOOP FOR AF THREAD
# Synchronize
# Wait for command from GUI
afCond.acquire()
idle = True # tell the world we're waiting
afCond.wait() # until notified by GUI thread
myFlag = flag_dual # get my own versions of parameters
mydsp = dsp # audio device string
myf1 = f1
myf2 = f2
mysecs = secs # Our "second" is 32768/44100 sec.
mywave = wave
amp = 50.
idle = False # tell the world we are working
afCond.release() # let go until next time
# GUI has launched us.
afout = ossaudiodev.open(mydsp,'w')
afout.setparameters(ossaudiodev.AFMT_S16_LE,1,speed)
buflen = afout.bufsize()
#Compute buffer
fmin = float(speed)/float(buflen) # 1 cycle/buffer
ff1= round(myf1 / fmin) # cycles / buffer
ff2= round(myf2 / fmin)
factual = ff1* fmin # actual freq to be generated
factual2= ff2* fmin
runmessage = "Running: F1 = %.2f Hz" % factual
if myFlag:
runmessage = runmessage + ", F2 = %.2f Hz" % factual2
data = ''
aa = 32767.0 * (amp /100.0) # assumes 16 bit signed data
oo = 2.0 * math.pi / float(buflen)
for iy in range(buflen):
if mywave == ID_SIN:
vv = f_sin(iy, amp, ff1, ff2, buflen, myFlag)
elif mywave == ID_SQR:
vv = f_sqr(iy, amp, ff1, ff2, buflen, myFlag)
else: # ID_TRI
vv = f_tri(iy, amp, ff1, ff2, buflen, myFlag)
val = int(vv)
c1 = chr(val & 0xff)
c2 = chr( (val & 0xff00) >> 8 )
data = data + c1 + c2
# Send buffer as requested.
if flag_continuous:
while not flag_stop:
afout.write(data)
else:
for ix in range(mysecs):
if not flag_stop: # allow GUI to interrupt us
afout.write(data)
while afout.obufcount() > 0:
time.sleep(0.01) # loop until buffer empty
afout.close() # Close audio channel
# Application starts here.
app = MyApp(0)
app.MainLoop()