commit 37f36cfc864c4366ca56e4030e77115b9258bc95 Author: Martin Ewing Date: Thu May 1 13:50:42 2014 -0400 Initial commit diff --git a/fft_bench.ipy b/fft_bench.ipy new file mode 100755 index 0000000..9977295 --- /dev/null +++ b/fft_bench.ipy @@ -0,0 +1,10 @@ +#!/usr/bin/env ipython +# FFT timing benchmarks (requires ipython package) + +import math +import numpy as np + +for n in [224, 256, 257, 288, 320, 384, 448, 512, 513, 576]: + print "Size =", n, + %timeit np.fft.fft(np.random.random(n)) + diff --git a/iq.py b/iq.py new file mode 100755 index 0000000..3eeaf65 --- /dev/null +++ b/iq.py @@ -0,0 +1,719 @@ +#!/usr/bin/env python + +# Program iq.py - spectrum displays from quadrature sampled IF data. +# Copyright (C) 2013 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 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 . +# +# Contact the author by e-mail: aa6e@arrl.net +# +# Our goal is to display a zero-centered spectrum and waterfall on small +# computers, such as the BeagleBone Black or the Raspberry Pi, +# spanning up to +/- 48 kHz (96 kHz sampling) with input from audio card +# or +/- 1.024 MHz from RTL dongle. +# +# We use pyaudio, pygame, and pyrtlsdr Python libraries, which depend on +# underlying C/C++ libraries PortAudio, SDL, and rtl-sdr. +# + +# TO DO: +# Document sources of non-std modules + +import sys,time, threading, os, subprocess +import pygame as pg +import numpy as np +import iq_dsp as dsp +import iq_wf as wf +import iq_opt as options + +# Some colors in PyGame style +BLACK = ( 0, 0, 0) +WHITE = (255, 255, 255) +GREEN = ( 0, 255, 0) +BLUE = ( 0, 0, 255) +RED = (255, 0, 0) +YELLOW = (192, 192, 0) +DARK_RED = (128, 0, 0) +LITE_RED = (255, 100, 100) +BGCOLOR = (255, 230, 200) +BLUE_GRAY= (100, 100, 180) +ORANGE = (255, 150, 0) +GRAY = (192, 192, 192) +# RGBA colors - with alpha +TRANS_YELLOW = (255,255,0,150) + +# Adjust for best graticule color depending on display gamma, resolution, etc. +GRAT_COLOR = DARK_RED # Color of graticule (grid) +GRAT_COLOR_2 = WHITE # Color of graticule text +TRANS_OVERLAY = TRANS_YELLOW # for info overlay +TCOLOR2 = ORANGE # text color on info screen + +INFO_CYCLE = 8 # Display frames per help info update + +opt = options.opt # Get option object from options module + + +# print list of parameters to console. +print "identification:", opt.ident +print "source :", opt.source +print "waterfall :", opt.waterfall +print "sample rate :", opt.sample_rate +print "size :", opt.size +print "buffers :", opt.buffers +print "taking :", opt.taking +print "hamlib :", opt.hamlib +print "hamlib rigtype:", opt.hamlib_rigtype +print "hamlib device :", opt.hamlib_device +print "rtl frequency :", opt.rtl_frequency +print "rtl gain :", opt.rtl_gain +print "pulse :", opt.pulse +print "fullscreen :", opt.fullscreen +print "hamlib intvl :", opt.hamlib_interval +print "cpu load intvl:", opt.cpu_load_interval +print "wf accum. :", opt.waterfall_accumulation +print "wf palette :", opt.waterfall_palette +print "max queue dept:", opt.max_queue +print "PCM290x lagfix:", opt.lagfix +if opt.lcd4: + print "LCD4 brightnes:", opt.lcd4_brightness + +def quit_all(): + """ Quit pygames and close std outputs somewhat gracefully. + Minimize console error messages. + """ + pg.quit() + try: + sys.stdout.close() + except: + pass + try: + sys.stderr.close() + except: + pass + sys.exit() + +class LED(object): + """ Make an LED indicator surface in pygame environment. + Does not include title + """ + def __init__(self, width): + """ width = pixels width (& height) + colors = dictionary with color_values and PyGame Color specs + """ + self.surface = pg.Surface((width, width)) + self.wd2 = width/2 + #self.colors = colors + return + + def get_LED_surface(self, color): + """ Set LED surface to requested color + Return square surface ready to blit + """ + self.surface.fill(BGCOLOR) + # Always make full-size black circle with no fill. + pg.draw.circle(self.surface,BLACK,(self.wd2,self.wd2),self.wd2,2) + if color == None: + return self.surface + # Make inset filled color circle. + pg.draw.circle(self.surface,color,(self.wd2,self.wd2),self.wd2-2,0) + return self.surface + +class Graticule(object): + """ Create a pygame surface with freq / power (dB) grid + and units. + input: options, pg font, graticule height, width, line color, and text color + """ + def __init__(self, opt, font, h, w, color_l, color_t): + self.opt = opt + self.sp_max = -20 # default max value (dB) + self.sp_min = -120 # default min value + self.font = font # font to use for text + self.h = h # height of graph area + self.w = w # width + self.color_l = color_l # color for lines + self.color_t = color_t # color for text + self.surface = pg.Surface((self.w, self.h)) + return + + def make(self): + """ Make or re-make the graticule. + Returns pygame surface + """ + self.surface.fill(BLACK) + # yscale is screen units per dB + yscale = float(self.h)/(self.sp_max-self.sp_min) + # Define vertical dB scale - draw line each 10 dB. + for attn in range(self.sp_min, self.sp_max, 10): + yattn = ((attn - self.sp_min) * yscale) + 3. + yattnflip = self.h - yattn # screen y coord increases downward + # Draw a single line, dark red. + pg.draw.line(self.surface, self.color_l, (0, yattnflip), + (self.w, yattnflip)) + # Render and blit the dB value at left, just above line + self.surface.blit(self.font.render("%3d" % attn, 1, self.color_t), + (5, yattnflip-12)) + + # add unit (dB) to topmost label + ww, hh = self.font.size("%3d" % attn) + self.surface.blit(self.font.render("dB", 1, self.color_t), + (5+ww, yattnflip-12)) + + # Define freq. scale - draw vert. line at convenient intervals + frq_range = float(self.opt.sample_rate)/1000. # kHz total bandwidth + xscale = self.w/frq_range # pixels/kHz x direction + srate2 = frq_range/2 # plus or minus kHz + # Choose the best tick that will work with RTL or sound cards. + for xtick_max in [ 800, 400, 200, 100, 80, 40, 20, 10 ]: + if xtick_max < srate2: + break + ticks = [ -xtick_max, -xtick_max/2, 0, xtick_max/2, xtick_max ] + for offset in ticks: + x = offset*xscale + self.w/2 + pg.draw.line(self.surface, self.color_l, (x, 0), (x, self.h)) + fmt = "%d kHz" if offset == 0 else "%+3d" + self.surface.blit(self.font.render(fmt % offset, 1, self.color_t), + (x+2, 0)) + return self.surface + + def set_range(self, sp_min, sp_max): + """ Set desired range for vertical scale in dB, min. and max. + 0 dB is maximum theoretical response for 16 bit sampling. + Lines are always drawn at 10 dB intervals. + """ + if not sp_max > sp_min: + print "Invalid dB scale setting requested!" + quit_all() + self.sp_max = sp_max + self.sp_min = sp_min + return + +# THREAD: Hamlib, checking Rx frequency, and changing if requested. +if opt.hamlib: + import Hamlib + rigfreq_request = None + rigfreq = 7.0e6 # something reasonable to start + def updatefreq(interval, rig): + """ Read/set rig frequency via Hamlib. + Interval defines repetition time (float secs) + Return via global variable rigfreq (float kHz) + To be run as thread. + (All Hamlib I/O is done through this thread.) + """ + global rigfreq, rigfreq_request + rigfreq = float(rig.get_freq()) * 0.001 # freq in kHz + while True: # forever! + # With KX3 @ 38.4 kbs, get_freq takes 100-150 ms to complete + # If a new vfo setting is desired, we will have rigfreq_request + # set to the new frequency, otherwise = None. + if rigfreq_request: # ordering of loop speeds up freq change + if rigfreq_request != rigfreq: + rig.set_freq(rigfreq_request*1000.) + rigfreq_request = None + rigfreq = float(rig.get_freq()) * 0.001 # freq in kHz + time.sleep(interval) + +# THREAD: CPU load checking, monitoring cpu stats. +cpu_usage = [0., 0., 0.] +def cpu_load(interval): + """ Check CPU user and system time usage, along with load average. + User & system reported as fraction of wall clock time in + global variable cpu_usage. + Interval defines sleep time between checks (float secs). + To be run as thread. + """ + global cpu_usage + times_store = np.array(os.times()) + # Will return: fraction usr time, sys time, and 1-minute load average + cpu_usage = [0., 0., os.getloadavg()[0]] + while True: + time.sleep(interval) + times = np.array(os.times()) + dtimes = times - times_store # difference since last loop + usr = dtimes[0]/dtimes[4] # fraction, 0 - 1 + sys = dtimes[1]/dtimes[4] + times_store = times + cpu_usage = [usr, sys, os.getloadavg()[0]] + +# Screen setup parameters + +if opt.lcd4: # setup for directfb (non-X) graphics + SCREEN_SIZE = (480,272) # default size for the 4" LCD (480x272) + SCREEN_MODE = pg.FULLSCREEN + # If we are root, we can set up LCD4 brightness. + brightness = str(min(100, max(0, opt.lcd4_brightness))) # validated string + # Find path of script (same directory as iq.py) and append brightness value + cmd = os.path.join( os.path.split(sys.argv[0])[0], "lcd4_brightness.sh") \ + + " %s" % brightness + # (The subprocess script is a no-op if we are not root.) + subprocess.call(cmd, shell=True) # invoke shell script +else: + SCREEN_MODE = pg.FULLSCREEN if opt.fullscreen else 0 + SCREEN_SIZE = (640, 512) if opt.waterfall \ + else (640,310) # NB: graphics may not scale well +WF_LINES = 50 # How many lines to use in the waterfall + +# Initialize pygame (pg) +# We should not use pg.init(), because we don't want pg audio functions. +pg.display.init() +pg.font.init() + +# Define the main window surface +surf_main = pg.display.set_mode(SCREEN_SIZE, SCREEN_MODE) +w_main = surf_main.get_width() + +# derived parameters +w_spectra = w_main-10 # Allow a small margin, left and right +w_middle = w_spectra/2 # mid point of spectrum +x_spectra = (w_main-w_spectra) / 2.0 # x coord. of spectrum on screen + +h_2d = 2*SCREEN_SIZE[1]/3 if opt.waterfall \ + else SCREEN_SIZE[1] # height of 2d spectrum display +h_2d -= 25 # compensate for LCD4 overscan? +y_2d = 20. # y position of 2d disp. (screen top = 0) + +# NB: transform size must be <= w_spectra. I.e., need at least one +# pixel of width per data point. Otherwise, waterfall won't work, etc. +if opt.size > w_spectra: + for n in [1024, 512, 256, 128]: + if n <= w_spectra: + print "*** Size was reset from %d to %d." % (opt.size, n) + opt.size = n # Force size to be 2**k (ok, but may not be best choice) + break +chunk_size = opt.buffers * opt.size # No. samples per chunk (pyaudio callback) +chunk_time = float(chunk_size) / opt.sample_rate + +# Initialize input mode, RTL or AF +if opt.source=="rtl": # input from RTL dongle + import iq_rtl as rtl + dataIn = rtl.RTL_In(opt) +elif opt.source=='audio': # input from audio card + import iq_af as af + dataIn = af.DataInput(opt) +else: + print "unrecognized mode" + quit_all() + +myDSP = dsp.DSP(opt) # Establish DSP logic + +# Surface for the 2d spectrum +surf_2d = pg.Surface((w_spectra, h_2d)) # Initialized to black +surf_2d_graticule = pg.Surface((w_spectra, h_2d)) # to hold fixed graticule + +# define two LED widgets +led_urun = LED(10) +led_clip = LED(10) + +# Waterfall geometry +h_wf = SCREEN_SIZE[1]/3 # Height of waterfall (3d spectrum) +y_wf = y_2d + h_2d # Position just below 2d surface + +# Surface for waterfall (3d) spectrum +surf_wf = pg.Surface((w_spectra, h_wf)) + +pg.display.set_caption(opt.ident) # Title for main window + +# Establish fonts for screen text. +lgfont = pg.font.SysFont('sans', 16) +lgfont_ht = lgfont.get_linesize() # text height +medfont = pg.font.SysFont('sans', 12) +medfont_ht = medfont.get_linesize() +smfont = pg.font.SysFont('mono', 9) +smfont_ht = smfont.get_linesize() + +# Define the size of a unit pixel in the waterfall +wf_pixel_size = (w_spectra/opt.size, h_wf/WF_LINES) + +# min, max dB for wf palette +v_min = -120 # lower end (dB) +v_max = -20 # higher end +nsteps = 50 # number of distinct colors + +if opt.waterfall: + # Instantiate the waterfall and palette data + mywf = wf.Wf(opt, v_min, v_max, nsteps, wf_pixel_size) + +if opt.hamlib: + import Hamlib + # start up Hamlib rig connection + Hamlib.rig_set_debug (Hamlib.RIG_DEBUG_NONE) + rig = Hamlib.Rig(opt.hamlib_rigtype) + rig.set_conf ("rig_pathname",opt.hamlib_device) + rig.set_conf ("retry","5") + rig.open () + + # Create thread for Hamlib freq. checking. + # Helps to even out the loop timing, maybe. + hl_thread = threading.Thread(target=updatefreq, args = (opt.hamlib_interval, rig)) + hl_thread.daemon = True + hl_thread.start() + print "Hamlib thread started." +else: + print "Hamlib not requested." + +# Create thread for cpu load monitor +lm_thread = threading.Thread(target=cpu_load, args = (opt.cpu_load_interval,)) +lm_thread.daemon = True +lm_thread.start() +print "CPU monitor thread started." + +# Create graticule providing 2d graph calibration. +mygraticule = Graticule(opt, smfont, h_2d, w_spectra, GRAT_COLOR, GRAT_COLOR_2) +sp_min, sp_max = sp_min_def, sp_max_def = -120, -20 +mygraticule.set_range(sp_min, sp_max) +surf_2d_graticule = mygraticule.make() + +# Pre-formatx "static" text items to save time in real-time loop +# Useful operating parameters +parms_msg = "Fs = %d Hz; Res. = %.1f Hz;" \ + " chans = %d; width = %d px; acc = %.3f sec" % \ + (opt.sample_rate, float(opt.sample_rate)/opt.size, opt.size, w_spectra, + float(opt.size*opt.buffers)/opt.sample_rate) +wparms, hparms = medfont.size(parms_msg) +parms_matter = pg.Surface((wparms, hparms) )#, flags=pg.SRCALPHA) +parms_matter.blit(medfont.render(parms_msg, 1, TCOLOR2), (0,0)) + + +# ** MAIN PROGRAM LOOP ** + +run_flag = True # set false to suspend for help screen etc. +info_phase = 0 # > 0 --> show info overlay +info_counter = 0 +tloop = 0. +t_last_data = 0. +nframe = 0 +t_frame0 = time.time() +led_overflow_ct = 0 +print "Update interval = %.2f ms" % float(1000*chunk_time) + +while True: + + nframe += 1 # keep track of loops for possible bookkeeping + + # Each time through the main loop, we reconstruct the main screen + surf_main.fill(BGCOLOR) # Erase with background color + + # Each time through this loop, we receive an audio chunk, containing + # multiple buffers. The buffers have been transformed and the log power + # spectra from each buffer will be provided in sp_log, which will be + # plotted in the "2d" graph area. After a number of log spectra are + # displayed in the "2d" graph, a new line of the waterfall is generated. + + #surf_main.blit(top_matter, (10,10)) # static operating info + + # Line of text with receiver center freq. if available + if opt.hamlib: + msg = "%.3f kHz" % rigfreq # take current rigfreq from hamlib thread + elif opt.source=='rtl': + msg = "%.3f MHz" % (dataIn.rtl.get_center_freq()/1.e6) + if opt.hamlib or (opt.source=='rtl'): + # Center it and blit just above 2d display + ww, hh = lgfont.size(msg) + surf_main.blit(lgfont.render(msg, 1, BLACK, BGCOLOR), + (w_middle + x_spectra - ww/2, y_2d-hh)) + + # show overflow & underrun indicators (for audio, not rtl) + if opt.source=='audio': + if af.led_underrun_ct > 0: # underflow flag in af module + sled = led_urun.get_LED_surface(RED) + af.led_underrun_ct -= 1 # count down to extinguish + else: + sled = led_urun.get_LED_surface(None) #off! + msg = "Buffer underrun" + ww, hh = medfont.size(msg) + ww1 = SCREEN_SIZE[0]-ww-10 + surf_main.blit(medfont.render(msg, 1, BLACK, BGCOLOR), (ww1, y_2d-hh)) + surf_main.blit(sled, (ww1-15, y_2d-hh)) + if myDSP.led_clip_ct > 0: # overflow flag + sled = led_clip.get_LED_surface(RED) + myDSP.led_clip_ct -= 1 + else: + sled = led_clip.get_LED_surface(None) #off! + msg = "Pulse clip" + ww, hh = medfont.size(msg) + surf_main.blit(medfont.render(msg, 1, BLACK, BGCOLOR), (25, y_2d-hh)) + surf_main.blit(sled, (10, y_2d-hh)) + + if opt.source=='rtl': # Input from RTL-SDR dongle + iq_data_cmplx = dataIn.ReadSamples(chunk_size) + time.sleep(0.05) # slow down if fast PC + stats = [ 0, 0] # for now... + else: # Input from audio card + # In its separate thread, a chunk of audio data has accumulated. + # When ready, pull log power spectrum data out of queue. + while dataIn.dataqueue.qsize() < 2: + time.sleep(0.1 * chunk_time ) + my_in_data_s = dataIn.dataqueue.get(True, 2.0) # block w/timeout + dataIn.dataqueue.task_done() + + # Convert string of 16-bit I,Q samples to complex floating + iq_local = np.fromstring(my_in_data_s,dtype=np.int16).astype('float32') + re_d = np.array(iq_local[1::2]) # right input (I) + im_d = np.array(iq_local[0::2]) # left input (Q) + + # The PCM290x chip has 1 lag offset of R wrt L channel. Fix, if needed. + if opt.lagfix: + im_d = np.roll(im_d, 1) + # Get some stats (max values) to monitor gain settings, etc. + stats = [int(np.amax(re_d)), int(np.amax(im_d))] + iq_data_cmplx = np.array(re_d + im_d*1j) + + sp_log = myDSP.GetLogPowerSpectrum(iq_data_cmplx) + if opt.source=='rtl': # Boost rtl spectrum (arbitrary amount) + sp_log += 60 # RTL data were normalized to +/- 1. + + yscale = float(h_2d)/(sp_max-sp_min) # yscale is screen units per dB + # Set the 2d surface to background/graticule. + surf_2d.blit(surf_2d_graticule, (0, 0)) + + # Draw the "2d" spectrum graph + sp_scaled = ((sp_log - sp_min) * yscale) + 3. + ylist = list(sp_scaled) + ylist = [ h_2d - x for x in ylist ] # flip the y's + lylist = len(ylist) + xlist = [ x* w_spectra/lylist for x in xrange(lylist) ] + # Draw the spectrum based on our data lists. + pg.draw.lines(surf_2d, WHITE, False, zip(xlist,ylist), 1) + + # Place 2d spectrum on main surface + surf_main.blit(surf_2d, (x_spectra, y_2d)) + + if opt.waterfall: + # Calculate the new Waterfall line and blit it to main surface + nsum = opt.waterfall_accumulation # 2d spectra per wf line + mywf.calculate(sp_log, nsum, surf_wf) + surf_main.blit(surf_wf, (x_spectra, y_wf+1)) + + if info_phase > 0: + # Assemble and show semi-transparent overlay info screen + # This takes cpu time, so don't recompute it too often. (DSP & graphics + # are still running.) + info_counter = ( info_counter + 1 ) % INFO_CYCLE + if info_counter == 1: # First time through, and every INFO_CYCLE-th time thereafter. + # Some button labels to show at right of LCD4 window + # Add labels for LCD4 buttons. + place_buttons = False + if opt.lcd4 or (w_main==480): + place_buttons = True + button_names = [ " LT", " RT ", " UP", " DN", "ENT" ] + button_vloc = [ 20, 70, 120, 170, 220 ] + button_surfs = [] + for bb in button_names: + button_surfs.append(medfont.render(bb, 1, WHITE, BLACK)) + + # Help info will be placed toward top of window. + # Info comes in 4 phases (0 - 3), cycle among them with + if info_phase == 1: + lines = [ "KEYBOARD CONTROLS:", + "(R) Reset display; (Q) Quit program", + "Change upper plot dB limit: (U) increase; (u) decrease", + "Change lower plot dB limit: (L) increase; (l) decrease", + "Change WF palette upper limit: (B) increase; (b) decrease", + "Change WF palette lower limit: (D) increase; (d) decrease" ] + if opt.source=='rtl' or opt.hamlib: + lines.append("Change rcvr freq: (rt arrow) increase; (lt arrow) decrease") + lines.append(" Use SHIFT for bigger steps") + lines.append("RETURN - Cycle to next Help screen") + elif info_phase == 2: + lines = [ "SPECTRUM ADJUSTMENTS:", + "UP - upper screen level +10 dB", + "DOWN - upper screen level -10 dB", + "RIGHT - lower screen level +10 dB", + "LEFT - lower screen level -10 dB", + "RETURN - Cycle to next Help screen" ] + elif info_phase == 3: + lines = [ "WATERFALL PALETTE ADJUSTMENTS:", + "UP - upper threshold INCREASE", + "DOWN - upper threshold DECREASE", + "RIGHT - lower threshold INCREASE", + "LEFT - lower threshold DECREASE", + "RETURN - Cycle Help screen OFF" ] + else: + lines = [ "Invalid info phase!"] # we should never arrive here. + info_phase = 0 + wh = (0, 0) + for il in lines: # Find max line width, height + wh = map(max, wh, medfont.size(il)) + help_matter = pg.Surface((wh[0]+24, len(lines)*wh[1]+15) )#, flags=pg.SRCALPHA) + for ix,x in enumerate(lines): + help_matter.blit(medfont.render(x, 1, TCOLOR2), (20,ix*wh[1]+15)) + + # "Live" info is placed toward bottom of window... + # Width of this surface is a guess. (It should be computed.) + live_surface = pg.Surface((430,48), 0) + # give live sp_min, sp_max, v_min, v_max + msg = "dB scale min= %d, max= %d" % (sp_min, sp_max) + live_surface.blit(medfont.render(msg, 1, TCOLOR2), (10,0)) + if opt.waterfall: + # Palette adjustments info + msg = "WF palette min= %d, max= %d" % (v_min, v_max) + live_surface.blit(medfont.render(msg, 1, TCOLOR2), (200, 0)) + live_surface.blit(parms_matter, (10,16)) + if opt.source=='audio': + msg = "ADC max I:%05d; Q:%05d" % (stats[0], stats[1]) + live_surface.blit(medfont.render(msg, 1, TCOLOR2), (10, 32)) + # Show the live cpu load information from cpu_usage thread. + msg = "Load usr=%3.2f; sys=%3.2f; load avg=%.2f" % \ + (cpu_usage[0], cpu_usage[1], cpu_usage[2]) + live_surface.blit(medfont.render(msg, 1, TCOLOR2), (200, 32)) + # Blit newly formatted -- or old -- screen to main surface. + if place_buttons: # Do we have rt hand buttons to place? + for ix, bb in enumerate(button_surfs): + surf_main.blit(bb, (449, button_vloc[ix])) + surf_main.blit(help_matter, (20,20)) + surf_main.blit(live_surface,(20,SCREEN_SIZE[1]-60)) + + # Check for pygame events - keyboard, etc. + for event in pg.event.get(): + if event.type == pg.QUIT: + quit_all() + elif event.type == pg.KEYDOWN: + if info_phase <= 1: # Normal operation (0) or help phase 1 (1) + # We usually want left or right shift treated the same! + shifted = event.mod & (pg.KMOD_LSHIFT | pg.KMOD_RSHIFT) + if event.key == pg.K_q: + quit_all() + elif event.key == pg.K_u: # 'u' or 'U' - chg upper dB + if shifted: # 'U' move up + if sp_max < 0: + sp_max += 10 + else: # 'u' move dn + if sp_max > -130 and sp_max > sp_min + 10: + sp_max -= 10 + mygraticule.set_range(sp_min, sp_max) + surf_2d_graticule = mygraticule.make() + elif event.key == pg.K_l: # 'l' or 'L' - chg lower dB + if shifted: # 'L' move up lower dB + if sp_min < sp_max -10: + sp_min += 10 + else: # 'l' move down lower dB + if sp_min > -140: + sp_min -= 10 + mygraticule.set_range(sp_min, sp_max) + surf_2d_graticule = mygraticule.make() + elif event.key == pg.K_b: # 'b' or 'B' - chg upper pal. + if shifted: + if v_max < -10: + v_max += 10 + else: + if v_max > v_min + 20: + v_max -= 10 + mywf.set_range(v_min,v_max) + elif event.key == pg.K_d: # 'd' or 'D' - chg lower pal. + if shifted: + if v_min < v_max - 20: + v_min += 10 + else: + if v_min > -130: + v_min -= 10 + mywf.set_range(v_min,v_max) + elif event.key == pg.K_r: # 'r' or 'R' = reset levels + sp_min, sp_max = sp_min_def, sp_max_def + mygraticule.set_range(sp_min, sp_max) + surf_2d_graticule = mygraticule.make() + if opt.waterfall: + v_min, v_max = mywf.reset_range() + + # Note that LCD peripheral buttons are Right, Left, Up, Down arrows + # and "Enter". (Same as keyboard buttons) + + elif event.key == pg.K_RIGHT: # right arrow + freq + if opt.source=='rtl': + finc = 100e3 if shifted else 10e3 + dataIn.rtl.center_freq = dataIn.rtl.get_center_freq()+finc + else: # audio mode + if opt.hamlib: + finc = 1.0 if shifted else 0.1 + rigfreq_request = rigfreq + finc + else: + print "Rt arrow ignored, no Hamlib" + elif event.key == pg.K_LEFT: # left arrow - freq + if opt.source=='rtl': + finc = -100e3 if shifted else -10e3 + dataIn.rtl.center_freq = dataIn.rtl.get_center_freq()+finc + else: # audio mode + if opt.hamlib: + finc = -1.0 if shifted else -0.1 + rigfreq_request = rigfreq + finc + else: + print "Lt arrow ignored, no Hamlib" + elif event.key == pg.K_UP: + print "Up" + elif event.key == pg.K_DOWN: + print "Down" + elif event.key == pg.K_RETURN: + info_phase += 1 # Jump to phase 1 or phase 2 overlay + info_counter = 0 # (next time) + + # We can have an alternate set of keyboard (LCD button) responses for each + # "phase" of the on-screen help system. + + elif info_phase == 2: # Listen for info phase 2 keys + # Showing 2d spectrum gain/offset adjustments + # Note: making graticule is moderately slow. + # Do not repeat range changes too quickly! + if event.key == pg.K_UP: + if sp_max < 0: + sp_max += 10 + mygraticule.set_range(sp_min, sp_max) + surf_2d_graticule = mygraticule.make() + elif event.key == pg.K_DOWN: + if sp_max > -130 and sp_max > sp_min + 10: + sp_max -= 10 + mygraticule.set_range(sp_min, sp_max) + surf_2d_graticule = mygraticule.make() + elif event.key == pg.K_RIGHT: + if sp_min < sp_max -10: + sp_min += 10 + mygraticule.set_range(sp_min, sp_max) + surf_2d_graticule = mygraticule.make() + elif event.key == pg.K_LEFT: + if sp_min > -140: + sp_min -= 10 + mygraticule.set_range(sp_min, sp_max) + surf_2d_graticule = mygraticule.make() + elif event.key == pg.K_RETURN: + info_phase = 3 if opt.waterfall \ + else 0 # Next is phase 3 unless no WF. + info_counter = 0 + + elif info_phase == 3: # Listen for info phase 3 keys + # Showing waterfall pallette adjustments + # Note: recalculating palette is quite slow. + # Do not repeat range changes too quickly! (1 per second max?) + if event.key == pg.K_UP: + if v_max < -10: + v_max += 10 + mywf.set_range(v_min,v_max) + elif event.key == pg.K_DOWN: + if v_max > v_min + 20: + v_max -= 10 + mywf.set_range(v_min,v_max) + elif event.key == pg.K_RIGHT: + if v_min < v_max - 20: + v_min += 10 + mywf.set_range(v_min,v_max) + elif event.key == pg.K_LEFT: + if v_min > -130: + v_min -= 10 + mywf.set_range(v_min,v_max) + elif event.key == pg.K_RETURN: + info_phase = 0 # Turn OFF overlay + info_counter = 0 + + # Finally, update display for user + pg.display.update() + + # End of main loop + +# END OF IQ.PY diff --git a/iq.sh b/iq.sh new file mode 100755 index 0000000..1e42e81 --- /dev/null +++ b/iq.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# start iq on BeagleBone Black with SB1240 sound card (typical) + +nice -20 ./iq/iq.py -i 1 --hamlib -z 256 -b 14 --waterfall + diff --git a/iq_af.py b/iq_af.py new file mode 100755 index 0000000..e0bd7d2 --- /dev/null +++ b/iq_af.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +# Program iq_af.py - manage I/Q audio from soundcard using pyaudio +# Copyright (C) 2013 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 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 . +# +# Contact the author by e-mail: aa6e@arrl.net +# +# Part of the iq.py program. +# + +import sys, time, threading +import Queue +import pyaudio as pa + +# Global variables (in this module's namespace!) +# globals are required to communicate with callback thread. +led_underrun_ct = 0 # buffer underrun LED +cbcount = 0 +cbqueue = None # will be queue to transmit af data + +# CALLBACK ROUTINE +# pyaudio callback routine is called when in_data buffer is ready. +# See pyaudio and portaudio documentation for details. +# Callback may not be called at a uniform rate. +def pa_callback_iqin(in_data, f_c, time_info, status): + global cbcount, cbqueue + global led_underrun_ct + + cbcount += 1 + if status == pa.paAbort: + led_underrun_ct = 1 # signal LED "underrun" + try: + cbqueue.put_nowait(in_data) # send to queue for iq main to pick up + except Queue.Full: + print "ERROR: Internal queue is filled. Reconfigure to use less CPU." + print "\n\n (Ignore remaining errors!)" + sys.exit() + return (None, pa.paContinue) # Return to pyaudio. All OK. +# END OF CALLBACK ROUTINE + +class DataInput(object): + """ Set up audio input, optionally using callback mode. + """ + def __init__(self, opt=None): + global cbqueue + + self.opt = opt # command line options, as parsed. + + # Initialize pyaudio (A python mapping of PortAudio) + # Consult pyaudio documentation. + self.audio = pa.PyAudio() # generates lots of warnings. + print + # set up stereo / 48K IQ input channel. Stream will be started. + if self.opt.index < 0: # Find pyaudio's idea of default index + defdevinfo = self.audio.get_default_input_device_info() + print "Default device index is %d; id='%s'"% (defdevinfo['index'], defdevinfo['name']) + af_using_index = defdevinfo['index'] + else: + af_using_index = opt.index # Use user's choice of index + devinfo = self.audio.get_device_info_by_index(af_using_index) + print "Using device index %d; id='%s'" % (devinfo['index'], devinfo['name']) + try: + # Verify this is a supported mode. + support = self.audio.is_format_supported( + input_format=pa.paInt16, # 16 bit samples + input_channels=2, # 2 channels + rate=self.opt.sample_rate, # typ. 48000 + input_device=af_using_index) # maybe the default device? + except ValueError as e: + print "ERROR self.audio.is_format_supported", e + sys.exit() + print "Requested audio mode is supported:", support + self.afiqstream = self.audio.open( + format=pa.paInt16, # 16 bit samples + channels=2, # 2 channels + rate=self.opt.sample_rate, # typ. 48000 + frames_per_buffer= self.opt.buffers*opt.size, + input_device_index=af_using_index, # maybe the default device + input=True, # being used for input, not output + stream_callback=pa_callback_iqin ) + + self.dataqueue = Queue.Queue(opt.max_queue) # needs to be "big enough" + cbqueue = self.dataqueue + return + + def Start(self): # Start pyaudio stream + self.afiqstream.start_stream() + return + + def Stop(self): # Stop pyaudio stream + self.afiqstream.stop_stream() + return + + def Terminate(self): # Stop and release all resources + self.afiqstream.stop_stream() + self.afiqstream.close() + self.audio.terminate() + +if __name__ == '__main__': + print 'debug' # Insert module test code below + diff --git a/iq_dsp.py b/iq_dsp.py new file mode 100755 index 0000000..b72567b --- /dev/null +++ b/iq_dsp.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# Program iq_dsp.py - Compute spectrum from I/Q data. +# Copyright (C) 2013 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 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 . +# +# Contact the author by e-mail: aa6e@arrl.net +# +# Part of the iq.py program. + +import math, time +import numpy as np +import numpy.fft as fft + +class DSP(object): + def __init__(self, opt): + self.opt = opt + self.stats = list() + # This is dB output for full scale 16bit input = max signal. + self.db_adjust = 20. * math.log10(self.opt.size * 2**15) + self.rejected_count = 0 + self.led_clip_ct = 0 + # Use "Hanning" window function + self.w = np.empty(self.opt.size) + for i in range(self.opt.size): + self.w[i] = 0.5 * (1. - math.cos((2*math.pi*i)/(self.opt.size-1))) + return + + def GetLogPowerSpectrum(self, data): + size = self.opt.size # size of FFT in I,Q samples. + power_spectrum = np.zeros(size) + if self.opt.taking > 0: + nbuf_taking = min(self.opt.taking, self.opt.buffers) # if need to shuck load + else: + nbuf_taking = self.opt.buffers # faster systems + + # Time-domain analysis: Often we have long normal signals interrupted + # by huge wide-band pulses that degrade our power spectrum average. + # We find the "normal" signal level, by computing the median of the + # absolute value. We only do this for the first buffer of a chunk, + # using the median for the remaining buffers in the chunk. + # A "noise pulse" is a signal level greater than some threshold + # times the median. When such a pulse is found, we skip the current + # buffer. It would be better to blank out just the pulse, but that + # would be more costly in CPU time. + + # Find the median abs value of first buffer to use for this chunk. + td_median = np.median(np.abs(data[:size])) + # Calculate our current threshold relative to measured median. + td_threshold = self.opt.pulse * td_median + nbuf_taken = 0 # Actual number of buffers accumulated + for ic in range(nbuf_taking): + td_segment = data[ic*size:(ic+1)*size] + td_max = np.amax(np.abs(td_segment)) # Do we have a noise pulse? + if td_max < td_threshold: # No, get pwr spectrum etc. + # EXPERIMENTAL TAPER + td_segment *= self.w + fd_spectrum = fft.fft(td_segment) + # Frequency-domain: + # Rotate array to place 0 freq. in center. (It was at left.) + fd_spectrum_rot = np.fft.fftshift(fd_spectrum) + # Compute the real-valued squared magnitude (ie power) and + # accumulate into pwr_acc. + # fastest way to sum |z|**2 ?? + nbuf_taken += 1 + power_spectrum = power_spectrum + \ + np.real(fd_spectrum_rot*fd_spectrum_rot.conj()) + else: # Yes, abort buffer. + self.rejected_count += 1 + self.led_clip_ct = 1 # flash a red light + #if DEBUG: print "REJECT! %d" % self.rejected_count + if nbuf_taken > 0: + power_spectrum = power_spectrum / nbuf_taken # normalize the sum. + else: + power_spectrum = np.ones(size) # if no good buffers! + # Convert to dB. Note log(0) = "-inf" in Numpy. It can happen if ADC + # isn't working right. Numpy issues a warning. + log_power_spectrum = 10. * np.log10(power_spectrum) + return log_power_spectrum - self.db_adjust # max poss. signal = 0 dB + diff --git a/iq_opt.py b/iq_opt.py new file mode 100755 index 0000000..eaef7a0 --- /dev/null +++ b/iq_opt.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +# Program iq_opt.py - Handle program options and command line parameters. +# Copyright (C) 2013 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 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 . +# +# Contact the author by e-mail: aa6e@arrl.net +# +# Part of the iq.py program. + +import optparse + +# This module gets command-line options from the invocation of the main program, +# iq.py. + +# Set up command line parser. (Use iq.py --help to see a formatted qlisting.) +op = optparse.OptionParser() + +# Boolean options / modes. +op.add_option("--FULLSCREEN", action="store_true", dest="fullscreen", + help="Switch to full screen display.") +op.add_option("--HAMLIB", action="store_true", dest="hamlib", + help="use Hamlib to monitor/control rig frequency.") +op.add_option("--LAGFIX", action="store_true", dest="lagfix", + help="Special mode to fix PCM290x R/L offset.") +op.add_option("--LCD4", action="store_true", dest="lcd4", + help='Use 4" LCD instead of large screen') +op.add_option("--RPI", action="store_true", dest="device_rpi", + help="Set up some defaults for Raspberry Pi") +op.add_option("--RTL", action="store_true", dest="source_rtl", + help="Set source to RTL-SDR") +op.add_option("--WATERFALL", action="store_true", dest="waterfall", + help="Use waterfall display.") + +# Options with a parameter. +op.add_option("--cpu_load_intvl", action="store", type="float", dest="cpu_load_interval", + help="Seconds delay between CPU load calculations") +op.add_option("--rate", action="store", type="int", dest="sample_rate", + help="sample rate (Hz), eg 48000, 96000, or 1024000 or 2048000 (for rtl)") +op.add_option("--hamlib_device", action="store", type="string", dest="hamlib_device", + help="Hamlib serial port. Default /dev/ttyUSB0.") +op.add_option("--hamlib_intvl", action="store", type="float", dest="hamlib_interval", + help="Seconds delay between Hamlib operations") +op.add_option("--hamlib_rig", action="store", type="int", dest="hamlib_rigtype", + help="Hamlib rig type (int). Run 'rigctl --list' for possibilities. Default " + "is 229 (Elecraft K3/KX3).") +op.add_option("--index", action="store", type="int", dest="index", + help="index of audio input card. Use pa.py to examine choices. Index -1 " \ + "selects default input device.") +op.add_option("--lcd4_brightness", action="store", type="int", dest="lcd4_brightness", + help="LCD4 display brightness 0 - 100") +op.add_option("--max_queue", action="store", type="int", dest="max_queue", + help="Real-time queue depth") +op.add_option("--n_buffers", action="store", type="int", dest="buffers", + help="Number of FFT buffers in 'chunk', default 12") +op.add_option("--pulse_clip", action="store", type="int", dest="pulse", + help="pulse clipping threshold, default 10.") +op.add_option("--rtl_freq", action="store", type="float", dest="rtl_frequency", + help="Initial RTL operating frequency (float kHz)") +op.add_option("--rtl_gain", action="store", type="int", dest="rtl_gain", + help="RTL_SDR gain, default 0.") +op.add_option("--size", action="store", type="int", dest="size", + help="size of FFT. Default is 512.") +op.add_option("--take", action="store", type="int", dest="taking", + help="No. of buffers to take per chunk, must be <= buffers.") +op.add_option("--waterfall_acc", action="store", type="int", dest="waterfall_accumulation", + help="No. of spectra per waterfall line") +op.add_option("--waterfall_palette", action="store", type="int", dest="waterfall_palette", + help="Waterfall color palette (1 or 2)") + +# The following are the default values which are used if not specified in the +# command line. You may want to edit them to be close to your normal operating needs. +op.set_defaults( + buffers = 12, # no. buffers in sample chunk (RPi-40) + cpu_load_interval = 3.0, # cycle time for CPU monitor thread + device = None, # Possibly "BBB" or "RPI" (set up appropriately) + fullscreen = False, # Use full screen mode? (if not LCD4) + hamlib = False, # Using Hamlib? T/F (RPi-False) + hamlib_device = "/dev/ttyUSB0", # Device address for Hamlib I/O + hamlib_interval = 1.0, # Wait between hamlib freq. checks (secs) + hamlib_rigtype = 229, # Elecraft K3/KX3. + index = -1, # index of audio device (-1 use default) + lagfix = False, # Fix up PCM 290x bug + lcd4 = False, # default large screen + lcd4_brightness = 75, # brightness 0 - 100 + max_queue = 30, # max depth of queue from audio callback + pulse = 10, # pulse clip threshold + rtl_frequency = 146.e6, # RTL center freq. Hz + rtl_gain = 0, # auto + sample_rate = 48000, # (stereo) frames/second (Hz) (RTL up to 2048000) + size = 384, # size of FFT --> freq. resolution (RPi-256) + source_rtl = False, # Use sound card, not RTL-SDR input + taking = -1, # 0 < taking < buffers to cut cpu load, -1=all + waterfall = False, # Using waterfall? T/F + waterfall_accumulation = 4, # No. of spectra per waterfall line + waterfall_palette = 2 # choose a waterfall color scheme + ) + +opt, args = op.parse_args() + +# This is an "option" that the user can't change. +opt.ident = "IQ.PY v. 0.30 de AA6E" + +# --RTL option forces source=rtl, but normally source=audio +opt.source = "rtl" if opt.source_rtl else "audio" + +if opt.device_rpi: + # adjust to comfortable settings for Raspberry Pi + opt.buffers = 15 + opt.taking = 4 # reduce CPU load (to 4/15 of max.) + opt.size = 256 + +# Main module will use: options.opt to pick up this 'opt' instance. + +if __name__ == '__main__': + print 'debug' + # Print the variables in opt. Opt is a weird thing, not a dictionary. + #print dir(opt) + for x in dir(opt): + if x[0] != "_" and x.find("read_") < 0 and x != "ensure_value": + y = eval("opt."+x) + print x, "=", y, type(y) + diff --git a/iq_rtl.py b/iq_rtl.py new file mode 100755 index 0000000..4f85fb4 --- /dev/null +++ b/iq_rtl.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +# Program iq_rtl.py - Manage input from RTL_SDR dongle. +# Copyright (C) 2013 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 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 . +# +# Contact the author by e-mail: aa6e@arrl.net +# +# Part of the iq.py program. + +import rtlsdr + +class RTL_In(object): + def __init__(self, opt): + self.opt = opt + self.rtl = rtlsdr.RtlSdr() + # Set up rtl-sdr dongle with options from command line. + self.rtl.sample_rate = opt.sample_rate + self.rtl.center_freq = opt.rtl_frequency + self.rtl.set_gain(opt.rtl_gain) + return + + def ReadSamples(self,size): + return self.rtl.read_samples(size) + +if __name__ == '__main__': + print "Debug" + diff --git a/iq_wf.py b/iq_wf.py new file mode 100755 index 0000000..fe84a80 --- /dev/null +++ b/iq_wf.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +# Program iq_wf.py - Create waterfall spectrum display. +# Copyright (C) 2013 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 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 . +# +# Contact the author by e-mail: aa6e@arrl.net +# +# Part of the iq.py program. + +import pygame as pg +import numpy as np +import math, sys + +def palette_color(palette, val, vmin0, vmax0): + """ translate a data value into a color according to several different + methods. (PALETTE variable) + input: value of data, minimum value, maximum value for transform + return: pygame color tuple + """ + f = (float(val) - vmin0) / (vmax0 - vmin0) # btw 0 and 1.0 + f *= 2 + f = min(1., max(0., f)) + if palette == 1: + g, b = 0, 0 + if f < 0.333: + r = int(f*255*3) + elif f < 0.666: + r = 200 + g = int((f-.333)*255*3) + else: + r = 200 + g = 200 + b = int((f-.666)*255*3) + elif palette == 2: + bright = min (1.0, f + 0.15) + tpi = 2 * math.pi + r = bright * 128 *(1.0 + math.cos(tpi*f)) + g = bright * 128 *(1.0 + math.cos(tpi*f + tpi/3)) + b = bright * 128 *(1.0 + math.cos(tpi*f + 2*tpi/3)) + else: + print "Invalid palette requested!" + sys.exit() + return ( max(0,min(255,r)), max(0,min(255,g)), max(0,min(255,b)) ) + +class Wf(object): + """ Make a waterfall '3d' display of spectral power vs frequency & time. + init: min, max palette parameter, no. of steps between min & max, + size for each freq,time data plot 'pixel' (a box) + """ + def __init__(self, opt, vmin, vmax, nsteps, pxsz): + """ Initialize data and + pre-calculate palette & filled rect surfaces, based on vmin, vmax, + no. of surfaces = nsteps + """ + self.opt = opt + self.vmin = vmin + self.vmin_rst = vmin + self.vmax = vmax + self.vmax_rst = vmax + self.nsteps = nsteps + self.pixel_size = pxsz + self.firstcalc = True + self.initialize_palette() + + def initialize_palette(self): + """ Set up surfaces for each possible color value in list self.pixels. + """ + self.pixels = list() + for istep in range(self.nsteps): + ps = pg.Surface(self.pixel_size) + val = float(istep)*(self.vmax-self.vmin)/self.nsteps + self.vmin + color = palette_color(self.opt.waterfall_palette, val, self.vmin, self.vmax) + ps.fill( color ) + self.pixels.append(ps) + + def set_range(self, vmin, vmax): + """ define a new data range for palette calculation going forward. + input: vmin, vmax + """ + self.vmin = vmin + self.vmax = vmax + self.initialize_palette() + + def reset_range(self): + """ reset palette data range to original settings. + """ + self.vmin = self.vmin_rst + self.vmax = self.vmax_rst + self.initialize_palette() + return self.vmin, self.vmax + + def calculate(self, datalist, nsum, surface): # (datalist is np.array) + if self.firstcalc: # First time through, + self.datasize = len(datalist) # pick up dimension of datalist + self.wfacc = np.zeros(self.datasize) # and establish accumulator + self.dx = float(surface.get_width()) / self.datasize # x spacing of wf cells + # Note: self.dx must be >= 1 + self.wfcount = 0 + self.firstcalc = False + self.wfcount += 1 + self.wfacc += datalist # Accumulate data + if self.wfcount % nsum != 0: # Don't plot wf data until enough spectra accumulated + return + else: + surface.blit(surface, (0, self.pixel_size[1])) # push old wf down one row + for ix in xrange(self.datasize): + v = datalist[ix] #self.wfacc[ix] / nsum #datalist[ix] # dB units + vi = int( self.nsteps * (v-self.vmin) / (self.vmax-self.vmin) ) + vi = max(0, min(vi, self.nsteps-1) ) + px_surf = self.pixels[vi] + x = int(ix * self.dx) + surface.blit(px_surf, (x, 0)) + self.wfcount = 0 # Initialize counter + self.wfacc.fill(0) # and accumulator diff --git a/lcd4_brightness.sh b/lcd4_brightness.sh new file mode 100755 index 0000000..88d23af --- /dev/null +++ b/lcd4_brightness.sh @@ -0,0 +1,9 @@ +#! /bin/bash +# Set LCD4 brightness 0-100 from command line. +# Insist on being root +if [[ $EUID -ne 0 ]]; then + echo "Warning: can't adjust brightness - we are not root." 2>&1 + exit 1 +fi +echo $1 > /sys/class/backlight/backlight.11/brightness + diff --git a/pa.py b/pa.py new file mode 100755 index 0000000..45c40a1 --- /dev/null +++ b/pa.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +# File: pa.py +# This program prints out your system's audio input configuration as seen +# by pyaudio (PortAudio). + +import pyaudio as pa + +print """First, you will receive a number of ALSA warnings about unknown PCM cards, etc. +This is an annoying but harmless feature of PortAudio.""" +print +print "-------------------------" +x = pa.PyAudio() +print "-------------------------" +print +print "API'S FOUND (TYPICALLY ALSA and OSS):" +for i in range(x.get_host_api_count()): + print "API %d:" % i + print x.get_host_api_info_by_index(i) +print +print "DEFAULT HOST API INFO:", x.get_default_host_api_info()['name'] +print +print "DEVICE COUNT =", x.get_device_count() +print +print "ALL DEVICE INFO: (For iq.py, choose one of these as 'index'.)" +print +for i in range(x.get_device_count()): + di = x.get_device_info_by_index(i) + print "DEVICE: %d; NAME: '%s'" % (i, di['name']) + for j in ['defaultSampleRate', 'maxInputChannels', 'maxOutputChannels']: + print j, ":", di[j] + print +print "DEFAULT INPUT DEVICE FULL INFO:" +ddi = x.get_default_input_device_info() +print ddi +print +print "DEFAULT INDEX =", ddi['index'] +