From www.AA6E.net
Jump to: navigation, search
#!/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()