5 Commits
Author SHA1 Message Date
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
12 changed files with 800 additions and 253 deletions
+44
View File
@@ -1,3 +1,47 @@
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 Quisk Version 4.2.30 March 2024
================================ ================================
A new option "Tx-Rx silence msec" on the Timing configuration screen controls how long A new option "Tx-Rx silence msec" on the Timing configuration screen controls how long
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.30 #Quisk version 4.2.35
from .quisk import main from .quisk import main
+390 -198
View File
@@ -1,13 +1,9 @@
import sys, wx, wx.lib, os, re, pickle, traceback, json, copy
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import sys, wx, wx.lib, os, re, pickle, traceback, json
# Quisk will alter quisk_conf_defaults to include the user's config file. # Quisk will alter quisk_conf_defaults to include the user's config file.
import quisk_conf_defaults as conf import quisk_conf_defaults as conf
import _quisk as QS import _quisk as QS
from quisk_widgets import QuiskPushbutton, QuiskCheckbutton, QuiskBitField, SliderBoxH, SliderBoxHH from quisk_widgets import QuiskPushbutton, QuiskCheckbutton, QuiskBitField, SliderBoxH, SliderBoxHH
from quisk_widgets import FreqFormatter
from quisk_widgets import wxVersion from quisk_widgets import wxVersion
if wxVersion in ('2', '3'): if wxVersion in ('2', '3'):
import wx.combo as wxcombo import wx.combo as wxcombo
@@ -170,12 +166,16 @@ class Configuration:
radio_dict["Hermes_BandDictTx"] = {} radio_dict["Hermes_BandDictTx"] = {}
def UpdateGlobals(self): def UpdateGlobals(self):
self.RadioName = Settings[1] self.RadioName = Settings[1]
application.BandPlanC2T = {}
if self.RadioName == "ConfigFileRadio": if self.RadioName == "ConfigFileRadio":
application.BandPlan = conf.BandPlan application.BandPlan = conf.BandPlan
for mode, color in conf.BandPlanColors:
application.BandPlanC2T[color] = mode
elif "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]: elif "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]:
mode_dict = {"End":None} mode_dict = {"End":None}
for mode, color in Settings[4]["BandPlanColors"]: for mode, color in Settings[4]["BandPlanColors"]:
mode_dict[mode] = color mode_dict[mode] = color
application.BandPlanC2T[color] = mode
plan = [] plan = []
for freq, mode in Settings[4]["BandPlan"]: for freq, mode in Settings[4]["BandPlan"]:
freq = int(float(freq) * 1E6 + 0.1) freq = int(float(freq) * 1E6 + 0.1)
@@ -188,6 +188,8 @@ class Configuration:
application.BandPlan = plan application.BandPlan = plan
else: else:
application.BandPlan = conf.BandPlan application.BandPlan = conf.BandPlan
for mode, color in conf.BandPlanColors:
application.BandPlanC2T[color] = mode
if "MidiNoteDict" not in Settings[4]: if "MidiNoteDict" not in Settings[4]:
Settings[4]["MidiNoteDict"] = {} Settings[4]["MidiNoteDict"] = {}
self.settings_changed = True self.settings_changed = True
@@ -829,12 +831,10 @@ class QPowerMeterCalibration(wx.Frame):
## Note: The amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL. ## Note: The amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL.
## October 2020: changed to make RxTx frequency and VFO two independent variables. ## 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): class QAdjustPhase(wx.Frame):
"""Create a window with amplitude and phase adjustment controls""" """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): def __init__(self, parent, width, rx_tx):
self.width = width
self.rx_tx = rx_tx # Must be "rx" or "tx" self.rx_tx = rx_tx # Must be "rx" or "tx"
if rx_tx == 'tx': if rx_tx == 'tx':
self.is_tx = 1 self.is_tx = 1
@@ -842,121 +842,231 @@ class QAdjustPhase(wx.Frame):
else: else:
self.is_tx = 0 self.is_tx = 0
t = "Adjust Sound Card Receive Amplitude and Phase" 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.panel = wx.Panel(self)
self.MakeControls() self.MakeControls()
self.Redraw() self.Redraw()
self.Show() 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 def MakeControls(self): # Make controls for phase/amplitude adjustment
panel = self.panel panel = self.panel
width = self.width sizer = wx.BoxSizer(wx.VERTICAL)
self.old_amplitude, self.old_phase = application.GetAmplPhase(self.rx_tx) panel.SetSizer(sizer)
self.new_amplitude, self.new_phase = self.old_amplitude, self.old_phase sl_max = application.screen_width * 4 // 10 # maximum +/- value for slider
sl_max = width * 4 // 10 # maximum +/- value for slider
self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max
self.phase_scale = float(conf.rx_max_phase_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) wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)
self.SetFont(font) self.SetFont(main_font)
panel.SetFont(main_font)
charx = self.GetCharWidth() charx = self.GetCharWidth()
self.tab1 = tab1 = charx
chary = self.GetCharHeight() chary = self.GetCharHeight()
deltay = chary * 12 // 10 # Create the grid heading and Destroy button
y = chary * 3 // 10 self.cell_amph = wx.StaticText(panel, -1, "")
self.controls = [] font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL,
txt = wx.StaticText(panel, -1, "No data", pos=(tab1, y)) wx.FONTWEIGHT_BOLD, False, conf.quisk_typeface)
self.controls.append(txt) self.cell_amph.SetFont(font)
y += txt.GetSize().GetHeight() self.cell_amph.SetLabel("Cell frequency %s, gain %.3f, phase %.3f\u00B0 " % (FreqFormatter(144000000), -1.123, -12.456))
#y += deltay * 14 // 10 dv = wx.lib.buttons.GenButton(panel, label=" Destroy ")
self.t_ampl = wx.StaticText(panel, -1, self.f_ampl % self.old_amplitude, pos=(tab1, y)) dv.SetBezelWidth(3)
self.controls.append(self.t_ampl) dv.SetBackgroundColour("#DDD")
y += deltay dv.Bind(wx.EVT_BUTTON, self.OnBtnDestroy)
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y)) hbox = wx.BoxSizer(wx.HORIZONTAL)
self.controls.append(fine) sizer.Add(hbox, flag=wx.BOTTOM|wx.EXPAND, border=chary * 3 // 10)
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay)) hbox.Add(self.cell_amph, flag=wx.ALL|wx.ALIGN_CENTER_VERTICAL, border=chary // 2)
self.controls.append(coarse) hbox.Add(dv, flag=wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=charx * 5)
tab2 = tab1 + coarse.GetSize().GetWidth() + charx # Create the grid
sliderX = width - tab2 - charx self.grid = wx.grid.Grid(panel, style=wx.BORDER_SIMPLE)
self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) self.grid.CreateGrid(4, 3)
self.controls.append(self.ampl1) self.grid.HideRowLabels()
y += deltay self.selected_row = 0
self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) self.selected_col = 0
self.controls.append(self.ampl2) self.cell_bg_color = self.grid.GetDefaultCellBackgroundColour()
y += deltay * 14 // 10 self.grid.SetDefaultCellFont(main_font)
self.PosAmpl(self.old_amplitude) width, h = self.GetTextExtent("987-24 000")
self.t_phase = wx.StaticText(panel, -1, self.f_phase % self.old_phase, pos=(tab1, y)) self.grid.SetDefaultColSize(width, True)
self.controls.append(self.t_phase) self.grid.EnableEditing(False)
y += deltay self.grid.SetColLabelValue(0, "Band")
fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y)) self.grid.SetColLabelValue(1, "VFO")
self.controls.append(fine) self.grid.SetColLabelValue(2, "Tune")
coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay)) self.grid.SetColSize(0, width // 2)
self.controls.append(coarse) self.grid.SetColSize(1, width * 14 // 10)
self.phase1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) num_tune = (self.client_width - width * 2) // width
self.controls.append(self.phase1) #print ("num_tune", num_tune)
y += deltay for col in range(3, num_tune + 2):
self.phase2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) self.grid.AppendCols()
self.controls.append(self.phase2) self.grid.SetColLabelValue(col, "Tune")
y += deltay #self.grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnGridLeftClick)
sv = QuiskPushbutton(panel, self.OnBtnSave, 'Save') self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnGridSelectCell)
self.controls.append(sv) gbox = wx.BoxSizer(wx.VERTICAL)
dv = QuiskPushbutton(panel, self.OnBtnDestroyVFO, 'Destroy VFO') sizer.Add(gbox, proportion=1, flag=wx.LEFT|wx.RIGHT|wx.EXPAND, border=charx)
self.controls.append(dv) gbox.Add(self.grid, proportion=1)
ds = QuiskPushbutton(panel, self.OnBtnDestroyALL, 'Destroy ALL') # Create the New Cell text, Manual/Measure, AddCell button
self.controls.append(ds) sbs = wx.StaticBoxSizer(wx.VERTICAL, panel, " New Cell")
cn = QuiskPushbutton(panel, self.OnBtnFinished, 'Finished') sizer.Add(sbs, flag=wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT|wx.BOTTOM, border=charx*3)
self.controls.append(cn) self.add_cell_text = wx.StaticText(panel, -1, "Band")
hl = QuiskPushbutton(panel, self.OnBtnHelp, 'Help') self.add_cell_text.SetFont(main_font)
self.controls.append(hl) sbs.Add(self.add_cell_text, flag=wx.LEFT|wx.TOP, border=charx)
w, h = ds.GetSize().Get() box0 = wx.BoxSizer(wx.HORIZONTAL)
sv.SetSize((w, h)) sbs.Add(1, charx)
cn.SetSize((w, h)) sbs.Add(box0, flag=wx.EXPAND)
hl.SetSize((w, h)) rb_manual = wx.RadioButton(panel, -1, "Manual adjustment", style=wx.RB_GROUP)
y += h * 5 // 10 self.Bind(wx.EVT_RADIOBUTTON, self.OnBtnManMeasure)
x = (width - w * 5) // 6 rb_measure = wx.RadioButton(panel, -1, "Measure")
sv.SetPosition((x, y)) if self.is_tx:
dv.SetPosition((x*2 + w, y)) rb_manual.Enable(False)
ds.SetPosition((x*3 + w*2, y)) rb_measure.Enable(False)
cn.SetPosition((x*4 + w*3, y)) b = wx.lib.buttons.GenButton(panel, label=" Add cell ")
hl.SetPosition((x*5 + w*4, y)) b.Bind(wx.EVT_BUTTON, self.OnBtnAddCell)
sv.SetBackgroundColour('light blue') b.SetBezelWidth(3)
dv.SetBackgroundColour('light blue') b.SetBackgroundColour("#DDD")
ds.SetBackgroundColour('light blue') box0.Add(rb_manual, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx)
cn.SetBackgroundColour('light blue') box0.Add(rb_measure, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL, border=charx*5)
hl.SetBackgroundColour('light blue') box0.Add(b, flag=wx.LEFT, border=charx*5)
y += h # Create the amplitude slider controls
y += h * 4 // 10 box1 = wx.BoxSizer(wx.HORIZONTAL)
self.PosPhase(self.old_phase) box2 = wx.BoxSizer(wx.HORIZONTAL)
self.SetClientSize(wx.Size(width, y)) sbs.Add(box1, flag=wx.EXPAND)
self.ampl1.Bind(wx.EVT_SCROLL, self.OnChange) 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.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2)
self.phase1.Bind(wx.EVT_SCROLL, self.OnChange) # Create the phase slider controls
self.phase2.Bind(wx.EVT_SCROLL, self.OnPhase2) 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)
for b in btns:
b.SetBezelWidth(3)
b.SetBackgroundColour("#DDD")
b.SetSize(size)
sizer.Add(charx, chary)
def Redraw(self): def Redraw(self):
# Print available data points self.grid.ClearGrid()
self.band = application.lastBand # Print available data cells
data = application.bandAmplPhase.get(self.band, {}) row = 0
data = data.get(self.rx_tx, []) bands = []
if data: self.cell_dict = {}
t = "Band %s VFO and frequency list\n" % self.band for band in self.bandAmplPhase:
for vfo, items in data: if band != "Version" and self.rx_tx in self.bandAmplPhase[band]:
t += "%10d:" % vfo bands.append(band)
for freq, ampl, phase in items: bands.sort(reverse=True)
t += " %7d" % freq for band in bands:
t += "\n" 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: else:
t = "Band %s: No data." % self.band self.new_tune, gain, phase = QS.softrock_corrections(2)
x, y = self.controls[0].GetPosition() ampl = gain - 1.0
height = self.controls[0].GetSize().GetHeight() self.new_amplitude = ampl
self.controls[0].Destroy() phase *= 57.2958
txt = wx.StaticText(self.panel, -1, t, pos=(self.tab1, y)) self.new_phase = phase
self.controls[0] = txt self.PosAmpl(ampl)
delta = txt.GetSize().GetHeight() - height self.PosPhase(phase)
for ctrl in self.controls[1:]: self.SetNewCellText()
x, y = ctrl.GetPosition() def OnGridSelectCell(self, event=None):
y += delta self.grid.SetCellBackgroundColour(self.selected_row, self.selected_col, self.cell_bg_color)
ctrl.Move(x, y) if event:
h = self.GetClientSize().GetHeight() event.Skip()
self.SetClientSize(wx.Size(self.width, h + delta)) 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 def PosAmpl(self, ampl): # set pos1, pos2 for amplitude
pos2 = round(ampl / self.ampl_scale) pos2 = round(ampl / self.ampl_scale)
remain = ampl - pos2 * self.ampl_scale remain = ampl - pos2 * self.ampl_scale
@@ -967,81 +1077,172 @@ class QAdjustPhase(wx.Frame):
pos2 = round(phase / self.phase_scale) pos2 = round(phase / self.phase_scale)
remain = phase - pos2 * self.phase_scale remain = phase - pos2 * self.phase_scale
pos1 = round(remain / self.phase_scale * 50.0) pos1 = round(remain / self.phase_scale * 50.0)
self.phase1.SetValue(pos1) self.phas1.SetValue(pos1)
self.phase2.SetValue(pos2) self.phas2.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
def OnAmpl2(self, event): # re-center the fine slider when the coarse slider is adjusted 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() ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue()
self.PosAmpl(ampl) 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 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.PosPhase(phase)
self.OnChange(event) self.OnPhase1(event)
def OnBtnDestroyVFO(self, event): # Remove entry with the application VFO def OnPhase1(self, event):
data = application.bandAmplPhase.get(self.band, {}) phase = self.phase_scale * self.phas1.GetValue() / 50.0 + self.phase_scale * self.phas2.GetValue()
data = data.get(self.rx_tx, []) self.new_phase = phase
for i in range(len(data)): self.SetNewCellText()
if data[i][0] == application.VFO: QS.set_ampl_phase(self.new_amplitude, self.new_phase, self.is_tx)
del data[i] 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 break
else:
tunes.append([tune, ampl, phase])
tunes.sort()
self.dirty = True
self.Redraw() self.Redraw()
return
vfos_tunes.append([vfo, [[tune, ampl, phase]]])
vfos_tunes = vfos_tunes.sort()
self.dirty = True
self.Redraw()
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()
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): def OnBtnSave(self, event):
data = application.bandAmplPhase application.bandAmplPhase = copy.deepcopy(self.bandAmplPhase)
if self.band not in data: self.dirty = False
data[self.band] = {} def OnBtnExit(self, event):
data = data[self.band] if self.dirty:
if self.rx_tx not in data: dlg = wx.MessageDialog(self,
data[self.rx_tx] = [] "Your changes are not saved. Do you want to save them?", "Changes Were Made",
data = data[self.rx_tx] wx.OK|wx.CANCEL|wx.CANCEL_DEFAULT|wx.ICON_WARNING)
for vfo, items in data: # All items must be lists, not tuples dlg.SetOKLabel("Discard Changes")
if vfo == application.VFO: ret = dlg.ShowModal()
for i in range(len(items)): dlg.Destroy()
item = items[i] if ret == wx.ID_CANCEL:
if item[0] == application.rxFreq: return
item[1] = self.new_amplitude QS.softrock_corrections(0)
item[2] = self.new_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()
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.Redraw()
def OnBtnFinished(self, event=None):
ampl, phase = application.GetAmplPhase(self.rx_tx) ampl, phase = application.GetAmplPhase(self.rx_tx)
QS.set_ampl_phase(ampl, phase, self.is_tx) QS.set_ampl_phase(ampl, phase, self.is_tx)
application.w_phase = None application.w_phase = None
self.Destroy() self.Destroy()
def OnBtnHelp(self, event=None): def OnBtnHelp(self, event=None):
dlg = wx.MessageDialog(self, 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. \ '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 VFO and the frequency as desired. Then adjust the sliders to minimize the image. Press "Save" when satisfied. \ Adjust the amplitude and phase sliders to minimize the image. To save the adjustment, press Add Cell. \
To adjust the VFO, use the band Up/Down buttons, or right click the graph at the desired VFO. \ 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. \ 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 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) , "Adjustment Help", style=wx.OK)
dlg.ShowModal() dlg.ShowModal()
@@ -1484,7 +1685,7 @@ class MidiKnob(QuiskControl):
self.menu = wx.Menu() self.menu = wx.Menu()
for name in application.midiControls: for name in application.midiControls:
ctrl, func = application.midiControls[name] ctrl, func = application.midiControls[name]
if not ctrl or not func: if name != "Tune" and (not ctrl or not func):
continue continue
item = self.menu.Append(-1, name) item = self.menu.Append(-1, name)
self.Bind(wx.EVT_MENU, self.OnMenu, item) self.Bind(wx.EVT_MENU, self.OnMenu, item)
@@ -1519,7 +1720,7 @@ class MidiJogWheel(QuiskControl):
controls = wx.Menu() controls = wx.Menu()
for name in application.midiControls: for name in application.midiControls:
ctrl, func = application.midiControls[name] ctrl, func = application.midiControls[name]
if not ctrl or not func: if name != "Tune" and (not ctrl or not func):
continue continue
item = controls.Append(-1, name) item = controls.Append(-1, name)
self.Bind(wx.EVT_MENU, self.OnMenu, item) self.Bind(wx.EVT_MENU, self.OnMenu, item)
@@ -2005,7 +2206,7 @@ class ControlMixin:
if self.radio_name == Settings[1]: # changed for current radio 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', 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', 'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr',
'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec'): 'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle', 'TxRxSilenceMsec', 'hermes_lite2_enable'):
setattr(conf, name, x) setattr(conf, name, x)
application.ImmediateChange(name) application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'): elif name[0:4] in ('lin_', 'win_'):
@@ -2022,6 +2223,9 @@ class ControlMixin:
elif name == "digital_output_level": elif name == "digital_output_level":
setattr(conf, name, x) setattr(conf, name, x)
QS.set_sparams(digital_output_level=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_': elif name[0:7] == 'hermes_':
if name == 'hermes_TxLNA_dB': if name == 'hermes_TxLNA_dB':
application.Hardware.ChangeTxLNA(x) application.Hardware.ChangeTxLNA(x)
@@ -3399,14 +3603,20 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
('', '', '', '', 'digital_rx8_name'), ('', '', '', '', 'digital_rx8_name'),
('', '', '', '', 'digital_rx9_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 label_help = ( # Same order as sound_names
(1, "Radio Sound Output", "This is the radio sound going to the headphones or speakers."), (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, "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."), (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, "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."), (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."), (0, "Digital Tx0 Input", hTx0),
(1, "Digital Rx0 Output", "This is the main receiver Rx0 audio going to a digital mode program."), (1, "Digital Rx0 Output", hRx0),
(1, "Digital Rx1 Output", "This is the sub-receiver 1 audio going to a digital mode program."), (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 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."), (1, "Digital Rx3 Output", "This is the sub-receiver 3 audio going to a digital mode program."),
@@ -3425,13 +3635,13 @@ class RadioSound(BaseWindow): # The Sound page in the second-level notebook for
self.radio_dict = local_conf.GetRadioDict(self.radio_name) self.radio_dict = local_conf.GetRadioDict(self.radio_name)
self.num_cols = 8 self.num_cols = 8
for name, text, fmt, help_text, values in local_conf.GetSectionData('Sound'): 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) value = self.GetValue(name, self.radio_dict)
no_edit = "choice" in fmt or fmt == 'boolean' no_edit = "choice" in fmt or fmt == 'boolean'
txt, cb, btn = self.AddTextComboHelp(1, text, value, values, help_text, no_edit) txt, cb, btn = self.AddTextComboHelp(1, text, value, values, help_text, no_edit)
cb.handler = self.OnChange cb.handler = self.OnChange
cb.quisk_data_name = name cb.quisk_data_name = name
break self.NextRow()
self.NextRow() self.NextRow()
# Add the grid for the sound settings # Add the grid for the sound settings
sizer = wx.GridBagSizer(2, 2) sizer = wx.GridBagSizer(2, 2)
@@ -3978,28 +4188,7 @@ class BandPlanDlg(wx.Dialog, ControlMixin):
if "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]: if "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]:
colors = Settings[4]["BandPlanColors"] colors = Settings[4]["BandPlanColors"]
else: else:
Get = conf.__dict__.get # Get colors from the user's config file, if any. colors = conf.BandPlanColors
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)]
]
self.modes = [] self.modes = []
self.color_dict = {None:"End"} self.color_dict = {None:"End"}
self.mode_dict = {"End":None} self.mode_dict = {"End":None}
@@ -4206,6 +4395,9 @@ The mode begins at the start frequency and ends at the next frequency.'
plan.append([hertz, color]) plan.append([hertz, color])
Settings[4]["BandPlan"] = freq_mode Settings[4]["BandPlan"] = freq_mode
application.BandPlan = plan application.BandPlan = plan
application.BandPlanC2T = {}
for mode, color in mode_color:
application.BandPlanC2T[color] = mode
local_conf.settings_changed = True local_conf.settings_changed = True
self.EndModal(wx.ID_OK) self.EndModal(wx.ID_OK)
def OnCancel(self, event): def OnCancel(self, event):
+22 -14
View File
@@ -836,13 +836,14 @@ by hand if you are a Python expert. Otherwise you can just read the values.
<br> <br>
To create a Receive correction point for a given VFO and frequency, attach a signal generator to the SoftRock through an attenuator, 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. 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 Choose "Measure" to measure the corrections and then "Add Cell".
the amplitude and phase sliders on the adjustment screen, and then press "Save". 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>
<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, 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 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>
<br> <br>
</p> </p>
@@ -1470,22 +1471,29 @@ remember the settings.
<br> <br>
<br> <br>
If you don't have PulseAudio or don't want to use it, you can use the ALSA loopback device. If your digital program only uses Alsa devices or you don't want to use PulseAudio,
The ALSA loopback device works the same way as the Windows VAC. First you can use the ALSA loopback device.
create the loopback device with the command "modprobe snd-aloop" (you It works the same way as the Windows VAC. First create the loopback device with this command:
will need to be root). You can create the loopback device <pre>
when the system starts, but the way to do that depends on your version of sudo modprobe snd-aloop index=19
Linux. I added snd-aloop to /etc/modules. You could put the modprobe command in /etc/rc.local instead. </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 Restart Linux. Now you can enter "cat /proc/asound/cards" to
print out your sound cards, and you should see a "Loopback" card print out your sound cards, and you should see a "Loopback" card listed.
listed. The cards are also shown on the Quisk Sound The Loopback card has one side that connects to Quisk and
screen. The Loopback card has one side that connects to Quisk and
another side that connects to your digital program. For the Quisk 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 Note that the Loopback card is full duplex, and handles both
input and output. There are actually eight loopbacks created at once, 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 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 Your audio is now connected and you
should be able to receive digital signals. Be sure to test your should be able to receive digital signals. Be sure to test your
transmit signal off the air. You may need to reduce power to transmit signal off the air. You may need to reduce power to
+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) 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. { // 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; int sample_rate = 8000;
double dsample, dtmp, magn; double dsample, dtmp, magn;
complex double csample; complex double csample;
@@ -423,9 +423,10 @@ static int tx_filter(complex double * filtered, int count)
} }
return 0; return 0;
} }
mic_interp = MIC_OUT_RATE / sample_rate;
// check size of dsamples[] and csamples[] buffer // check size of dsamples[] and csamples[] buffer
if (count > samples_size) { if (count > samples_size) {
samples_size = count * 2; samples_size = count * 2 * mic_interp;
if (dsamples) if (dsamples)
free(dsamples); free(dsamples);
if (csamples) if (csamples)
@@ -541,8 +542,8 @@ static int tx_filter(complex double * filtered, int count)
// remove clipping distortion // remove clipping distortion
count = quisk_cDecimate(csamples, count, &cfiltAudio3, 1); count = quisk_cDecimate(csamples, count, &cfiltAudio3, 1);
// Interpolate up to 48000 // Interpolate up to 48000
if (MIC_OUT_RATE != sample_rate) if (mic_interp != 1)
count = quisk_cInterpolate(csamples, count, &cfiltInterp, MIC_OUT_RATE / sample_rate); count = quisk_cInterpolate(csamples, count, &cfiltInterp, mic_interp);
// convert back to 16 bits // convert back to 16 bits
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
filtered[i] = csamples[i] * CLIP16; filtered[i] = csamples[i] * CLIP16;
@@ -573,8 +574,8 @@ static int tx_filter(complex double * filtered, int count)
// remove clipping distortion // remove clipping distortion
count = quisk_dFilter(dsamples, count, &dfiltAudio3); count = quisk_dFilter(dsamples, count, &dfiltAudio3);
// Interpolate up to 48000 // Interpolate up to 48000
if (MIC_OUT_RATE != sample_rate) if (mic_interp != 1)
count = quisk_dInterpolate(dsamples, count, &dfiltInterp, MIC_OUT_RATE / sample_rate); count = quisk_dInterpolate(dsamples, count, &dfiltInterp, mic_interp);
// convert back to 16 bits // convert back to 16 bits
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
filtered[i] = dsamples[i] * CLIP16; filtered[i] = dsamples[i] * CLIP16;
+111 -1
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_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 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 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 complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python
static int PySampleCount; // count of samples in buffer 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_data_width; // width of graph data to return
static int multirx_fft_width; // size of FFT samples static int multirx_fft_width; // size of FFT samples
@@ -2741,7 +2746,7 @@ int quisk_process_samples(complex double * cSamples, int nSamples)
multirx_cSamples[0][i] = dsamples2[i] + I * dsamples2[i]; multirx_cSamples[0][i] = dsamples2[i] + I * dsamples2[i];
process_agc(&Agc3, multirx_cSamples[0], n, 0); 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 // Perhaps decimate by an additional fraction
@@ -3156,6 +3161,10 @@ static PyObject * get_params(PyObject * self, PyObject * args)
return PyInt_FromLong(quisk_serial_ptt); return PyInt_FromLong(quisk_serial_ptt);
if (strcmp(name, "hl2_txbuf_errors") == 0) if (strcmp(name, "hl2_txbuf_errors") == 0)
return PyInt_FromLong(hl2_txbuf_errors); 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); Py_INCREF (Py_None);
return Py_None; return Py_None;
} }
@@ -3759,6 +3768,7 @@ static int read_rx_udp10(complex double * samp) // Read samples from UDP using t
//code_version = quisk_hermes_to_pc[3]; //code_version = quisk_hermes_to_pc[3];
if ((quisk_hermes_to_pc[0] & 0x01) != 0) // C1 if ((quisk_hermes_to_pc[0] & 0x01) != 0) // C1
quisk_sound_state.overrange++; 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 hardware_ptt = buf[start] & 0x01; // C0 bit zero is PTT
quisk_hardware_cwkey = (buf[start] & 0x04) >> 2; // C0 bit two is CW key state quisk_hardware_cwkey = (buf[start] & 0x04) >> 2; // C0 bit two is CW key state
switch (hl2_txbuf_state) { switch (hl2_txbuf_state) {
@@ -3870,6 +3880,10 @@ static int read_rx_udp17(complex double * cSamples0) // Read samples from UDP
fft_data * ptFFT; fft_data * ptFFT;
fd_set fds; fd_set fds;
static int block_number=0; 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 // Data from the receiver is little-endian
if ( ! rx_udp_gain_correct) { // correct for second stage CIC decimation JIM JIM if ( ! rx_udp_gain_correct) { // correct for second stage CIC decimation JIM JIM
@@ -3975,6 +3989,16 @@ static int read_rx_udp17(complex double * cSamples0) // Read samples from UDP
if (xr & 0x100) { // channel 1 if (xr & 0x100) { // channel 1
if (quisk_invert_spectrum) // Invert spectrum if (quisk_invert_spectrum) // Invert spectrum
sample = conj(sample); 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. // Put samples into the fft input array.
ptFFT = fft_data_array + fft_data_index; ptFFT = fft_data_array + fft_data_index;
if ( ! (xi & 0x100)) { // zero marker for start of first block if ( ! (xi & 0x100)) { // zero marker for start of first block
@@ -4242,6 +4266,13 @@ static PyObject * change_scan(PyObject * self, PyObject * args) // Called from G
return Py_None; 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 static PyObject * change_rates(PyObject * self, PyObject * args) // Called from GUI thread
{ // Change to new sample rates { // Change to new sample rates
@@ -5029,6 +5060,79 @@ static PyObject * get_bandscope(PyObject * self, PyObject * args) // Called by t
return Py_None; return Py_None;
} }
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 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; int i, j, k, m, n, index, ffts, ii, mm, m0, deltam;
@@ -5102,6 +5206,8 @@ static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the G
for (i = 0; i < fft_size; i++) // multiply by window for (i = 0; i < fft_size; i++) // multiply by window
ptFft->samples[i] *= fft_window[i]; ptFft->samples[i] *= fft_window[i];
fftw_execute_dft(quisk_fft_plan, ptFft->samples, ptFft->samples); // Calculate FFT 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 // Create RMS s-meter value at known bandwidth
// The pass band is (rx_tune_freq + filter_start_offset) to += bandwidth // The pass band is (rx_tune_freq + filter_start_offset) to += bandwidth
// d1 is the tune frequency // d1 is the tune frequency
@@ -5152,6 +5258,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); //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 { 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++; count_fft++;
k = 0; k = 0;
for (i = fft_size / 2; i < fft_size; i++) // Negative frequencies for (i = fft_size / 2; i < fft_size; i++) // Negative frequencies
@@ -5977,6 +6086,7 @@ static PyMethodDef QuiskMethods[] = {
{"get_filter_rate", get_filter_rate, METH_VARARGS, "Return the sample rate used for the filters."}, {"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_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."}, {"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_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."}, {"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."}, {"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 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 struct sound_dev * quiskPlaybackDevices[]; // array of Playback sound devices
extern int quisk_close_file_play; extern int quisk_close_file_play;
extern double digital_output_level;
extern PyObject * quisk_set_spot_level(PyObject * , PyObject *); extern PyObject * quisk_set_spot_level(PyObject * , PyObject *);
extern PyObject * quisk_get_tx_filter(PyObject * , PyObject *); extern PyObject * quisk_get_tx_filter(PyObject * , PyObject *);
+108 -10
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/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 # This free software is licensed for use under the GNU General Public
# License (GPL), see http://www.opensource.org. # License (GPL), see http://www.opensource.org.
# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! # Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!!
@@ -658,6 +658,16 @@ class HamlibHandlerSerial:
self.app.pttButton.SetValue(0, True) self.app.pttButton.SetValue(0, True)
else: else:
self.Error(cmd, data) 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 def ZZSP(self, cmd, data, length): # the split status
if length == 0: if length == 0:
if self.app.split_rxtx: if self.app.split_rxtx:
@@ -1151,7 +1161,7 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
def GetLevel(self): def GetLevel(self):
name = self.GetParamName() name = self.GetParamName()
if name == '?': # send back supported parameters if name == '?': # send back supported parameters
self.Reply2('AGC AF', 0) self.Reply2('AGC AF STRENGTH', 0)
elif name == 'AGC': elif name == 'AGC':
if self.app.BtnAGC.GetValue(): if self.app.BtnAGC.GetValue():
self.Reply2(4, 0) self.Reply2(4, 0)
@@ -1160,6 +1170,9 @@ class HamlibHandlerRig2: # Test with telnet localhost 4532
elif name == 'AF': elif name == 'AF':
x = self.app.audio_volume # audio_volume is 0 to 1.000 x = self.app.audio_volume # audio_volume is 0 to 1.000
self.Reply2("%.5f" % x, 0) self.Reply2("%.5f" % x, 0)
elif name == 'STRENGTH':
i = round(self.app.hamlib_strength)
self.Reply2(i, 0)
else: else:
self.ErrParam() self.ErrParam()
def SetLevel(self): def SetLevel(self):
@@ -1762,6 +1775,12 @@ class GraphDisplay(wx.Window):
dc.SetTextForeground(conf.color_graph_msg_fg) dc.SetTextForeground(conf.color_graph_msg_fg)
dc.SetBackgroundMode(wx.SOLID) dc.SetBackgroundMode(wx.SOLID)
dc.DrawText(self.display_text, 0, 0) 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): def DrawFilter(self, dc):
dc.SetPen(wx.TRANSPARENT_PEN) dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetLogicalFunction(wx.COPY) dc.SetLogicalFunction(wx.COPY)
@@ -1873,6 +1892,12 @@ class GraphScreen(wx.Window):
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) 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() self.MakeDisplay()
def MakeDisplay(self): def MakeDisplay(self):
self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary)
@@ -2224,6 +2249,44 @@ class GraphScreen(wx.Window):
self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseMotion', event=event, rx_freq=freq2) self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseMotion', event=event, rx_freq=freq2)
else: # Mouse motion changes the transmit frequency else: # Mouse motion changes the transmit frequency
self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event=event) 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): def OnWheel(self, event):
if conf.freq_spacing: if conf.freq_spacing:
wm = conf.freq_spacing wm = conf.freq_spacing
@@ -3497,6 +3560,8 @@ class App(wx.App):
self.midi_message = [] self.midi_message = []
self.idName2Button = {} self.idName2Button = {}
self.midi_handler = None self.midi_handler = None
self.tx_inhibit = 0
self.old_tx_inhibit = 0
if conf.use_rx_udp == 10: # Hermes UDP protocol if conf.use_rx_udp == 10: # Hermes UDP protocol
self.bandscope_clock = conf.rx_udp_clock self.bandscope_clock = conf.rx_udp_clock
else: else:
@@ -3557,12 +3622,13 @@ class App(wx.App):
self.smeter_avg_seconds = 1.0 # seconds for S-meter average self.smeter_avg_seconds = 1.0 # seconds for S-meter average
self.smeter_sunits = -87.0 self.smeter_sunits = -87.0
self.smeter_usage = "smeter" # specify use of s-meter display self.smeter_usage = "smeter" # specify use of s-meter display
self.hamlib_strength = 0.0;
self.timer = time.time() # A seconds clock self.timer = time.time() # A seconds clock
self.heart_time0 = self.timer # timer to call HeartBeat at intervals self.heart_time0 = self.timer # timer to call HeartBeat at intervals
self.save_time0 = self.timer self.save_time0 = self.timer
self.smeter_db_time0 = self.timer self.smeter_db_time0 = self.timer
self.smeter_sunits_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_index = 0
self.multi_rx_timer = self.timer self.multi_rx_timer = self.timer
self.band_up_down = 0 # Are band Up/Down buttons in use? self.band_up_down = 0 # Are band Up/Down buttons in use?
@@ -3718,6 +3784,7 @@ class App(wx.App):
pass #traceback.print_exc() pass #traceback.print_exc()
if "Version" not in self.bandAmplPhase: if "Version" not in self.bandAmplPhase:
self.FixAmplPhase() # Convert to new format Oct 2020 self.FixAmplPhase() # Convert to new format Oct 2020
self.FixAmplPhase2() # New logic June 2024
if Hardware.VarDecimGetChoices(): # Hardware can change the decimation. if Hardware.VarDecimGetChoices(): # Hardware can change the decimation.
self.sample_rate = Hardware.VarDecimSet() # Get the sample rate. self.sample_rate = Hardware.VarDecimSet() # Get the sample rate.
self.vardecim_set = self.sample_rate self.vardecim_set = self.sample_rate
@@ -4384,6 +4451,25 @@ class App(wx.App):
except: except:
traceback.print_exc() traceback.print_exc()
self.bandAmplPhase[band] = {} 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): def ImmediateChange(self, name):
if name == "pulse_audio_verbose_output": if name == "pulse_audio_verbose_output":
value = getattr(conf, name) value = getattr(conf, name)
@@ -5035,6 +5121,7 @@ class App(wx.App):
self.smeter_sunits_time0 = self.timer self.smeter_sunits_time0 = self.timer
s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit 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. 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: if s < 0:
s = 0 s = 0
if s >= 9.5: if s >= 9.5:
@@ -5816,10 +5903,8 @@ class App(wx.App):
self.graph.SetVFO(vfo) self.graph.SetVFO(vfo)
self.waterfall.SetVFO(vfo) self.waterfall.SetVFO(vfo)
self.station_screen.Refresh() self.station_screen.Refresh()
if self.w_phase:
self.w_phase.Redraw()
if change: 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') ampl, phase = self.GetAmplPhase('rx')
QS.set_ampl_phase(ampl, phase, 0) QS.set_ampl_phase(ampl, phase, 0)
ampl, phase = self.GetAmplPhase('tx') ampl, phase = self.GetAmplPhase('tx')
@@ -6370,7 +6455,7 @@ class App(wx.App):
Hardware.RepeaterOffset(0) Hardware.RepeaterOffset(0)
QS.set_ctcss(0) QS.set_ctcss(0)
QS.tx_hold_state(1) 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: if not self.tx_indicator:
self.tx_indicator = True self.tx_indicator = True
self.pttButton.Tx.TurnOn(True) self.pttButton.Tx.TurnOn(True)
@@ -6431,6 +6516,8 @@ class App(wx.App):
self.hot_key_ptt_active = False self.hot_key_ptt_active = False
self.hot_key_ptt_was_down = self.hot_key_ptt_is_down self.hot_key_ptt_was_down = self.hot_key_ptt_is_down
self.hot_key_ptt_pressed = False self.hot_key_ptt_pressed = False
if self.tx_inhibit:
ptt = False
if ptt is True and not ptt_button_down: if ptt is True and not ptt_button_down:
self.SetPTT(True) self.SetPTT(True)
elif ptt is False and ptt_button_down: elif ptt is False and ptt_button_down:
@@ -6501,6 +6588,16 @@ class App(wx.App):
if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks: if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks:
self.heart_time0 = self.timer self.heart_time0 = self.timer
Hardware.HeartBeat() 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() msg = QS.GetQuiskPrintf()
if msg: if msg:
print(msg, end='') print(msg, end='')
@@ -6519,9 +6616,10 @@ class App(wx.App):
self.ChangeDisplayFrequency(tune - vfo, vfo) self.ChangeDisplayFrequency(tune - vfo, vfo)
self.FldigiPoll() self.FldigiPoll()
self.HamlibPoll() self.HamlibPoll()
#if self.timer - self.fewsec_time0 > 3.0: if self.timer - self.slowheart_time0 > 0.5:
# self.fewsec_time0 = self.timer self.slowheart_time0 = self.timer
# print ('fewswc') if self.w_phase:
self.w_phase.SlowHeartBeat()
if self.timer - self.save_time0 > 20.0: if self.timer - self.save_time0 > 20.0:
self.save_time0 = self.timer self.save_time0 = self.timer
if self.CheckState(): if self.CheckState():
+36 -1
View File
@@ -473,6 +473,13 @@ hermes_iob_rxin = 'J10 available'
#hermes_iob_rxin = 'HL2 Rx to J9' #hermes_iob_rxin = 'HL2 Rx to J9'
#hermes_iob_rxin = 'Use J9 and J10' #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. # These are known power meter calibration tables. This table is not present in the JSON settings file.
power_meter_std_calibrations = {} power_meter_std_calibrations = {}
@@ -1077,7 +1084,15 @@ digital_rx1_name = ""
## digital_output_level Digital output level, number ## digital_output_level Digital output level, number
# This is the volume control 0.0 to 1.0 for digital playback to fldigi, etc. # This is the volume control 0.0 to 1.0 for digital playback to fldigi, etc.
# Changes are immediate (no need to restart). # 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: # Sound card names:
# #
@@ -2096,6 +2111,26 @@ RxOnly = '#AAAAAA'
# Colors start at the indicated frequency and continue until the # Colors start at the indicated frequency and continue until the
# next frequency. The special color "None" turns off color. # 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]
]
+4 -2
View File
@@ -7,7 +7,7 @@ import os
# You must define the version here. A title string including # You must define the version here. A title string including
# the version will be written to __init__.py and read by quisk.py. # the version will be written to __init__.py and read by quisk.py.
Version = '4.2.30' Version = '4.2.35'
fp = open("__init__.py", "w") # write title string fp = open("__init__.py", "w") # write title string
fp.write("#Quisk version %s\n" % Version) fp.write("#Quisk version %s\n" % Version)
@@ -39,8 +39,10 @@ if sys.platform == "darwin": # Build for Macintosh
libraries = ['portaudio', 'fftw3', 'm'] libraries = ['portaudio', 'fftw3', 'm']
if os.path.isdir('/opt/local/include'): # MacPorts if os.path.isdir('/opt/local/include'): # MacPorts
base_dir = '/opt/local' 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' base_dir = '/usr/local'
elif os.path.isdir('/opt/homebrew/include'): # HomeBrew on Apple Silicon
base_dir = '/opt/homebrew'
else: # Regular build? else: # Regular build?
base_dir = '/usr' base_dir = '/usr'
if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"): if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"):
+47 -11
View File
@@ -85,7 +85,8 @@ char * sound_format_names[4] = {"Int32", "Int16", "Float32", "Int24"} ;
static struct wav_file file_rec_audio, file_rec_samples, file_rec_mic; static struct wav_file file_rec_audio, file_rec_samples, file_rec_mic;
static int close_file_rec; 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 dc_remove_bw=100; // bandwidth of DC removal filter
static ty_sample_start pt_sample_start; static ty_sample_start pt_sample_start;
@@ -175,9 +176,10 @@ static void correct_sample (struct sound_dev * dev, complex double * cSamples, i
for (i = 0; i < nSamples; i++) { for (i = 0; i < nSamples; i++) {
re = creal(cSamples[i]); re = creal(cSamples[i]);
im = cimag(cSamples[i]); im = cimag(cSamples[i]);
re = re * dev->AmPhAAAA; cSamples[i] = re * dev->AmPhAAAA + I * (re * dev->AmPhCCCC + im * dev->AmPhDDDD);
im = re * dev->AmPhCCCC + im * dev->AmPhDDDD; //re = re * dev->AmPhAAAA;
cSamples[i] = re + I * im; //im = re * dev->AmPhCCCC + im * dev->AmPhDDDD;
//cSamples[i] = re + I * im;
} }
} }
} }
@@ -730,8 +732,11 @@ void * quisk_make_txIQ(struct sound_dev * dev, int rewind)
} }
// amplitude and phase corrections // amplitude and phase corrections
if (dev->doAmplPhase) { if (dev->doAmplPhase) {
dSamp0 = dSamp0 * dev->AmPhAAAA; //dSamp0 = dSamp0 * dev->AmPhAAAA;
dSamp1 = dSamp0 * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD; //dSamp1 = dSamp0 * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
d = dSamp0;
dSamp0 = d * dev->AmPhAAAA;
dSamp1 = d * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD;
} }
switch (dev->sound_format) { switch (dev->sound_format) {
case Int16: case Int16:
@@ -836,6 +841,34 @@ int quisk_play_sidetone(struct sound_dev * dev)
return 0; 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 int quisk_read_sound(void) // Called from sound thread
{ // called in an infinite loop by the main program { // called in an infinite loop by the main program
int i, nSamples, mic_count, mic_interp, retval, is_cw, mic_sample_rate; int i, nSamples, mic_count, mic_interp, retval, is_cw, mic_sample_rate;
@@ -913,9 +946,10 @@ int quisk_read_sound(void) // Called from sound thread
//QuiskPrintTime("quisk_read_sound end", 0); //QuiskPrintTime("quisk_read_sound end", 0);
if (Capture.channel_Delay >= 0) // delay the I or Q channel by one sample if (Capture.channel_Delay >= 0) // delay the I or Q channel by one sample
delay_sample(&Capture, (double *)cSamples, nSamples); 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 if (Capture.doAmplPhase) // amplitude and phase corrections
correct_sample(&Capture, cSamples, nSamples); correct_sample(&Capture, cSamples, nSamples);
DCremove(cSamples, nSamples, quisk_sound_state.sample_rate, key_state);
if (nSamples <= 0) if (nSamples <= 0)
QuiskSleepMicrosec(2000); QuiskSleepMicrosec(2000);
#if DEBUG_IO > 1 #if DEBUG_IO > 1
@@ -969,7 +1003,7 @@ int quisk_read_sound(void) // Called from sound thread
if (quisk_record_state == TMP_PLAY_SPKR_MIC) if (quisk_record_state == TMP_PLAY_SPKR_MIC)
quisk_tmp_playback(cSamples, nSamples, 1.0); // replace radio sound quisk_tmp_playback(cSamples, nSamples, 1.0); // replace radio sound
else if (quisk_record_state == FILE_PLAY_SPKR_MIC) 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 // Play the demodulated audio
#if DEBUG_MIC != 2 #if DEBUG_MIC != 2
if ( ! quisk_play_sidetone(&quisk_Playback)) // play sidetone if ( ! quisk_play_sidetone(&quisk_Playback)) // play sidetone
@@ -1401,6 +1435,8 @@ void quisk_open_sound(void) // Called from GUI thread
RawSamplePlayback.channel_I = 0; RawSamplePlayback.channel_I = 0;
RawSamplePlayback.channel_Q = 1; RawSamplePlayback.channel_Q = 1;
file_play_level = QuiskGetConfigDouble("file_play_level", 1.0);
set_num_channels (&Capture); set_num_channels (&Capture);
set_num_channels (&quisk_Playback); set_num_channels (&quisk_Playback);
set_num_channels (&MicCapture); set_num_channels (&MicCapture);
@@ -1565,10 +1601,10 @@ PyObject * quisk_micplay_channels(PyObject * self, PyObject * args) // Called fr
PyObject * quisk_set_sparams(PyObject * self, PyObject * args, PyObject * keywds) PyObject * quisk_set_sparams(PyObject * self, PyObject * args, PyObject * keywds)
{ /* Call with keyword arguments ONLY; change local parameters */ { /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"dc_remove_bw", "digital_output_level", "remote_control_slave", "remote_control_head", static char * kwlist[] = {"dc_remove_bw", "digital_output_level", "remote_control_slave", "remote_control_head",
NULL} ; "file_play_level", NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idii", kwlist, &dc_remove_bw, &digital_output_level, if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idiid", kwlist, &dc_remove_bw, &digital_output_level,
&remote_control_slave, &remote_control_head)) &remote_control_slave, &remote_control_head, &file_play_level))
return NULL; return NULL;
Py_INCREF (Py_None); Py_INCREF (Py_None);
return 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; snd_pcm_format_t format = SND_PCM_FORMAT_UNKNOWN;
dev->sample_bytes = 0; 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); 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 (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S32) == 0) {
if (!dev->sample_bytes) { if (!dev->sample_bytes) {
strncat(dev->msg1, "*", QUISK_SC_SIZE); 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) { if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_U24) == 0) {
strncat(dev->msg1, "U24 ", QUISK_SC_SIZE); 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 (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S24_3LE) == 0) {
if (!dev->sample_bytes) { if (!dev->sample_bytes) {
strncat(dev->msg1, "*", QUISK_SC_SIZE); strncat(dev->msg1, "*", QUISK_SC_SIZE);