From www.AA6E.net
Jump to: navigation, search
#!/bin/env python

# File: p100.py
# Version: 0.10
#
# Z100 software emulator
# See www.cliftonlaboratories.com about the Z100.
# Copyright (c) 2008 Martin Ewing
#
# 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 2
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  
# 02110-1301, USA.
#
# Contact ewing @@ alum.mit.edu or c/o San Pasqual Consulting, 28
# Wood Road, Branford CT 06405, USA.

# Requires wxPython and numpy packages
# Uses ossaudiodev package, which is only available for Linux or FreeBSD.

# P100 is a re-envisioning of K8ZOA's Z100 tuning indicator as a Python 
# program running under Linux. (It was developed with Python 2.5.1
# and Fedora 8.)  It is a simple audio spectrum analyzer with a LED-like 
# display.  Unlike the Z100, which uses a hard clipped audio signal,
# P100 works with 16-bit sound samples, processed with an FFT.  This
# results in better performance when the signal of interest is not strong.
# Two display modes are provided - linear and log.  A number of discrete
# center frequencies are available from 400 to 1000 Hz.  "LED resolution"
# is fixed at 28.7 Hz, which is convenient to calculate with a 256-point
# transform. To keep the CPU load low (~ 4% on an Athlon XP 2000+) and to
# provide reasonable real-time responsiveness, we only look at roughly 
# 14% of the available data.  This program started as a sort of toy,
# but it might actually be useful for CW or RTTY operations.

#  -- Martin Ewing, 1/28/08

IDENT = 'p100.py v 0.10'

BACKGROUND_COLOR =          '#ffffee'
FOREGROUND_COLOR =          '#000066'
FOREGROUND_COLOR_FAINT =    '#aaaadd'

import ossaudiodev as oss
# I am using my second soundcard, but you might want '/dev/dsp' here.
AUDIO_DEVICE = '/dev/dsp1'
AF_FORMAT = oss.AFMT_S16_LE	# signed 16 bit little endian
REDUCE = 6              # Boxcar integration factor on incoming audio samples
AF_SAMPRATE = 44100		# samples per second (8000 | 11025 | 44100)
                        # Faster sampling can minimize buffering delays.
CHANNELS = 1            # 1 = mono; 2 = stereo

# Z100 parameters
R=      [255,0,0]       # nominal LED color choices - for wx.Colour
Y=      [239,239,0]
G=      [0,255,0]
NLEDS=  24
WLED=   10      # width of one LED
HLED=   20      # height of one LED

import wx, sys, operator, math, random, time
import numpy
import numpy.fft as fft
import array

# Some code based (with thanks) on examples from Rappin & Dunn, "wxPython 
# in Action", Manning Publications

# This would be the square of the hypotenuse...
def power(z):
    return z.real*z.real + z.imag*z.imag

class RigAudioIn(object):
    """
    Provides input access to an OSS audio device.
    """
    def connect(self,afdev):
        self.af = oss.open(afdev, 'r')
        self.parms = self.af.setparameters( AF_FORMAT, CHANNELS, AF_SAMPRATE, True )

    def disconnect(self):
        self.af.reset()
        self.af.close()

# Get an asynchronous chunk of audio, for scope display or debugging.
# Return a list of signed ints, of length 'size'.
    def get_chunk(self, size):    # close/open to get fresh data, size = samples
        string = self.af.read(REDUCE*2*size)    # 2 bytes per sample
        # Convert string to list of signed ints.
        xx = []
        for i in range(REDUCE*size):
            q = ord(string[2*i+1])<<8 | ord(string[2*i]) 
            if q & 1<<15: q = q - int(1<<16)
            xx.append(q)
        if REDUCE > 1:
            #reduce by REDUCE
            xr = []
            for i in range(size):
                v = 0
                for j in range(REDUCE):
                    v += xx[REDUCE*i + j]
                xr.append( v / REDUCE )
            return xr
        else:           # no reduction
            return xx

    def discard(self,size):     # Throw away REDUCE * size samples
        self.af.read(REDUCE*2*size)

class BarGraphPanel(wx.Panel):          #includes the bar graph and space for text
    """
    Sets up a panel to hold the "LED" bargraph display, along with
    some text notations. The bargraph itself is in a subpanel.
    """
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, size=(255,120),style=wx.NO_BORDER)
        self.SetBackgroundColour(BACKGROUND_COLOR)
        self.bg = BarGraphPanelInset(self, pos=(0,14), size=(255,95))
        lf = wx.StaticText(self, label="-340", pos=(0,0))
        lf.SetFont(wx.SMALL_FONT)
        rf = wx.StaticText(self, label="+340", pos=(208,0))
        rf.SetFont(wx.SMALL_FONT)
        w,h = self.GetClientSize()
        ctr = wx.StaticText(self, label="0", pos=(w/2-10,0))
        ctr.SetFont(wx.SMALL_FONT)
#        wx.StaticText(self, label="???", pos=(0,40))
        
class BarGraphPanelInset(wx.Panel):
    """
    Defines a panel solely for the LED bargraph.
    """
    def __init__(self, parent, pos=(-1,-1), size=(-1,-1)):
        wx.Panel.__init__(self, parent, pos=pos, size=size, style=wx.NO_BORDER)
        self.SetClientSizeWH(NLEDS*WLED,HLED)
        self.data = NLEDS * [[0,0,0]]           # wx.Colour arguments
        self.InitBuffer()
        self.Bind(wx.EVT_SIZE, self.OnSize)     # Won't be needed if fixed size
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        
    def InitBuffer(self):
        w, h = self.GetClientSize()
        self.buffer = wx.EmptyBitmap(w, h)
        dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
        self.DrawLEDS(dc)
        
    def OnSize(self, evt):
        self.InitBuffer()
        
    def OnPaint(self, evt):
        dc = wx.BufferedPaintDC(self, self.buffer)

    def SetData(self, newData):
        assert len(newData) == len(self.data)
        self.data = newData[:]
        for i in range(len(self.data)):
            self.data[i] = map(lambda x: max(0,min(255,x)), self.data[i])
        dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
        self.DrawLEDS(dc)

    def SetDataColored(self, data):     # "monochrome" input gets colored
        val3 = NLEDS*[0]                # in the style of the Z100
        for ledno in range(NLEDS):
            if ledno == 11 or ledno ==12:   # 2 central green LEDS
                val3[ledno] = map(lambda x:data[ledno]*x, G)
            elif ledno == 9 or ledno == 10 or ledno == 13 or ledno == 14: # 4 yellows
                val3[ledno] = map(lambda x:data[ledno]*x, Y)
            else:
                val3[ledno] = map(lambda x:data[ledno]*x, R)    # 18 reds
        self.SetData(val3)
        
    def DrawLEDS(self, dc):
        dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
        dc.Clear()
        dw, dh = dc.GetSize()
        ddw = WLED
        dc.SetPen(wx.Pen("black", 1))
        i = 0
        for x in self.data:
            dc.SetBrush(wx.Brush(wx.Colour(x[0],x[1],x[2]),wx.SOLID))
            dc.DrawRectangle(i*ddw, 0, ddw, dh)
            i += 1
        
class StatusPanel(wx.Panel):
    """
    Provides a panel with program identification and
    a Quit button.
    """
    def __init__(self, parent):
        wx.Panel.__init__(self, parent, style=wx.NO_BORDER)
        self.SetBackgroundColour(BACKGROUND_COLOR)
        self.SetForegroundColour(FOREGROUND_COLOR)
        bx = wx.Button(self, -1, "Quit", pos=(190, 0),size=(50,30))
        self.Bind(wx.EVT_BUTTON, self.doExit, bx)
        
    def setIdent(self):
        tt = wx.StaticText(self, -1, IDENT, (10,6))
        tt.SetForegroundColour(FOREGROUND_COLOR_FAINT)
        
    def doExit(self, event):
        sys.exit()

class ParmPanel(wx.Panel):
    """
    Provides a panel for setting up operating modes: log/linear,
    center frequency, etc.
    """
    def __init__(self,parent):
        wx.Panel.__init__(self, parent, style=wx.NO_BORDER)
        self.SetBackgroundColour(BACKGROUND_COLOR)
        self.SetForegroundColour(FOREGROUND_COLOR)
        self.linlog = wx.RadioBox(self, label="resp.", pos=(10,2),
            choices=['linear','log'], style=wx.RA_SPECIFY_ROWS)
        wx.StaticText(self, label="Center Freq. (Hz)", pos=(100,2))
        cf_choices = ['400', '500', '600', '700', '800', '900', '1000']
        self.centerfreq = wx.Choice(self, pos=(110,20), choices=cf_choices)
        self.centerfreq.SetSelection(3)         # default = 700 Hz
        
class Communicate(wx.Frame):
    """
    Defines the application's single frame, sets up the timer-based
    loop, and performs the calculations.
    """
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(255, 160))
        self.SetMinSize((255,160))
        self.SetMaxSize((255,160))
        self.listener = RigAudioIn()
        self.listener.connect(AUDIO_DEVICE)
        
        panel = wx.Panel(self)
        panel.SetBackgroundColour(BACKGROUND_COLOR)
        panel.SetForegroundColour(FOREGROUND_COLOR_FAINT)
        
        self.statusPanel = StatusPanel(panel)
        self.statusPanel.setIdent()
        
        self.parmPanel = ParmPanel(panel)
        self.bgPanel = BarGraphPanel(panel)

        box = wx.StaticBox(panel,label="from AA6E.NET")
        vbox = wx.StaticBoxSizer(box, wx.VERTICAL)

        vbox.Add(self.bgPanel, 0, wx.ALL)
        vbox.Add(self.parmPanel, 0, wx.ALL)
        vbox.Add(self.statusPanel, 0, wx.ALL)
        panel.SetSizer(vbox) 
        
        self.Show(True)
        
        self.timer1 = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer1)
        self.timer1.Start(milliseconds=1, oneShot=True)
        self.count = 0  # number of timer passes

    def OnTimer(self, evt):
        self.listener.discard(1200)        # Throw away lots? (we normally block here)
        chunk = self.listener.get_chunk(256)
        led_vals = self.Calculate(chunk)
        self.bgPanel.bg.SetDataColored(led_vals)
        self.count += 1
        self.timer1.Start(milliseconds=100, oneShot=True)   # restart the timer
        
    def Calculate(self,chunk):
        a = numpy.array( map(float,chunk) )
        spec = fft.rfft(a)  # 256 xfrm, 7.35 kHz eff sample -> 28.71 Hz per bin
        pwr_spec = map(lambda x: power(x)/len(a), spec)     # wasteful...
        cf = int( self.parmPanel.centerfreq.GetStringSelection() )  # requested center freq.
        cindx = int( round(float(cf)/28.71 ))    # more or less
        led_slice = pwr_spec[cindx-NLEDS/2+1 : cindx+NLEDS/2+1]
        led_slice_max = max(led_slice)
        if self.parmPanel.linlog.GetSelection():                 # log response
            led_vals = NLEDS * [0.0]
            for i in range(len(led_slice)):
                t = max(1.0e-3, led_slice[i]/led_slice_max + 2.5)   # heuristic choice
                led_vals[i] = 0.7 * math.log(t)                     # also heuristic
        else:
            led_vals = led_slice/led_slice_max      # linear response
        return led_vals
#
# Main entry
#
print IDENT
app = wx.App()
Communicate(None, -1, 'P100')
app.MainLoop()