diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 01762e3..6d99c51 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,14 @@
+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
diff --git a/__init__.py b/__init__.py
index d6786d8..5fdc8b4 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,2 +1,2 @@
-#Quisk version 4.2.31
+#Quisk version 4.2.32
from .quisk import main
diff --git a/configure.py b/configure.py
index d4a064e..08369af 100644
--- a/configure.py
+++ b/configure.py
@@ -2005,7 +2005,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', 'TxRxSilenceMsec'):
+ '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_'):
@@ -3399,14 +3399,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."),
diff --git a/docs.html b/docs.html
index f11695a..c42abdd 100644
--- a/docs.html
+++ b/docs.html
@@ -1470,22 +1470,29 @@ remember the settings.
-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:
+
+sudo modprobe snd-aloop index=19 ++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: +
+options snd-aloop index=19 ++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 diff --git a/microphone.c b/microphone.c index ad3dada..f3191d3 100644 --- a/microphone.c +++ b/microphone.c @@ -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; diff --git a/quisk.c b/quisk.c index fe503e1..16c7313 100644 --- a/quisk.c +++ b/quisk.c @@ -155,6 +155,7 @@ static int freedv_monitor; // pass the Freedv audio to the speakers instead of 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 @@ -2741,7 +2742,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 @@ -3156,6 +3157,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, "quisk_tx_inhibit") == 0) + return PyInt_FromLong((long)quisk_tx_inhibit); Py_INCREF (Py_None); return Py_None; } @@ -3759,6 +3762,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) { diff --git a/quisk.h b/quisk.h index 2daa148..f7c595c 100644 --- a/quisk.h +++ b/quisk.h @@ -320,6 +320,7 @@ extern int quisk_start_cw_delay; // milliseconds to delay output on serial or M extern int quisk_start_ssb_delay; // milliseconds to discard output for all modes except CW extern struct sound_dev * quiskPlaybackDevices[]; // array of Playback sound devices extern int quisk_close_file_play; +extern double digital_output_level; extern PyObject * quisk_set_spot_level(PyObject * , PyObject *); extern PyObject * quisk_get_tx_filter(PyObject * , PyObject *); diff --git a/quisk.py b/quisk.py index 960bcd6..cf02b61 100644 --- a/quisk.py +++ b/quisk.py @@ -1762,6 +1762,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) @@ -3497,6 +3503,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: @@ -6370,7 +6378,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) @@ -6431,6 +6439,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: @@ -6501,6 +6511,14 @@ 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() msg = QS.GetQuiskPrintf() if msg: print(msg, end='') diff --git a/quisk_conf_defaults.py b/quisk_conf_defaults.py index f5cabc7..4c8ca65 100644 --- a/quisk_conf_defaults.py +++ b/quisk_conf_defaults.py @@ -473,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 = {} diff --git a/setup.py b/setup.py index 9047bc7..47f9d0b 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.31' +Version = '4.2.32' fp = open("__init__.py", "w") # write title string fp.write("#Quisk version %s\n" % Version) diff --git a/sound.c b/sound.c index 9376152..10ca582 100644 --- a/sound.c +++ b/sound.c @@ -85,7 +85,7 @@ 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 int dc_remove_bw=100; // bandwidth of DC removal filter static ty_sample_start pt_sample_start; diff --git a/sound_alsa.c b/sound_alsa.c index c4fbcbe..403918a 100644 --- a/sound_alsa.c +++ b/sound_alsa.c @@ -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);