9 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
James C. Ahlstrom 36d717636c Quisk version 4.2.30 2024-03-20 12:28:39 -04:00
James C. Ahlstrom 26773d5d09 Quisk version 4.2.29 2024-01-11 12:31:13 -05:00
James C. Ahlstrom 15375aa4ea Change README.md 2023-12-27 16:34:05 -05:00
James C. Ahlstrom 7f516653d5 Changes to README.md. 2023-12-26 15:57:36 -05:00
26 changed files with 1199 additions and 574 deletions
+60
View File
@@ -1,3 +1,63 @@
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"
item to the FreeDV menu. It routes the sound to the speakers instead of to the demodulator.
Turn on both the monitor and FDX to listen to your transmitted FreeDV signals. Please test.
Modes 700x and 1600 are finished. The other modes will be added shortly. If you use any of them
post here and I will move them to the top of the list.
Quisk Version 4.2.28 December 2023
===================================
This version includes a patch from Jon, AB8WU, to build Quisk on FreeBSD. Thanks Jon!
+4 -4
View File
@@ -1,5 +1,5 @@
# Quisk Software Defined Radio by N2ADR
**December 26, 2023**
**December 27, 2023**
This is Quisk, a Software Defined Radio (SDR).
You supply radio hardware such as Hermes Lite2 or SoftRock to convert
@@ -14,8 +14,8 @@ The web page for this project is [https://groups.io/g/n2adr-sdr](https://groups
The change log is [here](CHANGELOG.txt).
The documentation is [here](docs.html).
Installation instructions are [here](http://james.ahlstrom.name/quisk/docs.html#Installation).
The help file is [here](help.html).
The documentation is [here](http://james.ahlstrom.name/quisk/docs.html).
This site is under construction. Check back soon.
The help file is [here](http://james.ahlstrom.name/quisk/help.html).
+1 -1
View File
@@ -1,2 +1,2 @@
#Quisk version 4.2.29
#Quisk version 4.2.35
from .quisk import main
+389 -197
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
@@ -829,12 +831,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 +842,231 @@ 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)
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,81 +1077,172 @@ 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):
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()
@@ -1484,7 +1685,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 +1720,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 +2206,7 @@ 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'):
setattr(conf, name, x)
application.ImmediateChange(name)
elif name[0:4] in ('lin_', 'win_'):
@@ -2022,6 +2223,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)
@@ -3399,14 +3603,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 +3635,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)
@@ -3978,28 +4188,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 +4395,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):
+107 -175
View File
@@ -131,7 +131,7 @@ button on the lower right.
<h2>News</h2>
<p>
The newest versions of Linux and Raspberry Pi OS do not allow installing Quisk in the system Python package directory.
I am working on a solution for this.
Use the Linux Source Installation below instead.
</p>
<br>
<p>
@@ -189,23 +189,22 @@ Many others contributed to Quisk, and are mentioned in comments in the source co
<h2 id="Installation">Installation</h2>
<p>
Quisk is free open source software written in Python and C. It is hosted on the PyPi repository
<a href="https://pypi.org">https://pypi.org</a>.
It can be installed using the standard Python setup tools. See specific directions below.
<a href="https://pypi.org">https://pypi.org</a> and it is available on github.
</p>
<br>
<p>
If you have Python 3 installed on your computer, just continue to use that version.
<b>Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</b>
If you want to install a more recent version, uninstall the old version first.
<b>On Windows Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11.</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.
</p>
<br>
<h3 id="g0.2.2">Windows Initial Installation</h3>
<p>
Windows does not include Python, so you must first install the most recent version of 64-bit Python 3.
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>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.</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
@@ -280,186 +279,111 @@ python -m pip uninstall quisk
</p>
<h3 id="g0.2.1">Linux Initial Installation</h3>
<p>
<br>
The libsoundio-dev module is NO LONGER required!
<br>
<br>
Linux runs on a wide variety of computers with different processors and versions of Linux.
Therefore Quisk is compiled for each one. To do this, Python and a number of other packages are required.
Most likely both Python2 and Python3 are already installed on your computer. Either the 32-bit or 64-bit
version will work. On my Ubuntu machine, "python2" starts Python2 and "python3" starts Python3.
Most likely both Python2 and Python3 are already installed on your computer.
On my Ubuntu machine, "python2" starts Python2 and "python3" starts Python3.
Just "python" starts Python2, but this will change as Python2 is phased out.
Python2 is obsolete, so you should only install Quisk to Python3, and use "python3" to start it.
Install these packages. Some of these can be installed from Python using "pip",
but it is better to use your package manager because pip and the package manager do not coordinate.
<b>Python2 is obsolete</b>, so you should only install Quisk to Python3, and use "python3" to start it.
Install these packages by using your package manager, not Python pip.
If newer versions of these packages become available, Linux will notify you.
Use the most recent version of python3-wxgtk available.
Use the most recent version of python3-wxgtk available for your Python version.
<br><br>
</p>
<table>
<tr>
<th>Package Description</th>
<th>Python3 Package</th>
</tr>
<tr>
<td>Discrete fourier transform</td>
<td>sudo apt-get install libfftw3-dev</td>
</tr>
<tr>
<td>Alsa sound library</td>
<td>sudo apt-get install libasound2-dev</td>
</tr>
<tr>
<td>Portaudio sound library</td>
<td>sudo apt-get install portaudio19-dev</td>
</tr>
<tr>
<td>Pulseaudio sound library</td>
<td>sudo apt-get install libpulse-dev</td>
</tr>
<tr>
<td>Python development</td>
<td>sudo apt-get install python3-dev</td>
</tr>
<tr>
<td>Python library development</td>
<td>sudo apt-get install libpython3-dev</td>
</tr>
<tr>
<td>wxPython GUI library</td>
<td>sudo apt-get install python3-wxgtk4.0</td>
</tr>
<tr>
<td>Python USB</td>
<td>sudo apt-get install python3-usb</td>
</tr>
<tr>
<td>Python serial port</td>
<td>sudo apt-get install python3-serial</td>
</tr>
<tr>
<td>Python installation tools</td>
<td>sudo apt-get install python3-setuptools</td>
</tr>
<tr>
<td>Pip package installer</td>
<td>sudo apt-get install python3-pip</td>
</tr>
</table>
<p>
<br><br>
These are part of your operating system. You only install them once unless it changes.
</p>
<br>
<pre>
sudo apt-get install libfftw3-dev
sudo apt-get install libasound2-dev
sudo apt-get install portaudio19-dev
sudo apt-get install libpulse-dev
sudo apt-get install python3-dev
sudo apt-get install libpython3-dev
</pre>
<br>
<p>
These are part of Python. You only install them once unless the Python started by "python3" changes.
</p>
<br>
<pre>
sudo apt-get install python3-wxgtk4.0
sudo apt-get install python3-usb
sudo apt-get install python3-serial
sudo apt-get install python3-setuptools
</pre>
<br>
<p>
If you want to use the SoapySDR module make sure that SoapySDR is installed from its Pothosware site before you build Quisk.
Otherwise Quisk will not have the files it needs. You can install SoapySDR later, but then you
will need to reinstall Quisk. This is also true of any other external packages that have their own C source.
will need to run "make" again. This is also true of any other external packages that have their own C source.
<br>
<br>
</p>
<h3>Linux Pip Installation</h3>
<br>
<p>
You can use the Python program "pip" to install Quisk into the Python package directory. This is the easiest
method, but it will not install the source files. If you want the source files see below.
<br>
<br>
sudo -H python3 -m pip install --upgrade quisk
<br>
<br>
This will install the Python files and all other files except for the C source. This is the easiest method
if you do not want to work in C.
To run Quisk, use this command from a terminal:
<br>
<br>
python3 -m quisk
<br>
<br>
You can create a panel launcher on your desktop with the command to run Quisk.
<br>
<br>
If the installation of Quisk fails, you can force re-installation:
<br>
<br>
<b> Installation of Quisk using Python tools like pip is deprecated</b>.
</p>
<pre>
sudo -H python3 -m pip uninstall quisk
sudo -H python3 -m pip install --no-cache-dir --force-reinstall quisk
</pre>
<br>
<p>
Installation to the system Python is no longer allowed, and you may get the message "error: externally-managed-environment".
If you used pip to install Quisk you can continue to upgrade with "sudo python3 -m pip install --upgrade quisk"
until you get this error message. But it is best to uninstall the pip Quisk. If you installed with pipx, uninstall that too.
</p>
<br>
You can also install an older version of Quisk. You may need to do that if the most recent version fails for some reason. Use:
<pre>
sudo python3 -m pip uninstall quisk
pipx uninstall quisk
</pre>
<br>
<p>
Then use the Linux Source Installation.
</p>
<br>
<br>
sudo -H python3 -m pip install quisk==4.1.51
<h3 id="LSI">Linux Source Installation</h3>
<p>
On Linux, Quisk is installed from github, and it is contained in a "quisk" directory. First you need to decide where to put the
quisk directory. Anywhere will do, but it is best to use a directory where you have write permission so
you don't need to use "sudo". The examples below assume you used your home directory indicated by "~". Just replace "~"
by your desired location.
</p>
<br>
<p>
To install a fresh copy of Quisk, change to your desired directory, clone the github site, change to the quisk directory and run "make".
Then run the python program quisk.py.
</p>
<br>
<pre>
cd ~
git clone https://github.com/jimahlstrom/quisk
cd quisk
make
python3 quisk.py
</pre>
<br>
<p>
If "make" fails, you probably have missing packages or missing "-dev" packages. Try to figure out what is missing from the error messages.
You only clone github for the initial installation. To update Quisk, use pull:
</p>
<br>
<pre>
cd ~/quisk
git pull
</pre>
<br>
<p>
You run Quisk by running the file quisk.py with python3. For example, "python3 /home/jim/quisk/quisk.py".
You can create a desktop icon to do that, but the method depends on your version of Linux.
<b>Never run Quisk with root privelege by using sudo. Fix the permission problem instead.</b>
To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios.
Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration
is (mostly) from the Config button. Ignore old directions and don't bother with a config file.
<br>
<br>
To edit the Quisk Python files you need to know where they are. To find out, import quisk and print it:
<br>
<br>
</p>
<pre>
jim@IntelNUC:~$ python3
Python 3.6.9 (default, Oct 8 2020, 12:12:24)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
--- import quisk
--- quisk
module 'quisk' from '/usr/local/lib/python3.6/dist-packages/quisk/__init__.py'
---
</pre>
<br>
<h3>Linux Pip Upgrade</h3>
<p>
To upgrade to a newer version of Quisk, use pip:
<br>
<br>
sudo python3 -m pip install --upgrade quisk
<br>
<br>
</p>
<h3>Linux Uninstall Quisk</h3>
<p>
sudo python3 -m pip uninstall quisk
To uninstall Quisk, just delete the quisk directory.
</p>
<br>
<br>
<h3>Linux Source Installation</h3>
<p>
The previous method using pip is simple, but will not install the C source files. If you want those proceed as follows.
Go to <a href="https://pypi.org/project/quisk/">https://pypi.org/project/quisk</a> and under "Navigation"
select "Download files". Download the quisk-x-x-xx.tar.gz file. This is the source file. The ".whl" files are for Windows.
Change directory to where the file was downloaded, probably "Downloads", and uncompress and untar it.
<br><br>
cd ~/Downloads
<br>
tar zxf quisk-x.x.xx.tar.gz
<br><br>
Change directories to the Quisk directory:
<br><br>
cd quisk-x.x.xx
<br><br>
Now compile Quisk with the "make" command:
<br><br>
make quisk3
<br><br>
If "make" fails, you probably have missing packages or missing "-dev" packages. Try to figure out what is missing from the error messages.
Now you can run quisk with the command:
<br><br>
python3 quisk.py
<br><br>
At this point you have a choice of where to install Quisk. You could just rename the quisk-x.x.xx directory
to a more convenient name, like "quisk" in your home directory, and run Quisk from there.
This is convenient if you want to
alter the Quisk code. Or you could install Quisk into the Python system.
<br><br>
python2 setup.py build
<br>
sudo python2 setup.py install
<br><br>
</p>
<h3 id="g0.2.4">Quisk Files</h3>
<p>
<br>
@@ -912,13 +836,14 @@ by hand if you are a Python expert. Otherwise you can just read the values.
<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>
@@ -1546,22 +1471,29 @@ 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.
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 -1
View File
@@ -586,7 +586,7 @@ double quiskAudio24p6Coefs[36] = { 0.001199140008010727, 0.005953815908571521, 0
-0.008973143363910490, 0.011839435384729619, 0.003122601697996235, -0.008665733839154772, -0.000319602525571569, 0.008621055763448699,
0.005953815908571521, 0.001199140008010727} ;
// Sample 48 kHz, pass 6, stop 8, ripple 0.5 dB, atten 80 dB. Stop 0.16667.
// Sample 48 kHz, pass 6, stop 8, ripple 0.5 dB, atten 80 dB. Pass 0.125. Stop 0.16667.
double quiskAudio48p6Coefs[71] = { 0.000324677779792651, 0.001038192405843851, 0.002082337464742640, 0.002938055116822665,
0.002811674671240099, 0.001132278163758895, -0.001835394694865431, -0.004766409947196539, -0.005833007003128954, -0.003914015660218843,
0.000296099005521212, 0.004239593489554206, 0.004918390640978499, 0.001100872619673516, -0.005241634309386784,
+37 -63
View File
@@ -32,8 +32,17 @@ typedef void (*freedv_callback_datarx)(void *, unsigned char *packet, size_t siz
typedef void (*freedv_callback_datatx)(void *, unsigned char *packet, size_t *size);
/* advanced freedv open options required by some modes */
struct freedv_advanced { // from freedv_api.h
int interleave_frames;
struct freedv_advanced {
int interleave_frames; // now unused but remains to prevent breaking API for
// legacy apps
// parameters for FREEDV_MODE_FSK_LDPC
int M; // 2 or 4 FSK
int Rs; // Symbol rate Hz
int Fs; // Sample rate Hz
int first_tone; // Freq of first tone Hz
int tone_spacing; // Spacing between tones Hz
char *codename; // LDPC codename, from codes listed in ldpc_codes.c
};
#ifdef MS_WINDOWS
@@ -66,11 +75,14 @@ static int handle_index = -1;
// need access to these here and in quisk.c
int n_speech_sample_rate = 8000;
int n_modem_sample_rate = 8000;
int n_max_modem_samples = 1000;
#define SPEECH_BUF_SIZE 12000 // speech buffer size increased from 3000
#define MAX_MODEM_SAMPLES 8192 // maximum of n_max_modem_samples for any mode
#define MAX_SPEECH_SAMPLES 8192 // maximum of n_speech_samples for any mode
static struct _rx_channel{
struct freedv * hFreedv;
COMP * demod_in;
short * demod_in;
int rxdata_index;
short speech_out[SPEECH_BUF_SIZE]; // output buffer
int speech_available; // number of samples in output buffer
@@ -164,12 +176,8 @@ static void GetAddrs(void)
handle_index = 2;
GET_HANDLE2;
if ( ! hLib) {
handle_index = 3;
GET_HANDLE3;
if ( ! hLib) {
handle_index = 4;
GET_HANDLE4;
}
handle_index = 4;
GET_HANDLE4;
}
}
if (hLib) {
@@ -231,19 +239,17 @@ static void GetAddrs(void)
return;
}
static int quisk_freedv_rx(complex double * cSamples, double * dsamples, int count, int bank) // Called from the sound thread.
{ // Input digital modulation is cSamples; decoded voice is dsamples. Each "bank" is a stream of audio.
static int quisk_freedv_rx(short * sSamples, double * dsamples, int count, int bank) // Called from the sound thread.
{ // Input digital modulation is sSamples; decoded voice is dsamples. Each "bank" is a stream of audio.
// caller will have decimated input audio down to n_modem_sample_rate and reduced block size to match reduction
// output data will be returned at n_speech_sample_rate with corresponding block sizes
int i, nout, need, have, sync;
int n_speech_samples;
complex double cx;
double scale = (double)CLIP32 / CLIP16; // convert 32 bits to 16 bits
struct freedv * hF;
struct _rx_channel * pCh;
int nCountOut = 1; // In case we have a higher (or lower) value for o/p count
if (cSamples == NULL) { // shutdown
if (sSamples == NULL) { // shutdown
for (i = 0; i < MAX_RECEIVERS; i++) {
if (rx_channel[i].demod_in) {
free(rx_channel[i].demod_in);
@@ -277,20 +283,11 @@ static int quisk_freedv_rx(complex double * cSamples, double * dsamples, int cou
nout = 0;
need = freedv_nin(hF);
for (i = 0; i < count; i++) {
cx = cRxFilterOut(cSamples[i], bank, 0);
if (rxMode == FDV_L) // lower sideband
cx = conj(cx);
#if 1 // Try it the other way and remove scaling factor
pCh->demod_in[pCh->rxdata_index].real = creal(cx);
pCh->demod_in[pCh->rxdata_index].imag = cimag(cx);
#else
pCh->demod_in[pCh->rxdata_index].real = (creal(cx) - cimag(cx));
pCh->demod_in[pCh->rxdata_index].imag = 0;
#endif
pCh->demod_in[pCh->rxdata_index] = sSamples[i];
pCh->rxdata_index++;
if (pCh->rxdata_index >= need) {
if (pCh->speech_available + n_speech_samples < SPEECH_BUF_SIZE) { // check for buffer space
have = freedv_comprx(hF, pCh->speech_out + pCh->speech_available, pCh->demod_in);
have = freedv_rx(hF, pCh->speech_out + pCh->speech_available, pCh->demod_in);
if (freedv_version > 10)
sync = freedv_get_sync(hF);
else
@@ -325,7 +322,7 @@ static int quisk_freedv_rx(complex double * cSamples, double * dsamples, int cou
}
}
for (nout = 0; nout < pCh->speech_available && nout < nCountOut; nout++)
dsamples[nout] = pCh->speech_out[nout] * scale * 0.7;
dsamples[nout] = pCh->speech_out[nout] * ((double)CLIP32 / CLIP16 * 0.99);
if (nout) {
pCh->speech_available -= nout;
memmove(pCh->speech_out, pCh->speech_out + nout, (pCh->speech_available) * sizeof(short));
@@ -339,7 +336,7 @@ static int quisk_freedv_rx(complex double * cSamples, double * dsamples, int cou
return nout;
}
static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int count) // Called from the sound thread.
static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int count, int is_real) // Called from the sound thread.
{ // Input voice samples are dsamples; output digital modulation is cSamples.
int i, nout;
int n_speech_samples;
@@ -351,16 +348,7 @@ static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int cou
static short *real_mod_out = NULL;
if (dsamples == NULL) { // shutdown
if (mod_out)
free(mod_out);
mod_out = NULL;
if (real_mod_out)
free(real_mod_out);
real_mod_out = NULL;
if (speech_in)
free(speech_in);
speech_in = NULL;
return 0;
return 0; // nothing to do
}
if ( ! rx_channel[0].hFreedv)
return 0;
@@ -371,13 +359,13 @@ static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int cou
if( nRatio < 1 || nRatio > 6 )
nRatio = 1;
if (mod_out == NULL) { // initialize
mod_out = (COMP *)malloc(sizeof(COMP) * n_nom_modem_samples);
memset(mod_out, 0, sizeof(COMP) * n_nom_modem_samples);
speech_in = (short*)malloc(sizeof(short) * n_speech_samples);
mod_out = (COMP *)malloc(sizeof(COMP) * MAX_MODEM_SAMPLES);
memset(mod_out, 0, sizeof(COMP) * MAX_MODEM_SAMPLES);
speech_in = (short*)malloc(sizeof(short) * MAX_SPEECH_SAMPLES);
speech_index=0;
mod_index=0;
real_mod_out = (short *)malloc(sizeof(short) * n_nom_modem_samples);
memset(real_mod_out, 0, sizeof(short) * n_nom_modem_samples);
real_mod_out = (short *)malloc(sizeof(short) * MAX_MODEM_SAMPLES);
memset(real_mod_out, 0, sizeof(short) * MAX_MODEM_SAMPLES);
//if( DEBUG ) printf("Initialise tx buffer size = %d, i/p sample size = %d\n", n_nom_modem_samples, n_speech_samples );
}
nout = 0;
@@ -386,7 +374,7 @@ static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int cou
if (speech_index >= n_speech_samples) {
// Calculate a new block, but first write out the rest of the old block
// 800xa uses real not complex
if( freedv_current_mode == FREEDV_MODE_800XA ) {
if( is_real ) {
for ( ; mod_index < n_nom_modem_samples; mod_index++)
cSamples[nout++] = real_mod_out[mod_index];
freedv_tx(rx_channel[0].hFreedv, real_mod_out, speech_in);
@@ -403,7 +391,7 @@ static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int cou
else { // write out samples
for( int j=0; j<nRatio; j++) {
if (mod_index < n_nom_modem_samples ) {
if( freedv_current_mode == FREEDV_MODE_800XA ) {
if( is_real ) {
cSamples[nout++] = real_mod_out[mod_index];
}
else {
@@ -471,21 +459,15 @@ static void CloseFreedv(void) // Called from the GUI thread or sound thread
freedv_close(rx_channel[i].hFreedv);
rx_channel[i].hFreedv = NULL;
}
#if 0 // g8kbb - unnecessary - quisk_freedv_rx() will do it
if (rx_channel[i].demod_in) {
free(rx_channel[i].demod_in);
rx_channel[i].demod_in = NULL;
}
#endif
}
quisk_freedv_rx(NULL, NULL, 0, 0);
quisk_freedv_tx(NULL, NULL, 0);
quisk_freedv_tx(NULL, NULL, 0, 0);
freedv_current_mode = -1;
}
static int OpenFreedv(void) // Called from the GUI thread or sound thread
{
int i, n_max_modem_samples;
int i;
struct freedv * hF;
if ( ! hLib)
@@ -500,7 +482,7 @@ static int OpenFreedv(void) // Called from the GUI thread or sound thread
break;
case 3:
case 4:
printf("freedv_open: codec2 library freedvpkg/libcodec2_32|64.dll|so found, version %d\n", freedv_version);
printf("freedv_open: codec2 library freedvpkg/libcodec2_64 found, version %d\n", freedv_version);
break;
default:
printf("freedv_open: Could not find the FreeDV codec2 shared library\n");
@@ -530,18 +512,10 @@ static int OpenFreedv(void) // Called from the GUI thread or sound thread
return 0; // failure
}
}
if (requested_mode == FREEDV_MODE_700E) {
if (handle_index >= 3) { // Quisk provided codec2
CloseFreedv();
requested_mode = -1;
if (DEBUG)
printf("freedv_open: Failure because mode 700E requires a system installation of codec2\n");
return 0; // failure
}
}
if ((requested_mode == FREEDV_MODE_700D || requested_mode == FREEDV_MODE_700E) && freedv_open_advanced) {
struct freedv_advanced adv;
memset(&adv, 0, sizeof(adv));
adv.interleave_frames = interleave_frames;
hF = freedv_open_advanced(requested_mode, &adv);
}
@@ -579,7 +553,7 @@ static int OpenFreedv(void) // Called from the GUI thread or sound thread
rx_channel[i].playing = 0;
if (rx_channel[i].demod_in)
free(rx_channel[i].demod_in);
rx_channel[i].demod_in = (COMP *)malloc(sizeof(COMP) * n_max_modem_samples);
rx_channel[i].demod_in = (short *)malloc(sizeof(short) * n_max_modem_samples);
if( rx_channel[i].demod_in == NULL) {
CloseFreedv();
requested_mode = -1;
+22 -10
View File
@@ -1,12 +1,24 @@
extern int freedv_current_mode; // the current FreeDV running mode
#define FREEDV_MODE_1600 0
#define FREEDV_MODE_700 1
#define FREEDV_MODE_700B 2
#define FREEDV_MODE_2400A 3
#define FREEDV_MODE_2400B 4
#define FREEDV_MODE_800XA 5
#define FREEDV_MODE_700C 6
#define FREEDV_MODE_700D 7
#define FREEDV_MODE_2020 8
#define FREEDV_MODE_700E 13
// available speech modes
#define FREEDV_MODE_1600 0
#define FREEDV_MODE_2400A 3
#define FREEDV_MODE_2400B 4
#define FREEDV_MODE_800XA 5
#define FREEDV_MODE_700C 6
#define FREEDV_MODE_700D 7
#define FREEDV_MODE_700E 13
#define FREEDV_MODE_2020 8
#define FREEDV_MODE_2020B 16
// available data modes
#define FREEDV_MODE_FSK_LDPC 9
#define FREEDV_MODE_DATAC1 10
#define FREEDV_MODE_DATAC3 12
#define FREEDV_MODE_DATAC0 14
#define FREEDV_MODE_DATAC4 18
#define FREEDV_MODE_DATAC13 19
// Sample rates used
#define FREEDV_FS_8000 8000
#define FREEDV_FS_16000 16000
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+70 -52
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;
@@ -626,32 +627,32 @@ static int tx_filter_digital(complex double * filtered, int count)
static int tx_filter_freedv(complex double * filtered, int count, int encode)
{ // Input samples are creal(filtered), output is filtered.
// This filter is used for digital voice.
// changes by Dave Roberts, G8KBB, for additional modes June, 2020.
// Changes by Dave Roberts, G8KBB, for additional modes June, 2020.
// Modified by N2ADR January, 2024.
int i;
// The input sample rate is mic_sample_rate, either 8000 or 48000 sps.
// The FreeDV codec requires an input n_speech_sample_rate of either 8000 or 16000 sps depending on the mode. Input is always real.
// The FreeDV codec will output samples at n_modem_sample_rate of 8000, 16000 or 48000 sps depending on the mode, and output may be real or complex.
// The FreeDV codec requires an input speech_sample_rate of either 8000 or 16000 sps depending on the mode. Input is always real.
// The FreeDV codec will output samples at modem_sample_rate of 8000 or 48000 sps depending on the mode, and output may be real or complex.
double dtmp, magn, dsample;
int modem_sample_rate, speech_sample_rate, use_filter, is_real;
static int samples_size = 0;
static int last_rx_mode;
static int last_freedv_mode;
static int do_init = 1;
static double aaa, bbb, ccc, Xmin, Xmax, Ymax;
static double time_long, time_short;
static double inMax=0.3;
static double * dsamples = NULL;
static struct quisk_cFilter filter2;
static struct quisk_dFilter filtDecim;
static struct quisk_cFilter cfiltInterp;
static struct quisk_cFilter filter1, filter2;
static struct quisk_dFilter filtDecim48to16, filtDecim16to8;;
static struct quisk_cFilter cfiltInterp8to48;
if (do_init) { // initialization
//QuiskWavWriteOpen(&hWav, "jim.wav", 3, 1, 4, 8000, 1.0 / CLIP16);
do_init = 0;
last_rx_mode = -1;
last_freedv_mode = -1;
memset(&filter2, 0, sizeof(filter2));
quisk_filt_dInit(&filtDecim, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); // pass 3000, stop 4000
quisk_filt_cInit(&cfiltInterp, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double));
quisk_filt_cInit(&filter1, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); // at 48000 sps pass 3000 stop 4000 times two plus 3000
quisk_filt_cInit(&filter2, quiskFilt53D2Coefs, sizeof(quiskFilt53D2Coefs)/sizeof(double)); // at 8000 sps pass 1500 stop 1800 times two plus 1500
quisk_filt_cInit(&cfiltInterp8to48, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double));
quisk_filt_dInit(&filtDecim48to16, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); // pass 6000 stop 8000
quisk_filt_dInit(&filtDecim16to8, quiskFilt16dec8Coefs, sizeof(quiskFilt16dec8Coefs)/sizeof(double)); // pass 3000 stop 4000
Ymax = pow(10.0, - 1 / 20.0); // maximum y
Xmax = pow(10.0, 3 / 20.0); // x where slope is zero; for x > Xmax, y == Ymax
Xmin = Ymax - fabs(Ymax - Xmax); // x where slope is 1 and y = x; start of compression
@@ -660,36 +661,38 @@ static int tx_filter_freedv(complex double * filtered, int count, int encode)
bbb = -2.0 * aaa * Xmax;
ccc = Ymax - aaa * Xmax * Xmax - bbb * Xmax;
}
if (last_freedv_mode != freedv_current_mode) { // correct filter2 depends on freedv mode
last_freedv_mode = freedv_current_mode;
last_rx_mode = -1; // re-tune new filter
if (filter2.cSamples)
free(filter2.cSamples);
filter2.cSamples = NULL;
if (filter2.cpxCoefs)
free(filter2.cpxCoefs);
filter2.cpxCoefs = NULL;
if (filter2.cBuf)
free(filter2.cBuf);
filter2.cBuf = NULL;
switch(freedv_current_mode) {
case FREEDV_MODE_700D: // filter2 is not used
switch (freedv_current_mode) { // n_modem_sample_rate and n_speech_sample_rate may not be valid
case FREEDV_MODE_2400A:
case FREEDV_MODE_2400B:
use_filter = 1; // center frequency 3000
is_real = 1;
modem_sample_rate = 48000;
speech_sample_rate = 8000;
break;
case FREEDV_MODE_2020:
use_filter = 0;
is_real = 0;
modem_sample_rate = 8000;
speech_sample_rate = 16000;
break;
case FREEDV_MODE_800XA:
use_filter = 2; // center frequency 1500
is_real = 1;
modem_sample_rate = 8000;
speech_sample_rate = 8000;
break;
// TODO: add modes that return real samples that must be converted to complex
default:
quisk_filt_cInit(&filter2, quiskFilt53D2Coefs, sizeof(quiskFilt53D2Coefs)/sizeof(double)); // pass 1500, stop 1800
use_filter = 0;
is_real = 0;
modem_sample_rate = 8000;
speech_sample_rate = 8000;
break;
}
}
if (last_rx_mode != (int)rxMode) { // change to sideband
last_rx_mode = rxMode;
if (rxMode == FDV_U) // upper sideband
quisk_filt_tune((struct quisk_dFilter *)&filter2, 1600.0 / n_modem_sample_rate, 1);
else if (rxMode == FDV_L) // lower sideband
quisk_filt_tune((struct quisk_dFilter *)&filter2, 1600.0 / n_modem_sample_rate, 0);
}
if ( ! filtered)
if ( ! filtered) { // rxMode changed
quisk_filt_tune((struct quisk_dFilter *)&filter1, 3000.0 / 48000.0, rxMode != FDV_L);
quisk_filt_tune((struct quisk_dFilter *)&filter2, 1500.0 / 8000.0, rxMode != FDV_L);
return 0;
}
// check size of dsamples[] buffer
if (count > samples_size) {
samples_size = count * 2;
@@ -701,12 +704,25 @@ static int tx_filter_freedv(complex double * filtered, int count, int encode)
for (i = 0; i < count; i++)
dsamples[i] = creal(filtered[i]) / CLIP16; // normalize to 1.0000
// Decimate to 8000 Hz or to 16000 depending on mode (and hence setting of sample rate)
if (quisk_sound_state.mic_sample_rate == 48000)
count = quisk_dDecimate(dsamples, count, &filtDecim, quisk_sound_state.mic_sample_rate / n_speech_sample_rate);
else if (quisk_sound_state.mic_sample_rate == 8000 && n_speech_sample_rate != 8000)
if (quisk_sound_state.mic_sample_rate == 48000) {
switch (speech_sample_rate) {
case 16000:
count = quisk_dDecimate(dsamples, count, &filtDecim48to16, 3);
break;
case 8000:
count = quisk_dDecimate(dsamples, count, &filtDecim48to16, 3);
count = quisk_dDecimate(dsamples, count, &filtDecim16to8, 2);
break;
default:
QuiskPrintf("Failure to convert speech input rate in tx_filter_freedv\n");
break;
}
}
else if (quisk_sound_state.mic_sample_rate == 8000 && speech_sample_rate != 8000) {
QuiskPrintf("Failure to convert input rate in tx_filter_freedv\n");
}
// Measure average peak input audio level and limit
dtmp = 1.0 / n_speech_sample_rate; // sample time
dtmp = 1.0 / speech_sample_rate; // sample time
time_long = 1.0 - exp(- dtmp / 3.000);
time_short = 1.0 - exp(- dtmp / 0.005);
for (i = 0; i < count; i++) {
@@ -730,13 +746,15 @@ static int tx_filter_freedv(complex double * filtered, int count, int encode)
}
//QuiskWavWriteD(&hWav, dsamples, count);
if (encode && pt_quisk_freedv_tx) // Encode audio into digital modulation
count = (* pt_quisk_freedv_tx)(filtered, dsamples, count);
count = (* pt_quisk_freedv_tx)(filtered, dsamples, count, is_real);
//quisk_calc_audio_graph(CLIP16, filtered, NULL, count, 0);
if (filter2.cSamples) // convert float samples to complex with an analytic filter
if (use_filter == 1) // convert float samples to complex with an analytic filter
count = quisk_cCDecimate(filtered, count, &filter1, 1);
else if (use_filter == 2)
count = quisk_cCDecimate(filtered, count, &filter2, 1);
// Interpolate up to 48000
if (MIC_OUT_RATE != n_modem_sample_rate )
count = quisk_cInterpolate(filtered, count, &cfiltInterp, MIC_OUT_RATE / n_modem_sample_rate);
if (MIC_OUT_RATE != modem_sample_rate )
count = quisk_cInterpolate(filtered, count, &cfiltInterp8to48, MIC_OUT_RATE / modem_sample_rate);
return count;
}
+219 -31
View File
@@ -5,6 +5,7 @@
#include <complex.h> // Use native C99 complex type for fftw3
#include <fftw3.h>
#include <sys/types.h>
#include <stdbool.h>
#ifdef MS_WINDOWS
#include <winsock2.h>
@@ -107,7 +108,7 @@ static PyObject * pyApp; // Application instance
static int fft_size; // size of fft, e.g. 1024
int data_width; // number of points to return as graph data; fft_size * n
static int graph_width; // width of the graph in pixels
rx_mode_type rxMode; // 0 to 13: CWL, CWU, LSB, USB, AM, FM, EXT, DGT-U, DGT-L, DGT-IQ, IMD, FDV-U, FDV-L, DGT-FM
rx_mode_type rxMode; // 0 to 13: CWL, CWU, LSB, USB, AM, FM, EXT, DGT_U, DGT_L, DGT_IQ, IMD, FDV_U, FDV_L, DGT_FM
int quisk_noise_blanker; // noise blanker level, 0 for off
int quiskTxHoldState; // hold Tx until the repeater frequency shift is complete
int quisk_is_vna; // zero for normal program, one for the VNA program
@@ -150,9 +151,15 @@ static int waterfall_scroll_mode = 1; // draw the first lines multiple times
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
@@ -179,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
@@ -1843,6 +1851,7 @@ static int quisk_process_decimate(complex double * cSamples, int nSamples, int b
static int quisk_process_demodulate(complex double * cSamples, double * dsamples, int nSamples, int bank, int nFilter, rx_mode_type rx_mode)
{ // Changes here will require changes to get_filter_rate();
int i;
bool isLSB;
complex double cx;
double d, di, dd;
static struct AgcState Agc1 = {0.3, 16000, 0}, Agc2 = {0.3, 16000, 0};
@@ -1918,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);
@@ -1937,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);
@@ -2138,40 +2155,100 @@ static int quisk_process_demodulate(complex double * cSamples, double * dsamples
}
break;
case FDV_U: // digital voice
case FDV_L: // Extra modes added by Dave Roberts, G8KBB, June 2020.
case FDV_L: // Extra modes added by Dave Roberts, G8KBB, June 2020. Modified by N2ADR 2024.
quisk_check_freedv_mode();
// added conditional for 2400A/B mmodes
// current coding assumes modem sample rate is 8000 16000 or 48000
if( n_modem_sample_rate <= 16000 )
// current coding assumes input rate n_modem_sample_rate is 8000 or 48000.
// decimate:
switch (n_modem_sample_rate) {
case 8000:
quisk_filter_srate = quisk_decim_srate / 6;
nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to16, 3);
if (bank == 0)
process_agc(&Agc1, cSamples, nSamples, 1);
else
process_agc(&Agc2, cSamples, nSamples, 1);
nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim16to8, 2);
break;
case 48000:
quisk_filter_srate = quisk_decim_srate;
break;
default:
quisk_filter_srate = quisk_decim_srate / 6;
QuiskPrintf("Bad FreeDV modem rate in quisk_process_demodulate\n");
break;
}
// filter and demodulate to mono audio:
isLSB = (rx_mode == FDV_L);
complex double cBuf[nSamples];
for (i = 0; i < nSamples; i++) {
cx = cRxFilterOut(cSamples[i], bank, nFilter);
if (isLSB)
dd = creal(cx) + cimag(cx);
else
dd = creal(cx) - cimag(cx);
if(bank == 0) {
measure_audio_sum += dd * dd;
measure_audio_count += 1;
}
cBuf[i] = dd;
}
// Perhaps decimate by an additional fraction
if (quisk_decim_srate != 48000) {
dd = quisk_decim_srate / 48000.0;
nSamples = cFracDecim(cSamples, nSamples, dd);
nSamples = cFracDecim(cBuf, nSamples, dd);
quisk_decim_srate = 48000;
}
quisk_filter_srate = n_speech_sample_rate;
// added conditional for 2400A/B mmodes
if( n_modem_sample_rate == 8000 )
nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim16to8, 2);
if (bank == 0)
process_agc(&Agc1, cBuf, nSamples, 0);
else
process_agc(&Agc2, cBuf, nSamples, 0);
// pass data decimated down to n_modem_sample_rate
// return data is as n_speech_sample_rate
if (pt_quisk_freedv_rx)
nSamples = (* pt_quisk_freedv_rx)(cSamples, dsamples, nSamples, bank);
if(bank == 0) {
for (i = 0; i < nSamples; i++) {
measure_audio_sum += dsamples[i] * dsamples[i];
measure_audio_count += 1;
}
// return data is at n_speech_sample_rate
#if 0
static double dmax_in = 0;
for (i = 0; i < nSamples; i++) {
dd = fabs(creal(cBuf[i])) / CLIP16;
if (dd > dmax_in)
dmax_in = dd;
}
// current coding assumes speech sample rate is only 8000 or 16000 - needs tweak for others
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p3, 3);
if( n_speech_sample_rate == 8000 )
if (bank == 0 && measure_audio_count >= quisk_filter_srate * measure_audio_time) {
//printf("Max sample in %.0lf\n", dmax_in);
dmax_in = 0;
}
#endif
if (nSamples > n_max_modem_samples) {
printf ("FreeDV: nSamples %d exceeds max_modem_samples %d\n", nSamples, n_max_modem_samples);
nSamples = n_max_modem_samples;
}
short sBuf[nSamples];
for (i = 0; i < nSamples; i++)
sBuf[i] = (short)(creal(cBuf[i]) / CLIP16);
if (freedv_monitor) {
for (i = 0; i < nSamples; i++)
dsamples[i] = (float)sBuf[i] * CLIP16;
}
else {
if (pt_quisk_freedv_rx)
nSamples = (* pt_quisk_freedv_rx)(sBuf, dsamples, nSamples, bank);
}
#if 0
static double dmax_out = 0;
for (i = 0; i < nSamples; i++) {
dd = fabs(dsamples[i]) / CLIP16;
if (dd > dmax_out)
dmax_out = dd;
}
if (bank == 0 && measure_audio_count >= quisk_filter_srate * measure_audio_time) {
//printf(" Max sample out %.0lf\n", dmax_out);
dmax_out = 0;
}
#endif
// current coding assumes output rate n_speech_sample_rate is 8000 or 16000
switch (n_speech_sample_rate) {
case 8000:
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p3, 3);
nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7);
break;
case 16000:
nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p3, 3);
break;
}
break;
}
if (bank == 0 && measure_audio_count >= quisk_filter_srate * measure_audio_time) {
@@ -2395,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
@@ -2669,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
@@ -2877,7 +2954,7 @@ static PyObject * get_filter_rate(PyObject * self, PyObject * args)
break;
case FDV_U: // digital voice
case FDV_L:
filter_srate = n_speech_sample_rate;
filter_srate = n_modem_sample_rate;
break;
}
//QuiskPrintf("Filter rate %d\n", filter_srate);
@@ -3084,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;
}
@@ -3113,13 +3194,15 @@ static PyObject * read_fftw_wisdom(PyObject * self, PyObject * args)
static PyObject * set_params(PyObject * self, PyObject * args, PyObject * keywds)
{ /* Call with keyword arguments ONLY; change local parameters */
static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip",
"bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause", NULL} ;
"bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause",
"freedv_monitor", NULL} ;
int i, nbytes, read_error, clip, bscope_size, hermes_pause;
nbytes = read_error = clip = bscope_size = hermes_pause = -1;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidi", kwlist,
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidii", kwlist,
&quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip,
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause))
&py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause,
&freedv_monitor))
return NULL;
if (nbytes != -1) {
py_sample_rx_bytes = nbytes;
@@ -3685,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) {
@@ -3796,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
@@ -3901,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
@@ -4168,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
@@ -4629,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;
}
@@ -4952,6 +5060,79 @@ static PyObject * get_bandscope(PyObject * self, PyObject * args) // Called by t
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
{
int i, j, k, m, n, index, ffts, ii, mm, m0, deltam;
@@ -5025,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
ptFft->samples[i] *= fft_window[i];
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
@@ -5075,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);
}
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
@@ -5786,6 +5972,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
@@ -5899,6 +6086,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."},
+4 -2
View File
@@ -315,10 +315,12 @@ extern play_state_t quisk_play_state; // startup, receiving, sidetone
extern int freedv_current_mode; // current FreeDV mode; 700D, @)@) etc.
extern int n_modem_sample_rate; // Receive data, decimate to modem_sample_rate, FreeDV codec output data at speech_sample_rate
extern int n_speech_sample_rate; // Microphone decimate to speech_sample_rate, Freedv codec output data at modem_sample_rate, interpolate to 48000
extern int n_max_modem_samples; // maximum input to freedv_rx()
extern int quisk_start_cw_delay; // milliseconds to delay output on serial or MIDI CW key down
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 *);
@@ -404,8 +406,8 @@ void quisk_set_play_state(void);
void quisk_poll_hardware_key(void);
// Functions supporting digital voice codecs
typedef int (* ty_dvoice_codec_rx)(complex double *, double *, int, int);
typedef int (* ty_dvoice_codec_tx)(complex double *, double *, int);
typedef int (* ty_dvoice_codec_rx)(short *, double *, int, int);
typedef int (* ty_dvoice_codec_tx)(complex double *, double *, int, int);
extern ty_dvoice_codec_rx pt_quisk_freedv_rx;
extern ty_dvoice_codec_tx pt_quisk_freedv_tx;
+152 -12
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!!
@@ -658,6 +658,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:
@@ -1151,7 +1161,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 +1170,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 +1775,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 +1892,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)
@@ -2224,6 +2249,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
@@ -3497,6 +3560,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:
@@ -3557,12 +3622,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 +3784,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
@@ -3762,7 +3829,8 @@ class App(wx.App):
# 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.
if conf.window_width > 0:
wFrame, h = frame.GetClientSize().Get() # client window width
#wFrame, h = frame.GetClientSize().Get() # client window width
wFrame = conf.window_width - 15 # Thanks to Nick Abramenko, RA3PKJ
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
graph.Destroy()
@@ -4383,6 +4451,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)
@@ -4704,11 +4791,15 @@ class App(wx.App):
self.freedv_menu = QuiskMenu("freedv_menu")
msg = conf.freedv_tx_msg
QS.freedv_set_options(mode=conf.freedv_modes[0][1], tx_msg=msg, DEBUG=0, squelch=1)
self.freedv_menu.AppendCheckItem("Monitor", self.OnFreedvMonitor)
self.freedv_menu.AppendSeparator()
for mode, index in conf.freedv_modes:
item = self.freedv_menu.AppendRadioItem(mode, self.OnFreedvMenu, mode == self.freedv_mode)
self.freedv_menu_items[index] = item
if mode == self.freedv_mode: # Restore mode
QS.freedv_set_options(mode=index)
if mode in ("Mode 2020", "Mode 2400A", "Mode 2400B"):
item.Enable(False)
if conf.button_layout == 'Large screen':
b = QuiskCycleCheckbutton(frame, None, ('FDV-U', 'FDV-L'), is_radio=True)
b.idName = "FDV"
@@ -5030,6 +5121,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:
@@ -5123,7 +5215,10 @@ class App(wx.App):
elif mode in ('DGT-IQ', 'DGT-FM'):
center = 0
elif mode in ('FDV-U', 'FDV-L'):
center = max(1500, bandwidth // 2)
if bandwidth <= 3000:
center = 1500
else:
center = bandwidth // 2
elif mode in ('IMD',):
center = 300 + bandwidth // 2
else:
@@ -5180,9 +5275,20 @@ class App(wx.App):
self.multi_rx_screen.waterfall.pane2.filter_center = center
if self.screen is self.filter_screen:
self.screen.NewFilter()
def OnFreedvMonitor(self, event):
for item in self.freedv_menu.GetMenuItems():
if item.GetKind() == wx.ITEM_CHECK:
if item.IsChecked():
QS.set_params(freedv_monitor=1)
else:
QS.set_params(freedv_monitor=0)
break
def OnFreedvMenu(self, event):
text = ''
for item in self.freedv_menu.GetMenuItems():
kind = item.GetKind()
if kind != wx.ITEM_RADIO:
continue
if item.IsChecked():
text = item.GetItemLabel()
break
@@ -5206,6 +5312,29 @@ class App(wx.App):
else:
self.freedv_menu_items[mode].Check(1)
print ("FreeDV change mode failed.")
self.OnChangeFilter()
def OnChangeFilter(self):
# Used by OnFreedvMenu because the FreeDV mode may change the filter sample rate
bw = int(self.filterButns.GetLabel())
# We can not use QS.get_filter_srate() because the FreeDV mode is not changed yet.
if self.freedv_mode in ("Mode 2400A", "Mode 2400B"):
frate = 48000;
else:
frate = 8000;
bw = min(bw, frate // 2)
self.filter_bandwidth = bw
center = self.GetFilterCenter(self.mode, bw)
filtI, filtQ = self.MakeFilterCoef(frate, None, bw, center)
lower_edge = center - bw // 2
QS.set_filters(filtI, filtQ, bw, lower_edge, 0)
self.multi_rx_screen.graph.filter_bandwidth = bw
self.multi_rx_screen.graph.filter_center = center
self.multi_rx_screen.waterfall.pane1.filter_bandwidth = bw
self.multi_rx_screen.waterfall.pane1.filter_center = center
self.multi_rx_screen.waterfall.pane2.filter_bandwidth = bw
self.multi_rx_screen.waterfall.pane2.filter_center = center
if self.screen is self.filter_screen:
self.screen.NewFilter()
def OnBtnHelp(self, event):
if event.GetEventObject().GetValue():
self.OnBtnScreen(None, 'Help')
@@ -5774,10 +5903,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')
@@ -6328,7 +6455,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)
@@ -6389,6 +6516,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:
@@ -6459,6 +6588,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='')
@@ -6477,9 +6616,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():
+49 -3
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:
#
@@ -1646,6 +1663,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.
@@ -1759,7 +1785,7 @@ freedv_tx_msg = ''
# This is the list of FreeDV modes and their index number. The starting mode is the first listed.
freedv_modes = (('Mode 1600', 0),
('Mode 2400A', 3), ('Mode 2400B', 4), ('Mode 800XA', 5),
('Mode 700C', 6), ('Mode 700D', 7), ('Mode 2020', 8), ('Mode 700E', 13))
('Mode 700C', 6), ('Mode 700D', 7), ('Mode 700E', 13), ('Mode 2020', 8))
# These are the filter bandwidths for each mode. Quisk has built-in optimized filters
# for these values, but you can change them if you want.
@@ -1770,7 +1796,7 @@ FilterBwFM = (8000, 10000, 12000, 16000, 18000, 20000)
FilterBwIMD = FilterBwSSB
FilterBwDGT = (200, 400, 1500, 3200, 4800, 10000)
FilterBwEXT = (8000, 10000, 12000, 15000, 17000, 20000)
FilterBwFDV = (1500, 2000, 3000, '', '', '')
FilterBwFDV = (1500, 2000, 3000, '6000', '', '')
# If your hardware file defines the method OnButtonPTT(self, event), then Quisk will
# display a PTT button you can press. The method must switch your hardware to
@@ -2085,6 +2111,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
View File
@@ -1549,6 +1549,8 @@ class QuiskMenu(wx.Menu):
item.Check(is_checked)
self.InitData(item, handler)
return item
def AppendSeparator(self):
wx.Menu.AppendSeparator(self)
def IsItemChecked(self, item_text):
nid = self.item_text2id[item_text]
menu_item = self.FindItemById(nid)
+4 -2
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.28'
Version = '4.2.35'
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"):
+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 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 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++) {
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 +732,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 +841,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 +946,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 +1003,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
@@ -1401,6 +1435,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 +1601,10 @@ 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} ;
"file_play_level", NULL} ;
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idii", kwlist, &dc_remove_bw, &digital_output_level,
&remote_control_slave, &remote_control_head))
if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idiid", kwlist, &dc_remove_bw, &digital_output_level,
&remote_control_slave, &remote_control_head, &file_play_level))
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);