diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 7266254..14c2228 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +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. diff --git a/__init__.py b/__init__.py index 9644518..768eee0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ -#Quisk version 4.2.34 +#Quisk version 4.2.35 from .quisk import main diff --git a/configure.py b/configure.py index 3474e44..553ce61 100644 --- a/configure.py +++ b/configure.py @@ -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 @@ -835,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 @@ -848,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 @@ -973,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() @@ -2028,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) @@ -3437,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) diff --git a/docs.html b/docs.html index c42abdd..e8bd1a4 100644 --- a/docs.html +++ b/docs.html @@ -836,13 +836,14 @@ by hand if you are a Python expert. Otherwise you can just read the values.
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".

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".

diff --git a/quisk.c b/quisk.c index f5ce4bc..da8049c 100644 --- a/quisk.c +++ b/quisk.c @@ -152,6 +152,10 @@ 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 @@ -3157,6 +3161,8 @@ 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); @@ -4260,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 @@ -5047,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; @@ -5120,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 @@ -5170,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 @@ -5995,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."}, diff --git a/quisk.py b/quisk.py index f3f029c..a77bca6 100644 --- a/quisk.py +++ b/quisk.py @@ -3628,7 +3628,7 @@ class App(wx.App): 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? @@ -3784,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 @@ -4450,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) @@ -5883,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') @@ -6598,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(): diff --git a/quisk_conf_defaults.py b/quisk_conf_defaults.py index 736cc3b..e958f10 100644 --- a/quisk_conf_defaults.py +++ b/quisk_conf_defaults.py @@ -1084,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: # diff --git a/setup.py b/setup.py index e1d6b10..9fb4c39 100644 --- a/setup.py +++ b/setup.py @@ -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.34' +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"): diff --git a/sound.c b/sound.c index 10ca582..4977629 100644 --- a/sound.c +++ b/sound.c @@ -86,6 +86,7 @@ static struct wav_file file_rec_audio, file_rec_samples, file_rec_mic; static int close_file_rec; 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;