10 Commits
Author SHA1 Message Date
James C. Ahlstrom fb888a2e08 Quisk version 4.2.39 2024-10-01 14:22:23 -04:00
James C. Ahlstrom 78a1ca3d60 Quisk version 4.2.38 2024-09-01 13:57:53 -04:00
James C. Ahlstrom 14a80f09d5 Quisk version 4.2.37 2024-08-21 15:25:10 -04:00
James C. Ahlstrom 1c9f81254b Quisk version 4.2.36 2024-08-03 13:56:30 -04:00
James C. Ahlstrom 76208643a0 Quisk version 4.2.35 2024-06-28 13:54:55 -04:00
James C. Ahlstrom 31ecbce146 Quisk version 4.2.34 2024-05-23 21:17:45 -04:00
James C. Ahlstrom a1343c1264 Quisk version 4.2.33 2024-05-10 13:16:44 -04:00
James C. Ahlstrom 71146c9eaf Quisk version 4.2.32 2024-04-25 16:28:42 -04:00
James C. Ahlstrom 814e4ce08c Quisk version 4.2.31 2024-04-04 11:56:02 -04:00
James C. Ahlstrom 36d717636c Quisk version 4.2.30 2024-03-20 12:28:39 -04:00
18 changed files with 1377 additions and 340 deletions
+79
View File
@@ -1,3 +1,82 @@
Quisk Version 4.2.39 October 2024
==================================
I added an optional Hardware method PollGuiControl(self). If you define this in your Hardware file,
the GUI thread will call it frequently. It can be used to set PTT from your hardware.
I am trying to work around a wxPython quirk that causes mouse wheel tuning to fail. Please test.
Quisk Version 4.2.38 September 2024
====================================
This is a bugfix release. I fixed a bug in the Hamlib serial port when using a physical TTY instead
of a pseudo-tty. The Hamlib ZZFA command now changes the receive frequency instead of the transmit
frequency. I fixed some window sizing issues.
Quisk Version 4.2.37 August 2024
=================================
Quisk now has a new option "Fixed tune offset" on the Config/radio/Options screen. This is meant for
Softrock radios that need a constant audio frequency to simplify amplitude and phase corrections.
The value zero means no change to Quisk tuning. Otherwise enter the fixed frequency offset from center.
If you are using the obsolete "mouse_tune_method = 1" please delete it and use this new feature.
If you use softrock_tune_vfo.py as your hardware file, you should replace it with softrock/hardware_usb.py.
Quisk now works on Python 3.12. And I fixed a bug in the small screen graph width.
Quisk Version 4.2.36 August 2024
=================================
I added two new color schemes by Michael, DK1MI. The On/Off button color and text color can
now be configured. (Michael, send me the final colors). There are a few other small changes.
Quisk Version 4.2.35 June 2024
===============================
There is a new item "File play level" on the Config/radio/Sound screen to reduce the volume when playing files.
Use it to reduce the volume of the CQ message relative to radio sound.
Dan, AI2M, provided a patch to compile in MacOS on Apple Silicon. Thanks Dan!
I am working on better amplitude and phase adjustments when using SoftRock radios. The new corrections
screen has a spreadsheet to display the VFO and tune offset. And there is a feature to automatically
measure the corrections when using a signal generator as a source. If you use Quisk with SoftRock or similar
radios, please let me know what you think.
Quisk Version 4.2.34 May 2024
==============================
For the Hermes-Lite2 the inhibit Tx function (CN8) now releases the Spot button.
Quisk displays the band plan below the X-axis as colored bars, and there are eighteen possible colors.
If you hover the mouse over a bar, Quisk now displays the name of the mode. Recall that there is a dialog
box so you can set the band plan to what you want.
Quisk Version 4.2.33 May 2024
==============================
Added the get_level Hamlib command "l STRENGTH". It returns the S-meter reading. I finally found some Hamlib
documentation, often not an easy task. The return value of STRENGTH is in dB with zero dB equal to S9.
There are 6 dB per S-unit. So a STRENGTH of 4 is 4dB over S9, and -6, -12, -18 is S8, S7, S6. I also added
the corresponding FlexRadio command ZZSM.
Quisk Version 4.2.32 April 2024
================================
I found and fixed a bug in the microphone filters that can result in a buffer overrun. You should update Quisk.
Some Linux digital programs can not use the usual Quisk Pulse loopback device and must use an
Alsa loopback. I added more documentation for Alsa loopbacks, added more information on the Sound
screen and I changed the default Alsa sample size to 16 bit.
Quisk now responds to the Hermes Lite2 Tx inhibit input by displaying a message and extinguishing the "Tx" light.
There is a new item "Hermes Lite2 protocol" on the Hardware screen to control this behavior.
Quisk Version 4.2.31 March 2024
================================
The "Tune" item was missing from the Midi menus. I fixed it but I didn't test it because I did
not borrow Ben's Midi controller. Please test it yourself and let me know if it works. The Midi
speed can be set from the menu too.
Quisk Version 4.2.30 March 2024
================================
A new option "Tx-Rx silence msec" on the Timing configuration screen controls how long
to suppress Rx audio when switching from Tx to Rx. The previous fixed time was 50 msec.
The SSB squelch control now works on CW too. Please test.
Quisk Version 4.2.29 January 2024
==================================
This version updates the FreeDV library files and program logic. I added a "Monitor"
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.29
#Quisk version 4.2.39
from .quisk import main
+436 -221
View File
@@ -1,13 +1,9 @@
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import sys, wx, wx.lib, os, re, pickle, traceback, json
import sys, wx, wx.lib, os, re, pickle, traceback, json, copy
# Quisk will alter quisk_conf_defaults to include the user's config file.
import quisk_conf_defaults as conf
import _quisk as QS
from quisk_widgets import QuiskPushbutton, QuiskCheckbutton, QuiskBitField, SliderBoxH, SliderBoxHH
from quisk_widgets import FreqFormatter
from quisk_widgets import wxVersion
if wxVersion in ('2', '3'):
import wx.combo as wxcombo
@@ -170,12 +166,16 @@ class Configuration:
radio_dict["Hermes_BandDictTx"] = {}
def UpdateGlobals(self):
self.RadioName = Settings[1]
application.BandPlanC2T = {}
if self.RadioName == "ConfigFileRadio":
application.BandPlan = conf.BandPlan
for mode, color in conf.BandPlanColors:
application.BandPlanC2T[color] = mode
elif "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]:
mode_dict = {"End":None}
for mode, color in Settings[4]["BandPlanColors"]:
mode_dict[mode] = color
application.BandPlanC2T[color] = mode
plan = []
for freq, mode in Settings[4]["BandPlan"]:
freq = int(float(freq) * 1E6 + 0.1)
@@ -188,6 +188,8 @@ class Configuration:
application.BandPlan = plan
else:
application.BandPlan = conf.BandPlan
for mode, color in conf.BandPlanColors:
application.BandPlanC2T[color] = mode
if "MidiNoteDict" not in Settings[4]:
Settings[4]["MidiNoteDict"] = {}
self.settings_changed = True
@@ -282,10 +284,8 @@ class Configuration:
self.settings_changed = True
errors = errors + "Failed to set %s to %s using format %s\n" % (k, v, fmt)
#traceback.print_exc()
if conf.color_scheme == 'B':
conf.__dict__.update(conf.color_scheme_B)
elif conf.color_scheme == 'C':
conf.__dict__.update(conf.color_scheme_C)
if conf.color_scheme != 'A':
conf.__dict__.update(getattr(conf, 'color_scheme_' + conf.color_scheme))
self.RequiredValues(radio_dict) # Why not update conf too??? This only updates the radio_dict.
if errors:
dlg = wx.MessageDialog(None, errors,
@@ -328,7 +328,7 @@ class Configuration:
path = self.NormPath(path)
if not os.path.isfile(path):
dlg = wx.MessageDialog(None,
"Failure for hardware file %s!" % path,
"Can not find the hardware file %s!" % path,
'Hardware File', wx.OK|wx.ICON_ERROR)
ret = dlg.ShowModal()
dlg.Destroy()
@@ -399,11 +399,12 @@ class Configuration:
notebk.AddPage(self.radio_page, "Radios")
self.radios_page_start = notebk.GetPageCount()
notebk.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging, notebk)
if Settings[1] in Settings[2]:
page = RadioNotebook(notebk, Settings[1])
notebk.AddPage(page, "*%s*" % Settings[1])
for name in Settings[2]:
page = RadioNotebook(notebk, name)
if name == Settings[1]:
notebk.AddPage(page, "*%s*" % name)
else:
if name != Settings[1]:
page = RadioNotebook(notebk, name)
notebk.AddPage(page, name)
def GuessType(self):
udp = conf.use_rx_udp
@@ -477,7 +478,7 @@ class Configuration:
for rxname, data in self.receiver_data:
if rxname == receiver_name:
return data
return None
return ()
def GetReceiverDatum(self, receiver_name, item_name):
for rxname, data in self.receiver_data:
if rxname == receiver_name:
@@ -563,6 +564,8 @@ class Configuration:
# self.format4name with the items that Configuration understands.
# Dicts and lists are Python objects. All other items are text, not Python objects.
#
# This parses the quisk_conf_defaults.py file and user-defined radios in ./*pkg/quisk_hardware.py.
#
# Sections start with 16 #, section name
# self.sections is a list of [section_name, section_data]
# section_data is a list of [data_name, text, fmt, help_text, values]
@@ -595,11 +598,11 @@ class Configuration:
except:
traceback.print_exc()
def _ParserConf(self, filename):
re_AeqB = re.compile("^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b"
re_AeqB = re.compile(r"^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b"
section = None
data_name = None
multi_line = False
fp = open(filename, "r")
fp = open(filename, "r", encoding="utf8")
for line in fp:
line = line.strip()
if not line:
@@ -829,12 +832,10 @@ class QPowerMeterCalibration(wx.Frame):
## Note: The amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL.
## October 2020: changed to make RxTx frequency and VFO two independent variables.
## June 2024: Added a Grid control and automatic receive measurement.
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):
self.width = width
self.rx_tx = rx_tx # Must be "rx" or "tx"
if rx_tx == 'tx':
self.is_tx = 1
@@ -842,121 +843,232 @@ class QAdjustPhase(wx.Frame):
else:
self.is_tx = 0
t = "Adjust Sound Card Receive Amplitude and Phase"
wx.Frame.__init__(self, application.main_frame, -1, t, pos=(50, 100), style=wx.CAPTION)
wx.Frame.__init__(self, application.main_frame, -1, t, pos=(50, 100), style=wx.CAPTION|wx.RESIZE_BORDER)
self.client_width = application.screen_width * 5 // 10
self.SetClientSize((self.client_width, application.screen_height * 5 // 10))
self.new_amplitude = self.new_phase = 0.0
self.new_tune = 0
self.bandAmplPhase = copy.deepcopy(application.bandAmplPhase)
self.new_cell = None
self.dirty = False
self.manual = True
self.panel = wx.Panel(self)
self.MakeControls()
self.Redraw()
self.Show()
self.Bind(wx.EVT_CLOSE, self.OnBtnExit)
QS.softrock_corrections(1)
QS.set_ampl_phase(0.0, 0.0, self.is_tx)
self.grid.GoToCell(0, 2)
def MakeControls(self): # Make controls for phase/amplitude adjustment
panel = self.panel
width = self.width
self.old_amplitude, self.old_phase = application.GetAmplPhase(self.rx_tx)
self.new_amplitude, self.new_phase = self.old_amplitude, self.old_phase
sl_max = width * 4 // 10 # maximum +/- value for slider
sizer = wx.BoxSizer(wx.VERTICAL)
panel.SetSizer(sizer)
sl_max = application.screen_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(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
main_font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetFont(font)
self.SetFont(main_font)
panel.SetFont(main_font)
charx = self.GetCharWidth()
self.tab1 = tab1 = charx
chary = self.GetCharHeight()
deltay = chary * 12 // 10
y = chary * 3 // 10
self.controls = []
txt = wx.StaticText(panel, -1, "No data", pos=(tab1, y))
self.controls.append(txt)
y += txt.GetSize().GetHeight()
#y += deltay * 14 // 10
self.t_ampl = wx.StaticText(panel, -1, self.f_ampl % self.old_amplitude, pos=(tab1, y))
self.controls.append(self.t_ampl)
y += deltay
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y))
self.controls.append(fine)
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay))
self.controls.append(coarse)
tab2 = tab1 + coarse.GetSize().GetWidth() + charx
sliderX = width - tab2 - charx
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.ampl1)
y += deltay
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.ampl2)
y += deltay * 14 // 10
self.PosAmpl(self.old_amplitude)
self.t_phase = wx.StaticText(panel, -1, self.f_phase % self.old_phase, pos=(tab1, y))
self.controls.append(self.t_phase)
y += deltay
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y))
self.controls.append(fine)
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay))
self.controls.append(coarse)
self.phase1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.phase1)
y += deltay
self.phase2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1))
self.controls.append(self.phase2)
y += deltay
sv = QuiskPushbutton(panel, self.OnBtnSave, 'Save')
self.controls.append(sv)
dv = QuiskPushbutton(panel, self.OnBtnDestroyVFO, 'Destroy VFO')
self.controls.append(dv)
ds = QuiskPushbutton(panel, self.OnBtnDestroyALL, 'Destroy ALL')
self.controls.append(ds)
cn = QuiskPushbutton(panel, self.OnBtnFinished, 'Finished')
self.controls.append(cn)
hl = QuiskPushbutton(panel, self.OnBtnHelp, 'Help')
self.controls.append(hl)
w, h = ds.GetSize().Get()
sv.SetSize((w, h))
cn.SetSize((w, h))
hl.SetSize((w, h))
y += h * 5 // 10
x = (width - w * 5) // 6
sv.SetPosition((x, y))
dv.SetPosition((x*2 + w, y))
ds.SetPosition((x*3 + w*2, y))
cn.SetPosition((x*4 + w*3, y))
hl.SetPosition((x*5 + w*4, y))
sv.SetBackgroundColour('light blue')
dv.SetBackgroundColour('light blue')
ds.SetBackgroundColour('light blue')
cn.SetBackgroundColour('light blue')
hl.SetBackgroundColour('light blue')
y += h
y += h * 4 // 10
self.PosPhase(self.old_phase)
self.SetClientSize(wx.Size(width, y))
self.ampl1.Bind(wx.EVT_SCROLL, self.OnChange)
# Create the grid heading and Destroy button
self.cell_amph = wx.StaticText(panel, -1, "")
font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_BOLD, False, conf.quisk_typeface)
self.cell_amph.SetFont(font)
self.cell_amph.SetLabel("Cell frequency %s, gain %.3f, phase %.3f\u00B0 " % (FreqFormatter(144000000), -1.123, -12.456))
dv = wx.lib.buttons.GenButton(panel, label=" Destroy ")
dv.SetBezelWidth(3)
dv.SetBackgroundColour("#DDD")
dv.Bind(wx.EVT_BUTTON, self.OnBtnDestroy)
hbox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(hbox, flag=wx.BOTTOM|wx.EXPAND, border=chary * 3 // 10)
hbox.Add(self.cell_amph, flag=wx.ALL|wx.ALIGN_CENTER_VERTICAL, border=chary // 2)
hbox.Add(dv, flag=wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=charx * 5)
# Create the grid
self.grid = wx.grid.Grid(panel, style=wx.BORDER_SIMPLE)
self.grid.CreateGrid(4, 3)
self.grid.HideRowLabels()
self.selected_row = 0
self.selected_col = 0
self.cell_bg_color = self.grid.GetDefaultCellBackgroundColour()
self.grid.SetDefaultCellFont(main_font)
width, h = self.GetTextExtent("987-24 000")
self.grid.SetDefaultColSize(width, True)
self.grid.EnableEditing(False)
self.grid.SetColLabelValue(0, "Band")
self.grid.SetColLabelValue(1, "VFO")
self.grid.SetColLabelValue(2, "Tune")
self.grid.SetColSize(0, width // 2)
self.grid.SetColSize(1, width * 14 // 10)
num_tune = (self.client_width - width * 2) // width
#print ("num_tune", num_tune)
for col in range(3, num_tune + 2):
self.grid.AppendCols()
self.grid.SetColLabelValue(col, "Tune")
#self.grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnGridLeftClick)
self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnGridSelectCell)
gbox = wx.BoxSizer(wx.VERTICAL)
sizer.Add(gbox, proportion=1, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=charx)
gbox.Add(self.grid, proportion=1)
# Create the New Cell text, Manual/Measure, AddCell button
sbs = wx.StaticBoxSizer(wx.VERTICAL, panel, " New Cell")
sizer.Add(sbs, flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT|wx.BOTTOM, border=charx*3)
self.add_cell_text = wx.StaticText(panel, -1, "Band")
self.add_cell_text.SetFont(main_font)
sbs.Add(self.add_cell_text, flag=wx.LEFT|wx.TOP, border=charx)
box0 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(1, charx)
sbs.Add(box0, flag=wx.EXPAND)
rb_manual = wx.RadioButton(panel, -1, "Manual adjustment", style=wx.RB_GROUP)
self.Bind(wx.EVT_RADIOBUTTON, self.OnBtnManMeasure)
rb_measure = wx.RadioButton(panel, -1, "Measure")
if self.is_tx:
rb_manual.Enable(False)
rb_measure.Enable(False)
b = wx.lib.buttons.GenButton(panel, label=" Add cell ")
b.Bind(wx.EVT_BUTTON, self.OnBtnAddCell)
b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD")
box0.Add(rb_manual, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx)
box0.Add(rb_measure, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*5)
box0.Add(b, flag=wx.LEFT, border=charx*5)
# Create the amplitude slider controls
box1 = wx.BoxSizer(wx.HORIZONTAL)
box2 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(box1, flag=wx.EXPAND)
sbs.Add(box2, flag=wx.EXPAND)
# ST_ELLIPSIZE_END needed to work around a layout bug
fine = wx.StaticText(panel, -1, 'Gain Fine', style=wx.ST_ELLIPSIZE_END)
coarse = wx.StaticText(panel, -1, 'Gain Coarse', style=wx.ST_ELLIPSIZE_END)
box1.Add(fine, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
box2.Add(coarse, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
box1.Add(self.ampl1, flag=wx.LEFT, proportion=1, border=0)
box2.Add(self.ampl2, flag=wx.LEFT, proportion=1, border=0)
self.ampl1.Bind(wx.EVT_SCROLL, self.OnAmpl1)
self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2)
self.phase1.Bind(wx.EVT_SCROLL, self.OnChange)
self.phase2.Bind(wx.EVT_SCROLL, self.OnPhase2)
# Create the phase slider controls
box3 = wx.BoxSizer(wx.HORIZONTAL)
box4 = wx.BoxSizer(wx.HORIZONTAL)
sbs.Add(box3, flag=wx.EXPAND)
sbs.Add(box4, flag=wx.EXPAND)
fine = wx.StaticText(panel, -1, 'Phase Fine', style=wx.ST_ELLIPSIZE_END)
coarse = wx.StaticText(panel, -1, 'Phase Coarse', style=wx.ST_ELLIPSIZE_END)
box3.Add(fine, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
box4.Add(coarse, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*3)
self.phas1 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
self.phas2 = wx.Slider(panel, -1, 0, -sl_max, sl_max)
box3.Add(self.phas1, flag=wx.LEFT, proportion=1, border=0)
box4.Add(self.phas2, flag=wx.LEFT, proportion=1, border=0)
self.phas1.Bind(wx.EVT_SCROLL, self.OnPhase1)
self.phas2.Bind(wx.EVT_SCROLL, self.OnPhase2)
# Create the button row
btnbox = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(btnbox, flag=wx.EXPAND)
btns = []
btnbox.Add(1, 1, proportion=10)
b = wx.lib.buttons.GenButton(panel, label=" Save ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=5)
btns.append(b)
size = b.GetSize()
b.Bind(wx.EVT_BUTTON, self.OnBtnSave)
b = wx.lib.buttons.GenButton(panel, label=" Exit ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=5)
btns.append(b)
b.Bind(wx.EVT_BUTTON, self.OnBtnExit)
b = wx.lib.buttons.GenButton(panel, label=" Help ")
btnbox.Add(b)
btnbox.Add(1, 1, proportion=10)
btns.append(b)
b.Bind(wx.EVT_BUTTON, self.OnBtnHelp)
#b.Bind(wx.EVT_BUTTON, self.OnGraphData)
for b in btns:
b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD")
b.SetSize(size)
sizer.Add(charx, chary)
def Redraw(self):
# Print available data points
self.band = application.lastBand
data = application.bandAmplPhase.get(self.band, {})
data = data.get(self.rx_tx, [])
if data:
t = "Band %s VFO and frequency list\n" % self.band
for vfo, items in data:
t += "%10d:" % vfo
for freq, ampl, phase in items:
t += " %7d" % freq
t += "\n"
self.grid.ClearGrid()
# Print available data cells
row = 0
bands = []
self.cell_dict = {}
for band in self.bandAmplPhase:
if band != "Version" and self.rx_tx in self.bandAmplPhase[band]:
bands.append(band)
bands.sort(reverse=True)
for band in bands:
vfos_tunes = self.bandAmplPhase[band][self.rx_tx]
if vfos_tunes:
if self.grid.GetNumberRows() <= row:
self.grid.AppendRows()
self.grid.SetCellValue(row, 0, band)
self.grid.SetCellAlignment(row, 0, wx.ALIGN_CENTER, wx.ALIGN_CENTER)
for vfo, tunes in vfos_tunes:
if self.grid.GetNumberRows() <= row:
self.grid.AppendRows()
self.cell_dict[(row, 0)] = band, vfo, 0.0
self.grid.SetCellValue(row, 1, FreqFormatter(vfo))
self.grid.SetCellAlignment(row, 1, wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
col = 2
for tune, ampl, phase in tunes:
if self.grid.GetNumberCols() <= col:
self.grid.AppendCols()
self.grid.SetColLabelValue(col, "Tune")
self.grid.SetCellValue(row, col, FreqFormatter(tune))
self.grid.SetCellAlignment(row, col, wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
self.cell_dict[(row, col)] = tune, ampl, phase
if (vfo, tune) == self.new_cell:
self.grid.GoToCell(row, col)
col += 1
row += 1
self.new_cell = None
self.grid.ForceRefresh()
def SetNewCellText(self):
vfo = FreqFormatter(application.VFO)
self.add_cell_text.SetLabel("Band %s, Center (VFO) %s, Tune %d, Gain %5.3f, Phase %5.3f\u00B0" % (
application.lastBand, vfo, self.new_tune, self.new_amplitude + 1.0, self.new_phase))
def SlowHeartBeat(self):
if self.manual:
self.new_tune = application.txFreq
else:
t = "Band %s: No data." % self.band
x, y = self.controls[0].GetPosition()
height = self.controls[0].GetSize().GetHeight()
self.controls[0].Destroy()
txt = wx.StaticText(self.panel, -1, t, pos=(self.tab1, y))
self.controls[0] = txt
delta = txt.GetSize().GetHeight() - height
for ctrl in self.controls[1:]:
x, y = ctrl.GetPosition()
y += delta
ctrl.Move(x, y)
h = self.GetClientSize().GetHeight()
self.SetClientSize(wx.Size(self.width, h + delta))
self.new_tune, gain, phase = QS.softrock_corrections(2)
ampl = gain - 1.0
self.new_amplitude = ampl
phase *= 57.2958
self.new_phase = phase
self.PosAmpl(ampl)
self.PosPhase(phase)
self.SetNewCellText()
def OnGridSelectCell(self, event=None):
self.grid.SetCellBackgroundColour(self.selected_row, self.selected_col, self.cell_bg_color)
if event:
event.Skip()
row = self.selected_row = event.GetRow()
col = self.selected_col = event.GetCol()
else:
row = self.selected_row
col = self.selected_col
self.grid.SetCellBackgroundColour(row, col, '#FEA')
self.grid.ForceRefresh()
band, vfo, z = self.cell_dict.get((row, 0), ('', None, 0))
tune, am, ph = self.cell_dict.get((row, col), (None, None, None))
if not band or vfo is None:
self.cell_amph.SetLabel("Cell is empty")
elif col == 0:
self.cell_amph.SetLabel("Band %s" % band)
elif col == 1:
self.cell_amph.SetLabel("Center (VFO) frequency %s" % FreqFormatter(vfo))
elif tune is None:
self.cell_amph.SetLabel("Cell is empty")
else:
self.cell_amph.SetLabel("Cell frequency %s, gain %.3f, phase %.3f\u00B0" % (FreqFormatter(vfo + tune), am + 1.0, ph))
def PosAmpl(self, ampl): # set pos1, pos2 for amplitude
pos2 = round(ampl / self.ampl_scale)
remain = ampl - pos2 * self.ampl_scale
@@ -967,83 +1079,196 @@ class QAdjustPhase(wx.Frame):
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.is_tx)
self.new_amplitude, self.new_phase = ampl, phase
self.phas1.SetValue(pos1)
self.phas2.SetValue(pos2)
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)
self.OnAmpl1(event)
def OnAmpl1(self, event):
ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
self.new_amplitude = ampl
self.SetNewCellText()
QS.set_ampl_phase(self.new_amplitude, self.new_phase, self.is_tx)
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()
phase = self.phase_scale * self.phas1.GetValue() / 50.0 + self.phase_scale * self.phas2.GetValue()
self.PosPhase(phase)
self.OnChange(event)
def OnBtnDestroyVFO(self, event): # Remove entry with the application VFO
data = application.bandAmplPhase.get(self.band, {})
data = data.get(self.rx_tx, [])
for i in range(len(data)):
if data[i][0] == application.VFO:
del data[i]
break
self.Redraw()
def OnBtnSave(self, event):
data = application.bandAmplPhase
if self.band not in data:
data[self.band] = {}
data = data[self.band]
if self.rx_tx not in data:
data[self.rx_tx] = []
data = data[self.rx_tx]
for vfo, items in data: # All items must be lists, not tuples
if vfo == application.VFO:
for i in range(len(items)):
item = items[i]
if item[0] == application.rxFreq:
item[1] = self.new_amplitude
item[2] = self.new_phase
self.OnPhase1(event)
def OnPhase1(self, event):
phase = self.phase_scale * self.phas1.GetValue() / 50.0 + self.phase_scale * self.phas2.GetValue()
self.new_phase = phase
self.SetNewCellText()
QS.set_ampl_phase(self.new_amplitude, self.new_phase, self.is_tx)
def OnBtnAddCell(self, event):
band = application.lastBand
vfo = application.VFO
self.new_cell = (vfo, self.new_tune)
ampl = self.new_amplitude
phase = self.new_phase
tune = self.new_tune
if band not in self.bandAmplPhase:
self.bandAmplPhase[band] = {}
if self.rx_tx not in self.bandAmplPhase[band]:
self.bandAmplPhase[band][self.rx_tx] = []
vfos_tunes = self.bandAmplPhase[band][self.rx_tx]
for i in range(0, len(vfos_tunes)):
vfo_tunes = vfos_tunes[i]
if vfo_tunes[0] == vfo:
tunes = vfo_tunes[1]
for j in range(0, len(tunes)):
if tunes[j][0] == tune:
tunes[j] = [tune, ampl, phase]
break
else:
items.append([application.rxFreq, self.new_amplitude, self.new_phase])
items.sort()
break
else:
data.append([application.VFO, [[application.rxFreq, self.new_amplitude, self.new_phase]]])
data.sort()
tunes.append([tune, ampl, phase])
tunes.sort()
self.dirty = True
self.Redraw()
return
vfos_tunes.append([vfo, [[tune, ampl, phase]]])
vfos_tunes = vfos_tunes.sort()
self.dirty = True
self.Redraw()
def OnBtnDestroyALL(self, event):
dlg = wx.MessageDialog(self, "This will destroy all data for band %s!" % self.band,
"Destroy Data", style = wx.YES|wx.NO)
if dlg.ShowModal() == wx.ID_YES:
if self.band in application.bandAmplPhase:
del application.bandAmplPhase[self.band]
self.grid.ClearSelection()
def OnBtnDestroy(self, event):
# Create a list of cells to be deleted
col_row = []
# print("Rows", self.grid.GetSelectedRows())
# print("Cols", self.grid.GetSelectedCols())
# print("Cells", self.grid.GetSelectedCells())
# print("Left", self.grid.GetSelectionBlockTopLeft())
# print("Right", self.grid.GetSelectionBlockBottomRight())
#
# delete the cell at the grid cursor
row = self.grid.GetGridCursorRow()
col = self.grid.GetGridCursorCol()
cr = col, row
if cr not in col_row:
col_row.append(cr)
#rows = self.grid.GetSelectedRows() # Not used
# delete whole columns
cols = self.grid.GetSelectedCols()
for col in cols:
for row in range(0, self.grid.GetNumberRows()):
cr = col, row
if cr not in col_row:
col_row.append(cr)
# delete cells
cells = self.grid.GetSelectedCells()
for cell in cells:
row, col = cell.Get()
cr = col, row
if cr not in col_row:
col_row.append(cr)
# delete blocks
top_left_list = self.grid.GetSelectionBlockTopLeft()
bottom_right_list = self.grid.GetSelectionBlockBottomRight()
for i in range(len(top_left_list)):
row1, col1 = top_left_list[i].Get()
row2, col2 = bottom_right_list[i].Get()
for row in range(row1, row2 + 1):
for col in range(col1, col2 + 1):
cr = col, row
if cr not in col_row:
col_row.append(cr)
col_row.sort(reverse=True)
changed = False
for col, row in col_row:
try:
band, vfo, z = self.cell_dict[(row, 0)]
vfos_tunes = self.bandAmplPhase[band][self.rx_tx]
except:
continue
if col in (0, 1): # Destroy row
for i in range(0, len(vfos_tunes)):
if vfos_tunes[i][0] == vfo:
del vfos_tunes[i]
changed = True
break
else: # Destroy cell
try:
tune, am, ph = self.cell_dict[(row, col)]
except:
continue
for i in range(0, len(vfos_tunes)):
if vfos_tunes[i][0] != vfo:
continue
tunes = vfos_tunes[i][1]
for j in range(0, len(tunes)):
if tunes[j][0] == tune:
del tunes[j]
changed = True
break
if changed:
self.dirty = True
self.Redraw()
def OnBtnFinished(self, event=None):
self.selected_row = self.grid.GetGridCursorRow()
self.selected_col = self.grid.GetGridCursorCol()
self.OnGridSelectCell()
self.grid.ClearSelection()
def OnBtnManMeasure(self, event):
self.manual = event.GetEventObject().GetLabel()[0:3] == "Man"
self.ampl1.Enable(self.manual)
self.ampl2.Enable(self.manual)
self.phas1.Enable(self.manual)
self.phas2.Enable(self.manual)
if self.manual:
QS.softrock_corrections(1)
QS.set_ampl_phase(self.new_amplitude, self.new_phase, self.is_tx)
else:
QS.softrock_corrections(2)
QS.set_ampl_phase(0.0, 0.0, self.is_tx)
def OnBtnSave(self, event):
application.bandAmplPhase = copy.deepcopy(self.bandAmplPhase)
self.dirty = False
def OnBtnExit(self, event):
#self.OnGraphData(None)
if self.dirty:
dlg = wx.MessageDialog(self,
"Your changes are not saved. Do you want to save them?", "Changes Were Made",
wx.OK|wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_WARNING)
dlg.SetOKLabel("Discard Changes")
ret = dlg.ShowModal()
dlg.Destroy()
if ret == wx.ID_CANCEL:
return
QS.softrock_corrections(0)
ampl, phase = application.GetAmplPhase(self.rx_tx)
QS.set_ampl_phase(ampl, phase, self.is_tx)
application.w_phase = None
self.Destroy()
def OnBtnHelp(self, event=None):
dlg = wx.MessageDialog(self,
'The "VFO" is the frequency at the center of the graph screen. The Rx or Tx frequency is the offset from the VFO. \
Adjust the VFO and the frequency as desired. Then adjust the sliders to minimize the image. Press "Save" when satisfied. \
To adjust the VFO, use the band Up/Down buttons, or right click the graph at the desired VFO. \
'For manual adjustment attach a signal generator or look at a strong signal in the band. Set the Quisk frequency to the signal frequency. \
Adjust the amplitude and phase sliders to minimize the image. To save the adjustment, press Add Cell. \
For automatic measurement, attach a signal generator and choose some frequencies. Press Add Cell for each one. \
Adjustments must be made for both receive and transmit on each band. \
No changes are made to the table of adjustments until you press Save. \
The maximum slider adjustment range can be changed on the radio Hardware screen. \
The other buttons will delete the data for the current VFO, or for the whole band. \
For more information, press the main "Help" button, \
then "Documentation", then "SoftRock".'
For more information, press the main "Help" button, then "Documentation", then "SoftRock".'
, "Adjustment Help", style=wx.OK)
dlg.ShowModal()
def OnGraphData(self, event):
if not hasattr(self, "VFO"):
self.VFO = []
self.Tune = []
self.Gain = []
self.Phase = []
if event:
self.VFO.append(application.VFO)
self.Tune.append(self.new_tune)
self.Gain.append(self.new_amplitude + 1.0)
self.Phase.append(self.new_phase)
else:
for name in ("VFO", "Tune", "Gain", "Phase"):
print ("%s = [" % name)
lst = getattr(self, name)
for i in range(0, len(lst)):
if name[0] in "VT":
print("%d," % lst[i])
else:
print("%.5f," % lst[i])
print ("]")
class ListEditDialog(wx.Dialog): # Display a dialog with a List-Edit control, plus Ok/Cancel
def __init__(self, parent, title, choice, choices, width):
@@ -1484,7 +1709,7 @@ class MidiKnob(QuiskControl):
self.menu = wx.Menu()
for name in application.midiControls:
ctrl, func = application.midiControls[name]
if not ctrl or not func:
if name != "Tune" and (not ctrl or not func):
continue
item = self.menu.Append(-1, name)
self.Bind(wx.EVT_MENU, self.OnMenu, item)
@@ -1519,7 +1744,7 @@ class MidiJogWheel(QuiskControl):
controls = wx.Menu()
for name in application.midiControls:
ctrl, func = application.midiControls[name]
if not ctrl or not func:
if name != "Tune" and (not ctrl or not func):
continue
item = controls.Append(-1, name)
self.Bind(wx.EVT_MENU, self.OnMenu, item)
@@ -2005,7 +2230,8 @@ class ControlMixin:
if self.radio_name == Settings[1]: # changed for current radio
if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output',
'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle'):
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec', 'hermes_lite2_enable',
'fixed_tune_offset'):
setattr(conf, name, x)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
@@ -2022,6 +2248,9 @@ class ControlMixin:
elif name == "digital_output_level":
setattr(conf, name, x)
QS.set_sparams(digital_output_level=x)
elif name == "file_play_level":
setattr(conf, name, x)
QS.set_sparams(file_play_level=x)
elif name[0:7] == 'hermes_':
if name == 'hermes_TxLNA_dB':
application.Hardware.ChangeTxLNA(x)
@@ -2033,6 +2262,9 @@ class ControlMixin:
elif hasattr(application.Hardware, "ImmediateChange"):
setattr(conf, name, x)
application.Hardware.ImmediateChange(name)
elif name[0:6] == 'keyer_':
setattr(conf, name, x)
application.ImmediateChange(name)
def FormatOK(self, value, fmt4): # Check formats integer, number, boolean
ok, v = self.EvalItem(value, fmt4)
return ok
@@ -2435,6 +2667,7 @@ class Radios(BaseWindow): # The "Radios" first-level page
choices = []
for name, data in local_conf.receiver_data:
choices.append(name)
choices.sort()
self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True)
self.add_type.SetSelection(0)
item = self.AddTextL(3, "and name the new radio")
@@ -2518,11 +2751,9 @@ class Radios(BaseWindow): # The "Radios" first-level page
choices = Settings[2][:] # can rename any available radio
self.rename_old.SetItems(choices)
self.rename_old.SetSelection(0)
if "ConfigFileRadio" in choices:
if "ConfigFileRadio" in choices: # can not delete ConfigFileRadio
choices.remove("ConfigFileRadio")
if Settings[1] in choices:
choices.remove(Settings[1])
self.delete_name.SetItems(choices) # can not delete ConfigFileRadio nor the current radio
self.delete_name.SetItems(choices)
self.delete_name.SetSelection(0)
choices = Settings[2] + ["Ask me"]
if "ConfigFileRadio" not in choices:
@@ -2667,12 +2898,6 @@ class RadioSection(BaseWindow): # The pages for each section in the second-leve
hlp = "To delete a row, select it and press this button."
self.AddTextButtonHelp(2, "Delete Midi row", "Delete", self.OnMidiDelete, hlp)
self.NextRow()
if self.section == "Timing and CW":
if col != 1:
col = 1
self.NextRow()
self.NextRow()
self.AddTextL(1, 'Midi CW key moved to "Keys"', span=4)
if not Keys:
self.AddColSpacer(2, 20)
self.AddColSpacer(5, 20)
@@ -3399,14 +3624,20 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
('', '', '', '', 'digital_rx8_name'),
('', '', '', '', 'digital_rx9_name'),
)
hRx0 = "This is almost the same as the radio sound, but it is sent to a digital mode program on a different sound device."\
" It is analog audio and it is only sent if one of the DGT- modes is selected."\
" The volume is set by the digital output level instead of the volume control so you can mute the speaker if desired."\
" It can receive a greater bandwidth than the 3000 Hz limit of SSB."
hTx0 = "This is used instead of the microphone when one of the DGT- modes is selected."\
" The normal speech clipping and filtering is not used for DGT- modes."
label_help = ( # Same order as sound_names
(1, "Radio Sound Output", "This is the radio sound going to the headphones or speakers."),
(0, "Microphone Input", "This is the monophonic microphone source. Set the channel if the source is stereo."),
(0, "I/Q Rx Sample Input", "This is the sample source if it comes from a sound device, such as a SoftRock."),
(1, "I/Q Tx Sample Output", "This is the transmit sample audio sent to a SoftRock."),
(1, "Raw Digital Output", "This sends the received I/Q data to another program as stereo."),
(0, "Digital Tx0 Input", "This is the transmit audio coming from a digital mode program."),
(1, "Digital Rx0 Output", "This is the main receiver Rx0 audio going to a digital mode program."),
(0, "Digital Tx0 Input", hTx0),
(1, "Digital Rx0 Output", hRx0),
(1, "Digital Rx1 Output", "This is the sub-receiver 1 audio going to a digital mode program."),
(1, "Digital Rx2 Output", "This is the sub-receiver 2 audio going to a digital mode program."),
(1, "Digital Rx3 Output", "This is the sub-receiver 3 audio going to a digital mode program."),
@@ -3425,13 +3656,13 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
self.radio_dict = local_conf.GetRadioDict(self.radio_name)
self.num_cols = 8
for name, text, fmt, help_text, values in local_conf.GetSectionData('Sound'):
if name == 'digital_output_level':
if name in ('digital_output_level', 'file_play_level'):
value = self.GetValue(name, self.radio_dict)
no_edit = "choice" in fmt or fmt == 'boolean'
txt, cb, btn = self.AddTextComboHelp(1, text, value, values, help_text, no_edit)
cb.handler = self.OnChange
cb.quisk_data_name = name
break
self.NextRow()
self.NextRow()
# Add the grid for the sound settings
sizer = wx.GridBagSizer(2, 2)
@@ -3457,8 +3688,10 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
self.NextRow()
choices = (("48000", "96000", "192000"), ("0", "1"), ("0", "1"), (" ", "0", "1"))
r = 0
if "SoftRock" in self.radio_dict['hardware_file_type']: # Samples come from sound card
softrock = True
if "SoftRock" in self.radio_dict['hardware_file_type']:
softrock = True # Samples come from sound card
elif hasattr(application.Hardware, "use_softrock"):
softrock = application.Hardware.use_softrock
else:
softrock = False
last_row = 8
@@ -3978,28 +4211,7 @@ class BandPlanDlg(wx.Dialog, ControlMixin):
if "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]:
colors = Settings[4]["BandPlanColors"]
else:
Get = conf.__dict__.get # Get colors from the user's config file, if any.
dflt = "#555555"
colors = [
['CW, General', Get("CW", dflt)],
['CW, Extra', Get("eCW", dflt)],
['Phone, General', Get("Phone", dflt)],
['Phone, Extra', Get("ePhone", dflt)],
['AM', Get("AM", dflt)],
['Data', Get("Data", dflt)],
['DxData', Get("DxData", dflt)],
['RTTY', Get("RTTY", dflt)],
['SSTV', Get("SSTV", dflt)],
['Packet', Get("Packet", dflt)],
['Beacons', Get("Beacons", dflt)],
['Satellite', Get("Satellite",dflt)],
['Repeater out', Get("Repeater", dflt)],
['Repeater in', Get("RepInput", dflt)],
['Simplex', Get("Simplex", dflt)],
['Rx only', Get("RxOnly", dflt)],
['Special', Get("Special", dflt)],
['Other', Get("Other", dflt)]
]
colors = conf.BandPlanColors
self.modes = []
self.color_dict = {None:"End"}
self.mode_dict = {"End":None}
@@ -4206,6 +4418,9 @@ The mode begins at the start frequency and ends at the next frequency.'
plan.append([hertz, color])
Settings[4]["BandPlan"] = freq_mode
application.BandPlan = plan
application.BandPlanC2T = {}
for mode, color in mode_color:
application.BandPlanC2T[color] = mode
local_conf.settings_changed = True
self.EndModal(wx.ID_OK)
def OnCancel(self, event):
+50 -23
View File
@@ -194,7 +194,7 @@ Quisk is free open source software written in Python and C. It is hosted on the
<br>
<p>
If you have Python 3 installed on your computer, just continue to use that version.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</b>
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 or 3.12.</b>
If you want to install a more recent version of Python, uninstall the old version first.
Running multiple versions of Python is possible but can be confusing and is not necessary.
A Quisk installation is needed for each version of Python you have.
@@ -204,7 +204,7 @@ A Quisk installation is needed for each version of Python you have.
<p>
Windows does not include Python, so you must first install a recent version of 64-bit Python 3.
If you already have 64-bit Python 3 installed, just keep using it.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</b>
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11 or 3.12.</b>
There may be newer versions of Python, but until support is added, use the versions listed above.
Python is available from <a href="http://www.python.org/">http://www.python.org</a>.
There is an option to <b>Add Python to PATH</b>. You MUST select
@@ -239,13 +239,19 @@ python -m pip install --upgrade pyserial
<br><br>
python -m pip install --upgrade quisk
<br><br>
You should then be able to start Quisk with the command "quisk".
You can also start Quisk with "python -m quisk".
To create a Quisk shortcut on your desktop, right-click an empty space and select "New" and "Shortcut".
Use "quisk.exe" as the command and "Quisk" as the name.
The "quisk.exe" program is in <b>your_install_directory</b>\Scripts.
Enter this as the location of the item:
</p>
<br>
<pre>
pythonw -m quisk
</pre>
<br>
<p>
and then name the shortcut, perhaps "Quisk".
If you check the properties you will see that Windows expanded the location and start directory to the full path.
If you are curious, Quisk and its Python source files are installed in "<b>your_install_directory</b>\Lib\site-packages\quisk".
If you change to that directory you can run Quisk with "python quisk.py".
If you change to that directory you can run Quisk with "pythonw quisk.py".
</p>
<br>
<p>
@@ -824,8 +830,14 @@ SoftRock radios use an analog mixer to change the RF signal to stereo audio, and
A high quality sound card is advisable. The analog mixer is not perfect, and it will be necessary to adjust the I and Q
signals to equal amplitude and 90 degrees phase difference. Use the Config/Config screen buttons to
bring up an adjustment screen. Adjustments must be made for each band, and separately for transmit and receive.
The adjustment depends on both the VFO frequency and the tuning offset from the VFO. See my paper
The adjustment depends on both the VFO frequency and the tuning offset from the VFO.
Quisk has an option to fix the tuning offset to a constant and to tune with the hardware.
The tuning offset then has a constant correction and it is only necessary to correct for the VFO,
a much less sensitive correction.
See my paper
<a href="http://james.ahlstrom.name/phase_corr.html">http://james.ahlstrom.name/phase_corr.html</a> for more information.
<br>
<br>
A good strategy is to pick a VFO near the band center, and record corrections at an Rx frequency of -15000, -1000, 1000 and 15000 Hertz.
If the corrections are sensitive to VFO, record the corrections for these same Rx frequencies at VFOs equal to
or slightly outside the upper and lower band edges.
@@ -834,15 +846,20 @@ You can add as many correction points as desired. The corrections are saved in t
by hand if you are a Python expert. Otherwise you can just read the values.
<br>
<br>
If you fix the tuning offset to a constant, you can create a correction for VFO at the band center, or
the center and both edges.
<br>
<br>
To create a Receive correction point for a given VFO and frequency, attach a signal generator to the SoftRock through an attenuator,
and look at the image on the graph screen. If the signal is 3500 Hertz above the VFO, the image is 3500 Hertz below it.
If you don't have a signal generator, find a strong station on the band and look at its image. Minimize the image by adjusting
the amplitude and phase sliders on the adjustment screen, and then press "Save".
Choose "Measure" to measure the corrections and then "Add Cell".
If you don't have a signal generator, find a strong station on the band and look at its image. Choose "Manual adjustment".
Minimize the image by adjusting the amplitude and phase sliders on the adjustment screen, and then press "Add cell".
<br>
<br>
To create a Transmit correction point for a given VFO and frequency, attach the SoftRock RF output to a spectrum analyzer through an attenuator,
and look at the image. If you don't have a spectrum analyzer use a second receiver tuned to the image. Minimize the image by adjusting
the amplitude and phase sliders on the adjustment screen, and then press "Save".
the amplitude and phase sliders on the adjustment screen, and then press "Add cell".
<br>
<br>
</p>
@@ -1468,24 +1485,34 @@ show the devices being used. Change the Fldigi playback to QuiskDigitalInput, an
Monitor of QuiskDigitalOutput. Then everything should work. You do not need to use pavucontrol again because PulseAudio will
remember the settings.
<br>
<br>
If you don't have PulseAudio or don't want to use it, you can use the ALSA loopback device.
The ALSA loopback device works the same way as the Windows VAC. First
create the loopback device with the command "modprobe snd-aloop" (you
will need to be root). You can create the loopback device
when the system starts, but the way to do that depends on your version of
Linux. I added snd-aloop to /etc/modules. You could put the modprobe command in /etc/rc.local instead.
Michael, DK1MI/N1BSD, has written a nice description of how to set up Fldigi. It is available
at his site <a href="https://dk1mi.radio/quisk-fldigi/">https://dk1mi.radio/quisk-fldigi/</a>.
<br>
<br>
If your digital program only uses Alsa devices or you don't want to use PulseAudio,
you can use the ALSA loopback device.
It works the same way as the Windows VAC. First create the loopback device with this command:
<pre>
sudo modprobe snd-aloop index=19
</pre>
The "index" is the card number, and it is best to use a number higher than your regular cards.
The loopback device will be gone on the next restart.
You can create the loopback device every time the system starts, but the way to do that depends on your version of Linux.
You could add this line to /etc/modprobe.d/alsa-base.conf:
<pre>
options snd-aloop index=19
</pre>
Or you could add snd-aloop to /etc/modules or put the modprobe command in /etc/rc.local instead.
Restart Linux. Now you can enter "cat /proc/asound/cards" to
print out your sound cards, and you should see a "Loopback" card
listed. The cards are also shown on the Quisk Sound
screen. The Loopback card has one side that connects to Quisk and
print out your sound cards, and you should see a "Loopback" card listed.
The Loopback card has one side that connects to Quisk and
another side that connects to your digital program. For the Quisk
side connect both Digital Input and Digital Output to Loopback,0.
side connect both Digital Input and Digital Output to the Loopback at hw:19,0.
Note that the Loopback card is full duplex, and handles both
input and output. There are actually eight loopbacks created at once,
but we are only using subdevice 0. For the digital program side, set the input and
output to "Loopback,1".
output to the Loopback at hw:19,1.
Your audio is now connected and you
should be able to receive digital signals. Be sure to test your
transmit signal off the air. You may need to reduce power to
+1
View File
@@ -168,6 +168,7 @@ static void modem_status(void)
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <termios.h>
static int fdComm = -1; // File descriptor to read the serial port
+7 -6
View File
@@ -372,7 +372,7 @@ static void process_alc(complex double * cSamples, int count, struct alc * pt, r
static int tx_filter(complex double * filtered, int count)
{ // Input samples are creal(filtered), output is filtered. The input rate must be 8000 or 48000 sps.
int i, is_ssb;
int i, is_ssb, mic_interp;
int sample_rate = 8000;
double dsample, dtmp, magn;
complex double csample;
@@ -423,9 +423,10 @@ static int tx_filter(complex double * filtered, int count)
}
return 0;
}
mic_interp = MIC_OUT_RATE / sample_rate;
// check size of dsamples[] and csamples[] buffer
if (count > samples_size) {
samples_size = count * 2;
samples_size = count * 2 * mic_interp;
if (dsamples)
free(dsamples);
if (csamples)
@@ -541,8 +542,8 @@ static int tx_filter(complex double * filtered, int count)
// remove clipping distortion
count = quisk_cDecimate(csamples, count, &cfiltAudio3, 1);
// Interpolate up to 48000
if (MIC_OUT_RATE != sample_rate)
count = quisk_cInterpolate(csamples, count, &cfiltInterp, MIC_OUT_RATE / sample_rate);
if (mic_interp != 1)
count = quisk_cInterpolate(csamples, count, &cfiltInterp, mic_interp);
// convert back to 16 bits
for (i = 0; i < count; i++) {
filtered[i] = csamples[i] * CLIP16;
@@ -573,8 +574,8 @@ static int tx_filter(complex double * filtered, int count)
// remove clipping distortion
count = quisk_dFilter(dsamples, count, &dfiltAudio3);
// Interpolate up to 48000
if (MIC_OUT_RATE != sample_rate)
count = quisk_dInterpolate(dsamples, count, &dfiltInterp, MIC_OUT_RATE / sample_rate);
if (mic_interp != 1)
count = quisk_dInterpolate(dsamples, count, &dfiltInterp, mic_interp);
// convert back to 16 bits
for (i = 0; i < count; i++) {
filtered[i] = dsamples[i] * CLIP16;
+1
View File
@@ -0,0 +1 @@
#
+209
View File
@@ -0,0 +1,209 @@
# This file is part of the Multus project. See https://www.multus-sdr.com/.
# It controls what items appear on the Hardware configuration screen and
# provides logic for each radio.
################ Receivers Multus CW, A transceiver from the Multus project.
## hardware_file_name Hardware file path, rfile
# This is the file that contains the control logic for each radio.
#hardware_file_name = "multuspkg/quisk_hardware.py"
## widgets_file_name Widget file path, rfile
# This optional file adds additional controls for the radio.
#widgets_file_name = ""
## rx_max_amplitude_correct Max ampl correct, number
# 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 correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control.
#rx_max_amplitude_correct = 0.2
## rx_max_phase_correct Max phase correct, number
# 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 correction is
# entered using the controls from the "Rx Phase" button on the config screen.
# You must enter a positive number. This controls the range of the control in degrees.
#rx_max_phase_correct = 10.0
## tx_level Tx Level, dict
# This is the level of the Tx audio sent to SoftRock hardware after all processing as a percentage
# number from 0 to 100.
# The level should be below 100 to allow headroom for amplitude and phase adjustments.
# Changes are immediate (no need to restart).
#tx_level = {}
## digital_tx_level Digital Tx power %, integer
# Digital modes reduce power by the percentage on the config screen.
# This is the maximum value of the slider.
#digital_tx_level = 100
## keyer_speed Keyer speed WPM, integer
# This sets the keyer speed in words per minute.
#keyer_speed = 8
#keyer_speed = 13
#keyer_speed = 18
#keyer_speed = 20
#keyer_speed = 30
#keyer_speed = 40
## keyer_type Type of keyer, text choice
# Use "Straight" mode for a straight key or a bug. Connect the tip of the key to the transceiver.
# Otherwise, select Iambic-A or Iambic-B mode of operation.
#keyer_type = "Straight"
#keyer_type = "Iambic-A"
#keyer_type = "Iambic-B"
## keyer_space Keyer space, text choice
# This sets the type of spacing between elements or letters.
# "Element" attempts to provide proper spacing between the elements of a character.
# (The keyer always provides proper spacing between elements).
# "Letter" turns on letter spacing. This attempts to provide proper spacing between characters.
#keyer_space = "Element"
#keyer_space = "Letter"
## keyer_weight Keyer weight, integer choice
# A weight of 50% means that the dit time equals the inter-element time, which is standard.
# Reducing the weight increases the inter-element time, increasing the weight reduces it.
#keyer_weight = 50
#keyer_weight = 25
#keyer_weight = 75
## keyer_paddle Keyer paddle, text choice
# "Normal" means the left paddle is DIT. "Reverse" means the left paddle is DAH.
#keyer_paddle = "Normal"
#keyer_paddle = "Reverse"
import configure
import softrock
from softrock.hardware_usb import Hardware as BaseHardware
from softrock.hardware_usb import IN, OUT, UBYTE2, UBYTE4
import _quisk as QS
DEBUG = 0
class Hardware(BaseHardware):
def __init__(self, app, conf):
# Set constant parameters here:
conf.usb_vendor_id = 0x16c0
conf.usb_product_id = 0x05dc
conf.si570_i2c_address = 0x55
conf.si570_direct_control = False
conf.si570_xtal_freq = 114285000
conf.repeater_delay = 0.25
self.ptt_count = 200
self.ptt_on = 0
BaseHardware.__init__(self, app, conf)
QS.set_sparams(multus_cw_samples=1)
self.use_softrock = True
if DEBUG:
print ("The name of this radio is", configure.Settings[1])
print ("The hardware file type is", conf.hardware_file_type)
def TransferOut(self, address, message): # message is a bytes array or an integer 0 to 255
if isinstance(message, int):
message = message.to_bytes(1, 'big')
if self.usb_dev:
self.usb_dev.ctrl_transfer(OUT, address, self.si570_i2c_address + 0x700, 0, message)
if DEBUG:
print ("USB send to 0x%X" % address, message)
def TransferIn(self, address, length):
if self.usb_dev:
recv = self.usb_dev.ctrl_transfer(IN, address, self.si570_i2c_address + 0x700, 0, length)
if DEBUG:
print ("USB receive from 0x%X" % address, recv)
return recv
return None
def open(self):
text = BaseHardware.open(self)
self.InitKeyer()
return text
def ChangeMode(self, mode):
ret = BaseHardware.ChangeMode(self, mode)
if mode in ("CWL", "CWU"):
cw_mode = b'C' # Adding "b" does not affect the value sent
if DEBUG:
print ("Mode is CW")
else:
cw_mode = b'U'
if DEBUG:
print ("Mode is not CW")
self.TransferOut(0x70, cw_mode)
return ret
def PollCwKey(self): # Called frequently from the sound thread to check the CW key status
return # Quisk is always in Rx
def PollGuiControl(self): # Called frequently from the GUI thread
self.ptt_count -= 1
if self.ptt_count <= 0:
self.ptt_count = 200
reply = self.TransferIn(0xA5, 1)
if DEBUG:
print ("PollGuiControl got", reply)
if reply:
ptt = reply[0] # This is 255 for error
if ptt in (0, 1) and ptt != self.ptt_on:
self.ptt_on = ptt
self.application.pttButton.SetValue(ptt, True)
def InitKeyer(self):
# Initialize the keyer parameters
conf = self.conf
if not hasattr(conf, "keyer_speed"):
conf.keyer_speed = 18
if not hasattr(conf, "keyer_type"):
conf.keyer_type = "Straight"
if not hasattr(conf, "keyer_space"):
conf.keyer_space = "Element"
if not hasattr(conf, "keyer_weight"):
conf.keyer_weight = 50
if not hasattr(conf, "keyer_paddle"):
conf.keyer_paddle = "Normal"
# We need to initialize in case the persistent hardware values differ from the Quisk values.
for name in ("keyer_speed", "keyer_type", "keyer_space", "keyer_weight", "keyer_paddle", "cwTone"):
self.ImmediateChange(name)
def ImmediateChange(self, name):
BaseHardware.ImmediateChange(self, name)
value = getattr(self.conf, name)
if DEBUG:
print ("ImmediateChange", name, value)
if name == "keyer_speed":
self.TransferOut(0x7B, value)
elif name == "keyer_type":
if value == "Straight":
mode = 0
elif value == "Iambic-A":
mode = 1
elif value == "Iambic-B":
mode = 2
else:
mode = 0
self.TransferOut(0x71, mode)
elif name == "keyer_space":
if value == "Element":
spacing = 0
elif value == "Letter":
spacing = 1
else:
spacing = 0
self.TransferOut(0x75, spacing)
elif name == "keyer_weight":
weight = int(value)
self.TransferOut(0x77, weight)
elif name == "keyer_paddle":
if value == "Normal":
paddle = 0
elif value == "Reverse":
paddle = 1
else:
paddle = 0
self.TransferOut(0x73, paddle)
elif name == "cwTone":
if value < 500:
tone_index = 0 # 400 Hz
elif 500 <= value < 700:
tone_index = 1 # 600 Hz
elif 700 <= value < 900:
tone_index = 2 # 800 Hz
else:
tone_index = 3 # 1000 Hz
self.TransferOut(0x7F, tone_index)
+183 -3
View File
@@ -152,9 +152,14 @@ static int quisk_use_sidetone; // is there a sidetone volume control?
static int hl2_txbuf_errors; // errors in the Hermes-Lite2 Tx buffer
static int hl2_txbuf_state; // state machine for errors in the Hermes-Lite2 Tx buffer
static int freedv_monitor; // pass the Freedv audio to the speakers instead of to the demodulator
static int softrock_correct_freq; // gain and phase corrections for SoftRock
static double softrock_correct_gain;
static double softrock_correct_phase;
static int softrock_correct_active; // 0 No correction adjustment; 1 Manual adjustment; 2 Calculate Rx corrections
static complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python
static int PySampleCount; // count of samples in buffer
static bool quisk_tx_inhibit;
static int multirx_data_width; // width of graph data to return
static int multirx_fft_width; // size of FFT samples
@@ -181,6 +186,7 @@ int quisk_sidetoneFreq; // frequency in hertz for the sidetone
int quisk_start_cw_delay = 15; // milliseconds to delay output on CW key down
int quisk_start_ssb_delay = 100; // milliseconds to discard output on SSB etc. key down
static int maximum_tx_secs; // Failsafe timeout for Tx in seconds
static int TxRxSilenceMsec = 50; // Play silence in msec on Tx to Rx
static int key_is_down = 0; // internal key state up or down
static double agcReleaseGain=80; // AGC maximum gain
@@ -1921,6 +1927,10 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
}
if(bank == 0)
dAutoNotch(dsamples, nSamples, rit_freq, quisk_filter_srate);
if (ssb_squelch_enabled) {
ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank);
d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE);
}
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio12p2, 2);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand6);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7);
@@ -1940,6 +1950,10 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
}
if(bank == 0)
dAutoNotch(dsamples, nSamples, rit_freq, quisk_filter_srate);
if (ssb_squelch_enabled) {
ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank);
d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE);
}
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio12p2, 2);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand6);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7);
@@ -1962,7 +1976,7 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank);
d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE);
}
quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
//quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p4, 2);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7);
//quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1);
@@ -2458,7 +2472,7 @@ int quisk_process_samples(complex double * cSamples, int nSamples)
quisk_sound_state.sample_rate;
nout = (int)dOutCounter; // number of samples to output
dOutCounter -= nout;
playSilence = (int)(quisk_sound_state.playback_rate * 50e-3); // Play silence after sidetone ends, number of samples
playSilence = (int)(quisk_sound_state.playback_rate * 1E-3 * TxRxSilenceMsec); // Play silence after sidetone ends, number of samples
keyupEnvelope = 0;
if (quisk_active_sidetone == 2 && QUISK_CWKEY_DOWN) { // Play sidetone instead of radio for CW
if (! sidetoneIsOn) { // turn on sidetone
@@ -2732,7 +2746,7 @@ int quisk_process_samples(complex double * cSamples, int nSamples)
multirx_cSamples[0][i] = dsamples2[i] + I * dsamples2[i];
process_agc(&Agc3, multirx_cSamples[0], n, 0);
}
play_sound_interface(quiskPlaybackDevices[QUISK_INDEX_SUB_RX1], n, multirx_cSamples[0], 1, 1.0);
play_sound_interface(quiskPlaybackDevices[QUISK_INDEX_SUB_RX1], n, multirx_cSamples[0], 1, digital_output_level);
}
// Perhaps decimate by an additional fraction
@@ -3147,6 +3161,10 @@ static PyObject * get_params(PyObject * self, PyObject * args)
return PyInt_FromLong(quisk_serial_ptt);
if (strcmp(name, "hl2_txbuf_errors") == 0)
return PyInt_FromLong(hl2_txbuf_errors);
if (strcmp(name, "softrock_correct_active") == 0)
return PyInt_FromLong((long)softrock_correct_active);
if (strcmp(name, "quisk_tx_inhibit") == 0)
return PyInt_FromLong((long)quisk_tx_inhibit);
Py_INCREF (Py_None);
return Py_None;
}
@@ -3750,6 +3768,7 @@ static int read_rx_udp10(complex double * samp) // Read samples from UDP using t
//code_version = quisk_hermes_to_pc[3];
if ((quisk_hermes_to_pc[0] & 0x01) != 0) // C1
quisk_sound_state.overrange++;
quisk_tx_inhibit = (quisk_hermes_to_pc[0] & 0x02) == 0; // should limit to Hermes Lite2 only
hardware_ptt = buf[start] & 0x01; // C0 bit zero is PTT
quisk_hardware_cwkey = (buf[start] & 0x04) >> 2; // C0 bit two is CW key state
switch (hl2_txbuf_state) {
@@ -3861,6 +3880,10 @@ static int read_rx_udp17(complex double * cSamples0) // Read samples from UDP
fft_data * ptFFT;
fd_set fds;
static int block_number=0;
static complex double dc_average = 0; // Average DC component in samples
static complex double dc_sum = 0;
static int dc_count = 0;
static time_t time0 = 0;
// Data from the receiver is little-endian
if ( ! rx_udp_gain_correct) { // correct for second stage CIC decimation JIM JIM
@@ -3966,6 +3989,16 @@ static int read_rx_udp17(complex double * cSamples0) // Read samples from UDP
if (xr & 0x100) { // channel 1
if (quisk_invert_spectrum) // Invert spectrum
sample = conj(sample);
// correct the DC level
dc_sum += sample;
dc_count++;
sample -= dc_average;
if (dc_count >= 48000 && time(NULL) != time0) {
time0 = time(NULL);
dc_average = dc_sum / dc_count;
dc_sum = dc_count = 0;
//printf ("New DC average %.0lf\n", cabs(dc_average));
}
// Put samples into the fft input array.
ptFFT = fft_data_array + fft_data_index;
if ( ! (xi & 0x100)) { // zero marker for start of first block
@@ -4233,6 +4266,13 @@ static PyObject * change_scan(PyObject * self, PyObject * args) // Called from G
return Py_None;
}
static PyObject * softrock_corrections(PyObject * self, PyObject * args) // Called from GUI thread
{
if (!PyArg_ParseTuple (args, "i", &softrock_correct_active))
return NULL;
return Py_BuildValue("idd", softrock_correct_freq, softrock_correct_gain, softrock_correct_phase);
}
static PyObject * change_rates(PyObject * self, PyObject * args) // Called from GUI thread
{ // Change to new sample rates
@@ -4694,6 +4734,9 @@ static PyObject * ImmediateChange(PyObject * self, PyObject * args) // called fr
else if ( ! strcmp(name, "maximum_tx_secs")) {
maximum_tx_secs = QuiskGetConfigInt(name, 0);
}
else if ( ! strcmp(name, "TxRxSilenceMsec")) {
TxRxSilenceMsec = QuiskGetConfigInt(name, 50);
}
Py_INCREF (Py_None);
return Py_None;
}
@@ -5017,6 +5060,135 @@ static PyObject * get_bandscope(PyObject * self, PyObject * args) // Called by t
return Py_None;
}
#if 0
static void check_channel_delay(fft_data * ptFft)
{
int i, maxi, freq;
static int count=0, freq0;
static double gain=0, phase=0;
double d, ampl, maxa;
static double complex * samples1=NULL, * samples2;
if (samples1 == NULL) {
samples1 = (double complex *)malloc(fft_size * sizeof(double complex));
samples2 = (double complex *)malloc(fft_size * sizeof(double complex));
}
for (i = 0; i < fft_size; i++) {
samples1[i] = creal(ptFft->samples[i]);
samples2[i] = cimag(ptFft->samples[i]);
}
fftw_execute_dft(quisk_fft_plan, samples1, samples1);
fftw_execute_dft(quisk_fft_plan, samples2, samples2);
maxa = 0;
maxi = 0;
for (i = 0; i < fft_size / 2; i++) {
ampl = cabs(samples1[i]);
if (ampl > maxa) {
maxa = ampl;
maxi = i;
}
}
freq = maxi * fft_sample_rate / fft_size;
//freq = maxi * 44100 / fft_size;
gain += cabs(samples1[maxi]) / cabs(samples2[maxi]);
d = carg(samples1[maxi]) - carg(samples2[maxi]);
if (d > M_PI)
d -= M_PI * 2;
else if(d < - M_PI)
d += M_PI * 2;
phase += d;
count += 1;
if (count == 1) {
freq0 = freq;
}
else if (freq != freq0) {
gain = phase = count = 0;
return;
}
if (count > 10) {
gain /= count;
phase /= count;
//printf("Channel delay freq %6d, gain %6.3lf, phase %8.5lf radians, %6.3lf degrees\n",
// freq, gain, phase, phase * 57.29578);
printf("%6d, %6.3lf, %6.3lf\n", freq, gain, phase * 57.29578);
gain = phase = count = 0;
}
}
#endif
static void softrock_correct_fft(fft_data * ptFft, int fixit)
{ // See i/Q balance in Rocky: https://www.dxatlas.com/Rocky/Advanced.asp
double ampl, r1, r2, i1, i2, Zr, Zi, Pwr, Gain, Phase, OutR, OutI, S1R, S1I, S2R, S2I;
static double tot_ga=0, tot_ph=0;
static int count=0;
double maxa = 0;
int i, maxi = 0, freq;
double complex Z, s1, s2, Hr, Hi, S1, S2;
double tim;
static double time0 = 0;
for (i = 0; i < fft_size; i++) {
ampl = cabs(ptFft->samples[i]);
if (ampl > maxa) {
maxa = ampl;
maxi = i;
}
}
if (maxi < fft_size / 2)
freq = maxi;
else
freq = maxi - fft_size;
freq = freq * fft_sample_rate / fft_size;
s1 = ptFft->samples[maxi];
s2 = ptFft->samples[fft_size - maxi];
Z = s1 * s2;
r1 = creal(s1);
r2 = creal(s2);
i1 = cimag(s1);
i2 = cimag(s2);
Pwr = r1 * r1 + i1 * i1 + r2 * r2 + i2 * i2;
Zr = creal(Z) / Pwr;
Zi = cimag(Z) / Pwr;
Gain = sqrt((1.0 + 2 * Zr) / (1.0 - 2 * Zr));
Phase = asin(Zi * (Gain * Gain + 1.0) / Gain);
tot_ga += Gain;
tot_ph += Phase;
count += 1;
tim = QuiskTimeSec();
if (tim - time0 >= 0.5) {
time0 = tim;
softrock_correct_freq = freq;
softrock_correct_gain = tot_ga / count;
softrock_correct_phase = tot_ph / count;
//if (softrock_correct_active)
// printf("Freq domain Gain %10.7lf, Phase %10.7lf, %10.7lf, freq %d\n",
// softrock_correct_gain, softrock_correct_phase, softrock_correct_phase * 57.29578, freq);
tot_ga = tot_ph = count = 0;
}
if (fixit) {
Hr = softrock_correct_gain * cos(softrock_correct_phase);
Hi = softrock_correct_gain * sin(softrock_correct_phase);
for (i = 1; i < fft_size / 2; i++) {
S1 = ptFft->samples[fft_size - i];
S2 = ptFft->samples[i];
S1R = creal(S1);
S1I = cimag(S1);
S2R = creal(S2);
S2I = cimag(S2);
OutR = S2R + S1R - (S2I + S1I) * Hi + (S2R - S1R) * Hr;
OutI = S2I - S1I + (S2I + S1I) * Hr + (S2R - S1R) * Hi;
ptFft->samples[i] = OutR + I * OutI;
S1R = creal(S2);
S1I = cimag(S2);
S2R = creal(S1);
S2I = cimag(S1);
OutR = S2R + S1R - (S2I + S1I) * Hi + (S2R - S1R) * Hr;
OutI = S2I - S1I + (S2I + S1I) * Hr + (S2R - S1R) * Hi;
ptFft->samples[fft_size - i] = OutR + I * OutI;
}
}
}
static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the GUI thread
{
int i, j, k, m, n, index, ffts, ii, mm, m0, deltam;
@@ -5089,7 +5261,10 @@ static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the G
// Continue with FFT calculation
for (i = 0; i < fft_size; i++) // multiply by window
ptFft->samples[i] *= fft_window[i];
//check_channel_delay(ptFft);
fftw_execute_dft(quisk_fft_plan, ptFft->samples, ptFft->samples); // Calculate FFT
if (softrock_correct_active == 2)
softrock_correct_fft(ptFft, 0);
// Create RMS s-meter value at known bandwidth
// The pass band is (rx_tune_freq + filter_start_offset) to += bandwidth
// d1 is the tune frequency
@@ -5140,6 +5315,9 @@ static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the G
//QuiskPrintf(" %d %.4lf At %5d to %5d place %5d to %5d for block %d\n", fft_size, scan_valid, mm, m, ii, i, ptFft->block);
}
else {
// Zero frequency is at index fft_size / 2.
// There are fft_size/2 positive frequencies and fft_size/2-1 negative frequencies.
// The frequency at index k and (fft_size - k) are equal except for sign.
count_fft++;
k = 0;
for (i = fft_size / 2; i < fft_size; i++) // Negative frequencies
@@ -5851,6 +6029,7 @@ static PyObject * record_app(PyObject * self, PyObject * args)
quisk_start_cw_delay = QuiskGetConfigInt("start_cw_delay", 15);
quisk_start_ssb_delay = QuiskGetConfigInt("start_ssb_delay", 100);
maximum_tx_secs = QuiskGetConfigInt("maximum_tx_secs", 0);
TxRxSilenceMsec = QuiskGetConfigInt("TxRxSilenceMsec", 50);
quisk_sound_state.sample_rate = rate;
fft_sample_rate = rate;
is_little_endian = 1; // Test machine byte order
@@ -5964,6 +6143,7 @@ static PyMethodDef QuiskMethods[] = {
{"get_filter_rate", get_filter_rate, METH_VARARGS, "Return the sample rate used for the filters."},
{"get_tx_filter", quisk_get_tx_filter, METH_VARARGS, "Return the frequency response of the transmit filter."},
{"get_audio_graph", get_audio_graph, METH_VARARGS, "Return a tuple of the audio graph data."},
{"softrock_corrections", softrock_corrections, METH_VARARGS, "Control and return SoftRock amplitude and phase corrections."},
{"measure_frequency", measure_frequency, METH_VARARGS, "Set the method, return the measured frequency."},
{"measure_audio", measure_audio, METH_VARARGS, "Set the method, return the measured audio voltage."},
{"get_overrange", get_overrange, METH_VARARGS, "Return the count of overrange (clip) for the ADC."},
+1
View File
@@ -320,6 +320,7 @@ extern int quisk_start_cw_delay; // milliseconds to delay output on serial or M
extern int quisk_start_ssb_delay; // milliseconds to discard output for all modes except CW
extern struct sound_dev * quiskPlaybackDevices[]; // array of Playback sound devices
extern int quisk_close_file_play;
extern double digital_output_level;
extern PyObject * quisk_set_spot_level(PyObject * , PyObject *);
extern PyObject * quisk_get_tx_filter(PyObject * , PyObject *);
+177 -55
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# All QUISK software is Copyright (C) 2006-2021 by James C. Ahlstrom.
# All QUISK software is Copyright (C) 2006-2024 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!!
@@ -334,6 +334,7 @@ class HamlibHandlerSerial:
self.public_name = public_name # the public name for the serial port
self.tune_step = 1000
if sys.platform == 'win32' or public_name.startswith("/dev/"):
self.port_is_mod_serial = True # self.port is from the serial module
try:
import serial
except:
@@ -346,6 +347,7 @@ class HamlibHandlerSerial:
print ("The serial port %s could not be opened." % public_name)
else:
import tty
self.port_is_mod_serial = False # self.port is a file descriptor
if os.path.lexists(public_name):
try:
os.remove(public_name)
@@ -369,7 +371,7 @@ class HamlibHandlerSerial:
def open(self):
return
def close(self):
if sys.platform == 'win32':
if self.port_is_mod_serial:
if self.port:
self.port.close()
self.port = None
@@ -382,7 +384,7 @@ class HamlibHandlerSerial:
def Read(self):
if self.port is None:
return
if sys.platform == 'win32':
if self.port_is_mod_serial:
text = self.port.read(99)
if not isinstance(text, Q3StringTypes):
text = text.decode('utf-8')
@@ -433,22 +435,14 @@ class HamlibHandlerSerial:
return
if isinstance(data, Q3StringTypes):
data = data.encode('utf-8', errors='ignore')
if sys.platform == 'win32':
if self.port_is_mod_serial:
self.port.write(data)
else:
r, w, x = select.select((), (self.port,), (), 0)
if w:
os.write(self.port, data)
def set_frequency(self, freq):
tune = freq - self.app.VFO
d = self.app.sample_rate * 45 // 100
if -d <= tune <= d: # Frequency is on-screen
vfo = self.app.VFO
else: # Change the VFO
vfo = (freq // 5000) * 5000 - 5000
tune = freq - vfo
self.app.BandFromFreq(freq)
self.app.ChangeHwFrequency(tune, vfo, 'FreqEntry')
def set_frequency(self, freq): # This changes the Rx frequency
self.app.ChangeRxTxFrequency(freq, None)
if HAMLIB_DEBUG: print("New Freq rx,tx", self.app.txFreq + self.app.VFO, self.app.rxFreq + self.app.VFO)
def ZZAC(self, cmd, data, length): # Set or read tune steps
if length == 0:
@@ -658,6 +652,16 @@ class HamlibHandlerSerial:
self.app.pttButton.SetValue(0, True)
else:
self.Error(cmd, data)
def ZZSM(self, cmd, data, length): # return the S-meter value in dB * 2; 0 to 260
if length == 0: # 0 to 260 is -140 to -10 dB; S9 == -73 dB == 134; dB = ZZSM / 2 - 140
i = round((self.app.hamlib_strength + 67) * 2)
if i < 0:
i = 0
elif i > 260:
i = 260
self.Write('%s%03d;' % (cmd, i))
else:
self.Error(cmd, data)
def ZZSP(self, cmd, data, length): # the split status
if length == 0:
if self.app.split_rxtx:
@@ -826,6 +830,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
"""Send text back to the client."""
if isinstance(text, Q3StringTypes):
text = text.encode('utf-8', errors='ignore')
if HAMLIB_DEBUG:
print ("Rig2 send", text)
try:
self.sock.sendall(text)
except socket.error:
@@ -932,6 +938,8 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
else:
return 1
self.input = self.input.strip() # Here is our command line
if HAMLIB_DEBUG:
print ("Rig2 received", self.input)
while self.input:
# Parse the commands and call the appropriate handlers
self.params = ''
@@ -982,8 +990,12 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
if rx:
rx = rx[0]
self.Reply('Frequency', rx.txFreq + rx.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq rx", rx.txFreq, rx.VFO)
else:
self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0)
if HAMLIB_DEBUG:
print ("GetFreq app", self.app.rxFreq, self.app.txFreq, self.app.VFO)
def SetFreq(self): # The Rx frequency
freq = self.GetParamNumber()
try:
@@ -1151,7 +1163,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
def GetLevel(self):
name = self.GetParamName()
if name == '?': # send back supported parameters
self.Reply2('AGC AF', 0)
self.Reply2('AGC AF STRENGTH', 0)
elif name == 'AGC':
if self.app.BtnAGC.GetValue():
self.Reply2(4, 0)
@@ -1160,6 +1172,9 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
elif name == 'AF':
x = self.app.audio_volume # audio_volume is 0 to 1.000
self.Reply2("%.5f" % x, 0)
elif name == 'STRENGTH':
i = round(self.app.hamlib_strength)
self.Reply2(i, 0)
else:
self.ErrParam()
def SetLevel(self):
@@ -1762,6 +1777,12 @@ class GraphDisplay(wx.Window):
dc.SetTextForeground(conf.color_graph_msg_fg)
dc.SetBackgroundMode(wx.SOLID)
dc.DrawText(self.display_text, 0, 0)
if application.tx_inhibit:
dc.SetFont(self.font)
dc.SetTextBackground(conf.color_graph_msg_bg)
dc.SetTextForeground("red")
dc.SetBackgroundMode(wx.SOLID)
dc.DrawText(" *** Tx Inhibit ***", 0, self.chary * 15 // 10)
def DrawFilter(self, dc):
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetLogicalFunction(wx.COPY)
@@ -1873,6 +1894,12 @@ class GraphScreen(wx.Window):
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
# handle bandplan info
self.bandplanWindow = wx.PopupWindow (parent)
self.bandplanInfo = wx.TextCtrl(self.bandplanWindow, style=wx.TE_READONLY)
self.bandplanInfo.SetFont(wx.Font(conf.status_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface))
self.bandplanWindow.Hide();
self.MakeDisplay()
def MakeDisplay(self):
self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary)
@@ -2096,7 +2123,7 @@ class GraphScreen(wx.Window):
"""For mouse clicks in our display, translate to our screen coordinates."""
mouse_x, mouse_y = event.GetPosition()
win = event.GetEventObject()
if win is not self:
if win and win is not self:
x, y = win.GetPosition().Get()
mouse_x += x
mouse_y += y
@@ -2193,7 +2220,8 @@ class GraphScreen(wx.Window):
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseBtn1', event=event, rx_freq=freq)
else:
self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event)
self.CaptureMouse()
if not self.HasCapture():
self.CaptureMouse()
def OnLeftUp(self, event):
if self.HasCapture():
self.ReleaseMouse()
@@ -2206,7 +2234,7 @@ class GraphScreen(wx.Window):
mouse_x, mouse_y = self.GetMousePosition(event)
if wx.GetKeyState(wx.WXK_SHIFT):
mouse_x -= self.filter_center * self.data_width / sample_rate
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency
if False: #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 = float(x) * sample_rate / self.data_width
@@ -2224,6 +2252,44 @@ class GraphScreen(wx.Window):
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseMotion', event=event, rx_freq=freq2)
else: # Mouse motion changes the transmit frequency
self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event=event)
else:
mouse_x, mouse_y = self.GetMousePosition(event)
if not self.in_splitter and self.originY <= mouse_y <= self.originY + self.tick and (
self.originX <= mouse_x <= self.originX + self.graph_width):
#sample_rate = int(self.sample_rate * self.zoom) # Measure the frequency
#freq = float(mouse_x - self.x0) * sample_rate / self.data_width + self.zoom_deltaf + self.VFO
#freq = int(freq)
#color = None
#for start_freq, start_color in application.BandPlan:
# if start_freq >= freq:
# break
# color = start_color
dc = wx.ClientDC(self) # Read the pixel at the mouse position
pixel = dc.GetPixel(mouse_x, mouse_y)
pixel = "#%02X%02X%02X" % (pixel[0], pixel[1], pixel[2])
txt = application.BandPlanC2T.get(pixel, '')
if pixel == "#000000":
pass
elif txt:
self.bandplanInfo.Clear()
self.bandplanInfo.write(txt)
width, height = self.bandplanInfo.GetTextExtent(txt)
width += self.bandplanInfo.GetCharWidth() * 2
height += self.bandplanInfo.GetCharHeight() // 2
self.bandplanInfo.SetSize(wx.Size(width, height))
self.bandplanWindow.SetClientSize((width, height))
rect = self.GetScreenRect()
if mouse_x > self.originX + self.graph_width * 3 // 4:
self.bandplanWindow.Move(mouse_x + rect.GetX() - width, mouse_y + rect.GetY() - height * 12 // 10)
else:
self.bandplanWindow.Move(mouse_x + rect.GetX(), mouse_y + rect.GetY() - height * 12 // 10)
if not self.bandplanWindow.IsShown():
self.bandplanWindow.Show()
else:
if self.bandplanWindow.IsShown():
self.bandplanWindow.Hide()
elif self.bandplanWindow.IsShown():
self.bandplanWindow.Hide()
def OnWheel(self, event):
if conf.freq_spacing:
wm = conf.freq_spacing
@@ -2474,12 +2540,10 @@ class WaterfallDisplay(wx.Window):
self.top_key = 8
self.top_size = (self.top_key + 2) * (self.top_key - 1) // 2
# Make the palette
if conf.waterfall_palette == 'B':
pal2 = conf.waterfallPaletteB
elif conf.waterfall_palette == 'C':
pal2 = conf.waterfallPaletteC
else:
if conf.waterfall_palette == 'A':
pal2 = conf.waterfallPalette
else:
pal2 = getattr(conf, "waterfallPalette" + conf.waterfall_palette)
red = bytearray(256)
green = bytearray(256)
blue = bytearray(256)
@@ -2573,7 +2637,7 @@ class WaterfallScreen(wx.SplitterWindow):
self.y_zero = conf.waterfall_y_zero
self.zoom_control = 0
wx.SplitterWindow.__init__(self, frame)
self.SetSizeHints(width, -1, width)
self.SetSizeHints(width, -1)
self.SetSashGravity(0.50)
self.SetMinimumPaneSize(1)
self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size
@@ -2927,7 +2991,7 @@ class MultiReceiverScreen(wx.SplitterWindow):
self.rx_zero = self.graph
self.Initialize(self.rx_zero)
self.waterfall.Hide()
self.SetSizeHints(self.width, -1, self.width)
self.SetSizeHints(self.width, -1)
# Calculate control width
rx_btn = QuiskPushbutton(self, None, "Rx 8....", style=wx.BU_EXACTFIT)
self.rx_btn_width, self.rx_btn_height = rx_btn.GetSize().Get()
@@ -3460,6 +3524,7 @@ class App(wx.App):
if not os.path.isdir(self.QuiskFilesDir):
self.QuiskFilesDir = DefaultConfigDir
self.std_out_err = StdOutput(self)
self.std_out_err.Logfile("WxPython " + wx.version())
QS.set_params(quisk_is_vna=0) # We are not the VNA program
# Read in configuration from the selected radio
self.BandPlan = []
@@ -3497,6 +3562,8 @@ class App(wx.App):
self.midi_message = []
self.idName2Button = {}
self.midi_handler = None
self.tx_inhibit = 0
self.old_tx_inhibit = 0
if conf.use_rx_udp == 10: # Hermes UDP protocol
self.bandscope_clock = conf.rx_udp_clock
else:
@@ -3548,6 +3615,7 @@ class App(wx.App):
setattr(conf, 'widgets_file_name', '')
Hardware = self.Hardware
self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat")
self.poll_gui_control = hasattr(Hardware, "PollGuiControl")
# Initialization - may be over-written by persistent state
self.local_conf.Initialize()
self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow
@@ -3557,12 +3625,13 @@ class App(wx.App):
self.smeter_avg_seconds = 1.0 # seconds for S-meter average
self.smeter_sunits = -87.0
self.smeter_usage = "smeter" # specify use of s-meter display
self.hamlib_strength = 0.0;
self.timer = time.time() # A seconds clock
self.heart_time0 = self.timer # timer to call HeartBeat at intervals
self.save_time0 = self.timer
self.smeter_db_time0 = self.timer
self.smeter_sunits_time0 = self.timer
self.fewsec_time0 = self.timer
self.slowheart_time0 = self.timer
self.multi_rx_index = 0
self.multi_rx_timer = self.timer
self.band_up_down = 0 # Are band Up/Down buttons in use?
@@ -3718,6 +3787,7 @@ class App(wx.App):
pass #traceback.print_exc()
if "Version" not in self.bandAmplPhase:
self.FixAmplPhase() # Convert to new format Oct 2020
self.FixAmplPhase2() # New logic June 2024
if Hardware.VarDecimGetChoices(): # Hardware can change the decimation.
self.sample_rate = Hardware.VarDecimSet() # Get the sample rate.
self.vardecim_set = self.sample_rate
@@ -3756,15 +3826,16 @@ class App(wx.App):
self.std_out_err.Show(False)
else:
self.std_out_err.Show(True)
#w, h = frame.GetSize().Get()
#ww, hh = frame.GetClientSizeTuple()
#print ('Main frame: size', w, h, 'client', ww, hh)
wClient, hClient = self.main_frame.GetClientSize()
wFrame, hFrame = self.main_frame.GetSize()
main_border_w = wFrame - wClient
#print ('Main frame: size', wFrame, hFrame, 'client', wClient, hClient, 'border', main_border_w)
# Find the data width from a list of preferred sizes; it is the width of returned graph data.
# The graph_width is the width of data_width that is displayed.
# The total width of the graph is graph_width + self.graph_borders
if conf.window_width > 0:
wFrame, h = frame.GetClientSize().Get() # client window width
graph = GraphScreen(frame, self.width//2, self.width//2, None) # make a GraphScreen to calculate borders
self.graph_width = wFrame - (graph.width - graph.graph_width) # less graph borders equals actual graph_width
self.graph_width = wClient - (graph.width - graph.graph_width) # less graph borders equals actual graph_width
graph.Destroy()
del graph
if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers
@@ -3889,14 +3960,14 @@ class App(wx.App):
frame.SetSizer(vertBox)
# Add the screens
vertBox.Add(self.config_screen, 1, wx.EXPAND)
vertBox.Add(self.multi_rx_screen, 1)
vertBox.Add(self.scope, 1)
vertBox.Add(self.bandscope_screen, 1)
vertBox.Add(self.filter_screen, 1)
vertBox.Add(self.multi_rx_screen, 1, wx.EXPAND)
vertBox.Add(self.scope, 1, wx.EXPAND)
vertBox.Add(self.bandscope_screen, 1, wx.EXPAND)
vertBox.Add(self.filter_screen, 1, wx.EXPAND)
if self.rate_audio_fft:
vertBox.Add(self.audio_fft_screen, 1)
vertBox.Add(self.help_screen, 1)
vertBox.Add(self.station_screen)
vertBox.Add(self.audio_fft_screen, 1, wx.EXPAND)
vertBox.Add(self.help_screen, 1, wx.EXPAND)
vertBox.Add(self.station_screen, 0, wx.EXPAND)
# Add the spacer
vertBox.Add(Spacer(frame), 0, wx.EXPAND)
# Add the sizer for the controls
@@ -3909,15 +3980,17 @@ class App(wx.App):
vertBox.AddSpacer(5) # Thanks to Christof, DJ4CM
# End of vertical box.
self.MakeButtons(frame, gbs)
minw = width = self.graph.width
maxw = maxh = -1
minh = 100
if conf.window_width > 0:
minw = width = maxw = conf.window_width
minw = maxw = conf.window_width
else:
minw = self.graph.width + main_border_w
maxw = -1
if conf.window_height > 0:
minh = maxh = self.height = conf.window_height
else:
minh = 100
maxh = -1
self.main_frame.SetSizeHints(minw, minh, maxw, maxh)
self.main_frame.SetClientSize(wx.Size(width, self.height))
if hasattr(Hardware, 'pre_open'): # pre_open() is called before open()
Hardware.pre_open()
if self.local_conf.GetWidgets(self, Hardware, conf, frame, gbs, vertBox):
@@ -4383,6 +4456,25 @@ class App(wx.App):
except:
traceback.print_exc()
self.bandAmplPhase[band] = {}
def FixAmplPhase2(self): # New definition of phase
if self.bandAmplPhase["Version"]["rx"] == [1]:
return
degrad = 360.0 / 2 / math.pi
for band in self.bandAmplPhase:
if band == "Version":
continue
for rx_tx in self.bandAmplPhase[band]:
if rx_tx in ('rx', 'tx'):
data = self.bandAmplPhase[band][rx_tx]
for item in data:
vfo = item[0]
lst_framph = item[1]
for framph in lst_framph:
freq, am, ph = framph
p = math.atan(math.tan(ph / degrad) / (1.0 + am))
#print ("%5s %3s %8d %8d %8.4f %8.3f %8.3f" % (band, rx_tx, vfo, freq, am, ph, p * degrad))
framph[2] = p * degrad
self.bandAmplPhase["Version"]["rx"] = [1]
def ImmediateChange(self, name):
if name == "pulse_audio_verbose_output":
value = getattr(conf, name)
@@ -4621,7 +4713,7 @@ class App(wx.App):
b.char_shortcut = 'l'
self.MakeAccel(b)
self.splitButton = WrapMenu(b, self.split_menu)
if conf.mouse_tune_method: # Mouse motion changes the VFO frequency
if False: #conf.mouse_tune_method: # Mouse motion changes the VFO frequency
self.splitButton.Enable(False)
left_row3.append(self.splitButton)
b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test)
@@ -4831,7 +4923,8 @@ class App(wx.App):
self.freqDisplay.Display(self.txFreq + self.VFO)
# On/Off button
if conf.button_layout == 'Large screen':
b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color='#77DD77')
b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color=conf.color_onoff)
b_onoff.text_color = conf.color_onoff_text
b_onoff.SetValue(True, do_cmd=False)
h = b_freqdisp.height
b_onoff.SetSizeHints(h, h, h, h)
@@ -5034,6 +5127,7 @@ class App(wx.App):
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.
self.hamlib_strength = (s - 9.0) * 6.0 # convert to dB; 0 dB is S9
if s < 0:
s = 0
if s >= 9.5:
@@ -5786,16 +5880,31 @@ class App(wx.App):
The hardware will reply with the updated frequencies which may be different
from those requested; use and display the returned tune and vfo.
"""
#print ("ChangeHW", source, tune, vfo, vfo + tune)
if not self.split_rxtx:
self.rxFreq = tune # rxFreq must be correct before the call to Hardware.ChangeFrequency()
elif rx_freq is not None:
self.rxFreq = rx_freq
rx_offset = self.rxFreq - tune
if self.screen == self.bandscope_screen:
freq = vfo + tune
tune = freq % 10000
vfo = freq - tune
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
elif conf.fixed_tune_offset:
# Tune with the VFO and keep a constant audio offset from the center. Tx tune is always conf.fixed_tune_offset.
tune_freq = vfo + tune
self.rxFreq = conf.fixed_tune_offset + rx_offset
if source == "BtnUpDown":
tune_freq = vfo + conf.fixed_tune_offset
else:
vfo = tune_freq - conf.fixed_tune_offset
tune_freq, vfo = Hardware.ChangeFrequency(tune_freq, vfo, source, band, event)
self.ChangeDisplayFrequency(tune_freq - vfo, vfo, True)
else:
tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event)
self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None)
def ChangeDisplayFrequency(self, tune, vfo, new_rxfreq=False):
'Change the frequency displayed by Quisk'
change = 0
@@ -5815,10 +5924,8 @@ class App(wx.App):
self.graph.SetVFO(vfo)
self.waterfall.SetVFO(vfo)
self.station_screen.Refresh()
if self.w_phase:
self.w_phase.Redraw()
if change:
if conf.name_of_sound_capt or conf.name_of_mic_play:
if (conf.name_of_sound_capt or conf.name_of_mic_play) and QS.get_params("softrock_correct_active") == 0:
ampl, phase = self.GetAmplPhase('rx')
QS.set_ampl_phase(ampl, phase, 0)
ampl, phase = self.GetAmplPhase('tx')
@@ -6294,7 +6401,7 @@ class App(wx.App):
for client in self.hamlib_clients: # Service existing clients
if not client.Process(): # False return indicates a closed connection; remove the handler for this client
self.hamlib_clients.remove(client)
# print 'Remove', client.address
# print ('Remove', client.address)
break
def OnKeyHook(self, event):
event.Skip()
@@ -6350,6 +6457,8 @@ class App(wx.App):
self.hamlib_com2_handler.Process()
if self.use_fast_heart_beat:
Hardware.FastHeartBeat()
if self.poll_gui_control:
Hardware.PollGuiControl()
if conf.do_repeater_offset:
hold = QS.tx_hold_state(-1)
if hold == 2: # Tx is being held for an FM repeater TX frequency shift
@@ -6369,7 +6478,7 @@ class App(wx.App):
Hardware.RepeaterOffset(0)
QS.set_ctcss(0)
QS.tx_hold_state(1)
if QS.is_key_down(): # Tx indicator
if QS.is_key_down() and not self.tx_inhibit: # Tx indicator
if not self.tx_indicator:
self.tx_indicator = True
self.pttButton.Tx.TurnOn(True)
@@ -6430,6 +6539,8 @@ class App(wx.App):
self.hot_key_ptt_active = False
self.hot_key_ptt_was_down = self.hot_key_ptt_is_down
self.hot_key_ptt_pressed = False
if self.tx_inhibit:
ptt = False
if ptt is True and not ptt_button_down:
self.SetPTT(True)
elif ptt is False and ptt_button_down:
@@ -6500,6 +6611,16 @@ class App(wx.App):
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks:
self.heart_time0 = self.timer
Hardware.HeartBeat()
if conf.hermes_lite2_enable:
self.tx_inhibit = QS.get_params('quisk_tx_inhibit')
else:
self.tx_inhibit = 0
if self.tx_inhibit != self.old_tx_inhibit:
self.old_tx_inhibit = self.tx_inhibit
self.graph.display.Refresh()
self.waterfall.pane1.display.Refresh()
if self.tx_inhibit and self.spotButton.GetValue():
self.spotButton.SetValue(0, True)
msg = QS.GetQuiskPrintf()
if msg:
print(msg, end='')
@@ -6518,9 +6639,10 @@ class App(wx.App):
self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll()
self.HamlibPoll()
#if self.timer - self.fewsec_time0 > 3.0:
# self.fewsec_time0 = self.timer
# print ('fewswc')
if self.timer - self.slowheart_time0 > 0.5:
self.slowheart_time0 = self.timer
if self.w_phase:
self.w_phase.SlowHeartBeat()
if self.timer - self.save_time0 > 20.0:
self.save_time0 = self.timer
if self.CheckState():
+142 -4
View File
@@ -419,6 +419,8 @@ hermes_PTT_hang_time = 4
## hermes_antenna_tuner Antenna tuner, text choice
# This option only applies to the Hermes Lite 2. Set this to None if you don't have a tuner.
# Set this to "Tune" to control the Icom AH-4 compatible ATU attached to the Hermes Lite 2 end plate.
# Note that this option only applies to the HL2 end plate, not the IO board.
# A tuner attached to the IO board is controlled from a button on the last line of the screen.
# Then when the Spot button is pressed with a positive power, a tune request is sent to the ATU.
# If the Spot button is pressed with a zero power level, the tuner is set to bypass mode.
# Changes are immediate (no need to restart).
@@ -471,6 +473,13 @@ hermes_iob_rxin = 'J10 available'
#hermes_iob_rxin = 'HL2 Rx to J9'
#hermes_iob_rxin = 'Use J9 and J10'
## hermes_lite2_enable Hermes Lite2 protocol, boolean
# This turns on some of the protocol extensions for the Hermes Lite2 that are not part
# of the official Hermes Protocol 1. Currently this only enables Tx Inhibit (CN8).
# Changes are immediate (no need to restart).
hermes_lite2_enable = True
#hermes_lite2_enable = False
# These are known power meter calibration tables. This table is not present in the JSON settings file.
power_meter_std_calibrations = {}
@@ -1075,7 +1084,15 @@ digital_rx1_name = ""
## digital_output_level Digital output level, number
# This is the volume control 0.0 to 1.0 for digital playback to fldigi, etc.
# Changes are immediate (no need to restart).
digital_output_level = 0.7
digital_output_level = 0.700
## file_play_level File play level, number
# Use this to reduce the volume when playing files. For example, if the volume of the CQ
# message is too loud, reduce the level relative to radio sound.
# Changes are immediate (no need to restart).
file_play_level = 1.000
#file_play_level = 0.500
#file_play_level = 0.100
# Sound card names:
#
@@ -1270,6 +1287,18 @@ dc_remove_bw = 100
#dc_remove_bw = 200
#dc_remove_bw = 400
## fixed_tune_offset Fixed tune offset, integer
# If this is zero, Quisk tunes as usual. The hardware frequency (the VFO or center frequency)
# is fixed and Quisk tunes
# within the spectrum returned by the hardware. Otherwise enter a fixed offset between the
# center frequency and the tuning frequency. Then tuning is done by the hardware.
# This is useful with Softrock hardware as
# it simplifies the corrections for amplitude and phase imbalance.
# Changes are immediate (no need to restart).
fixed_tune_offset = 0
#fixed_tune_offset = 5000
#fixed_tune_offset = 10000
@@ -1646,6 +1675,15 @@ start_ssb_delay = 100
# Changes are immediate (no need to restart).
maximum_tx_secs = 0
## TxRxSilenceMsec Tx-Rx silence msec, integer
# When changing from transmit to receive a radio may receive either noise or the end of the transmission.
# This may be returned as received audio, and can be heard as a loud pop.
# This parameter plays silence for the specified number of milliseconds when changing from transmit to receive.
# Make it long enough to zero out the pop, but not too much longer.
# It is ignored when in full duplex mode.
# Changes are immediate (no need to restart).
TxRxSilenceMsec = 50
## keyupDelay Keyup delay msecs, integer
# This is the key hang time for semi-breakin CW. It is the time in milliseconds
# from the last CW key release until changing to receive.
@@ -1890,6 +1928,8 @@ btn_text_switch = u"\u21C4" # Character to switch left-right
color_scheme = 'A'
#color_scheme = 'B'
#color_scheme = 'C'
#color_scheme = 'DK1MI_GR'
#color_scheme = 'DK1MI_AM'
## waterfall_palette Waterfall colors, text choice
# This controls the colors used in the waterfall. The default color scheme is A, and you can change this scheme
@@ -1897,6 +1937,7 @@ color_scheme = 'A'
waterfall_palette = 'A'
#waterfall_palette = 'B'
#waterfall_palette = 'C'
#waterfall_palette = 'DK1MI_AM'
@@ -1931,6 +1972,8 @@ color_txline = 'red' # vertical line color for tx in graph
color_rxline = 'green' # vertical line color for rx in graph
color_graph_msg_fg = 'black' # text messages on the graph screen
color_graph_msg_bg = 'lemonchiffon2' # background of text messages on the graph screen
color_onoff = '#77DD77' # on/off power button
color_onoff_text = 'black' # on/off power button text
# This color scheme B, a dark color scheme designed by Steve Murphy, KB8RWQ.
# Additional colors added by N2ADR.
@@ -1960,6 +2003,8 @@ color_scheme_B = {
'color_rxline' : 'green',
'color_graph_msg_fg' : 'white',
'color_graph_msg_bg' : '#111111',
'color_onoff' : '#77DD77',
'color_onoff_text' : 'black',
}
# This is color scheme C:
@@ -1997,12 +2042,73 @@ color_scheme_C = {
'color_rxline' : '#3CC918',
'color_graph_msg_fg' : '#000000',
'color_graph_msg_bg' : 'lemonchiffon2',
'color_onoff' : '#77DD77',
'color_onoff_text' : 'black',
}
#############################################################################################
# This is a green dark color scheme designed by Michael
color_scheme_DK1MI_GR = {
'color_bg' : '#0F0F0F', # Lower screen background
'color_bg_txt' : '#00CC00', # Lower screen text color
'color_graph' : '#0F0F0F', # Graph background
'color_config2' : '#008000', # color in tab row of config screen
'color_gl' : '#2F4F4F', # Lines on the graph
'color_graphticks' : '#CCCCCC', # Graph ticks
'color_graphline' : '#00FF00', # graph data line color
'color_graphlabels' : '#00FF00', # graph label color
'color_btn' : '#141414', # button color
'color_check_btn' : '#005500', # color of a check button when it is checked
'color_cycle_btn' : '#005500', # color of a cycle button when it is checked
'color_adjust_btn' : '#005500', # color of an adjustable button when it is checked
'color_test' : '#005500', # color of a button used for test (turn off for tx)
'color_freq' : '#208B57', # background color of frequency and s-meter
'color_freq_txt' : '#00FF00', # text color of frequency display
'color_entry' : '#2E2E2E', # frequency entry box
'color_entry_txt' : '#00FF00', # text color of entry box
'color_enable' : '#00CC00', # text color for an enabled button
'color_disable' : '#FF0000', # text color for a disabled button
'color_popchoice' : '#555555', # text color for button that pops up a row of buttons
'color_bandwidth' : '#003300', # color for bandwidth display; thanks to WB4JFI
'color_txline' : '#00CC00', # vertical line color for tx in graph
'color_rxline' : '#00FF00', # vertical line color for rx in graph
'color_graph_msg_fg' : '#008800', # text messages on the graph screen
'color_graph_msg_bg' : '#1E1E1E', # background of text messages on the graph screen
'color_onoff' : '#005500', # On/Off button
'color_onoff_text' : '#FF0000', # On/Off text
}
#############################################################################################
# This is an amber dark color scheme designed by Michael
color_scheme_DK1MI_AM = {
'color_bg' : '#0F0F0F', # Lower screen background
'color_bg_txt' : '#FFBF00', # Lower screen text color
'color_graph' : '#0F0F0F', # Graph background
'color_config2' : '#FF8000', # Color in tab row of config screen
'color_gl' : '#2F4F4F', # Lines on the graph
'color_graphticks' : '#CCCCCC', # Graph ticks
'color_graphline' : '#FFBF00', # Graph data line color
'color_graphlabels' : '#FFBF00', # Graph label color
'color_btn' : '#141414', # Button color
'color_check_btn' : '#865E2E', # Color of a check button when it is checked
'color_cycle_btn' : '#865E2E', # Color of a cycle button when it is checked
'color_adjust_btn' : '#865E2E', # Color of an adjustable button when it is checked
'color_test' : '#865E2E', # Color of a button used for test (turn off for tx)
'color_freq' : '#FF9933', # Background color of frequency and s-meter
'color_freq_txt' : '#000000', # Text color of frequency display
'color_entry' : '#2E2E2E', # Frequency entry box
'color_entry_txt' : '#FFBF00', # Text color of entry box
'color_enable' : '#FFBF00', # Text color for an enabled button
'color_disable' : '#FF0000', # Text color for a disabled button
'color_popchoice' : '#555555', # Text color for button that pops up a row of buttons
'color_bandwidth' : '#4B2E0F', # Color for bandwidth display
'color_txline' : '#FFBF00', # Vertical line color for tx in graph
'color_rxline' : '#FF8000', # Vertical line color for rx in graph
'color_graph_msg_fg' : '#FF8000', # Text messages on the graph screen
'color_graph_msg_bg' : '#1E1E1E', # Background of text messages on the graph screen
'color_onoff' : '#865E2E', # On/Off button
'color_onoff_text' : '#FF0000', # On/Off text
}
# These are the palettes for the waterfall. The one used is named waterfallPallette,
# so to use a different one, overwrite this name in your configuration file.
# These are the palettes for the waterfall.
waterfallPalette = (
( 0, 0, 0, 0),
( 36, 85, 0, 255),
@@ -2060,6 +2166,18 @@ waterfallPaletteC = ( # from David Fainitski
(255, 245, 99, 3)
)
waterfallPaletteDK1MI_AM = ( # from Michael
(0, 0, 0, 0),
(32, 25, 20, 0),
(64, 58, 40, 6),
(96, 78, 50, 16),
(128, 120, 85, 29),
(160, 144, 100, 51),
(192, 195, 140, 43),
(224, 198, 150, 35),
(255, 255, 191, 0)
)
# 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:
@@ -2085,6 +2203,26 @@ RxOnly = '#AAAAAA'
# Colors start at the indicated frequency and continue until the
# next frequency. The special color "None" turns off color.
#
BandPlanColors = [ # Display name and corresponding color
['CW, General', CW],
['CW, Extra', eCW],
['Phone, General', Phone],
['Phone, Extra', ePhone],
['AM', AM],
['Data', Data],
['DxData', DxData],
['RTTY', RTTY],
['SSTV', SSTV],
['Packet', Packet],
['Beacons', Beacons],
['Satellite', Satellite],
['Repeater out', Repeater],
['Repeater in', RepInput],
['Simplex', Simplex],
['Rx only', RxOnly],
['Special', Special],
['Other', Other]
]
+2 -1
View File
@@ -29,9 +29,10 @@ class Hardware(BaseHardware):
# self.hamlib_rigctld_port = 4532 # Standard rigctld control port
# self.hamlib_poll_seconds = 0.2 # Time interval to poll for changes
def open(self):
# Thanks to Peter, DC6AR, Rig ID changed from 229 to 2045.
ret = BaseHardware.open(self)
if not self.hamlib_connected: # rigctld is not started. Try to start it.
os.system("rigctld -m 229 -r /dev/ttyUSB0 -s 4800 & ") # Check the baud rate menu setting
os.system("rigctld -m 2045 -r /dev/ttyUSB0 -s 4800 & ") # Check the baud rate menu setting
# If this fails, start rigctld by hand.
return ret
+2
View File
@@ -120,6 +120,8 @@ class Hardware:
self.application.bandscope_clock = clock
#def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status.
# pass # Do not define if not needed.
#def PollGuiControl(self): # Optional. Called frequently by the GUI thread to change GUI settings (PTT etc.)
# pass # Do not define if not needed.
def StartSamples(self): # Quisk calls this from the sound thread to start sending samples.
# If you return a string, it replaces the string returned from hardware open()
pass
+5 -3
View File
@@ -7,7 +7,7 @@ import os
# You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py.
Version = '4.2.29'
Version = '4.2.39'
fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version)
@@ -39,8 +39,10 @@ if sys.platform == "darwin": # Build for Macintosh
libraries = ['portaudio', 'fftw3', 'm']
if os.path.isdir('/opt/local/include'): # MacPorts
base_dir = '/opt/local'
elif os.path.isdir('/usr/local/include'): # HomeBrew
elif os.path.isdir('/usr/local/include'): # HomeBrew on macOS Intel
base_dir = '/usr/local'
elif os.path.isdir('/opt/homebrew/include'): # HomeBrew on Apple Silicon
base_dir = '/opt/homebrew'
else: # Regular build?
base_dir = '/usr'
if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"):
@@ -83,7 +85,7 @@ N1MM+ and software that uses Hamlib.
url = 'http://james.ahlstrom.name/quisk/',
packages = ['quisk', 'quisk.n2adr', 'quisk.softrock', 'quisk.freedvpkg',
'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg',
'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd'],
'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd', 'quisk.multuspkg'],
package_dir = {'quisk' : '.'},
package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']},
entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']},
+2
View File
@@ -178,6 +178,8 @@ class Hardware(BaseHardware):
vfo = self.vfo + int(offset * 1000) # Convert kHz to Hz
self.ChangeFrequency(vfo, vfo, 'repeater')
return False
def ImmediateChange(self, name):
return
def OnSpot(self, level):
pass
def HeartBeat(self): # Called at about 10 Hz by the main
+49 -13
View File
@@ -85,8 +85,10 @@ char * sound_format_names[4] = {"Int32", "Int16", "Float32", "Int24"} ;
static struct wav_file file_rec_audio, file_rec_samples, file_rec_mic;
static int close_file_rec;
static double digital_output_level = 0.7;
double digital_output_level = 0.7;
static double file_play_level = 1.0;
static int dc_remove_bw=100; // bandwidth of DC removal filter
static int multus_cw_samples = 0; // always send CW Tx samples to the Multus radio, even in Rx
static ty_sample_start pt_sample_start;
static ty_sample_stop pt_sample_stop;
@@ -175,9 +177,10 @@ static void correct_sample (struct sound_dev * dev, complex double * cSamples, i
for (i = 0; i < nSamples; i++) {
re = creal(cSamples[i]);
im = cimag(cSamples[i]);
re = re * dev->AmPhAAAA;
im = re * dev->AmPhCCCC + im * dev->AmPhDDDD;
cSamples[i] = re + I * im;
cSamples[i] = re * dev->AmPhAAAA + I * (re * dev->AmPhCCCC + im * dev->AmPhDDDD);
//re = re * dev->AmPhAAAA;
//im = re * dev->AmPhCCCC + im * dev->AmPhDDDD;
//cSamples[i] = re + I * im;
}
}
}
@@ -730,8 +733,11 @@ void * quisk_make_txIQ(struct sound_dev * dev, int rewind)
}
// amplitude and phase corrections
if (dev->doAmplPhase) {
dSamp0 = dSamp0 * dev->AmPhAAAA;
dSamp1 = dSamp0 * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
//dSamp0 = dSamp0 * dev->AmPhAAAA;
//dSamp1 = dSamp0 * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
d = dSamp0;
dSamp0 = d * dev->AmPhAAAA;
dSamp1 = d * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
}
switch (dev->sound_format) {
case Int16:
@@ -836,6 +842,34 @@ int quisk_play_sidetone(struct sound_dev * dev)
return 0;
}
#if 0
static void testing(double complex * cSamples, int nSamples)
{
int i;
static double AvgR=0, AvgQ=0, AvgRQ=0;
double d, p, Gain, Phase;
time_t tim;
static time_t time0;
for (i = 0; i < nSamples; i++) {
d = creal(cSamples[i]);
AvgR += d * d;
p = cimag(cSamples[i]);
AvgQ += p * p;
AvgRQ += d * p;
}
tim = time(NULL);
if (tim - time0 >= 2) {
time0 = tim;
Gain = sqrt(AvgR / AvgQ);
Phase = AvgRQ / AvgQ / Gain;
Phase = asin(Phase);
printf("Time domain Gain %10.7lf, Phase %10.7lf, %10.7lf degrees\n", Gain, Phase, Phase * 57.29578);
AvgR = AvgQ = AvgRQ = 0;
}
}
#endif
int quisk_read_sound(void) // Called from sound thread
{ // called in an infinite loop by the main program
int i, nSamples, mic_count, mic_interp, retval, is_cw, mic_sample_rate;
@@ -913,9 +947,10 @@ int quisk_read_sound(void) // Called from sound thread
//QuiskPrintTime("quisk_read_sound end", 0);
if (Capture.channel_Delay >= 0) // delay the I or Q channel by one sample
delay_sample(&Capture, (double *)cSamples, nSamples);
DCremove(cSamples, nSamples, quisk_sound_state.sample_rate, key_state);
//testing(cSamples, nSamples);
if (Capture.doAmplPhase) // amplitude and phase corrections
correct_sample(&Capture, cSamples, nSamples);
DCremove(cSamples, nSamples, quisk_sound_state.sample_rate, key_state);
if (nSamples <= 0)
QuiskSleepMicrosec(2000);
#if DEBUG_IO > 1
@@ -969,7 +1004,7 @@ int quisk_read_sound(void) // Called from sound thread
if (quisk_record_state == TMP_PLAY_SPKR_MIC)
quisk_tmp_playback(cSamples, nSamples, 1.0); // replace radio sound
else if (quisk_record_state == FILE_PLAY_SPKR_MIC)
quisk_file_playback(cSamples, nSamples, 1.0); // replace radio sound
quisk_file_playback(cSamples, nSamples, file_play_level); // replace radio sound
// Play the demodulated audio
#if DEBUG_MIC != 2
if ( ! quisk_play_sidetone(&quisk_Playback)) // play sidetone
@@ -1075,7 +1110,7 @@ int quisk_read_sound(void) // Called from sound thread
if (is_cw) { // Transmit CW; use capture device for timing, not microphone
cwCount += (double)retval * quisk_MicPlayback.sample_rate / quisk_sound_state.sample_rate;
mic_count = 0;
if (QUISK_CWKEY_DOWN || quiskSpotLevel >= 0) {
if (QUISK_CWKEY_DOWN || quiskSpotLevel >= 0 || multus_cw_samples) {
while (cwCount >= 1.0) {
if (cwEnvelope < 1.0) {
cwEnvelope += 1. / (quisk_MicPlayback.sample_rate * 5e-3); // 5 milliseconds
@@ -1401,6 +1436,8 @@ void quisk_open_sound(void) // Called from GUI thread
RawSamplePlayback.channel_I = 0;
RawSamplePlayback.channel_Q = 1;
file_play_level = QuiskGetConfigDouble("file_play_level", 1.0);
set_num_channels (&Capture);
set_num_channels (&quisk_Playback);
set_num_channels (&MicCapture);
@@ -1565,10 +1602,9 @@ PyObject * quisk_micplay_channels(PyObject * self, PyObject * args) // Called fr
PyObject * quisk_set_sparams(PyObject * self, PyObject * args, PyObject * keywds)
{ /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"dc_remove_bw", "digital_output_level", "remote_control_slave", "remote_control_head",
NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idii", kwlist, &dc_remove_bw, &digital_output_level,
&remote_control_slave, &remote_control_head))
"file_play_level", "multus_cw_samples", NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idiidi", kwlist, &dc_remove_bw, &digital_output_level,
&remote_control_slave, &remote_control_head, &file_play_level, &multus_cw_samples))
return NULL;
Py_INCREF (Py_None);
return Py_None;
+30 -10
View File
@@ -495,8 +495,37 @@ static snd_pcm_format_t check_formats(struct sound_dev * dev, snd_pcm_hw_params_
{
snd_pcm_format_t format = SND_PCM_FORMAT_UNKNOWN;
dev->sample_bytes = 0;
#if 0
char * card_name;
int card;
snd_pcm_info_t * pcm_info;
printf("driver %s\n", snd_ctl_card_info_get_driver(info));
int loopback = 0;
if (snd_pcm_info_malloc(&pcm_info) == 0) {
if (snd_pcm_info (dev->handle, pcm_info) == 0) {
card = snd_pcm_info_get_card (pcm_info);
if (card >= 0) {
if (snd_card_get_name(card, &card_name) == 0) {
if (strcmp(card_name, "Loopback") == 0)
loopback = 1;
printf("name %s\n", card_name);
free(card_name);
}
}
}
snd_pcm_info_free(pcm_info);
}
#endif
strMcpy (dev->msg1, "Available formats: ", QUISK_SC_SIZE);
if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S16) == 0) {
if (!dev->sample_bytes) {
strncat(dev->msg1, "*", QUISK_SC_SIZE);
dev->sample_bytes = 2;
dev->sound_format = Int16;
format = SND_PCM_FORMAT_S16;
}
strncat(dev->msg1, "S16 ", QUISK_SC_SIZE);
}
if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S32) == 0) {
if (!dev->sample_bytes) {
strncat(dev->msg1, "*", QUISK_SC_SIZE);
@@ -515,15 +544,6 @@ static snd_pcm_format_t check_formats(struct sound_dev * dev, snd_pcm_hw_params_
if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_U24) == 0) {
strncat(dev->msg1, "U24 ", QUISK_SC_SIZE);
}
if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S16) == 0) {
if (!dev->sample_bytes) {
strncat(dev->msg1, "*", QUISK_SC_SIZE);
dev->sample_bytes = 2;
dev->sound_format = Int16;
format = SND_PCM_FORMAT_S16;
}
strncat(dev->msg1, "S16 ", QUISK_SC_SIZE);
}
if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S24_3LE) == 0) {
if (!dev->sample_bytes) {
strncat(dev->msg1, "*", QUISK_SC_SIZE);