Created
February 22, 2011 16:44
-
-
Save csete/838950 to your computer and use it in GitHub Desktop.
Modified Quisk 3.5.1 to follow desktop theme instead of ugly custom colors
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/python | |
# All QUISK software is Copyright (C) 2006-2010 by James C. Ahlstrom. | |
# This free software is licensed for use under the GNU General Public | |
# License (GPL), see http://www.opensource.org. | |
# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! | |
"""The main program for Quisk, a software defined radio. | |
Usage: python quisk.py [-c | --config config_file_path] | |
This can also be installed as a package and run as quisk.main(). | |
""" | |
# Change to the directory of quisk.py. This is necessary to import Quisk packages | |
# and to load other extension modules that link against _quisk.so. It also helps to | |
# find ./__init__.py and ./help.html. | |
import sys, os | |
os.chdir(os.path.normpath(os.path.dirname(__file__))) | |
if sys.path[0] != "'.'": # Make sure the current working directory is on path | |
sys.path.insert(0, '.') | |
import wx, wx.html, wx.lib.buttons, wx.lib.stattext, wx.lib.colourdb | |
import math, cmath, time, traceback | |
import threading, pickle, webbrowser | |
import _quisk as QS | |
from types import * | |
# Command line parsing: be able to specify the config file. | |
from optparse import OptionParser | |
parser = OptionParser() | |
parser.add_option('-c', '--config', dest='config_file_path', | |
help='Specify the configuration file path') | |
argv_options = parser.parse_args()[0] | |
ConfigPath = argv_options.config_file_path # Get config file path | |
if not ConfigPath: # Use default path | |
if sys.platform == 'win32': | |
path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '') | |
for dir in ("My Documents", "Eigene Dateien", "Documenti"): | |
ConfigPath = os.path.join(path, dir) | |
if os.path.isdir(ConfigPath): | |
break | |
else: | |
ConfigPath = os.path.join(path, "My Documents") | |
ConfigPath = os.path.join(ConfigPath, "quisk_conf.py") | |
if not os.path.isfile(ConfigPath): # See if the user has a config file | |
try: | |
import shutil # Try to create an initial default config file | |
shutil.copyfile('quisk_conf_win.py', ConfigPath) | |
except: | |
pass | |
else: | |
ConfigPath = os.path.expanduser('~/.quisk_conf.py') | |
# These FFT sizes have multiple small factors, and are prefered for efficiency: | |
fftPreferedSizes = (416, 448, 480, 512, 576, 640, 672, 704, 768, 800, 832, | |
864, 896, 960, 1024, 1056, 1120, 1152, 1248, 1280, 1344, 1408, 1440, 1536, | |
1568, 1600, 1664, 1728, 1760, 1792, 1920, 2016, 2048, 2080, 2112, 2240, 2304, | |
2400, 2464, 2496, 2560, 2592, 2688, 2816, 2880, 2912) | |
def round(x): # round float to nearest integer | |
if x >= 0: | |
return int(x + 0.5) | |
else: | |
return - int(-x + 0.5) | |
class Timer: | |
"""Debug: measure and print times every ptime seconds. | |
Call with msg == '' to start timer, then with a msg to record the time. | |
""" | |
def __init__(self, ptime = 1.0): | |
self.ptime = ptime # frequency to print in seconds | |
self.time0 = 0 # time zero; measure from this time | |
self.time_print = 0 # last time data was printed | |
self.timers = {} # one timer for each msg | |
self.names = [] # ordered list of msg | |
self.heading = 1 # print heading on first use | |
def __call__(self, msg): | |
tm = time.time() | |
if msg: | |
if not self.time0: # Not recording data | |
return | |
if self.timers.has_key(msg): | |
count, average, highest = self.timers[msg] | |
else: | |
self.names.append(msg) | |
count = 0 | |
average = highest = 0.0 | |
count += 1 | |
delta = tm - self.time0 | |
average += delta | |
if highest < delta: | |
highest = delta | |
self.timers[msg] = (count, average, highest) | |
if tm - self.time_print > self.ptime: # time to print results | |
self.time0 = 0 # end data recording, wait for reset | |
self.time_print = tm | |
if self.heading: | |
self.heading = 0 | |
print "count, msg, avg, max (msec)" | |
print "%4d" % count, | |
for msg in self.names: # keep names in order | |
count, average, highest = self.timers[msg] | |
if not count: | |
continue | |
average /= count | |
print " %s %7.3f %7.3f" % (msg, average * 1e3, highest * 1e3), | |
self.timers[msg] = (0, 0.0, 0.0) | |
else: # reset the time to zero | |
self.time0 = tm # Start timer | |
if not self.time_print: | |
self.time_print = tm | |
## T = Timer() # Make a timer instance | |
class SoundThread(threading.Thread): | |
"""Create a second (non-GUI) thread to read, process and play sound.""" | |
def __init__(self): | |
self.do_init = 1 | |
threading.Thread.__init__(self) | |
self.doQuit = threading.Event() | |
self.doQuit.clear() | |
def run(self): | |
"""Read, process, play sound; then notify the GUI thread to check for FFT data.""" | |
if self.do_init: # Open sound using this thread | |
self.do_init = 0 | |
QS.start_sound() | |
wx.CallAfter(application.PostStartup) | |
while not self.doQuit.isSet(): | |
QS.read_sound() | |
wx.CallAfter(application.OnReadSound) | |
QS.close_sound() | |
def stop(self): | |
"""Set a flag to indicate that the sound thread should end.""" | |
self.doQuit.set() | |
class FrequencyDisplay(wx.lib.stattext.GenStaticText): | |
"""Create a frequency display widget.""" | |
def __init__(self, frame, gbs, width, height): | |
wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3', | |
style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE) | |
border = 4 | |
for points in range(30, 6, -1): | |
font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
self.SetFont(font) | |
w, h = self.GetTextExtent('333 444 555 Hz') | |
if w < width and h < height - border * 2: | |
break | |
self.SetSizeHints(w, h, w * 5, h) | |
self.height = h | |
self.points = points | |
border = self.border = (height - self.height) / 2 | |
self.height_and_border = h + border * 2 | |
self.SetBackgroundColour(conf.color_freq) | |
gbs.Add(self, (0, 0), (1, 3), | |
flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=border) | |
def Clip(self, clip): | |
"""Change color to indicate clipping.""" | |
if clip: | |
self.SetBackgroundColour('deep pink') | |
else: | |
self.SetBackgroundColour(conf.color_freq) | |
self.Refresh() | |
def Display(self, freq): | |
"""Set the frequency to be displayed.""" | |
freq = int(freq) | |
if freq >= 0: | |
t = str(freq) | |
minus = '' | |
else: | |
t = str(-freq) | |
minus = '- ' | |
l = len(t) | |
if l > 9: | |
txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:]) | |
elif l > 6: | |
txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:]) | |
elif l > 3: | |
txt = "%s%s %s" % (minus, t[0:-3], t[-3:]) | |
else: | |
txt = minus + t | |
self.SetLabel('%s Hz' % txt) | |
class SliderBoxV(wx.BoxSizer): | |
"""A vertical box containing a slider and a text heading""" | |
# Note: A vertical wx slider has the max value at the bottom. This is | |
# reversed for this control. | |
def __init__(self, parent, text, init, themax, handler, display=False): | |
wx.BoxSizer.__init__(self, wx.VERTICAL) | |
self.slider = wx.Slider(parent, -1, init, 0, themax, style=wx.SL_VERTICAL) | |
self.slider.Bind(wx.EVT_SCROLL, handler) | |
sw, sh = self.slider.GetSize() | |
self.text = text | |
self.themax = themax | |
if display: # Display the slider value when it is thumb'd | |
self.text_ctrl = wx.StaticText(parent, -1, str(themax), style=wx.ALIGN_CENTER) | |
w1, h1 = self.text_ctrl.GetSize() # Measure size with max number | |
self.text_ctrl.SetLabel(text) | |
w2, h2 = self.text_ctrl.GetSize() # Measure size with text | |
self.width = max(w1, w2, sw) | |
self.text_ctrl.SetSizeHints(self.width, -1, self.width) | |
self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change) | |
self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone) | |
else: | |
self.text_ctrl = wx.StaticText(parent, -1, text) | |
w2, h2 = self.text_ctrl.GetSize() # Measure size with text | |
self.width = max(w2, sw) | |
self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER) | |
self.Add(self.slider, 1, wx.ALIGN_CENTER) | |
def Change(self, event): | |
event.Skip() | |
self.text_ctrl.SetLabel(str(self.themax - self.slider.GetValue())) | |
def ChangeDone(self, event): | |
event.Skip() | |
self.text_ctrl.SetLabel(self.text) | |
def GetValue(self): | |
return self.themax - self.slider.GetValue() | |
def SetValue(self, value): | |
# Set slider visual position; does not call handler | |
self.slider.SetValue(self.themax - value) | |
class _QuiskText1(wx.lib.stattext.GenStaticText): | |
# Self-drawn text for QuiskText. | |
def __init__(self, parent, size_text, height, style): | |
wx.lib.stattext.GenStaticText.__init__(self, parent, -1, '', | |
pos = wx.DefaultPosition, size = wx.DefaultSize, | |
style = wx.ST_NO_AUTORESIZE|style, | |
name = "QuiskText1") | |
self.size_text = size_text | |
self.pen = wx.Pen(conf.color_btn, 2) | |
self.brush = wx.Brush(conf.color_freq) | |
self.SetSizeHints(-1, height, -1, height) | |
def _MeasureFont(self, dc, width, height): | |
# Set decreasing point size until size_text fits in the space available | |
for points in range(20, 6, -1): | |
font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
dc.SetFont(font) | |
w, h = dc.GetTextExtent(self.size_text) | |
if w < width and h < height: | |
break | |
self.size_text = '' | |
self.SetFont(font) | |
def OnPaint(self, event): | |
dc = wx.PaintDC(self) | |
width, height = self.GetClientSize() | |
if not width or not height: | |
return | |
dc.SetPen(self.pen) | |
dc.SetBrush(self.brush) | |
dc.DrawRectangle(1, 1, width-1, height-1) | |
label = self.GetLabel() | |
if not label: | |
return | |
if self.size_text: | |
self._MeasureFont(dc, width-4, height-4) | |
else: | |
dc.SetFont(self.GetFont()) | |
if self.IsEnabled(): | |
dc.SetTextForeground(self.GetForegroundColour()) | |
else: | |
dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) | |
style = self.GetWindowStyleFlag() | |
w, h = dc.GetTextExtent(label) | |
y = (height - h) / 2 | |
if y < 0: | |
y = 0 | |
if style & wx.ALIGN_RIGHT: | |
x = width - w - 4 | |
elif style & wx.ALIGN_CENTER: | |
x = (width - w)/2 | |
else: | |
x = 4 | |
dc.DrawText(label, x, y) | |
class QuiskText(wx.BoxSizer): | |
# A one-line text display left/right/center justified and vertically centered. | |
# The height of the control is fixed as "height". The width is expanded. | |
# The font is chosen so size_text fits in the client area. | |
def __init__(self, parent, size_text, height, style=0): | |
wx.BoxSizer.__init__(self, wx.HORIZONTAL) | |
self.TextCtrl = _QuiskText1(parent, size_text, height, style) | |
self.Add(self.TextCtrl, 1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL) | |
def SetLabel(self, label): | |
self.TextCtrl.SetLabel(label) | |
# Start of our button classes. They are compatible with wxPython GenButton | |
# buttons. Use the usual methods for access: | |
# GetLabel(self), SetLabel(self, label): Get and set the label | |
# Enable(self, flag), Disable(self), IsEnabled(self): Enable / Disable | |
# GetValue(self), SetValue(self, value): Get / Set check button state True / False | |
# SetIndex(self, index): For cycle buttons, set the label from its index | |
class QuiskButtons: | |
"""Base class for special buttons.""" | |
button_bezel = 1 # size of button bezel in pixels | |
def InitButtons(self, text): | |
#self.SetBezelWidth(self.button_bezel) | |
#self.SetBackgroundColour(conf.color_btn) | |
self.SetUseFocusIndicator(False) | |
#self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
#self.SetFont(self.font) | |
if text: | |
w, h = self.GetTextExtent(text) | |
else: | |
w, h = self.GetTextExtent("OK") | |
self.Disable() # create a size for null text, but Disable() | |
w += self.button_bezel * 2 + self.GetCharWidth() | |
h = h * 17 / 10 | |
#h += self.button_bezel * 2 | |
self.SetSizeHints(w, h, w * 6, h, 1, 1) | |
def OnKeyDown(self, event): | |
pass | |
def OnKeyUp(self, event): | |
pass | |
class QuiskPushbutton(QuiskButtons, wx.lib.buttons.ThemedGenButton): | |
"""A plain push button widget.""" | |
def __init__(self, parent, command, text, use_right=False): | |
wx.lib.buttons.ThemedGenButton.__init__(self, parent, -1, text) | |
self.command = command | |
self.Bind(wx.EVT_BUTTON, self.OnButton) | |
self.InitButtons(text) | |
self.direction = 1 | |
if use_right: | |
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) | |
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) | |
def OnButton(self, event): | |
if self.command: | |
self.command(event) | |
def OnRightDown(self, event): | |
self.direction = -1 | |
self.OnLeftDown(event) | |
def OnRightUp(self, event): | |
self.OnLeftUp(event) | |
self.direction = 1 | |
class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.ThemedGenButton): | |
"""A push button that repeats when held down.""" | |
def __init__(self, parent, command, text, up_command=None, use_right=False): | |
wx.lib.buttons.GenButton.__init__(self, parent, -1, text) | |
self.command = command | |
self.up_command = up_command | |
self.timer = wx.Timer(self) | |
self.Bind(wx.EVT_TIMER, self.OnTimer) | |
self.Bind(wx.EVT_BUTTON, self.OnButton) | |
self.InitButtons(text) | |
self.repeat_state = 0 # repeater button inactive | |
self.direction = 1 | |
if use_right: | |
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) | |
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) | |
def SendCommand(self, command): | |
if command: | |
event = wx.PyEvent() | |
event.SetEventObject(self) | |
command(event) | |
def OnLeftDown(self, event): | |
if self.IsEnabled(): | |
self.shift = event.ShiftDown() | |
self.control = event.ControlDown() | |
self.SendCommand(self.command) | |
self.repeat_state = 1 # first button push | |
self.timer.Start(milliseconds=300, oneShot=True) | |
wx.lib.buttons.GenButton.OnLeftDown(self, event) | |
def OnLeftUp(self, event): | |
if self.IsEnabled(): | |
self.SendCommand(self.up_command) | |
self.repeat_state = 0 | |
self.timer.Stop() | |
wx.lib.buttons.GenButton.OnLeftUp(self, event) | |
def OnRightDown(self, event): | |
if self.IsEnabled(): | |
self.shift = event.ShiftDown() | |
self.control = event.ControlDown() | |
self.direction = -1 | |
self.OnLeftDown(event) | |
def OnRightUp(self, event): | |
if self.IsEnabled(): | |
self.OnLeftUp(event) | |
self.direction = 1 | |
def OnTimer(self, event): | |
if self.repeat_state == 1: # after first push, turn on repeats | |
self.timer.Start(milliseconds=150, oneShot=False) | |
self.repeat_state = 2 | |
if self.repeat_state: # send commands until button is released | |
self.SendCommand(self.command) | |
def OnButton(self, event): | |
pass # button command not used | |
class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.ThemedGenToggleButton): | |
"""A button that pops up and down, and changes color with each push.""" | |
# Check button; get the checked state with self.GetValue() | |
def __init__(self, parent, command, text, color=None): | |
wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text) | |
self.InitButtons(text) | |
self.Bind(wx.EVT_BUTTON, self.OnButton) | |
self.button_down = 0 # used for radio buttons | |
self.command = command | |
#if color is None: | |
# self.color = conf.color_check_btn | |
#else: | |
# self.color = color | |
def SetValue(self, value, do_cmd=False): | |
wx.lib.buttons.GenToggleButton.SetValue(self, value) | |
self.button_down = value | |
#if value: | |
# self.SetBackgroundColour(self.color) | |
#else: | |
# self.SetBackgroundColour(conf.color_btn) | |
if do_cmd and self.command: | |
event = wx.PyEvent() | |
event.SetEventObject(self) | |
self.command(event) | |
def OnButton(self, event): | |
#if self.GetValue(): | |
# self.SetBackgroundColour(self.color) | |
#else: | |
# self.SetBackgroundColour(conf.color_btn) | |
if self.command: | |
self.command(event) | |
class QuiskCycleCheckbutton(QuiskCheckbutton): | |
"""A button that cycles through its labels with each push. | |
The button is up for labels[0], down for all other labels. Change to the | |
next label for each push. If you call SetLabel(), the label must be in the list. | |
The self.index is the index of the current label. | |
""" | |
def __init__(self, parent, command, labels, color=None, is_radio=False): | |
self.labels = list(labels) # Be careful if you change this list | |
self.index = 0 # index of selected label 0, 1, ... | |
self.direction = 0 # 1 for up, -1 for down, 0 for no change to index | |
self.is_radio = is_radio # Is this a radio cycle button? | |
#if color is None: | |
# color = conf.color_cycle_btn | |
QuiskCheckbutton.__init__(self, parent, command, labels[0])#, color) | |
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) | |
def SetLabel(self, label, do_cmd=False): | |
self.index = self.labels.index(label) | |
QuiskCheckbutton.SetLabel(self, label) | |
QuiskCheckbutton.SetValue(self, self.index) | |
if do_cmd and self.command: | |
event = wx.PyEvent() | |
event.SetEventObject(self) | |
self.command(event) | |
def SetIndex(self, index, do_cmd=False): | |
self.index = index | |
QuiskCheckbutton.SetLabel(self, self.labels[index]) | |
QuiskCheckbutton.SetValue(self, index) | |
if do_cmd and self.command: | |
event = wx.PyEvent() | |
event.SetEventObject(self) | |
self.command(event) | |
def OnButton(self, event): | |
if not self.is_radio or self.button_down: | |
self.direction = 1 | |
self.index += 1 | |
if self.index >= len(self.labels): | |
self.index = 0 | |
self.SetIndex(self.index) | |
else: | |
self.direction = 0 | |
if self.command: | |
self.command(event) | |
def OnRightDown(self, event): # Move left in the list of labels | |
if not self.is_radio or self.GetValue(): | |
self.index -= 1 | |
if self.index < 0: | |
self.index = len(self.labels) - 1 | |
self.SetIndex(self.index) | |
self.direction = -1 | |
if self.command: | |
self.command(event) | |
class RadioButtonGroup: | |
"""This class encapsulates a group of radio buttons. This class is not a button! | |
The "labels" is a list of labels for the toggle buttons. An item | |
of labels can be a list/tuple, and the corresponding button will | |
be a cycle button. | |
""" | |
def __init__(self, parent, command, labels, default): | |
self.command = command | |
self.buttons = [] | |
self.button = None | |
for text in labels: | |
if type(text) in (ListType, TupleType): | |
b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True) | |
for t in text: | |
if t == default and self.button is None: | |
b.SetLabel(t) | |
self.button = b | |
else: | |
b = QuiskCheckbutton(parent, self.OnButton, text) | |
if text == default and self.button is None: | |
b.SetValue(True) | |
self.button = b | |
self.buttons.append(b) | |
def SetLabel(self, label, do_cmd=False): | |
self.button = None | |
for b in self.buttons: | |
if self.button is not None: | |
b.SetValue(False) | |
elif isinstance(b, QuiskCycleCheckbutton): | |
try: | |
index = b.labels.index(label) | |
except ValueError: | |
b.SetValue(False) | |
continue | |
else: | |
b.SetIndex(index) | |
self.button = b | |
b.SetValue(True) | |
elif b.GetLabel() == label: | |
b.SetValue(True) | |
self.button = b | |
else: | |
b.SetValue(False) | |
if do_cmd and self.command and self.button: | |
event = wx.PyEvent() | |
event.SetEventObject(self.button) | |
self.command(event) | |
def GetButtons(self): | |
return self.buttons | |
def OnButton(self, event): | |
win = event.GetEventObject() | |
for b in self.buttons: | |
if b is win: | |
self.button = b | |
b.SetValue(True) | |
else: | |
b.SetValue(False) | |
if self.command: | |
self.command(event) | |
def GetLabel(self): | |
if not self.button: | |
return None | |
return self.button.GetLabel() | |
def GetSelectedButton(self): # return the selected button | |
return self.button | |
class ConfigScreen(wx.ScrolledWindow): | |
"""Display the configuration and status screen.""" | |
def __init__(self, parent, width, fft_size): | |
wx.ScrolledWindow.__init__(self, parent, | |
pos = (0, 0), | |
size = (width, 100), | |
style = wx.VSCROLL | wx.NO_BORDER) | |
self.SetBackgroundColour(conf.color_graph) | |
self.Bind(wx.EVT_PAINT, self.OnPaint) | |
self.Bind(wx.EVT_SCROLLWIN, self.OnScroll) | |
#self.Bind(wx.EVT_SCROLLWIN_THUMBRELEASE, self.OnScrollDone) | |
self.Bind(wx.EVT_IDLE, self.OnScrollDone) | |
self.width = width | |
self.setscroll = True | |
self.rx_phase = None | |
self.fft_size = fft_size | |
self.interupts = 0 | |
self.read_error = -1 | |
self.write_error = -1 | |
self.underrun_error = -1 | |
self.fft_error = -1 | |
self.latencyCapt = -1 | |
self.latencyPlay = -1 | |
self.y_scale = 0 | |
self.y_zero = 0 | |
self.rate_min = -1 | |
self.rate_max = -1 | |
self.chan_min = -1 | |
self.chan_max = -1 | |
self.mic_max_display = 0 | |
self.err_msg = "No response" | |
self.msg1 = "" | |
self.dev_capt, self.dev_play = QS.sound_devices() | |
self.controls = [] | |
self.controls_visible = True | |
self.tabstops = [0] * 9 | |
ts = self.tabstops | |
points = 24 | |
while points > 4: | |
self.font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
self.SetFont(self.font) | |
charx = self.charx = self.GetCharWidth() | |
chary = self.chary = self.GetCharHeight() | |
ts[0] = charx | |
w, h = self.GetTextExtent("Capture errors 99999") | |
ts[1] = ts[0] + w | |
ts[2] = ts[1] + charx * 2 | |
w, h = self.GetTextExtent("Capture latency 999999") | |
ts[3] = ts[2] + w | |
ts[4] = ts[3] + charx * 2 | |
w, h = self.GetTextExtent("Playback latency 999999") | |
ts[5] = ts[4] + w | |
ts[6] = ts[5] + charx * 2 | |
w, h = self.GetTextExtent("Total latency 999999") | |
ts[7] = ts[6] + w | |
ts[8] = ts[7] + charx * 2 | |
if ts[8] < width: | |
break | |
points -= 2 | |
self.dy = chary # line spacing | |
self.mem_height = self.dy * 4 | |
self.bitmap = wx.EmptyBitmap(width, self.mem_height) | |
self.mem_rect = wx.Rect(0, 0, width, self.mem_height) | |
self.mem_dc = wx.MemoryDC(self.bitmap) | |
br = wx.Brush(conf.color_graph) | |
self.mem_dc.SetBackground(br) | |
self.mem_dc.SetFont(self.font) | |
self.mem_dc.Clear() | |
def OnPaint(self, event): | |
dc = wx.PaintDC(self) | |
dc.SetFont(self.font) | |
dc.SetTextForeground('Black') | |
x0 = self.tabstops[0] | |
x, y = self.GetViewStart() | |
self.y = -y | |
# Make and blit variable data | |
self.MakeBitmap() | |
dc.Blit(0, self.y, self.width, self.mem_height, self.mem_dc, 0, 0) | |
self.y += self.mem_height # height of bitmap | |
if conf.config_file_exists: | |
t = "Using configuration file %s" % conf.config_file_path | |
else: | |
dc.SetTextForeground('Red') | |
t = "Configuration file %s was not found" % conf.config_file_path | |
dc.DrawText(t, x0, self.y) | |
dc.SetTextForeground('Black') | |
self.y += self.dy | |
dc.DrawText(application.config_text, x0, self.y) | |
self.y += self.dy | |
if conf.name_of_sound_play: | |
t = "Play rate %d to %s." % (conf.playback_rate, conf.name_of_sound_play) | |
else: | |
t = "No playback device" | |
dc.DrawText(t, x0, self.y) | |
self.y += self.dy | |
if conf.microphone_name: | |
t = "Microphone sample rate %d from %s." % (conf.mic_sample_rate, conf.microphone_name) | |
dc.DrawText(t, x0, self.y) | |
self.y += self.dy | |
if conf.name_of_mic_play: | |
t = "Microphone playback rate %d to %s." % (conf.mic_playback_rate, conf.name_of_mic_play) | |
dc.DrawText(t, x0, self.y) | |
self.y += self.dy | |
self.y += self.dy / 2 | |
if not self.rx_phase: | |
# Make controls | |
xxx = x0 | |
self.rx_phase = ph = wx.Button(self, -1, "Rx Phase...") | |
self.Bind(wx.EVT_BUTTON, self.OnBtnPhase, ph) | |
x1, y1 = ph.GetSizeTuple() | |
ycenter = self.y + y1 / 2 | |
ph.SetPosition((x0, self.y)) | |
self.controls.append(ph) | |
xxx += x1 + self.charx * 4 | |
self.control_height = y1 | |
# Choice (combo) box for decimation | |
lst = Hardware.VarDecimGetChoices() | |
if lst: | |
txt = Hardware.VarDecimGetLabel() | |
t = wx.StaticText(self, -1, txt) | |
x1, y1 = t.GetSizeTuple() | |
t.SetPosition((xxx, ycenter - y1 / 2)) | |
self.controls.append(t) | |
xxx += x1 + self.charx * 2 | |
c = wx.Choice(self, -1, choices=lst) | |
x1, y1 = c.GetSizeTuple() | |
c.SetPosition((xxx, ycenter - y1 / 2)) | |
self.controls.append(c) | |
xxx += x1 + self.charx * 4 | |
self.Bind(wx.EVT_CHOICE, application.OnBtnDecimation, c) | |
index = Hardware.VarDecimGetIndex() | |
c.SetSelection(index) | |
self.y += self.control_height + self.dy | |
dc.DrawText("Available devices for capture:", x0, self.y) | |
self.y += self.dy | |
for name in self.dev_capt: | |
dc.DrawText(' ' + name, x0, self.y) | |
self.y += self.dy | |
dc.DrawText("Available devices for playback:", x0, self.y) | |
self.y += self.dy | |
for name in self.dev_play: | |
dc.DrawText(' ' + name, x0, self.y) | |
self.y += self.dy | |
self.y += self.dy | |
# t = "Rx Phase..." | |
# w, h = dc.GetTextExtent(t) | |
# r = wx.Rect(x0, self.y, w + 10, h + 10) | |
# dc.DrawRoundedRectangleRect(r, 4) | |
# dc.DrawLabel(t, r, wx.ALIGN_CENTER) | |
# self.y += h + 10 | |
if self.setscroll: # Set the scroll size once | |
self.setscroll = False | |
self.height = self.y | |
self.SetScrollbars(1, 1, self.width, self.height) | |
def MakeRow2(self, dc, *args): | |
for col in range(len(args)): | |
x = self.tabstops[col] | |
t = args[col] | |
if t is not None: | |
t = str(t) | |
if col % 2 == 1: | |
w, h = dc.GetTextExtent(t) | |
x -= w | |
dc.DrawText(t, x, self.mem_y) | |
self.mem_y += self.dy | |
def MakeBitmap(self): | |
self.mem_dc.Clear() | |
self.mem_y = 0 | |
self.MakeRow2(self.mem_dc, "Interrupts", self.interupts, | |
"Capture latency", self.latencyCapt, | |
"Playback latency", self.latencyPlay, | |
"Total latency", self.latencyCapt + self.latencyPlay) | |
self.MakeRow2(self.mem_dc, "Capture errors", self.read_error, | |
"Playback errors", self.write_error, | |
"Underrun errors", self.underrun_error, | |
"FFT errors", self.fft_error) | |
if conf.microphone_name: | |
level = "%3.0f" % self.mic_max_display | |
else: | |
level = "None" | |
self.MakeRow2(self.mem_dc, "Sample rate", application.sample_rate, | |
"Mic level dB", level, | |
None, None, "FFT points", self.fft_size) | |
if self.err_msg: # Error message on line 4 | |
x = self.tabstops[0] | |
self.mem_dc.SetTextForeground('Red') | |
self.mem_dc.DrawText(self.err_msg, x, self.mem_y) | |
self.mem_dc.SetTextForeground('Black') | |
self.mem_y += self.dy | |
def OnGraphData(self, data=None): | |
(self.rate_min, self.rate_max, sample_rate, self.chan_min, self.chan_max, | |
self.msg1, self.unused, self.err_msg, | |
self.read_error, self.write_error, self.underrun_error, | |
self.latencyCapt, self.latencyPlay, self.interupts, self.fft_error, self.mic_max_display, | |
self.data_poll_usec | |
) = QS.get_state() | |
self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0) | |
self.RefreshRect(self.mem_rect) | |
def ChangeYscale(self, y_scale): | |
pass | |
def ChangeYzero(self, y_zero): | |
pass | |
def OnIdle(self, event): | |
pass | |
def SetTxFreq(self, tx_freq, rx_freq): | |
pass | |
def OnBtnPhase(self, event): | |
application.screenBtnGroup.SetLabel('Graph', do_cmd=True) | |
if application.w_phase: | |
application.w_phase.Raise() | |
else: | |
application.w_phase = QAdjustPhase(self, self.width, 'rx') | |
def OnScroll(self, event): | |
# Scrolling controls within this window works poorly, so we try | |
# to hide the controls until scrolling is finished. | |
event.Skip() | |
if self.controls_visible: | |
self.controls_visible = False | |
for c in self.controls: | |
c.Hide() | |
def OnScrollDone(self, event): | |
event.Skip() | |
self.controls_visible = True | |
for c in self.controls: | |
c.Show() | |
class GraphDisplay(wx.Window): | |
"""Display the FFT graph within the graph screen.""" | |
def __init__(self, parent, x, y, graph_width, height, chary): | |
wx.Window.__init__(self, parent, | |
pos = (x, y), | |
size = (graph_width, height), | |
style = wx.NO_BORDER) | |
self.parent = parent | |
self.chary = chary | |
self.graph_width = graph_width | |
self.line = [(0, 0), (1,1)] # initial fake graph data | |
self.SetBackgroundColour(conf.color_graph) | |
self.Bind(wx.EVT_PAINT, self.OnPaint) | |
self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) | |
self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) | |
self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) | |
self.Bind(wx.EVT_MOTION, parent.OnMotion) | |
self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) | |
self.tune_tx = graph_width / 2 # Current X position of the Tx tuning line | |
self.tune_rx = 0 # Current X position of Rx tuning line or zero | |
self.scale = 20 # pixels per 10 dB | |
self.peak_hold = 9999 # time constant for holding peak value | |
self.height = 10 | |
self.y_min = 1000 | |
self.y_max = 0 | |
self.max_height = application.screen_height | |
self.tuningPenTx = wx.Pen('Red', 1) | |
self.tuningPenRx = wx.Pen('Green', 1) | |
self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) | |
self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) | |
if sys.platform == 'win32': | |
self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter) | |
def OnEnter(self, event): | |
if not application.w_phase: | |
self.SetFocus() # Set focus so we get mouse wheel events | |
def OnPaint(self, event): | |
#print 'GraphDisplay', self.GetUpdateRegion().GetBox() | |
dc = wx.PaintDC(self) | |
#dc.SetPen(wx.BLACK_PEN) | |
pen = wx.Pen(conf.color_fft, 1, wx.SOLID) | |
dc.SetPen(pen) | |
dc.DrawLines(self.line) | |
x = self.tune_tx | |
dc.SetPen(self.tuningPenTx) | |
dc.DrawLine(x, 0, x, self.max_height) | |
if self.tune_rx: | |
dc.SetPen(self.tuningPenRx) | |
dc.DrawLine(self.tune_rx, 20, self.tune_rx, self.max_height) | |
if not self.parent.in_splitter: | |
dc.SetPen(self.horizPen) | |
chary = self.chary | |
y = self.zeroDB | |
for i in range(0, -99999, -10): | |
if y >= chary / 2: | |
dc.DrawLine(0, y, self.graph_width, y) # y line | |
y = y + self.scale | |
if y > self.height: | |
break | |
def SetHeight(self, height): | |
self.height = height | |
self.SetSize((self.graph_width, height)) | |
def OnGraphData(self, data): | |
x = 0 | |
for y in data: # y is in dB, -130 to 0 | |
y = self.zeroDB - int(y * self.scale / 10.0 + 0.5) | |
try: | |
y0 = self.line[x][1] | |
except IndexError: | |
self.line.append([x, y]) | |
else: | |
if y > y0: | |
y = min(y, y0 + self.peak_hold) | |
self.line[x] = [x, y] | |
x = x + 1 | |
self.Refresh() | |
def XXOnGraphData(self, data): | |
line = [] | |
x = 0 | |
y_min = 1000 | |
y_max = 0 | |
for y in data: # y is in dB, -130 to 0 | |
y = self.zeroDB - int(y * self.scale / 10.0 + 0.5) | |
if y > y_max: | |
y_max = y | |
if y < y_min: | |
y_min = y | |
line.append((x, y)) | |
x = x + 1 | |
ymax = max(y_max, self.y_max) | |
ymin = min(y_min, self.y_min) | |
rect = wx.Rect(0, ymin, 1000, ymax - ymin) | |
self.y_min = y_min | |
self.y_max = y_max | |
self.line = line | |
self.Refresh() #rect=rect) | |
def SetTuningLine(self, tune_tx, tune_rx): | |
dc = wx.ClientDC(self) | |
dc.SetPen(self.backgroundPen) | |
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.max_height) | |
if self.tune_rx: | |
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.max_height) | |
dc.SetPen(self.tuningPenTx) | |
dc.DrawLine(tune_tx, 0, tune_tx, self.max_height) | |
if tune_rx: | |
dc.SetPen(self.tuningPenRx) | |
dc.DrawLine(tune_rx, 20, tune_rx, self.max_height) | |
self.tune_tx = tune_tx | |
self.tune_rx = tune_rx | |
class GraphScreen(wx.Window): | |
"""Display the graph screen X and Y axis, and create a graph display.""" | |
def __init__(self, parent, data_width, graph_width, in_splitter=0): | |
wx.Window.__init__(self, parent, pos = (0, 0)) | |
self.in_splitter = in_splitter # Are we in the top of a splitter window? | |
if in_splitter: | |
self.y_scale = conf.waterfall_graph_y_scale | |
self.y_zero = conf.waterfall_graph_y_zero | |
else: | |
self.y_scale = conf.graph_y_scale | |
self.y_zero = conf.graph_y_zero | |
self.VFO = 0 | |
self.WheelMod = 50 # Round frequency when using mouse wheel | |
self.txFreq = 0 | |
self.sample_rate = application.sample_rate | |
self.data_width = data_width | |
self.graph_width = graph_width | |
self.doResize = False | |
#self.pen_tick = wx.Pen("Black", 1, wx.SOLID) | |
self.pen_tick = wx.Pen(conf.color_tick, 1, wx.SOLID) | |
self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
self.SetFont(self.font) | |
w = self.GetCharWidth() * 14 / 10 | |
h = self.GetCharHeight() | |
self.charx = w | |
self.chary = h | |
self.tick = max(2, h * 3 / 10) | |
self.originX = w * 5 | |
self.offsetY = h + self.tick | |
self.width = self.originX + self.graph_width + self.tick + self.charx * 2 | |
self.height = application.screen_height * 3 / 10 | |
self.x0 = self.originX + self.graph_width / 2 # center of graph | |
self.tuningX = self.x0 | |
self.originY = 10 | |
self.zeroDB = 10 # y location of zero dB; may be above the top of the graph | |
self.scale = 10 | |
self.SetSize((self.width, self.height)) | |
self.SetSizeHints(self.width, 1, self.width) | |
self.SetBackgroundColour(conf.color_graph) | |
self.Bind(wx.EVT_SIZE, self.OnSize) | |
self.Bind(wx.EVT_PAINT, self.OnPaint) | |
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) | |
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) | |
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) | |
self.Bind(wx.EVT_MOTION, self.OnMotion) | |
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) | |
self.MakeDisplay() | |
def MakeDisplay(self): | |
self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) | |
self.display.zeroDB = self.zeroDB | |
def OnPaint(self, event): | |
dc = wx.PaintDC(self) | |
if not self.in_splitter: | |
dc.SetFont(self.font) | |
self.MakeYTicks(dc) | |
self.MakeXTicks(dc) | |
def OnIdle(self, event): | |
if self.doResize: | |
self.ResizeGraph() | |
def OnSize(self, event): | |
self.doResize = True | |
event.Skip() | |
def ResizeGraph(self): | |
"""Change the height of the graph. | |
Changing the width interactively is not allowed because the FFT size is fixed. | |
Call after changing the zero or scale to recalculate the X and Y axis marks. | |
""" | |
w, h = self.GetClientSize() | |
if self.in_splitter: # Splitter window has no X axis scale | |
self.height = h | |
self.originY = h | |
else: | |
self.height = h - self.chary # Leave space for X scale | |
self.originY = self.height - self.offsetY | |
self.MakeYScale() | |
self.display.SetHeight(self.originY) | |
self.display.scale = self.scale | |
self.doResize = False | |
self.Refresh() | |
def ChangeYscale(self, y_scale): | |
self.y_scale = y_scale | |
self.doResize = True | |
def ChangeYzero(self, y_zero): | |
self.y_zero = y_zero | |
self.doResize = True | |
def MakeYScale(self): | |
chary = self.chary | |
scale = (self.originY - chary) * 10 / (self.y_scale + 20) # Number of pixels per 10 dB | |
scale = max(1, scale) | |
q = (self.originY - chary ) / scale / 2 | |
zeroDB = chary + q * scale - self.y_zero * scale / 10 | |
if zeroDB > chary: | |
zeroDB = chary | |
self.scale = scale | |
self.zeroDB = zeroDB | |
self.display.zeroDB = self.zeroDB | |
QS.record_graph(self.originX, self.zeroDB, self.scale) | |
def MakeYTicks(self, dc): | |
chary = self.chary | |
x1 = self.originX - self.tick * 1 # left of tick mark | |
x2 = self.originX - 1 # x location of y axis | |
x3 = self.originX + self.graph_width # end of graph data | |
dc.SetPen(self.pen_tick) | |
dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis | |
y = self.zeroDB | |
for i in range(0, -99999, -10): | |
if y >= chary / 2: | |
dc.SetPen(self.pen_tick) | |
dc.DrawLine(x1, y, x2, y) # y tick | |
t = `i` | |
w, h = dc.GetTextExtent(t) | |
dc.DrawText(`i`, x1 - (w+5), y - h / 2) # y text | |
y = y + self.scale | |
if y > self.originY: | |
break | |
def MakeXTicks(self, dc): | |
originY = self.originY | |
x3 = self.originX + self.graph_width # end of fft data | |
charx , z = dc.GetTextExtent('-30000XX') | |
tick0 = self.tick | |
tick1 = tick0 * 2 | |
tick2 = tick0 * 3 | |
# Draw the X axis | |
dc.SetPen(self.pen_tick) | |
dc.DrawLine(self.originX, originY, x3, originY) | |
# Draw the band plan colors below the X axis | |
x = self.originX | |
f = float(x - self.x0) * self.sample_rate / self.data_width | |
c = None | |
y = originY + 1 | |
for freq, color in conf.BandPlan: | |
freq -= self.VFO | |
if f < freq: | |
xend = int(self.x0 + float(freq) * self.data_width / self.sample_rate + 0.5) | |
if c is not None: | |
dc.SetPen(wx.TRANSPARENT_PEN) | |
dc.SetBrush(wx.Brush(c)) | |
dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis | |
if xend >= x3: | |
break | |
x = xend | |
f = freq | |
c = color | |
stick = 1000 # small tick in Hertz | |
mtick = 5000 # medium tick | |
ltick = 10000 # large tick | |
# check the width of the frequency label versus frequency span | |
df = charx * self.sample_rate / self.data_width | |
if df < 5000: | |
tfreq = 5000 # tick frequency for labels | |
elif df < 10000: | |
tfreq = 10000 | |
elif df < 20000: | |
tfreq = 20000 | |
elif df < 50000: | |
tfreq = 50000 | |
stick = 5000 | |
mtick = 10000 | |
ltick = 50000 | |
else: | |
tfreq = 100000 | |
stick = 5000 | |
mtick = 10000 | |
ltick = 50000 | |
# Draw the X axis ticks and frequency in kHz | |
dc.SetPen(self.pen_tick) | |
freq1 = self.VFO - self.sample_rate / 2 | |
freq1 = (freq1 / stick) * stick | |
freq2 = freq1 + self.sample_rate + stick + 1 | |
y_end = 0 | |
for f in range (freq1, freq2, stick): | |
x = self.x0 + int(float(f - self.VFO) / self.sample_rate * self.data_width) | |
if self.originX <= x <= x3: | |
if f % ltick is 0: # large tick | |
dc.DrawLine(x, originY, x, originY + tick2) | |
elif f % mtick is 0: # medium tick | |
dc.DrawLine(x, originY, x, originY + tick1) | |
else: # small tick | |
dc.DrawLine(x, originY, x, originY + tick0) | |
if f % tfreq is 0: # place frequency label | |
t = str(f/1000) | |
w, h = dc.GetTextExtent(t) | |
dc.DrawText(t, x - w / 2, originY + tick2) | |
y_end = originY + tick2 + h | |
if y_end: # mark the center of the display | |
dc.DrawLine(self.x0, y_end, self.x0, application.screen_height) | |
def OnGraphData(self, data): | |
i1 = (self.data_width - self.graph_width) / 2 | |
i2 = i1 + self.graph_width | |
self.display.OnGraphData(data[i1:i2]) | |
def SetVFO(self, vfo): | |
self.VFO = vfo | |
self.doResize = True | |
def SetTxFreq(self, tx_freq, rx_freq): | |
self.txFreq = tx_freq | |
tx_x = self.x0 + int(float(tx_freq) / self.sample_rate * self.data_width) | |
self.tuningX = tx_x | |
rx_x = self.x0 + int(float(rx_freq) / self.sample_rate * self.data_width) | |
if abs(tx_x - rx_x) < 2: # Do not display Rx line for small frequency offset | |
self.display.SetTuningLine(tx_x - self.originX, 0) | |
else: | |
self.display.SetTuningLine(tx_x - self.originX, rx_x - self.originX) | |
def GetMousePosition(self, event): | |
"""For mouse clicks in our display, translate to our screen coordinates.""" | |
mouse_x, mouse_y = event.GetPositionTuple() | |
win = event.GetEventObject() | |
if win is not self: | |
x, y = win.GetPositionTuple() | |
mouse_x += x | |
mouse_y += y | |
return mouse_x, mouse_y | |
def OnRightDown(self, event): | |
mouse_x, mouse_y = self.GetMousePosition(event) | |
freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width | |
freq = int(freq) | |
if self.VFO > 0: | |
vfo = self.VFO + freq | |
vfo = (vfo + 5000) / 10000 * 10000 # round to even number | |
tune = freq + self.VFO - vfo | |
self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event) | |
def OnLeftDown(self, event): | |
mouse_x, mouse_y = self.GetMousePosition(event) | |
self.mouse_x = mouse_x | |
x = mouse_x - self.originX | |
if self.display.tune_rx and abs(x - self.display.tune_tx) > abs(x - self.display.tune_rx): | |
self.mouse_is_rx = True | |
else: | |
self.mouse_is_rx = False | |
if mouse_y < self.originY: # click above X axis | |
freq = float(mouse_x - self.x0) * self.sample_rate / self.data_width | |
freq = int(freq) | |
if self.mouse_is_rx: | |
application.rxFreq = freq | |
application.screen.SetTxFreq(self.txFreq, freq) | |
QS.set_tune(freq + application.ritFreq, self.txFreq) | |
else: | |
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event) | |
self.CaptureMouse() | |
def OnLeftUp(self, event): | |
if self.HasCapture(): | |
self.ReleaseMouse() | |
def OnMotion(self, event): | |
if event.Dragging() and event.LeftIsDown(): | |
mouse_x, mouse_y = self.GetMousePosition(event) | |
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency | |
x = (mouse_x - self.mouse_x) # Thanks to VK6JBL | |
self.mouse_x = mouse_x | |
freq = x * self.sample_rate / self.data_width | |
freq = int(freq) | |
self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event) | |
else: # Mouse motion changes the tuning frequency | |
# Frequency changes more rapidly for higher mouse Y position | |
speed = max(10, self.originY - mouse_y) / float(self.originY) | |
x = (mouse_x - self.mouse_x) | |
self.mouse_x = mouse_x | |
freq = speed * x * self.sample_rate / self.data_width | |
freq = int(freq) | |
if self.mouse_is_rx: # Mouse motion changes the receive frequency | |
application.rxFreq += freq | |
application.screen.SetTxFreq(self.txFreq, application.rxFreq) | |
QS.set_tune(application.rxFreq + application.ritFreq, self.txFreq) | |
else: # Mouse motion changes the transmit frequency | |
self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event) | |
def OnWheel(self, event): | |
wm = self.WheelMod # Round frequency when using mouse wheel | |
mouse_x, mouse_y = self.GetMousePosition(event) | |
x = mouse_x - self.originX | |
if self.display.tune_rx and abs(x - self.display.tune_tx) > abs(x - self.display.tune_rx): | |
tune = application.rxFreq + wm * event.GetWheelRotation() / event.GetWheelDelta() | |
if tune >= 0: | |
tune = tune / wm * wm | |
else: # tune can be negative when the VFO is zero | |
tune = - (- tune / wm * wm) | |
application.rxFreq = tune | |
application.screen.SetTxFreq(self.txFreq, tune) | |
QS.set_tune(tune + application.ritFreq, self.txFreq) | |
else: | |
tune = self.txFreq + wm * event.GetWheelRotation() / event.GetWheelDelta() | |
if tune >= 0: | |
tune = tune / wm * wm | |
else: # tune can be negative when the VFO is zero | |
tune = - (- tune / wm * wm) | |
self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event) | |
def ChangeHwFrequency(self, tune, vfo, source, event): | |
application.ChangeHwFrequency(tune, vfo, source, event) | |
def PeakHold(self, name): | |
if name == 'GraphP1': | |
self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_1) | |
elif name == 'GraphP2': | |
self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_2) | |
else: | |
self.display.peak_hold = 9999 | |
if self.display.peak_hold < 1: | |
self.display.peak_hold = 1 | |
class WaterfallDisplay(wx.Window): | |
"""Create a waterfall display within the waterfall screen.""" | |
def __init__(self, parent, x, y, graph_width, height, margin): | |
wx.Window.__init__(self, parent, | |
pos = (x, y), | |
size = (graph_width, height), | |
style = wx.NO_BORDER) | |
self.parent = parent | |
self.graph_width = graph_width | |
self.margin = margin | |
self.height = 10 | |
self.sample_rate = application.sample_rate | |
self.SetBackgroundColour('Black') | |
self.Bind(wx.EVT_PAINT, self.OnPaint) | |
self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) | |
self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) | |
self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) | |
self.Bind(wx.EVT_MOTION, parent.OnMotion) | |
self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) | |
self.tune_tx = graph_width / 2 # Current X position of the Tx tuning line | |
self.tune_rx = 0 # Current X position of Rx tuning line or zero | |
self.tuningPen = wx.Pen('White', 3) | |
self.marginPen = wx.Pen(conf.color_graph, 1) | |
# Size of top faster scroll region is (top_key + 2) * (top_key - 1) / 2 | |
self.top_key = 8 | |
self.top_size = (self.top_key + 2) * (self.top_key - 1) / 2 | |
# Make the palette | |
pal2 = conf.waterfallPalette | |
red = [] | |
green = [] | |
blue = [] | |
n = 0 | |
for i in range(256): | |
if i > pal2[n+1][0]: | |
n = n + 1 | |
red.append((i - pal2[n][0]) * | |
(long)(pal2[n+1][1] - pal2[n][1]) / | |
(long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][1]) | |
green.append((i - pal2[n][0]) * | |
(long)(pal2[n+1][2] - pal2[n][2]) / | |
(long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][2]) | |
blue.append((i - pal2[n][0]) * | |
(long)(pal2[n+1][3] - pal2[n][3]) / | |
(long)(pal2[n+1][0] - pal2[n][0]) + pal2[n][3]) | |
self.red = red | |
self.green = green | |
self.blue = blue | |
bmp = wx.EmptyBitmap(0, 0) | |
bmp.x_origin = 0 | |
self.bitmaps = [bmp] * application.screen_height | |
if sys.platform == 'win32': | |
self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter) | |
def OnEnter(self, event): | |
if not application.w_phase: | |
self.SetFocus() # Set focus so we get mouse wheel events | |
def OnPaint(self, event): | |
dc = wx.BufferedPaintDC(self) | |
dc.SetBackground(wx.Brush('Black')) | |
dc.Clear() | |
y = 0 | |
dc.SetPen(self.marginPen) | |
x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) | |
for i in range(0, self.margin): | |
dc.DrawLine(0, y, self.graph_width, y) | |
y += 1 | |
index = 0 | |
if conf.waterfall_scroll_mode: # Draw the first few lines multiple times | |
for i in range(self.top_key, 1, -1): | |
b = self.bitmaps[index] | |
x = b.x_origin - x_origin | |
for j in range(0, i): | |
dc.DrawBitmap(b, x, y) | |
y += 1 | |
index += 1 | |
while y < self.height: | |
b = self.bitmaps[index] | |
x = b.x_origin - x_origin | |
dc.DrawBitmap(b, x, y) | |
y += 1 | |
index += 1 | |
dc.SetPen(self.tuningPen) | |
dc.SetLogicalFunction(wx.XOR) | |
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height) | |
if self.tune_rx: | |
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height) | |
def SetHeight(self, height): | |
self.height = height | |
self.SetSize((self.graph_width, height)) | |
def OnGraphData(self, data, y_zero, y_scale): | |
#T('graph start') | |
row = '' # Make a new row of pixels for a one-line image | |
for x in data: # x is -130 to 0, or so (dB) | |
l = int((x + y_zero / 3 + 100) * y_scale / 10) | |
l = max(l, 0) | |
l = min(l, 255) | |
row = row + "%c%c%c" % (chr(self.red[l]), chr(self.green[l]), chr(self.blue[l])) | |
#T('graph string') | |
bmp = wx.BitmapFromBuffer(len(row) / 3, 1, row) | |
bmp.x_origin = int(float(self.VFO) / self.sample_rate * self.data_width + 0.5) | |
self.bitmaps.insert(0, bmp) | |
del self.bitmaps[-1] | |
#self.ScrollWindow(0, 1, None) | |
#self.Refresh(False, (0, 0, self.graph_width, self.top_size + self.margin)) | |
self.Refresh(False) | |
#T('graph end') | |
def SetTuningLine(self, tune_tx, tune_rx): | |
dc = wx.ClientDC(self) | |
dc.SetPen(self.tuningPen) | |
dc.SetLogicalFunction(wx.XOR) | |
dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height) | |
if self.tune_rx: | |
dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height) | |
dc.DrawLine(tune_tx, 0, tune_tx, self.height) | |
if tune_rx: | |
dc.DrawLine(tune_rx, 0, tune_rx, self.height) | |
self.tune_tx = tune_tx | |
self.tune_rx = tune_rx | |
class WaterfallScreen(wx.SplitterWindow): | |
"""Create a splitter window with a graph screen and a waterfall screen""" | |
def __init__(self, frame, width, data_width, graph_width): | |
self.y_scale = conf.waterfall_y_scale | |
self.y_zero = conf.waterfall_y_zero | |
wx.SplitterWindow.__init__(self, frame) | |
self.SetSizeHints(width, -1, width) | |
self.SetMinimumPaneSize(1) | |
self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size | |
self.pane1 = GraphScreen(self, data_width, graph_width, 1) | |
self.pane2 = WaterfallPane(self, data_width, graph_width) | |
self.SplitHorizontally(self.pane1, self.pane2, conf.waterfall_graph_size) | |
def OnIdle(self, event): | |
self.pane1.OnIdle(event) | |
self.pane2.OnIdle(event) | |
def SetTxFreq(self, tx_freq, rx_freq): | |
self.pane1.SetTxFreq(tx_freq, rx_freq) | |
self.pane2.SetTxFreq(tx_freq, rx_freq) | |
def SetVFO(self, vfo): | |
self.pane1.SetVFO(vfo) | |
self.pane2.SetVFO(vfo) | |
def ChangeYscale(self, y_scale): # Test if the shift key is down | |
if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen | |
self.pane1.ChangeYscale(y_scale) | |
else: # Set waterfall screen | |
self.y_scale = y_scale | |
self.pane2.ChangeYscale(y_scale) | |
def ChangeYzero(self, y_zero): # Test if the shift key is down | |
if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen | |
self.pane1.ChangeYzero(y_zero) | |
else: # Set waterfall screen | |
self.y_zero = y_zero | |
self.pane2.ChangeYzero(y_zero) | |
def OnGraphData(self, data): | |
self.pane1.OnGraphData(data) | |
self.pane2.OnGraphData(data) | |
class WaterfallPane(GraphScreen): | |
"""Create a waterfall screen with an X axis and a waterfall display.""" | |
def __init__(self, frame, data_width, graph_width): | |
GraphScreen.__init__(self, frame, data_width, graph_width) | |
self.y_scale = conf.waterfall_y_scale | |
self.y_zero = conf.waterfall_y_zero | |
self.oldVFO = self.VFO | |
def MakeDisplay(self): | |
self.display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) | |
self.display.VFO = self.VFO | |
self.display.data_width = self.data_width | |
def SetVFO(self, vfo): | |
GraphScreen.SetVFO(self, vfo) | |
self.display.VFO = vfo | |
if self.oldVFO != vfo: | |
self.oldVFO = vfo | |
self.Refresh() | |
def MakeYTicks(self, dc): | |
pass | |
def ChangeYscale(self, y_scale): | |
self.y_scale = y_scale | |
def ChangeYzero(self, y_zero): | |
self.y_zero = y_zero | |
def OnGraphData(self, data): | |
i1 = (self.data_width - self.graph_width) / 2 | |
i2 = i1 + self.graph_width | |
self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale) | |
class ScopeScreen(wx.Window): | |
"""Create an oscilloscope screen (mostly used for debug).""" | |
def __init__(self, parent, width, data_width, graph_width): | |
wx.Window.__init__(self, parent, pos = (0, 0), | |
size=(width, -1), style = wx.NO_BORDER) | |
self.SetBackgroundColour(conf.color_graph) | |
self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
self.SetFont(self.font) | |
self.Bind(wx.EVT_SIZE, self.OnSize) | |
self.Bind(wx.EVT_PAINT, self.OnPaint) | |
self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) | |
self.y_scale = conf.scope_y_scale | |
self.y_zero = conf.scope_y_zero | |
self.running = 1 | |
self.doResize = False | |
self.width = width | |
self.height = 100 | |
self.originY = self.height / 2 | |
self.data_width = data_width | |
self.graph_width = graph_width | |
w = self.charx = self.GetCharWidth() | |
h = self.chary = self.GetCharHeight() | |
tick = max(2, h * 3 / 10) | |
self.originX = w * 3 | |
self.width = self.originX + self.graph_width + tick + self.charx * 2 | |
self.line = [(0,0), (1,1)] # initial fake graph data | |
self.fpout = None #open("jim96.txt", "w") | |
def OnIdle(self, event): | |
if self.doResize: | |
self.ResizeGraph() | |
def OnSize(self, event): | |
self.doResize = True | |
event.Skip() | |
def ResizeGraph(self, event=None): | |
# Change the height of the graph. Changing the width interactively is not allowed. | |
w, h = self.GetClientSize() | |
self.height = h | |
self.originY = h / 2 | |
self.doResize = False | |
self.Refresh() | |
def OnPaint(self, event): | |
dc = wx.PaintDC(self) | |
dc.SetFont(self.font) | |
self.MakeYTicks(dc) | |
self.MakeXTicks(dc) | |
self.MakeText(dc) | |
dc.SetPen(wx.BLACK_PEN) | |
dc.DrawLines(self.line) | |
def MakeYTicks(self, dc): | |
chary = self.chary | |
originX = self.originX | |
x3 = self.x3 = originX + self.graph_width # end of graph data | |
dc.SetPen(wx.BLACK_PEN) | |
dc.DrawLine(originX, 0, originX, self.originY * 3) # y axis | |
# Find the size of the Y scale markings | |
themax = 2.5e9 * 10.0 ** - ((160 - self.y_scale) / 50.0) # value at top of screen | |
themax = int(themax) | |
l = [] | |
for j in (5, 6, 7, 8): | |
for i in (1, 2, 5): | |
l.append(i * 10 ** j) | |
for yvalue in l: | |
n = themax / yvalue + 1 # Number of lines | |
ypixels = self.height / n | |
if n < 20: | |
break | |
dc.SetPen(self.horizPen) | |
for i in range(1, 1000): | |
y = self.originY - ypixels * i | |
if y < chary: | |
break | |
# Above axis | |
dc.DrawLine(originX, y, x3, y) # y line | |
# Below axis | |
y = self.originY + ypixels * i | |
dc.DrawLine(originX, y, x3, y) # y line | |
self.yscale = float(ypixels) / yvalue | |
self.yvalue = yvalue | |
def MakeXTicks(self, dc): | |
originY = self.originY | |
x3 = self.x3 | |
# Draw the X axis | |
dc.SetPen(wx.BLACK_PEN) | |
dc.DrawLine(self.originX, originY, x3, originY) | |
# Find the size of the X scale markings in microseconds | |
for i in (20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000): | |
xscale = i # X scale in microseconds | |
if application.sample_rate * xscale * 0.000001 > self.width / 30: | |
break | |
# Draw the X lines | |
dc.SetPen(self.horizPen) | |
for i in range(1, 999): | |
x = int(self.originX + application.sample_rate * xscale * 0.000001 * i + 0.5) | |
if x > x3: | |
break | |
dc.DrawLine(x, 0, x, self.height) # x line | |
self.xscale = xscale | |
def MakeText(self, dc): | |
if self.running: | |
t = " RUN" | |
else: | |
t = " STOP" | |
if self.xscale >= 1000: | |
t = "%s X: %d millisec/div" % (t, self.xscale) | |
else: | |
t = "%s X: %d microsec/div" % (t, self.xscale) | |
yt = `self.yvalue` | |
t = "%s Y: %sE%d/div" % (t, yt[0], len(yt) - 1) | |
dc.DrawText(t, self.originX, self.height - self.chary) | |
def OnGraphData(self, data): | |
if not self.running: | |
if self.fpout: | |
for cpx in data: | |
re = int(cpx.real) | |
im = int(cpx.imag) | |
ab = int(abs(cpx)) | |
self.fpout.write("%12d %12d %12d\n" % (re, im, ab)) | |
return # Preserve data on screen | |
line = [] | |
x = self.originX | |
ymax = self.height | |
for cpx in data: # cpx is complex raw samples +/- 0 to 2**31-1 | |
y = cpx.real | |
#y = abs(cpx) | |
y = self.originY - int(y * self.yscale + 0.5) | |
if y > ymax: | |
y = ymax | |
elif y < 0: | |
y = 0 | |
line.append((x, y)) | |
x = x + 1 | |
self.line = line | |
self.Refresh() | |
def ChangeYscale(self, y_scale): | |
self.y_scale = y_scale | |
self.doResize = True | |
def ChangeYzero(self, y_zero): | |
self.y_zero = y_zero | |
def SetTxFreq(self, tx_freq, rx_freq): | |
pass | |
class FilterScreen(GraphScreen): | |
"""Create a graph of the receive filter response.""" | |
def __init__(self, parent, data_width, graph_width): | |
GraphScreen.__init__(self, parent, data_width, graph_width) | |
self.y_scale = conf.filter_y_scale | |
self.y_zero = conf.filter_y_zero | |
self.VFO = 0 | |
self.txFreq = 0 | |
self.data = [] | |
self.sample_rate = QS.get_filter_rate() | |
def NewFilter(self): | |
self.data = QS.get_filter() | |
def OnGraphData(self, data): | |
GraphScreen.OnGraphData(self, self.data) | |
def ChangeHwFrequency(self, tune, vfo, source, event): | |
GraphScreen.SetTxFreq(self, tune, tune) | |
application.freqDisplay.Display(tune) | |
def SetTxFreq(self, tx_freq, rx_freq): | |
pass | |
class HelpScreen(wx.html.HtmlWindow): | |
"""Create the screen for the Help button.""" | |
def __init__(self, parent, width, height): | |
wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) | |
self.y_scale = 0 | |
self.y_zero = 0 | |
if "gtk2" in wx.PlatformInfo: | |
self.SetStandardFonts() | |
self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) | |
# read in text from file help.html in the directory of this module | |
self.LoadFile('help.html') | |
def OnGraphData(self, data): | |
pass | |
def ChangeYscale(self, y_scale): | |
pass | |
def ChangeYzero(self, y_zero): | |
pass | |
def OnIdle(self, event): | |
pass | |
def SetTxFreq(self, tx_freq, rx_freq): | |
pass | |
def OnLinkClicked(self, link): | |
webbrowser.open(link.GetHref(), new=2) | |
class QMainFrame(wx.Frame): | |
"""Create the main top-level window.""" | |
def __init__(self, width, height): | |
fp = open('__init__.py') # Read in the title | |
title = fp.readline().strip()[1:] + '-fcd1' | |
fp.close() | |
wx.Frame.__init__(self, None, -1, title, wx.DefaultPosition, | |
(width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame') | |
#self.SetBackgroundColour(conf.color_bg) | |
self.Bind(wx.EVT_CLOSE, self.OnBtnClose) | |
# window icon | |
icon = wx.Icon('icon.png', wx.BITMAP_TYPE_PNG, 48, 48) | |
self.SetIcon(icon) | |
def OnBtnClose(self, event): | |
application.OnBtnClose(event) | |
self.Destroy() | |
## Note: The new amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL | |
class QAdjustPhase(wx.Frame): | |
"""Create a window with amplitude and phase adjustment controls""" | |
f_ampl = "Amplitude adjustment %.6f" | |
f_phase = "Phase adjustment degrees %.6f" | |
def __init__(self, parent, width, rx_tx): | |
wx.Frame.__init__(self, application.main_frame, -1, | |
"Adjust Sound Card Amplitude and Phase", pos=(50, 100), | |
style=wx.CAPTION) | |
self.rx_tx = rx_tx | |
panel = wx.Panel(self) | |
self.MakeControls(panel, width) | |
self.Show() | |
def MakeControls(self, panel, width): # Make controls for phase/amplitude adjustment | |
self.old_amplitude, self.old_phase = application.GetAmplPhase() | |
self.new_amplitude, self.new_phase = self.old_amplitude, self.old_phase | |
sl_max = width * 4 / 10 # maximum +/- value for slider | |
self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max | |
self.phase_scale = float(conf.rx_max_phase_correct) / sl_max | |
font = wx.Font(12, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
chary = self.GetCharHeight() | |
y = chary * 3 / 10 | |
# Print available data points | |
if conf.bandAmplPhase.has_key("panadapter"): | |
self.band = "panadapter" | |
else: | |
self.band = application.lastBand | |
app_vfo = (application.VFO + 500) / 1000 | |
ap = application.bandAmplPhase | |
if not ap.has_key(self.band): | |
ap[self.band] = {} | |
if not ap[self.band].has_key(self.rx_tx): | |
ap[self.band][self.rx_tx] = [] | |
lst = ap[self.band][self.rx_tx] | |
freq_in_list = False | |
if lst: | |
t = "Band %s: VFO" % self.band | |
for l in lst: | |
vfo = (l[0] + 500) / 1000 | |
if vfo == app_vfo: | |
freq_in_list = True | |
t = t + (" %d" % vfo) | |
else: | |
t = "Band %s: No data." % self.band | |
txt = wx.StaticText(panel, -1, t, pos=(0, y)) | |
txt.SetFont(font) | |
y += txt.GetSizeTuple()[1] | |
self.t_ampl = wx.StaticText(panel, -1, self.f_ampl % self.old_amplitude, pos=(0, y)) | |
self.t_ampl.SetFont(font) | |
y += self.t_ampl.GetSizeTuple()[1] | |
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, | |
pos=(0, y), size=(width, -1)) | |
y += self.ampl1.GetSizeTuple()[1] | |
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, | |
pos=(0, y), size=(width, -1)) | |
y += self.ampl2.GetSizeTuple()[1] | |
self.PosAmpl(self.old_amplitude) | |
self.t_phase = wx.StaticText(panel, -1, self.f_phase % self.old_phase, pos=(0, y)) | |
self.t_phase.SetFont(font) | |
y += self.t_phase.GetSizeTuple()[1] | |
self.phase1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, | |
pos=(0, y), size=(width, -1)) | |
y += self.phase1.GetSizeTuple()[1] | |
self.phase2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, | |
pos=(0, y), size=(width, -1)) | |
y += self.phase2.GetSizeTuple()[1] | |
sv = QuiskPushbutton(panel, self.OnBtnSave, 'Save %d' % app_vfo) | |
ds = QuiskPushbutton(panel, self.OnBtnDiscard, 'Destroy %d' % app_vfo) | |
cn = QuiskPushbutton(panel, self.OnBtnCancel, 'Cancel') | |
w, h = ds.GetSizeTuple() | |
sv.SetSize((w, h)) | |
cn.SetSize((w, h)) | |
y += h / 4 | |
x = (width - w * 3) / 4 | |
sv.SetPosition((x, y)) | |
ds.SetPosition((x*2 + w, y)) | |
cn.SetPosition((x*3 + w*2, y)) | |
sv.SetBackgroundColour('light blue') | |
ds.SetBackgroundColour('light blue') | |
cn.SetBackgroundColour('light blue') | |
if not freq_in_list: | |
ds.Disable() | |
y += h | |
y += h / 4 | |
self.ampl1.SetBackgroundColour('aquamarine') | |
self.ampl2.SetBackgroundColour('orange') | |
self.phase1.SetBackgroundColour('aquamarine') | |
self.phase2.SetBackgroundColour('orange') | |
self.PosPhase(self.old_phase) | |
self.SetClientSizeWH(width, y) | |
self.ampl1.Bind(wx.EVT_SCROLL, self.OnChange) | |
self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2) | |
self.phase1.Bind(wx.EVT_SCROLL, self.OnChange) | |
self.phase2.Bind(wx.EVT_SCROLL, self.OnPhase2) | |
def PosAmpl(self, ampl): # set pos1, pos2 for amplitude | |
pos2 = round(ampl / self.ampl_scale) | |
remain = ampl - pos2 * self.ampl_scale | |
pos1 = round(remain / self.ampl_scale * 50.0) | |
self.ampl1.SetValue(pos1) | |
self.ampl2.SetValue(pos2) | |
def PosPhase(self, phase): # set pos1, pos2 for phase | |
pos2 = round(phase / self.phase_scale) | |
remain = phase - pos2 * self.phase_scale | |
pos1 = round(remain / self.phase_scale * 50.0) | |
self.phase1.SetValue(pos1) | |
self.phase2.SetValue(pos2) | |
def OnChange(self, event): | |
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue() | |
if abs(ampl) < self.ampl_scale * 3.0 / 50.0: | |
ampl = 0.0 | |
self.t_ampl.SetLabel(self.f_ampl % ampl) | |
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue() | |
if abs(phase) < self.phase_scale * 3.0 / 50.0: | |
phase = 0.0 | |
self.t_phase.SetLabel(self.f_phase % phase) | |
QS.set_ampl_phase(ampl, phase) | |
self.new_amplitude, self.new_phase = ampl, phase | |
def OnAmpl2(self, event): # re-center the fine slider when the coarse slider is adjusted | |
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue() | |
self.PosAmpl(ampl) | |
self.OnChange(event) | |
def OnPhase2(self, event): # re-center the fine slider when the coarse slider is adjusted | |
phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue() | |
self.PosPhase(phase) | |
self.OnChange(event) | |
def DeleteEqual(self): # Remove entry with the same VFO | |
ap = application.bandAmplPhase | |
lst = ap[self.band][self.rx_tx] | |
vfo = (application.VFO + 500) / 1000 | |
for i in range(len(lst)-1, -1, -1): | |
if (lst[i][0] + 500) / 1000 == vfo: | |
del lst[i] | |
def OnBtnSave(self, event): | |
data = (application.VFO, application.rxFreq, self.new_amplitude, self.new_phase) | |
self.DeleteEqual() | |
ap = application.bandAmplPhase | |
lst = ap[self.band][self.rx_tx] | |
lst.append(data) | |
lst.sort() | |
application.w_phase = None | |
self.Destroy() | |
def OnBtnDiscard(self, event): | |
self.DeleteEqual() | |
self.OnBtnCancel() | |
def OnBtnCancel(self, event=None): | |
QS.set_ampl_phase(self.old_amplitude, self.old_phase) | |
application.w_phase = None | |
self.Destroy() | |
class Spacer(wx.Window): | |
"""Create a bar between the graph screen and the controls""" | |
def __init__(self, parent): | |
wx.Window.__init__(self, parent, pos = (0, 0), | |
size=(-1, 6), style = wx.NO_BORDER) | |
self.Bind(wx.EVT_PAINT, self.OnPaint) | |
r, g, b = parent.GetBackgroundColour().Get() | |
dark = (r * 7 / 10, g * 7 / 10, b * 7 / 10) | |
light = (r + (255 - r) * 5 / 10, g + (255 - g) * 5 / 10, b + (255 - b) * 5 / 10) | |
self.dark_pen = wx.Pen(dark, 1, wx.SOLID) | |
self.light_pen = wx.Pen(light, 1, wx.SOLID) | |
self.width = application.screen_width | |
def OnPaint(self, event): | |
dc = wx.PaintDC(self) | |
w = self.width | |
dc.SetPen(self.dark_pen) | |
dc.DrawLine(0, 0, w, 0) | |
dc.DrawLine(0, 1, w, 1) | |
dc.DrawLine(0, 2, w, 2) | |
dc.SetPen(self.light_pen) | |
dc.DrawLine(0, 3, w, 3) | |
dc.DrawLine(0, 4, w, 4) | |
dc.DrawLine(0, 5, w, 5) | |
class App(wx.App): | |
"""Class representing the application.""" | |
freq60 = (5330500, 5346500, 5366500, 5371500, 5403500) | |
StateNames = [ # Names of state attributes to save and restore | |
'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode', | |
'vardecim_set', | |
] | |
def __init__(self): | |
global application | |
application = self | |
self.init_path = None | |
if sys.stdout.isatty(): | |
wx.App.__init__(self, redirect=False) | |
else: | |
wx.App.__init__(self, redirect=True) | |
def QuiskText(self, *args, **kw): # Make our text control available to widget files | |
return QuiskText(*args, **kw) | |
def QuiskPushbutton(self, *args, **kw): # Make our buttons available to widget files | |
return QuiskPushbutton(*args, **kw) | |
def QuiskRepeatbutton(self, *args, **kw): | |
return QuiskRepeatbutton(*args, **kw) | |
def QuiskCheckbutton(self, *args, **kw): | |
return QuiskCheckbutton(*args, **kw) | |
def QuiskCycleCheckbutton(self, *args, **kw): | |
return QuiskCycleCheckbutton(*args, **kw) | |
def RadioButtonGroup(self, *args, **kw): | |
return RadioButtonGroup(*args, **kw) | |
def OnInit(self): | |
"""Perform most initialization of the app here (called by wxPython on startup).""" | |
wx.lib.colourdb.updateColourDB() # Add additional color names | |
global conf # conf is the module for all configuration data | |
import quisk_conf_defaults as conf | |
setattr(conf, 'config_file_path', ConfigPath) | |
if os.path.isfile(ConfigPath): # See if the user has a config file | |
setattr(conf, 'config_file_exists', True) | |
d = {} | |
d.update(conf.__dict__) # make items from conf available | |
execfile(ConfigPath, d) # execute the user's config file | |
for k, v in d.items(): # add user's config items to conf | |
if k[0] != '_': # omit items starting with '_' | |
setattr(conf, k, v) | |
else: | |
setattr(conf, 'config_file_exists', False) | |
if conf.invertSpectrum: | |
QS.invert_spectrum(1) | |
self.bandState = {} | |
self.bandState.update(conf.bandState) | |
self.bandAmplPhase = conf.bandAmplPhase | |
# Open hardware file | |
global Hardware | |
if hasattr(conf, "Hardware"): # Hardware defined in config file | |
Hardware = conf.Hardware(self, conf) | |
else: | |
Hardware = conf.quisk_hardware.Hardware(self, conf) | |
# Initialization - may be over-written by persistent state | |
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow | |
self.smeter_db_count = 0 # average the S-meter | |
self.smeter_db_sum = 0 | |
self.smeter_db = 0 | |
self.smeter_sunits = -87.0 | |
self.timer = time.time() # A seconds clock | |
self.heart_time0 = self.timer # timer to call HeartBeat at intervals | |
self.smeter_db_time0 = self.timer | |
self.smeter_sunits_time0 = self.timer | |
self.band_up_down = 0 # Are band Up/Down buttons in use? | |
self.lastBand = 'Audio' | |
self.VFO = 0 | |
self.ritFreq = 0 | |
self.txFreq = 0 # Transmit frequency as +/- sample_rate/2 | |
self.rxFreq = 0 # Receive frequency as +/- sample_rate/2 | |
self.oldRxFreq = 0 # Last value of self.rxFreq | |
self.screen = None | |
self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0 | |
self.sidetone_volume = 0.0 # Set sidetone volume, 0.0 to 1.0 | |
self.sound_error = 0 | |
self.sound_thread = None | |
self.mode = conf.default_mode | |
self.bottom_widgets = None | |
self.color_list = None | |
self.color_index = 0 | |
self.vardecim_set = None | |
self.w_phase = None | |
self.split_rxtx = False # Are we in split Rx/Tx mode? | |
dc = wx.ScreenDC() # get the screen size | |
(self.screen_width, self.screen_height) = dc.GetSizeTuple() | |
del dc | |
self.Bind(wx.EVT_IDLE, self.OnIdle) | |
self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession) | |
# Restore persistent program state | |
if conf.persistent_state: | |
self.init_path = os.path.join(os.path.dirname(ConfigPath), '.quisk_init.pkl') | |
try: | |
fp = open(self.init_path, "rb") | |
d = pickle.load(fp) | |
fp.close() | |
for k, v in d.items(): | |
if k in self.StateNames: | |
if k == 'bandState': | |
self.bandState.update(v) | |
else: | |
setattr(self, k, v) | |
except: | |
pass #traceback.print_exc() | |
for k, (vfo, tune, mode) in self.bandState.items(): # Historical: fix bad frequencies | |
try: | |
f1, f2 = conf.BandEdge[k] | |
if not f1 <= vfo + tune <= f2: | |
self.bandState[k] = conf.bandState[k] | |
except KeyError: | |
pass | |
if self.bandAmplPhase and type(self.bandAmplPhase.values()[0]) is not DictType: | |
print """Old sound card amplitude and phase corrections must be re-entered (sorry). | |
The new code supports multiple corrections per band.""" | |
self.bandAmplPhase = {} | |
if Hardware.VarDecimGetChoices(): # Hardware can change the decimation. | |
self.sample_rate = Hardware.VarDecimSet() # Get the sample rate. | |
self.vardecim_set = self.sample_rate | |
else: # Use the sample rate from the config file. | |
self.sample_rate = conf.sample_rate | |
if not hasattr(conf, 'playback_rate'): | |
if conf.use_sdriq or conf.use_rx_udp: | |
conf.playback_rate = 48000 | |
else: | |
conf.playback_rate = conf.sample_rate | |
# Find the data width from a list of prefered sizes; it is the width of returned graph data. | |
# The graph_width is the width of data_width that is displayed. | |
width = self.screen_width * conf.graph_width | |
percent = conf.display_fraction # display central fraction of total width | |
percent = int(percent * 100.0 + 0.4) | |
width = width * 100 / percent | |
for x in fftPreferedSizes: | |
if x > width: | |
self.data_width = x | |
break | |
else: | |
self.data_width = fftPreferedSizes[-1] | |
self.graph_width = self.data_width * percent / 100 | |
if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers | |
self.graph_width += 1 | |
# The FFT size times the average_count controls the graph refresh rate | |
factor = float(self.sample_rate) / conf.graph_refresh / self.data_width | |
ifactor = int(factor + 0.5) | |
if conf.fft_size_multiplier >= ifactor: # Use large FFT and average count 1 | |
fft_mult = ifactor | |
average_count = 1 | |
elif conf.fft_size_multiplier > 0: # Specified fft_size_multiplier | |
fft_mult = conf.fft_size_multiplier | |
average_count = int(factor / fft_mult + 0.5) | |
if average_count < 1: | |
average_count = 1 | |
else: # Calculate the split between fft size and average | |
if self.sample_rate <= 240000: | |
maxfft = 8000 # Maximum fft size | |
else: | |
maxfft = 15000 | |
fft1 = maxfft / self.data_width | |
if fft1 >= ifactor: | |
fft_mult = ifactor | |
average_count = 1 | |
else: | |
av1 = int(factor / fft1 + 0.5) | |
if av1 < 1: | |
av1 = 1 | |
err1 = factor / (fft1 * av1) | |
av2 = av1 + 1 | |
fft2 = int(factor / av2 + 0.5) | |
err2 = factor / (fft2 * av2) | |
if 0.9 < err1 < 1.1 or abs(1.0 - err1) <= abs(1.0 - err2): | |
fft_mult = fft1 | |
average_count = av1 | |
else: | |
fft_mult = fft2 | |
average_count = av2 | |
self.fft_size = self.data_width * fft_mult | |
# if we have custom window size in conf use that | |
if hasattr(conf, 'window_width'): | |
self.width = conf.window_width | |
else: | |
self.width = self.screen_width * 8 / 10 | |
if hasattr(conf, 'window_height'): | |
self.height = conf.window_height | |
else: | |
self.height = self.screen_height * 5 / 10 | |
self.main_frame = frame = QMainFrame(self.width, self.height) | |
self.SetTopWindow(frame) | |
# Record the basic application parameters | |
if sys.platform == 'win32': | |
h = self.main_frame.GetHandle() | |
else: | |
h = 0 | |
QS.record_app(self, conf, self.data_width, self.fft_size, | |
average_count, self.sample_rate, h) | |
#print 'FFT size %d, FFT mult %d, average_count %d' % ( | |
# self.fft_size, self.fft_size / self.data_width, average_count) | |
#print 'Refresh %.2f Hz' % (float(self.sample_rate) / self.fft_size / average_count) | |
QS.record_graph(0, 0, 1.0) | |
# Make all the screens and hide all but one | |
self.graph = GraphScreen(frame, self.data_width, self.graph_width) | |
self.screen = self.graph | |
width = self.graph.width | |
button_width = width # try to estimate the final button width | |
self.config_screen = ConfigScreen(frame, width, self.fft_size) | |
self.config_screen.Hide() | |
self.waterfall = WaterfallScreen(frame, width, self.data_width, self.graph_width) | |
self.waterfall.Hide() | |
self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width) | |
self.scope.Hide() | |
self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width) | |
self.filter_screen.Hide() | |
self.help_screen = HelpScreen(frame, width, self.screen_height / 10) | |
self.help_screen.Hide() | |
frame.SetSizeHints(width, 100) | |
frame.SetClientSizeWH(width, self.screen_height * 5 / 10) | |
# Make a vertical box to hold all the screens and the bottom box | |
vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL) | |
frame.SetSizer(vertBox) | |
# Add the screens | |
vertBox.Add(self.config_screen, 1) | |
vertBox.Add(self.graph, 1) | |
vertBox.Add(self.waterfall, 1) | |
vertBox.Add(self.scope, 1) | |
vertBox.Add(self.filter_screen, 1) | |
vertBox.Add(self.help_screen, 1) | |
# Add the spacer | |
vertBox.Add(Spacer(frame), 0, wx.EXPAND) | |
# Add the bottom box | |
hBoxA = wx.BoxSizer(wx.HORIZONTAL) | |
vertBox.Add(hBoxA, 0, wx.EXPAND) | |
# End of vertical box. Add items to the horizontal box. | |
# Add two sliders on the left | |
margin = 3 | |
self.sliderVol = SliderBoxV(frame, 'Vol', 300, 1000, self.ChangeVolume) | |
button_width -= self.sliderVol.width + margin * 2 | |
self.ChangeVolume() # set initial volume level | |
hBoxA.Add(self.sliderVol, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) | |
if Hardware.use_sidetone: | |
self.sliderSto = SliderBoxV(frame, 'STo', 300, 1000, self.ChangeSidetone) | |
button_width -= self.sliderSto.width + margin * 2 | |
self.ChangeSidetone() | |
hBoxA.Add(self.sliderSto, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) | |
# Add the sizer for the middle | |
gap = 2 | |
gbs = wx.GridBagSizer(gap, gap) | |
self.gbs = gbs | |
button_width -= gap * 15 | |
hBoxA.Add(gbs, 1, wx.EXPAND, 0) | |
gbs.SetEmptyCellSize((5, 5)) | |
button_width -= 5 | |
for i in range(0, 6) + range(7, 13): | |
gbs.AddGrowableCol(i) | |
# Add two sliders on the right | |
self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True) | |
button_width -= self.sliderYs.width + margin * 2 | |
hBoxA.Add(self.sliderYs, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) | |
self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True) | |
button_width -= self.sliderYz.width + margin * 2 | |
hBoxA.Add(self.sliderYz, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, margin) | |
zbw, button_height = self.MakeButtons(frame, gbs) | |
button_width /= 12 # This is our estimate of the final button size | |
self.MakeTopRow(frame, gbs, button_width, button_height) | |
if conf.quisk_widgets: | |
self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox) | |
if QS.open_key(conf.key_method): | |
print 'open_key failed for name "%s"' % conf.key_method | |
if hasattr(conf, 'mixer_settings'): | |
for dev, numid, value in conf.mixer_settings: | |
err_msg = QS.mixer_set(dev, numid, value) | |
if err_msg: | |
print "Mixer", err_msg | |
# Create transmit audio filters | |
if conf.microphone_name: | |
filtI, filtQ = self.MakeFilterCoef(conf.mic_sample_rate, 540, 2700, 1650) | |
QS.set_tx_filters(filtI, filtQ, ()) | |
# Open the hardware. This must be called before open_sound(). | |
self.config_text = Hardware.open() | |
if not self.config_text: | |
self.config_text = "Missing config_text" | |
QS.capt_channels (conf.channel_i, conf.channel_q) | |
QS.play_channels (conf.channel_i, conf.channel_q) | |
QS.micplay_channels (conf.mic_play_chan_I, conf.mic_play_chan_Q) | |
# Note: Subsequent calls to set channels must not name a higher channel number. | |
# Normally, these calls are only used to reverse the channels. | |
QS.open_sound(conf.name_of_sound_capt, conf.name_of_sound_play, self.sample_rate, | |
conf.data_poll_usec, conf.latency_millisecs, | |
conf.microphone_name, conf.tx_ip, conf.tx_audio_port, | |
conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, | |
conf.mic_out_volume, conf.name_of_mic_play, conf.mic_playback_rate) | |
tune, vfo = Hardware.ReturnFrequency() # Request initial frequency to set band | |
if tune is not None: | |
for band, (f1, f2) in conf.BandEdge.items(): | |
if f1 <= tune <= f2: # Change to the correct band based on frequency | |
self.lastBand = band | |
break | |
self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True) | |
self.ChangeHwFrequency(None, None) # Request initial VFO and tuning | |
# Note: The filter rate is not valid until after the call to open_sound(). | |
# Create FM audio filter | |
frate = QS.get_filter_rate() # filter rate | |
filtI, filtQ = self.MakeFmFilterCoef(frate, 600, 340, 2800) | |
QS.set_fm_filters(filtI) | |
# Record filter rate for the filter screen | |
self.filter_screen.sample_rate = frate | |
#if info[8]: # error message | |
# self.sound_error = 1 | |
# self.config_screen.err_msg = info[8] | |
# print info[8] | |
if self.sound_error: | |
self.screenBtnGroup.SetLabel('Config', do_cmd=True) | |
frame.Show() | |
else: | |
self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True) | |
frame.Show() | |
self.Yield() | |
self.sound_thread = SoundThread() | |
self.sound_thread.start() | |
return True | |
def OnIdle(self, event): | |
if self.screen: | |
self.screen.OnIdle(event) | |
def OnEndSession(self, event): | |
event.Skip() | |
self.OnBtnClose() | |
def OnBtnClose(self, event): | |
if self.sound_thread: | |
self.sound_thread.stop() | |
for i in range(0, 20): | |
if threading.activeCount() == 1: | |
break | |
time.sleep(0.1) | |
def OnExit(self): | |
QS.close_rx_udp() | |
Hardware.close() | |
if self.init_path: # save current program state | |
d = {} | |
for n in self.StateNames: | |
d[n] = getattr(self, n) | |
try: | |
fp = open(self.init_path, "wb") | |
pickle.dump(d, fp) | |
fp.close() | |
except: | |
pass #traceback.print_exc() | |
def MakeTopRow(self, frame, gbs, button_width, button_height): | |
szr = wx.BoxSizer(wx.HORIZONTAL) | |
# Down button | |
b_down = QuiskRepeatbutton(frame, self.OnBtnDownBand, "Dn", | |
self.OnBtnUpDnBandDone, use_right=True) | |
szr.Add(b_down, 1, flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL) | |
# Up button | |
b_up = QuiskRepeatbutton(frame, self.OnBtnUpBand, "Up", | |
self.OnBtnUpDnBandDone, use_right=True) | |
szr.Add(b_up, 1, flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL) | |
gbs.Add(szr, (0, 4), flag=wx.EXPAND) | |
bw, bh = b_down.GetMinSize() # make top row buttons the same size | |
bw = (bw + button_width) / 2 | |
#b_down.SetSizeHints(bw, button_height, bw * 5, button_height) | |
#b_up.SetSizeHints(bw, button_height, bw * 5, button_height) | |
# RIT slider | |
self.ritScale = wx.Slider(frame, -1, self.ritFreq, -2000, 2000, size=(-1, -1), style=wx.SL_LABELS) | |
self.ritScale.Bind(wx.EVT_SCROLL, self.OnRitScale) | |
gbs.Add(self.ritScale, (0, 8), (1, 3), flag=wx.EXPAND) | |
sw, sh = self.ritScale.GetSize() | |
# Frequency display | |
h = max(button_height, sh) # larger of button and slider height | |
self.freqDisplay = FrequencyDisplay(frame, gbs, button_width * 25 / 10, h) | |
self.freqDisplay.Display(self.txFreq + self.VFO) | |
# Frequency entry | |
e = wx.TextCtrl(frame, -1, '', style=wx.TE_PROCESS_ENTER) | |
font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) | |
e.SetFont(font) | |
w, h = e.GetSizeTuple() | |
border = (self.freqDisplay.height_and_border - h) / 2 | |
e.SetMinSize((1, 1)) | |
e.SetBackgroundColour(conf.color_entry) | |
gbs.Add(e, (0, 3), flag = wx.EXPAND | wx.TOP | wx.BOTTOM, border=border) | |
frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e) | |
# S-meter | |
self.smeter = QuiskText(frame, 'ZZS 9 -100.00 dBZZ', bh, wx.ALIGN_CENTER) | |
gbs.Add(self.smeter, (0, 11), (1, 2), flag=wx.EXPAND) | |
def MakeButtons(self, frame, gbs): | |
# There are six columns, a small gap column, and then six more columns | |
# RIT button | |
self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT") | |
gbs.Add(self.ritButton, (0, 7), flag=wx.ALIGN_CENTER) | |
all_buttons = [self.ritButton] | |
# Split button | |
if not conf.mouse_tune_method: # Mouse motion changes the VFO frequency | |
self.splitButton = QuiskCheckbutton(frame, self.OnBtnSplit, "Split") | |
gbs.Add(self.splitButton, (0, 5), flag=wx.ALIGN_CENTER) | |
all_buttons.append(self.splitButton) | |
### Left bank of buttons | |
flag = wx.EXPAND | |
self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None) | |
btns = self.bandBtnGroup.buttons | |
all_buttons += btns | |
i = 0 | |
n1 = len(conf.bandLabels) / 2 | |
n2 = len(conf.bandLabels) - n1 | |
for col in range(0, n1): | |
gbs.Add(btns[i], (1, col), flag=flag) | |
i += 1 | |
for col in range(0, n2): | |
gbs.Add(btns[i], (2, col), flag=flag) | |
i += 1 | |
# Mute, AGC | |
buttons = [] | |
b = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute') | |
buttons.append(b) | |
b = QuiskCycleCheckbutton(frame, self.OnBtnAGC, ('AGC', 'AGC 1', 'AGC 2')) | |
buttons.append(b) | |
b.SetLabel('AGC 1', True) | |
b = QuiskCheckbutton(frame, self.OnBtnNB, text='') | |
buttons.append(b) | |
try: | |
labels = Hardware.rf_gain_labels | |
except: | |
labels = () | |
if labels: | |
self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, labels) | |
buttons.append(self.BtnRfGain) | |
else: | |
b = QuiskCheckbutton(frame, None, text='') | |
buttons.append(b) | |
self.BtnRfGain = None | |
#b = QuiskRepeatbutton(frame, self.OnBtnColor, '', use_right=True) | |
if conf.add_fdx_button: | |
b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test) | |
else: | |
b = QuiskCheckbutton(frame, None, text='') | |
buttons.append(b) | |
b = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test) | |
buttons.append(b) | |
all_buttons += buttons | |
for col in range(0, 6): | |
gbs.Add(buttons[col], (3, col), flag=flag) | |
### Right bank of buttons | |
labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', conf.add_extern_demod, ''] | |
if conf.add_imd_button: | |
labels[-1] = ('IMD', 'IMD -3dB', 'IMD -6dB') | |
self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None) | |
btns = self.modeButns.GetButtons() | |
all_buttons += btns | |
btns[-1].color = conf.color_test | |
for col in range(0, 6): | |
gbs.Add(btns[col], (1, col + 7), flag=flag) | |
labels = ('0',) * 6 | |
self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None) | |
btns = self.filterButns.GetButtons() | |
all_buttons += btns | |
for col in range(0, 6): | |
gbs.Add(btns[col], (2, col + 7), flag=flag) | |
labels = (('Graph', 'GraphP1', 'GraphP2'), 'WFall', ('Scope', 'Scope'), 'Config', 'RX Filter', 'Help') | |
self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen) | |
btns = self.screenBtnGroup.GetButtons() | |
all_buttons += btns | |
for col in range(0, 6): | |
gbs.Add(btns[col], (3, col + 7), flag=flag) | |
bw = bh = 0 | |
for b in all_buttons: # find the largest button size | |
w, h = b.GetMinSize() | |
bw = max(bw, w) | |
bh = max(bh, h) | |
for b in all_buttons: # set all buttons to the same size | |
b.SetSizeHints(bw, bh, bw * 5, bh) | |
return bw, bh # return the button size | |
def NewSmeter(self): | |
#avg_seconds = 5.0 # seconds for S-meter average | |
avg_seconds = 1.0 | |
self.smeter_db_count += 1 # count for average | |
x = QS.get_smeter() | |
self.smeter_db_sum += x # sum for average | |
if self.timer - self.smeter_db_time0 > avg_seconds: # average time reached | |
self.smeter_db = self.smeter_db_sum / self.smeter_db_count | |
self.smeter_db_count = self.smeter_db_sum = 0 | |
self.smeter_db_time0 = self.timer | |
if self.smeter_sunits < x: # S-meter moves to peak value | |
self.smeter_sunits = x | |
else: # S-meter decays at this time constant | |
self.smeter_sunits -= (self.smeter_sunits - x) * (self.timer - self.smeter_sunits_time0) | |
self.smeter_sunits_time0 = self.timer | |
s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit | |
s += Hardware.correct_smeter # S-meter correction for the gain, band, etc. | |
if s < 0: | |
s = 0 | |
if s >= 9.5: | |
s = (s - 9.0) * 6 | |
t = "S9 + %.0f %.2f dB" % (s, self.smeter_db) | |
else: | |
t = "S %.0f %.2f dB" % (s, self.smeter_db) | |
self.smeter.SetLabel(t) | |
def MakeFilterButtons(self, *args): | |
# Change the filter selections depending on the mode: CW, SSB, etc. | |
i = 0 | |
for b in self.filterButns.GetButtons(): | |
b.SetLabel(str(args[i])) | |
b.Refresh() | |
i += 1 | |
def MakeFilterCoef(self, rate, N, bw, center): | |
"""Make an I/Q filter with rectangular passband.""" | |
K = bw * N / rate | |
filtI = [] | |
filtQ = [] | |
pi = math.pi | |
sin = math.sin | |
cos = math.cos | |
tune = 2. * pi * center / rate | |
for k in range(-N/2, N/2 + 1): | |
# Make a lowpass filter | |
if k == 0: | |
z = float(K) / N | |
else: | |
z = 1.0 / N * sin(pi * k * K / N) / sin(pi * k / N) | |
# Apply a windowing function | |
if 1: # Blackman window | |
w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) | |
elif 0: # Hamming | |
w = 0.54 + 0.46 * cos(2. * pi * k / N) | |
elif 0: # Hanning | |
w = 0.5 + 0.5 * cos(2. * pi * k / N) | |
else: | |
w = 1 | |
z *= w | |
# Make a bandpass filter by tuning the low pass filter to new center frequency. | |
# Make two quadrature filters. | |
if tune: | |
z *= 2.0 * cmath.exp(-1j * tune * k) | |
filtI.append(z.real) | |
filtQ.append(z.imag) | |
else: | |
filtI.append(z) | |
filtQ.append(z) | |
return filtI, filtQ | |
def MakeFmFilterCoef(self, rate, N, f1, f2): | |
"""Make an audio filter with FM de-emphasis; remove CTCSS tones.""" | |
bw = f2 - f1 | |
center = (f1 + f2) / 2 | |
N2 = N / 2 # Half the number of points | |
K2 = bw * N / rate / 2 # Half the bandwidth in points | |
filtI = [] | |
filtQ = [] | |
passb = [0] * (N + 1) # desired passband response | |
idft = [0] * (N + 1) # inverse DFT of desired passband | |
pi = math.pi | |
sin = math.sin | |
cos = math.cos | |
tune = 2. * pi * center / rate | |
# indexing can be from - N2 thru + N2 inclusive; total points is 2 * N2 + 1 | |
# indexing can be from 0 thru 2 * N2 inclusive; total points is 2 * N2 + 1 | |
for j in range(-K2, K2 + 1): # Filter shape is -6 bB per octave | |
jj = j + N2 | |
freq = center - bw / 2.0 * float(j) / K2 | |
passb[jj] = float(center) / freq * 0.3 | |
for k in range(-N2 + 1, N2 + 1): # Take inverse DFT of passband response | |
kk = k + N2 | |
x = 0 + 0J | |
for m in range(-N2, N2 + 1): | |
mm = m + N2 | |
if passb[mm]: | |
x += passb[mm] * cmath.exp(1J * 2.0 * pi * m * k / N) | |
x /= N | |
idft[kk] = x | |
idft[0] = idft[-1] # this value is missing | |
for k in range(-N2, N2 + 1): | |
kk = k + N2 | |
z = idft[kk] | |
# Apply a windowing function | |
if 1: # Blackman window | |
w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) | |
elif 0: # Hamming | |
w = 0.54 + 0.46 * cos(2. * pi * k / N) | |
elif 0: # Hanning | |
w = 0.5 + 0.5 * cos(2. * pi * k / N) | |
else: | |
w = 1 | |
z *= w | |
# Make a bandpass filter by tuning the low pass filter to new center frequency. | |
# Make two quadrature filters. | |
if tune: | |
z *= 2.0 * cmath.exp(-1j * tune * k) | |
filtI.append(z.real) | |
filtQ.append(z.imag) | |
else: | |
filtI.append(z.real) | |
filtQ.append(z.real) | |
return filtI, filtQ | |
def OnBtnFilter(self, event, bw=None): | |
if event is None: # called by application | |
self.filterButns.SetLabel(str(bw)) | |
else: # called by button | |
btn = event.GetEventObject() | |
bw = int(btn.GetLabel()) | |
mode = self.mode | |
if mode in ("CWL", "CWU"): | |
N = 1000 | |
center = max(conf.cwTone, bw/2) | |
elif mode in ('LSB', 'USB'): | |
N = 540 | |
center = 300 + bw / 2 | |
else: # AM and FM | |
N = 140 | |
center = 0 | |
frate = QS.get_filter_rate() | |
filtI, filtQ = self.MakeFilterCoef(frate, N, bw, center) | |
QS.set_filters(filtI, filtQ, bw) | |
if self.screen is self.filter_screen: | |
self.screen.NewFilter() | |
def OnBtnScreen(self, event, name=None): | |
if event is not None: | |
win = event.GetEventObject() | |
name = win.GetLabel() | |
self.screen.Hide() | |
if name == 'Config': | |
self.screen = self.config_screen | |
elif name[0:5] == 'Graph': | |
self.screen = self.graph | |
self.screen.SetTxFreq(self.txFreq, self.rxFreq) | |
self.freqDisplay.Display(self.VFO + self.txFreq) | |
self.screen.PeakHold(name) | |
elif name == 'WFall': | |
self.screen = self.waterfall | |
self.screen.SetTxFreq(self.txFreq, self.rxFreq) | |
self.freqDisplay.Display(self.VFO + self.txFreq) | |
sash = self.screen.GetSashPosition() | |
elif name == 'Scope': | |
if win.direction: # Another push on the same button | |
self.scope.running = 1 - self.scope.running # Toggle run state | |
else: # Initial push of button | |
self.scope.running = 1 | |
self.screen = self.scope | |
elif name == 'RX Filter': | |
self.screen = self.filter_screen | |
self.freqDisplay.Display(self.screen.txFreq) | |
self.screen.NewFilter() | |
elif name == 'Help': | |
self.screen = self.help_screen | |
self.screen.Show() | |
self.vertBox.Layout() # This destroys the initialized sash position! | |
self.sliderYs.SetValue(self.screen.y_scale) | |
self.sliderYz.SetValue(self.screen.y_zero) | |
if name == 'WFall': | |
self.screen.SetSashPosition(sash) | |
def ChangeYscale(self, event): | |
self.screen.ChangeYscale(self.sliderYs.GetValue()) | |
def ChangeYzero(self, event): | |
self.screen.ChangeYzero(self.sliderYz.GetValue()) | |
def OnBtnMute(self, event): | |
btn = event.GetEventObject() | |
if btn.GetValue(): | |
QS.set_volume(0) | |
else: | |
QS.set_volume(self.audio_volume) | |
def OnBtnDecimation(self, event): | |
i = event.GetSelection() | |
rate = Hardware.VarDecimSet(i) | |
self.vardecim_set = rate | |
if rate != self.sample_rate: | |
self.sample_rate = rate | |
self.graph.sample_rate = rate | |
self.waterfall.pane1.sample_rate = rate | |
self.waterfall.pane2.sample_rate = rate | |
self.waterfall.pane2.display.sample_rate = rate | |
average_count = float(rate) / conf.graph_refresh / self.fft_size | |
average_count = int(average_count + 0.5) | |
average_count = max (1, average_count) | |
QS.change_rate(rate, average_count) | |
tune = self.txFreq | |
vfo = self.VFO | |
self.txFreq = self.VFO = -1 # demand change | |
self.ChangeHwFrequency(tune, vfo, 'NewDecim') | |
def ChangeVolume(self, event=None): | |
# Caution: event can be None | |
value = self.sliderVol.GetValue() | |
# Simulate log taper pot | |
x = (10.0 ** (float(value) * 0.003000434077) - 1) / 1000.0 | |
self.audio_volume = x # audio_volume is 0 to 1.000 | |
QS.set_volume(x) | |
def ChangeSidetone(self, event=None): | |
# Caution: event can be None | |
value = self.sliderSto.GetValue() | |
# Simulate log taper pot | |
x = (10.0 ** (float(value) * 0.003) - 1) / 1000.0 | |
self.sidetone_volume = x | |
QS.set_sidetone(x, self.ritFreq, conf.keyupDelay) | |
def OnRitScale(self, event=None): # Called when the RIT slider is moved | |
# Caution: event can be None | |
if self.ritButton.GetValue(): | |
value = self.ritScale.GetValue() | |
value = int(value) | |
self.ritFreq = value | |
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq) | |
QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) | |
def OnBtnSplit(self, event): # Called when the Split check button is pressed | |
self.split_rxtx = self.splitButton.GetValue() | |
if self.split_rxtx: | |
self.rxFreq = self.oldRxFreq | |
d = self.sample_rate * 49 / 100 # Move rxFreq on-screen | |
if self.rxFreq < -d: | |
self.rxFreq = -d | |
elif self.rxFreq > d: | |
self.rxFreq = d | |
else: | |
self.oldRxFreq = self.rxFreq | |
self.rxFreq = self.txFreq | |
self.screen.SetTxFreq(self.txFreq, self.rxFreq) | |
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq) | |
def OnBtnRit(self, event=None): # Called when the RIT check button is pressed | |
# Caution: event can be None | |
if self.ritButton.GetValue(): | |
self.ritFreq = self.ritScale.GetValue() | |
else: | |
self.ritFreq = 0 | |
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq) | |
QS.set_sidetone(self.sidetone_volume, self.ritFreq, conf.keyupDelay) | |
def SetRit(self, freq): | |
if freq: | |
self.ritButton.SetValue(1) | |
else: | |
self.ritButton.SetValue(0) | |
self.ritScale.SetValue(freq) | |
self.OnBtnRit() | |
def OnBtnFDX(self, event): | |
btn = event.GetEventObject() | |
if btn.GetValue(): | |
QS.set_fdx(1) | |
else: | |
QS.set_fdx(0) | |
def OnBtnTest1(self, event): | |
btn = event.GetEventObject() | |
if btn.GetValue(): | |
QS.add_tone(10000) | |
else: | |
QS.add_tone(0) | |
def OnBtnTest2(self, event): | |
return | |
def OnBtnColor(self, event): | |
if not self.color_list: | |
clist = wx.lib.colourdb.getColourInfoList() | |
self.color_list = [(0, clist[0][0])] | |
self.color_index = 0 | |
for i in range(1, len(clist)): | |
if self.color_list[-1][1].replace(' ', '') != clist[i][0].replace(' ', ''): | |
#if 'BLUE' in clist[i][0]: | |
self.color_list.append((i, clist[i][0])) | |
else: | |
btn = event.GetEventObject() | |
if btn.shift: | |
del self.color_list[self.color_index] | |
else: | |
self.color_index += btn.direction | |
if self.color_index >= len(self.color_list): | |
self.color_index = 0 | |
elif self.color_index < 0: | |
self.color_index = len(self.color_list) -1 | |
color = self.color_list[self.color_index][1] | |
print self.color_index, color | |
self.main_frame.SetBackgroundColour(color) | |
self.main_frame.Refresh() | |
self.screen.Refresh() | |
def OnBtnAGC(self, event): | |
btn = event.GetEventObject() | |
# Set AGC: agcInUse, agcAttack, agcRelease | |
if btn.index == 1: | |
QS.set_agc(1, 1.0, 0.01) | |
elif btn.index == 2: | |
QS.set_agc(2, 1.0, 0.1) | |
else: | |
QS.set_agc(0, 0, 0) | |
def OnBtnNB(self, event): | |
pass | |
def FreqEntry(self, event): | |
freq = event.GetString() | |
if not freq: | |
return | |
try: | |
if '.' in freq: | |
freq = int(float(freq) * 1E6 + 0.1) | |
else: | |
freq = int(freq) | |
except ValueError: | |
win = event.GetEventObject() | |
win.Clear() | |
win.AppendText("Error") | |
else: | |
for band, (f1, f2) in conf.BandEdge.items(): | |
if f1 <= freq <= f2: # Change to the correct band based on frequency | |
self.bandBtnGroup.SetLabel(band, do_cmd=True) | |
break | |
tune = freq % 10000 | |
vfo = freq - tune | |
self.ChangeHwFrequency(tune, vfo, 'FreqEntry') | |
def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None): | |
"""Change the VFO and tuning frequencies, and notify the hardware. | |
tune: the new tuning frequency in +- sample_rate/2; | |
vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio | |
source: a string indicating the source or widget requesting the change; | |
band: if source is "BtnBand", the band requested; | |
event: for a widget, the event (used to access control/shift key state). | |
Try to update the hardware by calling Hardware.ChangeFrequency(). | |
The hardware will reply with the updated frequencies which may be different | |
from those requested; use and display the returned tune and vfo. | |
If tune or vfo is None, query the hardware for the current frequency. | |
""" | |
change = 0 | |
if tune is None or vfo is None: | |
tune, vfo = Hardware.ReturnFrequency() | |
if tune is None or vfo is None: # hardware did not change the frequency | |
return change | |
else: | |
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event) | |
tune -= vfo | |
if tune != self.txFreq: | |
change = 1 | |
self.txFreq = tune | |
if not self.split_rxtx: | |
self.rxFreq = self.txFreq | |
self.screen.SetTxFreq(self.txFreq, self.rxFreq) | |
QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq) | |
if vfo != self.VFO: | |
change = 1 | |
self.VFO = vfo | |
self.graph.SetVFO(vfo) | |
self.waterfall.SetVFO(vfo) | |
if self.w_phase: # Phase adjustment screen can not change its VFO | |
self.w_phase.Destroy() | |
self.w_phase = None | |
ampl, phase = self.GetAmplPhase() | |
QS.set_ampl_phase(ampl, phase) | |
if change: | |
self.freqDisplay.Display(self.txFreq + self.VFO) | |
return change | |
def OnBtnMode(self, event, mode=None): | |
if event is None: # called by application | |
self.modeButns.SetLabel(mode) | |
else: # called by button | |
mode = self.modeButns.GetLabel() | |
Hardware.ChangeMode(mode) | |
self.mode = mode | |
if mode in ('CWL', 'CWU'): | |
if mode == 'CWL': | |
QS.set_rx_mode(0) | |
self.SetRit(conf.cwTone) | |
else: # CWU | |
QS.set_rx_mode(1) | |
self.SetRit(-conf.cwTone) | |
self.MakeFilterButtons(200, 300, 400, 500, 1000, 3000) | |
self.OnBtnFilter(None, 1000) | |
elif mode in ('LSB', 'USB'): | |
if mode == 'LSB': | |
QS.set_rx_mode(2) # LSB | |
else: | |
QS.set_rx_mode(3) # USB | |
self.SetRit(0) | |
self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) | |
self.OnBtnFilter(None, 2800) | |
elif mode == 'AM': | |
QS.set_rx_mode(4) | |
self.SetRit(0) | |
self.MakeFilterButtons(4000, 5000, 6000, 7000, 8000, 9000) | |
self.OnBtnFilter(None, 6000) | |
elif mode == 'FM': | |
QS.set_rx_mode(5) | |
self.SetRit(0) | |
self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) | |
self.OnBtnFilter(None, 12000) | |
elif mode[0:3] == 'IMD': | |
QS.set_rx_mode(10 + self.modeButns.GetSelectedButton().index) # 10, 11, 12 | |
self.SetRit(0) | |
self.MakeFilterButtons(1800, 2000, 2200, 2500, 2800, 3300) | |
self.OnBtnFilter(None, 2800) | |
elif mode == conf.add_extern_demod: # External demodulation | |
QS.set_rx_mode(6) | |
self.SetRit(0) | |
self.MakeFilterButtons(10000, 12000, 15000, 25000, 35000, 45000) | |
self.OnBtnFilter(None, 12000) | |
def OnBtnBand(self, event): | |
band = self.lastBand # former band in use | |
try: | |
f1, f2 = conf.BandEdge[band] | |
if f1 <= self.VFO + self.txFreq <= f2: | |
self.bandState[band] = (self.VFO, self.txFreq, self.mode) | |
except KeyError: | |
pass | |
btn = event.GetEventObject() | |
band = btn.GetLabel() # new band | |
self.lastBand = band | |
try: | |
vfo, tune, mode = self.bandState[band] | |
except KeyError: | |
vfo, tune, mode = (0, 0, 'LSB') | |
if band == '60': | |
freq = vfo + tune | |
if btn.direction: | |
vfo = self.VFO | |
if 5100000 < vfo < 5600000: | |
if btn.direction > 0: # Move up | |
for f in self.freq60: | |
if f > vfo + self.txFreq: | |
freq = f | |
break | |
else: | |
freq = self.freq60[0] | |
else: # move down | |
l = list(self.freq60) | |
l.reverse() | |
for f in l: | |
if f < vfo + self.txFreq: | |
freq = f | |
break | |
else: | |
freq = self.freq60[-1] | |
half = self.sample_rate / 2 * self.graph_width / self.data_width | |
while freq - vfo <= -half + 1000: | |
vfo -= 10000 | |
while freq - vfo >= +half - 5000: | |
vfo += 10000 | |
tune = freq - vfo | |
elif band == 'Time': | |
vfo, tune, mode = conf.bandTime[btn.index] | |
self.OnBtnMode(None, mode) | |
self.txFreq = self.VFO = -1 # demand change | |
self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band) | |
Hardware.ChangeBand(band) | |
def OnBtnUpDnBandDelta(self, event, is_band_down): | |
btn = event.GetEventObject() | |
if btn.direction > 0: # left button was used, move a bit | |
d = max(10000, int(self.sample_rate / 9)) | |
else: # right button was used, move to edge | |
d = max(10000, int(self.sample_rate * 49 / 100)) | |
d = (d / 10000) * 10000 | |
if is_band_down: | |
d = -d | |
self.VFO += d | |
self.txFreq -= d | |
self.rxFreq -= d | |
# Set the display but do not change the hardware | |
self.graph.SetVFO(self.VFO) | |
self.waterfall.SetVFO(self.VFO) | |
self.screen.SetTxFreq(self.txFreq, self.rxFreq) | |
self.freqDisplay.Display(self.txFreq + self.VFO) | |
def OnBtnDownBand(self, event): | |
self.band_up_down = 1 | |
self.OnBtnUpDnBandDelta(event, True) | |
def OnBtnUpBand(self, event): | |
self.band_up_down = 1 | |
self.OnBtnUpDnBandDelta(event, False) | |
def OnBtnUpDnBandDone(self, event): | |
self.band_up_down = 0 | |
tune = self.txFreq | |
vfo = self.VFO | |
self.txFreq = self.VFO = 0 # Force an update | |
self.ChangeHwFrequency(tune, vfo, 'BtnUpDown') | |
def GetAmplPhase(self): | |
if conf.bandAmplPhase.has_key("panadapter"): | |
band = "panadapter" | |
else: | |
band = self.lastBand | |
try: | |
lst = self.bandAmplPhase[band]["rx"] | |
except KeyError: | |
return (0.0, 0.0) | |
length = len(lst) | |
if length == 0: | |
return (0.0, 0.0) | |
elif length == 1: | |
return lst[0][2], lst[0][3] | |
elif self.VFO < lst[0][0]: # before first data point | |
i1 = 0 | |
i2 = 1 | |
elif lst[length - 1][0] < self.VFO: # after last data point | |
i1 = length - 2 | |
i2 = length - 1 | |
else: | |
# Binary search for the bracket VFO | |
i1 = 0 | |
i2 = length | |
index = (i1 + i2) / 2 | |
for i in range(length): | |
diff = lst[index][0] - self.VFO | |
if diff < 0: | |
i1 = index | |
elif diff > 0: | |
i2 = index | |
else: # equal VFO's | |
return lst[index][2], lst[index][3] | |
if i2 - i1 <= 1: | |
break | |
index = (i1 + i2) / 2 | |
d1 = self.VFO - lst[i1][0] # linear interpolation | |
d2 = lst[i2][0] - self.VFO | |
dx = d1 + d2 | |
ampl = (d1 * lst[i2][2] + d2 * lst[i1][2]) / dx | |
phas = (d1 * lst[i2][3] + d2 * lst[i1][3]) / dx | |
return ampl, phas | |
def PostStartup(self): # called once after sound attempts to start | |
self.config_screen.OnGraphData(None) # update config in case sound is not running | |
def OnReadSound(self): # called at frequent intervals | |
self.timer = time.time() | |
if self.screen == self.scope: | |
data = QS.get_graph(0) # get raw data | |
if data: | |
self.scope.OnGraphData(data) # Send message to draw new data | |
return 1 # we got new scope data | |
else: | |
data = QS.get_graph(1) # get FFT data | |
if data: | |
#T('') | |
self.NewSmeter() # update the S-meter | |
if self.screen == self.graph: | |
self.waterfall.OnGraphData(data) # save waterfall data | |
self.graph.OnGraphData(data) # Send message to draw new data | |
elif self.screen == self.config_screen: | |
pass | |
else: | |
self.screen.OnGraphData(data) # Send message to draw new data | |
#T('graph data') | |
#application.Yield() | |
#T('Yield') | |
return 1 # We got new graph/scope data | |
if QS.get_overrange(): | |
self.clip_time0 = self.timer | |
self.freqDisplay.Clip(1) | |
if self.clip_time0: | |
if self.timer - self.clip_time0 > 1.0: | |
self.clip_time0 = 0 | |
self.freqDisplay.Clip(0) | |
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks | |
self.heart_time0 = self.timer | |
if self.screen == self.config_screen: | |
self.screen.OnGraphData() # Send message to draw new data | |
Hardware.HeartBeat() | |
if not self.band_up_down: | |
# Poll the hardware for changed frequency. This is used for hardware | |
# that can change its frequency independently of Quisk; eg. K3. | |
if self.ChangeHwFrequency(None, None): # Returns true for a change | |
try: | |
f1, f2 = conf.BandEdge[self.lastBand] | |
if f1 <= self.VFO + self.txFreq <= f2: | |
self.bandState[self.lastBand] = (self.VFO, self.txFreq, self.mode) | |
return | |
except KeyError: | |
pass | |
# Frequency is not within the current band. Change to the correct band based on frequency. | |
for band, (f1, f2) in conf.BandEdge.items(): | |
if f1 <= self.VFO + self.txFreq <= f2: | |
self.lastBand = band | |
self.bandBtnGroup.SetLabel(band, do_cmd=True) | |
break | |
def main(): | |
"""If quisk is installed as a package, you can run it with quisk.main().""" | |
App() | |
application.MainLoop() | |
if __name__ == '__main__': | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Please do not change the configuration file quisk_conf_defaults.py. | |
# Instead copy one of the other quisk_conf_*.py files to your own | |
# .quisk_conf.py and make changes there. For a normal sound card | |
# configuration, copy quisk_conf_model.py to your .quisk_conf.py. | |
# | |
# Quisk imports quisk_conf_defaults to set its configuration. | |
# If you have a configuration file, it then overwrites the defaults | |
# with your parameters. Your configuration file must be named | |
# ~/.quisk_conf.py, where "~" means your home directory. Or | |
# you may specify a different name with the -c or --config command | |
# line option. Try --help. Check the config screen to make sure that | |
# the correct configuration file is in use. | |
# | |
# The Quisk receiver can use a high quality sound card for capture and playback, | |
# or it can use the SDR-IQ by RfSpace for capture and a lower quality | |
# sound card for playback. Quisk can also be used as a panadapter. | |
# Quisk can control some rigs. See quisk_hardware_*.py. If you have a rig | |
# to control, copy one of the quisk_hardware_*.py files to your own file named | |
# quisk_hardware.py, and edit that file. If there is no quisk_hardware.py, then | |
# quisk_hardware_model.py is used instead. | |
import sys | |
# Import the default Hardware module. You can import a different module in | |
# your .quisk_conf.py. | |
import quisk_hardware_model as quisk_hardware | |
# Module for additional widgets (advanced usage). | |
quisk_widgets = None | |
# Select the default screen when Quisk starts: | |
default_screen = 'Graph' | |
#default_screen = 'WFall' | |
#default_screen = 'Config' | |
# The width of the graph data as a fraction of the total screen size. This | |
# will be adjusted by Quisk to accommodate preferred FFT sizes. It can | |
# not be changed once Quisk starts. It can not be made too small because | |
# of the space needed for all the buttons. If you have two displays, Quisk | |
# will try to fill both, so set graph_width to 0.4 or smaller. | |
graph_width = 0.8 | |
# This controls the speed of the graph peak hold. Lower numbers give a longer time constant. | |
graph_peak_hold_1 = 0.25 | |
graph_peak_hold_2 = 0.10 | |
# Select the default mode when Quisk starts (overruled by persistent_state): | |
# default_mode = 'FM' | |
default_mode = 'USB' | |
# Select the way the waterfall screen scrolls: | |
# waterfall_scroll_mode = 0 # scroll at a constant rate. | |
waterfall_scroll_mode = 1 # scroll faster at the top so that a new signal appears sooner. | |
# Select the initial size in pixels (minimum 1) of the graph at the top of the waterfall. | |
waterfall_graph_size = 80 | |
# These are the initial values for the Y-scale and Y-zero sliders for each screen. | |
# The sliders go from zero to 160. | |
graph_y_scale = 100 | |
graph_y_zero = 0 | |
waterfall_y_scale = 80 | |
waterfall_y_zero = 40 | |
waterfall_graph_y_scale = 100 | |
waterfall_graph_y_zero = 60 | |
scope_y_scale = 80 | |
scope_y_zero = 0 # Currently doesn't do anything | |
filter_y_scale = 90 | |
filter_y_zero = 0 | |
# Quisk can save its current state in a file on exit, and restore it when you restart. | |
# State includes band, frequency and mode, but not every item of state (not screen). | |
# The file is .quisk_init.pkl in the same directory as your config file. If this file | |
# becomes corrupted, just delete it and it will be reconstructed. | |
#persistent_state = False | |
persistent_state = True | |
# This converts from dB to S-units for the S-meter (it is in S-units). | |
correct_smeter = 15.5 | |
# This is the fraction of spectrum to display from zero to one. It is needed if | |
# the passband edges are not valid. Use 0.85 for the SDR-IQ. | |
display_fraction = 1.00 | |
# Define colors used by all widgets in wxPython colour format: | |
color_bg = 'light steel blue' # Lower screen background | |
color_graph = 'lemonchiffon1' # Graph background | |
color_gl = 'grey' # Lines on the graph | |
color_btn = 'steelblue2' # button color | |
color_check_btn = 'yellow2' # color of a check button when it is checked | |
color_cycle_btn = 'goldenrod3' # color of a cycle button when it is checked | |
color_test = 'hot pink' # color of a button used for test (turn off for tx) | |
color_freq = 'lightcyan1' # background color of frequency and s-meter | |
color_entry = color_freq # frequency entry box | |
# Added by AC | |
color_tick = 'black' | |
color_fft = (0x4c,0x4c,0xff) | |
# These are the palettes for the waterfall. The one used is named waterfallPallette, | |
# so to use a different one, overwrite this name in your .quisk_conf.py. | |
waterfallPalette = ( | |
( 0, 0, 0, 0), | |
( 36, 85, 0, 255), | |
( 73, 153, 0, 255), | |
(109, 255, 0, 128), | |
(146, 255, 119, 0), | |
(182, 85, 255, 100), | |
(219, 255, 255, 0), | |
(255, 255, 255, 255) | |
) | |
digipanWaterfallPalette = ( | |
( 0, 0, 0, 0), | |
( 32, 0, 0, 62), | |
( 64, 0, 0, 126), | |
( 96, 145, 142, 96), | |
(128, 181, 184, 48), | |
(160, 223, 226, 105), | |
(192, 254, 254, 4), | |
(255, 255, 58, 0) | |
) | |
# Quisk can access your sound card through PortAudio or through ALSA drivers. | |
# In PortAudio, soundcards have an index number 0, 1, 2, ... and a name. | |
# The name can be something like "HDA NVidia: AD198x Analog (hw:0,0)" or | |
# "surround41". In Quisk, all PortAudio device names start with "portaudio". | |
# A device name like "portaudio#6" directly specifies the index. A name like | |
# "portaudio:text" means to search for "text" in all available devices. And | |
# there is a default device "portaudiodefault". So these portaudio names are useful: | |
#name_of_sound_capt = "portaudio:(hw:0,0)" # First sound card | |
#name_of_sound_capt = "portaudio:(hw:1,0)" # Second sound card, etc. | |
#name_of_sound_capt = "portaudio#1" # Directly specified index | |
#name_of_sound_capt = "portaudiodefault" # May give poor performance on capture | |
# In ALSA, soundcards have these names. The "hw" devices are the raw | |
# hardware devices, and should be used for soundcard capture. | |
#name_of_sound_capt = "hw:0" # First sound card | |
#name_of_sound_capt = "hw:1" # Second sound card, etc. | |
#name_of_sound_capt = "plughw" | |
#name_of_sound_capt = "plughw:1" | |
#name_of_sound_capt = "default" | |
# Normally you would capture and play on the same soundcard to avoid problems with the | |
# two clocks running at slightly different rates. But you can define name_of_sound_play | |
# to play back on a different device. Define this as the empty string "" to turn off | |
# play (for a panadapter). | |
# | |
# For the SDR-IQ the soundcard is not used for capture; it only plays back audio. | |
# Playback is always 48 kHz stereo. | |
# Configuration for soundcard capture and playback | |
use_sdriq = 0 # Get ADC samples from SDR-IQ is not used | |
use_rx_udp = 0 # Get ADC samples from UDP is not used | |
sample_rate = 48000 # ADC hardware sample rate in Hertz | |
if sys.platform == "win32": | |
name_of_sound_capt = "Primary" | |
else: | |
name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device. | |
name_of_sound_play = name_of_sound_capt # Use the same device for play back | |
#name_of_sound_play = "" # Panadapter: Do not play | |
channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ... | |
channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ... | |
# Thanks to Franco Spinelli for this fix: | |
# The H101 hardware using the PCM2904 chip has a one-sample delay between | |
# channels, which must be fixed in software. If you have this problem, | |
# change channel_delay to either channel_i or channel_q. Use -1 for no delay. | |
channel_delay = -1 | |
# If you use a soundcard with Ethernet control of the VFO, set these parameters: | |
rx_ip = "" # Receiver IP address for VFO control | |
# If you use an SDR-IQ for capture, set these parameters: | |
# import quisk_hardware_sdriq as quisk_hardware # Use different hardware file | |
# use_sdriq = 1 # Capture device is the SDR-IQ | |
# sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open | |
# sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) | |
# sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 | |
# sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this | |
# name_of_sound_capt = "" # We do not capture from the soundcard | |
# name_of_sound_play = "hw:0" # Play back on this soundcard | |
# playback_rate = 48000 # Radio sound play rate, default 48000 | |
# channel_i = 0 # Soundcard index of left channel | |
# channel_q = 1 # Soundcard index of right channel | |
# display_fraction = 0.85 # The edges of the full bandwidth are not valid | |
# If you receive ADC samples from a UDP port, set these parameters: | |
# import quisk_hardware_n2adr as quisk_hardware # Use different hardware file | |
# use_rx_udp = 1 # Get ADC samples from UDP | |
# rx_udp_ip = "192.168.1.91" # Sample source IP address | |
# rx_udp_port = 0xBC77 # Sample source UDP port | |
# rx_udp_clock = 122880000 # ADC sample rate in Hertz | |
# rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate | |
# The allowable decimations are 8 times 8 times (2 or 4 or 8) times (1 or 5). | |
# So you could enter 8 * 8 * one of (2, 4, 5, 8, 10, 20, 40). | |
# These decimations result in a sample rate of 48 to 960 kHz. | |
# sample_rate = int(float(rx_udp_clock) /rx_udp_decimation + 0.5) # Don't change this | |
# name_of_sound_capt = "" # We do not capture from the soundcard | |
# name_of_sound_play = "hw:0" # Play back on this soundcard | |
# playback_rate = 48000 # Radio sound play rate, default 48000 | |
# This is the received radio sound playback rate. The default will | |
# be 48 kHz for the SDR-IQ and UDP port samples, and sample_rate for sound | |
# card capture. Set it yourself for other rates or hardware. | |
# playback_rate = 48000 | |
# If you use quisk_hardware_fixed.py, this is the fixed VFO frequency in Hertz | |
fixed_vfo_freq = 7056000 | |
# Some hardware must be polled to get the key up/down state. This is the time | |
# between polls in milliseconds. Use zero to turn off the poll. | |
key_poll_msec = 0 | |
# This determines what happens when you tune by dragging the mouse. The correct | |
# choice depends on how your hardware performs tuning. You may want to use a | |
# custom hardware file with a custom ChangeFrequency() method too. | |
mouse_tune_method = 0 # The Quisk tune frequency changes and the VFO frequency is unchanged. | |
#mouse_tune_method = 1 # The Quisk tune frequency is unchanged and the VFO changes. | |
# This is the CW tone frequency in Hertz | |
cwTone = 600 | |
# If you use the microphone feature, the mic_channel_I and Q are the two capture | |
# microphone channels. Quisk uses a monophonic mic, so audio is taken from the I | |
# channel, and the Q channel is (currently) ignored. It is OK to set the same | |
# channel number for both, and this is necessary for a USB mono mic. If you | |
# change the sample rate, you will need to change the C code to use different filters. | |
# Mic samples can be sent to an Ethernet device (use tx_ip and name_of_mic_play = "") | |
# or to a sound card (use name_of_mic_play="hw:1" or other device). | |
# If mic samples are sent to a sound card for Tx, the samples are tuned to the audio | |
# transmit frequency, and are set to zero unless the key is down. | |
# If there is no mic (microphone_name = ""), it is still possible to transmit CW, | |
# and you should set mic_playback_rate to the I/Q receive capture rate. | |
# Microphone capture: | |
microphone_name = "" # Name of microphone capture device (or "hw:1") | |
mic_sample_rate = 48000 # Microphone capture sample rate in Hertz, must be 48000 | |
mic_channel_I = 0 # Soundcard index of mic capture audio channel | |
mic_channel_Q = 0 # Soundcard index of ignored capture channel | |
# Microphone samples sent to soundcard: | |
name_of_mic_play = "" # Name of play device if mic I/Q is sent to a sound card | |
mic_playback_rate = 48000 # Playback rate must be a multiple 1, 2, ... of mic_sample_rate | |
mic_play_chan_I = 0 # Soundcard index of mic I play channel | |
mic_play_chan_Q = 1 # Soundcard index of mic Q play channel | |
mic_out_volume = 1.0 # Microphone output volume (after all processing) as a fraction 0.0 to 1.0 | |
# Microphone samples sent to UDP: | |
tx_ip = "" # Transmit IP address for mic sent to UDP (or "192.168.2.195") | |
tx_audio_port = 0 # UDP port for mic samples (or 0x553B) | |
# Microphone audio processing: | |
# The original audio processing used mic_clip = 4.0; mic_preemphasis = -1.0 | |
# For no mic audio processing, use mic_clip = 1.0; mic_preemphasis = 0.0 | |
mic_clip = 3.0 # Mic amplitude clipping; larger numbers give more clipping | |
mic_preemphasis = 0.6 # Mic pre-emphasis 0.0 (none) to 1.0; or -1.0 for a Butterworth filter | |
# If your mixing scheme inverts the RF spectrum, set this option to un-invert it | |
invertSpectrum = 0 | |
# Use "amixer -c 1 contents" to get a list of mixer controls and their numid's for | |
# card 1 (or "-c 0" for card 0). Then make a list of (device_name, numid, value) | |
# for each control you need to set. The sample settings are for my USB microphone. | |
#mixer_settings = [ | |
# ("hw:1", 2, 0.80), # numid of microphone volume control, volume 0.0 to 1.0; | |
# ("hw:1", 1, 1.0) # numid of capture on/off control, turn on with 1.0; | |
# ] | |
# If you want Quisk to add a button to generate a 2-tone IMD test signal, | |
# set this to 1. This feature requires the microphone to work. | |
add_imd_button = 0 | |
# If you want Quisk to add a full duplex button (transmit and receive at the | |
# same time), set this to 1. | |
add_fdx_button = 0 | |
# If you want to write your own I/Q filter and demodulation module, set | |
# this to the name of the button to add, and change extdemod.c. | |
# add_extern_demod = "WFM" | |
add_extern_demod = "" | |
# This is the data used to draw colored lines on the frequency X axis to | |
# indicate CW and Phone sub-bands. You can make it anything you want. | |
# These are the colors used for sub-bands: | |
CW = '#FF4444' # General class CW | |
eCW = '#FF8888' # Extra class CW | |
Phone = '#4444FF' # General class phone | |
ePhone = '#8888FF' # Extra class phone | |
# ARRL band plan special frequencies | |
Data = '#FF9900' | |
DxData = '#CC6600' | |
RTTY = '#FF9900' | |
SSTV = '#FFFF00' | |
AM = '#00FF00' | |
Packet = '#00FFFF' | |
Beacons = '#66FF66' | |
Satellite = '#22AA88' | |
Repeater = '#AA00FF' | |
Simplex = '#00FF44' | |
Other = '#888888' | |
# Colors start at the indicated frequency and continue until the | |
# next frequency. The special color "None" turns off color. | |
BandPlan = [ | |
# 160 meters | |
[ 1800000, Data], | |
[ 1809000, Other], | |
[ 1811000, CW], | |
[ 1843000, Phone], | |
[ 1908000, Other], | |
[ 1912000, Phone], | |
[ 1995000, Other], | |
[ 2000000, None], | |
# 80 meters | |
[ 3500000, eCW], | |
[ 3525000, CW], | |
[ 3570000, Data], | |
[ 3589000, DxData], | |
[ 3591000, Data], | |
[ 3600000, ePhone], | |
[ 3790000, Other], | |
[ 3800000, Phone], | |
[ 3844000, SSTV], | |
[ 3846000, Phone], | |
[ 3880000, AM], | |
[ 3890000, Phone], | |
[ 4000000, None], | |
# 60 meters | |
[ 5330600, Phone], | |
[ 5333400, None], | |
[ 5346600, Phone], | |
[ 5349400, None], | |
[ 5366600, Phone], | |
[ 5369400, None], | |
[ 5371600, Phone], | |
[ 5374400, None], | |
[ 5403600, Phone], | |
[ 5406400, None], | |
# 40 meters | |
[ 7000000, eCW], | |
[ 7025000, CW], | |
[ 7039000, DxData], | |
[ 7041000, CW], | |
[ 7080000, Data], | |
[ 7125000, ePhone], | |
[ 7170000, SSTV], | |
[ 7172000, ePhone], | |
[ 7175000, Phone], | |
[ 7285000, AM], | |
[ 7295000, Phone], | |
[ 7300000, None], | |
# 30 meters | |
[10100000, CW], | |
[10130000, RTTY], | |
[10140000, Packet], | |
[10150000, None], | |
# 20 meters | |
[14000000, eCW], | |
[14025000, CW], | |
[14070000, RTTY], | |
[14095000, Packet], | |
[14099500, Other], | |
[14100500, Packet], | |
[14112000, CW], | |
[14150000, ePhone], | |
[14225000, Phone], | |
[14229000, SSTV], | |
[14231000, Phone], | |
[14281000, AM], | |
[14291000, Phone], | |
[14350000, None], | |
# 17 meters | |
[18068000, CW], | |
[18100000, RTTY], | |
[18105000, Packet], | |
[18110000, Phone], | |
[18168000, None], | |
# 15 meters | |
[21000000, eCW], | |
[21025000, CW], | |
[21070000, RTTY], | |
[21110000, CW], | |
[21200000, ePhone], | |
[21275000, Phone], | |
[21339000, SSTV], | |
[21341000, Phone], | |
[21450000, None], | |
# 12 meters | |
[24890000, CW], | |
[24920000, RTTY], | |
[24925000, Packet], | |
[24930000, Phone], | |
[24990000, None], | |
# 10 meters | |
[28000000, CW], | |
[28070000, RTTY], | |
[28150000, CW], | |
[28200000, Beacons], | |
[28300000, Phone], | |
[28679000, SSTV], | |
[28681000, Phone], | |
[29000000, AM], | |
[29200000, Phone], | |
[29300000, Satellite], | |
[29520000, Repeater], | |
[29590000, Simplex], | |
[29610000, Repeater], | |
[29700000, None], | |
# 6 meters | |
[50000000, Beacons], | |
[50100000, Phone], | |
[54000000, None], | |
] | |
# For each band, this dictionary gives the lower and upper band edges. Frequencies | |
# outside these limits will not be remembered as the last frequency in the band. | |
BandEdge = { | |
'160':( 1800000, 2000000), '80' :( 3500000, 4000000), | |
'60' :( 5300000, 5430000), '40' :( 7000000, 7300000), | |
'30' :(10100000, 10150000), '20' :(14000000, 14350000), | |
'17' :(18068000, 18168000), '15' :(21000000, 21450000), | |
'12' :(24890000, 24990000), '10' :(28000000, 29700000), | |
'6' :(50000000, 54000000), | |
} | |
# For each band, this dictionary gives the initial center frequency, tuning | |
# frequency as an offset from the center frequency, and the mode. This is | |
# no longer too useful because the persistent_state feature saves and then | |
# overwrites these values anyway. | |
bandState = {'Audio':(0, 0, 'LSB'), | |
'160':( 1890000, -10000, 'LSB'), '80' :( 3660000, -10000, 'LSB'), | |
'60' :( 5370000, 1500, 'USB'), '40' :( 7180000, -5000, 'LSB'), '30':(10120000, -10000, 'CWL'), | |
'20' :(14200000, -10000, 'USB'), '17' :(18120000, 10000, 'USB'), '15':(21250000, -10000, 'USB'), | |
'12' :(24940000, 10000, 'USB'), '10' :(28400000, -10000, 'USB'), | |
'Time':( 5000000, 0, 'AM'), '6' :(50040000, 10000, 'USB'), | |
} | |
# For the Time band, this is the center frequency, tuning frequency and mode: | |
bandTime = [ | |
( 2500000-10000, 10000, 'AM'), | |
( 3330000-10000, 10000, 'AM'), | |
( 5000000-10000, 10000, 'AM'), | |
( 7335000-10000, 10000, 'AM'), | |
(10000000-10000, 10000, 'AM'), | |
(14670000-10000, 10000, 'AM'), | |
(15000000-10000, 10000, 'AM'), | |
(20000000-10000, 10000, 'AM'), | |
] | |
# This is the list of band buttons that Quisk displays, and it should have | |
# a length of 12. Empty buttons can have a null string "" label. | |
# Note that the 60 meter band and the Time band have buttons that support | |
# multiple presses. | |
bandLabels = ['Audio', '160', '80', ('60',) * 5, '40', '30', '20', '17', | |
'15', '12', '10', ('Time',) * len(bandTime)] | |
# If you get your I/Q samples from a sound card, you will need to correct the | |
# amplitude and phase for inaccuracies in the analog hardware. The data is | |
# entered using the controls from the "Rx Phase" button on the config screen. | |
# The corrections are saved by the persistent_state feature. | |
# | |
# The available range of the amplitude and phase controls for receive: | |
rx_max_amplitude_correct = 0.2 # Correction relative to 1.000000 (ideally 0.0000) | |
rx_max_phase_correct = 10.0 # Correction in degrees of phase (ideally 0.0000) | |
# | |
# The bandAmplPhase dictionary gives the amplitude and phase corrections for | |
# sound card data. The format is a dictionary with key "band", giving a dictionary | |
# with key "rx" or "tx", giving a list of tuples (VFO, tune, amplitude, phase). | |
# | |
# If you use Quisk as a panadapter, the corrections will not depend on the band. | |
# In that case create a band "panadapter" in your config file, and all corrections | |
# will be read/written to that band. | |
bandAmplPhase = {} # Empty dictionary to start | |
#bandAmplPhase = {'panadapter':{}} # Create "panadapter" band for all corrections | |
# The program polls the soundcard or SDR-IQ for data every data_poll_usec microseconds. | |
# A lower time reduces latency; a higher time is less taxing on the hardware. | |
if sys.platform == "win32": | |
data_poll_usec = 20000 # poll time in microseconds | |
else: | |
data_poll_usec = 5000 # poll time in microseconds | |
# The fft_size is the width of the data on the screen (about 800 to | |
# 1200 pixels) times the fft_size_multiplier. Multiple FFTs are averaged | |
# together to achieve your graph refresh rate. If fft_size_multiplier is | |
# too small you will get many fft errors. You can specify fft_size_multiplier, | |
# or enter a large number (use 9999) to maximize it, or enter zero to let | |
# quisk calculate it for you. Look for fft_size_multiplier in quisk.py. | |
# If your hardware can change the decimation, there are further compilcations. | |
# The FFT size is fixed, and only the average count can change to adjust the | |
# refresh rate. | |
fft_size_multiplier = 0 | |
# The graph_refresh is the frequency at which the graph is updated, | |
# and should be about 5 to 10 Hertz. Higher rates require more processor power. | |
graph_refresh = 7 # update the graph at this rate in Hertz | |
# latency_millisecs determines how many samples are in the soundcard play buffer. | |
# A larger number makes it less likely that you will run out of samples to play, | |
# but increases latency. It is OK to suffer a certain number of play buffer | |
# underruns in order to get lower latency. | |
latency_millisecs = 150 # latency time in milliseconds | |
# Select the method to test the state of the key; see is_key_down.c | |
key_method = "" # No keying, or internal method | |
# key_method = "/dev/parport0" # Use the named parallel port | |
# key_method = "/dev/ttyS0" # Use the named serial port | |
# key_method = "192.168.1.44" # Use UDP from this address | |
# If you are using keying, key-down throws away the current capture buffer | |
# and starts a sidetone with a rise time of 5 milliseconds. For | |
# key-up, the sidetone is ended with a fall time of 5 milliseconds, then | |
# a silent period starts, then normal audio starts with a rise time of | |
# 5 milliseconds. The length of the silent period is given by keyupDelay, | |
# but will be at least the time necessary to collect enough samples to | |
# refill the filters. A larger keyupDelay may be needed to accomodate | |
# antenna switching or other requirement of your hardware. | |
keyupDelay = 23 # extra milliseconds silence on key up |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment