diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 660750c..2b1390d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Quisk Version 4.2.53 June 2026 +=============================== +I added TCI support for Software Defined Connectors CW skimmer. Please test. + +This version of Quisk now works on Python 3.14 and higher. I changed to a new build system because +the old one has been deprecated by Python. They no longer support running setup.py. +The Linux version has a new makefile. The Windows version is installed by pip as usual. +Please test. + Quisk Version 4.2.52 May 2026 ============================== This is a fix for various TCI bugs. Please test with WSJT-X and let me know if all problems are solved. diff --git a/__init__.py b/__init__.py index 3840ae7..002f4aa 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ -#Quisk version 4.2.52 +#Quisk version 4.2.53 from .quisk import main diff --git a/ac2yd/remote.c b/ac2yd/remote.c index 6f53e4c..46a33de 100644 --- a/ac2yd/remote.c +++ b/ac2yd/remote.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/afedrinet/afedrinet_io.c b/afedrinet/afedrinet_io.c index af179aa..cb19f36 100644 --- a/afedrinet/afedrinet_io.c +++ b/afedrinet/afedrinet_io.c @@ -3,6 +3,7 @@ #ifdef MS_WINDOWS #include #include +#include #else #include #include @@ -105,7 +106,7 @@ static PyObject * open_rx_udp(const char * ip, int port) wVersionRequested = MAKEWORD(2, 2); if (WSAStartup(wVersionRequested, &wsaData) != 0) { sprintf(buf, "Failed to initialize Winsock (WSAStartup)"); - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } #endif // quisk_using_udp = 1; @@ -145,7 +146,7 @@ static PyObject * open_rx_udp(const char * ip, int port) else { sprintf(buf, "Failed to open socket"); } - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } static PyObject * close_rx_udp(PyObject * self, PyObject * args) @@ -343,7 +344,7 @@ static PyObject * open_samples(PyObject * self, PyObject * args) quisk_sample_source(NULL, NULL, &afedri_read_rx_udp); ////////////// return open_rx_udp(ip, port); // AFEDRI specific -// return PyString_FromString(buf); // return a string message +// return PyUnicode_FromString(buf); // return a string message } // Miscellaneous functions needed by the SDR-IQ; called from the GUI thread as diff --git a/afedrinet/makefile b/afedrinet/makefile index e0fb199..33d0ddd 100644 --- a/afedrinet/makefile +++ b/afedrinet/makefile @@ -1,7 +1,24 @@ +MODULE_NAME := afedrinet_io +OBJDIR := ../build/local -afedrinet2: - python2 setup.py build_ext --force --inplace +CFLAGS := $(shell python3 ../makefil.py --cflags) +LDFLAGS := $(shell python3 ../makefil.py --ldflags) +EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix) +TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX) -afedrinet3: +# Default target: Link the shared library object +$(TARGET): afedrinet_io.o $(OBJDIR)/import_quisk_api.o $(OBJDIR)/is_key_down.o + $(CC) -o $@ afedrinet_io.o $(LDFLAGS) $(OBJDIR)/import_quisk_api.o $(OBJDIR)/is_key_down.o + +# Compile the C source files into object files +afedrinet_io.o: afedrinet_io.c ../quisk.h + $(CC) -o $@ -c $< $(CFLAGS) -I.. + +.PHONY: clean +clean: + -rm -f *.o + -rm -f $(MODULE_NAME)*.so + +.PHONY: afedrinet-old +afedrinet-old: python3 setup.py build_ext --force --inplace - diff --git a/docs.html b/docs.html index 7483689..631c2bb 100644 --- a/docs.html +++ b/docs.html @@ -84,7 +84,7 @@ h3 {font-size:1.6rem;}

-Welcome to QUISK (September 2025) +Welcome to QUISK (June 2026)

@@ -197,7 +197,7 @@ Quisk is free open source software written in Python and C. It is hosted on the

If you have Python 3 installed on your computer, just continue to use that version. -On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13. +On Windows Quisk requires Python 3.8 or higher. 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. If you upgrade to a newer Python @@ -208,8 +208,7 @@ you must re-install Quisk. But your Quisk settings will be preserved.

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. -On Windows Quisk requires Python 3.8, 3.9, 3.10, 3.11, 3.12 or 3.13. -There may be newer versions of Python, but until support is added, use the versions listed above. +On Windows Quisk requires Python 3.8 or higher. Python is available from http://www.python.org. There is an option to Add Python to PATH. You MUST select this option so that you can start python by just typing "python" instead of the whole path @@ -383,6 +382,7 @@ You only clone github for the initial installation. To update Quisk, use pull fo

 cd ~/quisk
 git pull
+make clean
 make
 

diff --git a/extdemod.c b/extdemod.c index 26636d4..a77e668 100644 --- a/extdemod.c +++ b/extdemod.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include "quisk.h" diff --git a/filter.c b/filter.c index 670d2c9..e7b1d4b 100644 --- a/filter.c +++ b/filter.c @@ -1,6 +1,7 @@ #include #include #include +#include #include // Use native C99 complex type for fftw3 #include "quisk.h" #include "filter.h" diff --git a/import_quisk_api.c b/import_quisk_api.c index dcda154..abc7e15 100644 --- a/import_quisk_api.c +++ b/import_quisk_api.c @@ -32,6 +32,8 @@ with this: */ #include +#include +#include void ** Quisk_API; // array of pointers to functions and variables from module _quisk struct sound_conf * pt_quisk_sound_state; // pointer to quisk_sound_state diff --git a/is_key_down.c b/is_key_down.c index 5262911..16aab74 100644 --- a/is_key_down.c +++ b/is_key_down.c @@ -1,5 +1,6 @@ #include // used by quisk.h #include // Used by quisk.h +#include #include "quisk.h" // This module provides methods to access the state of the key. diff --git a/makefil.py b/makefil.py new file mode 100644 index 0000000..b67016b --- /dev/null +++ b/makefil.py @@ -0,0 +1,70 @@ +# This provides helper functions for our makefile. + +# Changes for MacOS support thanks to Mario, DL3LSM. +# Changes for building from macports provided by Eric, KM4DSJ +# Updated code for a Mac build contributed by Christoph, DL1YCF, December 2020. +# Converted from setup.py to makefile by Jim Ahlstrom, June 2026 + +import sys, os, subprocess, sysconfig + +if len(sys.argv) < 2: + sys.exit(2) + +job = sys.argv[1] + +def flags (): + # Fetch compiler flags and link flags from Python + result = subprocess.run(["python3-config", "--cflags"], capture_output=True, text=True, check=True) + cflags = result.stdout.strip() + cflags += " -fPIC" + result = subprocess.run(["python3-config", "--ldflags"], capture_output=True, text=True, check=True) + ldflags = result.stdout.strip() + ldflags += " -shared -lm -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -lfftw3" + if sys.platform == "darwin": # Build for Macintosh + cflags += " -DQUISK_HAVE_PORTAUDIO " + ldflags += " -lportaudio " + if os.path.isdir('/opt/local/include'): # MacPorts + base_dir = '/opt/local' + 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"): + cflags += " -DQUISK_HAVE_PULSEAUDIO " + ldflags += " -lpulse " + cflags += " -I. -I%s/include " % base_dir + ldflags += " -L. -L%s/lib " % base_dir + elif "freebsd" in sys.platform: #Build for FreeBSD + cflags += " -DQUISK_HAVE_PULSEAUDIO -I. -I/usr/local/include" + ldflags += " -lpulse -L. -L/usr/local/lib" + else: # Linux + cflags += " -DQUISK_HAVE_ALSA -DQUISK_HAVE_PULSEAUDIO" + ldflags += " -lasound -lpulse" + if os.path.isfile("/usr/include/portaudio.h"): + cflags += " -DQUISK_HAVE_PORTAUDIO" + ldflags += " -lportaudio" + return cflags, ldflags + +if job == "--soapy": + # See if soapy is available + if os.path.isdir("/usr/include/SoapySDR") or os.path.isdir("/usr/local/include/SoapySDR"): + print('1') + else: + print('0') + +elif job == "--suffix": + # Determine the platform-specific extension suffix (e.g., .cpython-311-x86_64-linux-gnu.so) + print(sysconfig.get_config_var('EXT_SUFFIX')) + +elif job == "--cflags": + cc, ld = flags() + print(cc) + +elif job == "--ldflags": + cc, ld = flags() + print(ld) + +else: + sys.exit(1) diff --git a/makefile b/makefile index bfb9c69..63b6e75 100644 --- a/makefile +++ b/makefile @@ -1,18 +1,93 @@ -.PHONY: quisk +# This makefile replaces the old Python "setup.py build_ext" which has been deprecated. +# See "quisk-old" below to make Quisk using the old setup.py. -quisk: +MODULE_NAME := _quisk +OBJDIR := build/local +SUBDIRS := afedrinet + +ifeq ($(shell python3 makefil.py --soapy), 1) +SUBDIRS := $(SUBDIRS) soapypkg +endif + +SRCS := \ +base64.c \ +extdemod.c \ +filter.c \ +freedv.c \ +handshake.c \ +is_key_down.c \ +microphone.c \ +quisk.c \ +quisk_wdsp.c \ +sha1.c \ +sound.c\ +sound_alsa.c \ +sound_directx.c \ +sound_portaudio.c \ +sound_pulseaudio.c \ +sound_wasapi.c \ +tci.c \ +utf8.c \ +utility.c \ +ws.c + +CFLAGS := $(shell python3 makefil.py --cflags) +LDFLAGS := $(shell python3 makefil.py --ldflags) +EXTENSION_SUFFIX := $(shell python3 makefil.py --suffix) +TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX) + +#$(info Info CFLAGS $(CFLAGS)) +#$(info Info LDFLAGS $(LDFLAGS)) +#$(info Info TARGET $(TARGET)) +#$(info Info SUBDIRS $(SUBDIRS)) + +OBJS := $(patsubst %.c, $(OBJDIR)/%.o, $(SRCS)) + +.PHONY: all clean test +.PHONY: $(SUBDIRS) + +# Default target +all: $(OBJDIR) $(OBJDIR)/import_quisk_api.o ac2yd/remote.o $(TARGET) $(SUBDIRS) + +$(SUBDIRS): + $(MAKE) -C $@ + +$(OBJDIR): + @mkdir -p $(OBJDIR) + +# Link the shared library object +$(TARGET): $(OBJS) + $(CC) -o $@ $(OBJS) ac2yd/remote.o $(LDFLAGS) + +# Define the location for dependency files +DEPS = $(patsubst %.o,$(OBJDIR)/%.d,$(notdir $(OBJS))) +DEPFLAGS = -MMD -MP -MF $(OBJDIR)/$(notdir $*).d +-include $(DEPS) + +# Compile the C source files into object files +ac2yd/remote.o: ac2yd/remote.c quisk.h filter.h + $(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS) + +$(OBJDIR)/import_quisk_api.o: import_quisk_api.c + $(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS) + +$(OBJDIR)/%.o: %.c | $(OBJDIR) + $(CC) -o $@ -c $< $(CFLAGS) $(DEPFLAGS) + +clean: + -rm -f $(OBJDIR)/*.o $(OBJDIR)/*.d + -rm -f ac2yd/*.o + -rm -f $(MODULE_NAME)*.so + for dir in $(SUBDIRS); do \ + $(MAKE) -C $$dir clean; \ + done + +# Run a test script to verify that the module can be imported +test: $(TARGET) + @python3 -c "import $(MODULE_NAME); print('Successfully imported:', $(MODULE_NAME).__file__)" + + + +.PHONY: quisk-old +quisk-old: python3 setup.py build_ext --force --inplace - @echo - -quisk3: - python3 setup.py build_ext --force --inplace - @echo - -soapy3: - (cd soapypkg; make soapy3) - -afedrinet3: - (cd afedrinet; make afedrinet3) - -perseus3: - (cd perseuspkg; make perseus3) diff --git a/microphone.c b/microphone.c index ba66d2e..dc9056f 100644 --- a/microphone.c +++ b/microphone.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "quisk.h" #include diff --git a/perseuspkg/makefile b/perseuspkg/makefile index 08b7f11..87ffca0 100644 --- a/perseuspkg/makefile +++ b/perseuspkg/makefile @@ -1,7 +1,25 @@ +MODULE_NAME := perseus +OBJDIR := ../build/local -perseus2: - python2 setup.py build_ext --force --inplace +CFLAGS := $(shell python3 ../makefil.py --cflags) +LDFLAGS := $(shell python3 ../makefil.py --ldflags) +EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix) +TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX) -perseus3: +# Default target: Link the shared library object +$(TARGET): perseus.o $(OBJDIR)/import_quisk_api.o + $(CC) -o $@ perseus.o $(LDFLAGS) $(OBJDIR)/import_quisk_api.o + +# Compile the C source files into object files +perseus.o: perseus.c ../quisk.h ../filter.h + $(CC) -o $@ -c $< $(CFLAGS) -I.. + +.PHONY: clean +clean: + -rm -f *.o + -rm -f $(TARGET) + +.PHONY: perseus-old +perseus-old: python3 setup.py build_ext --force --inplace diff --git a/perseuspkg/perseus.c b/perseuspkg/perseus.c index c830cf5..df44ec0 100644 --- a/perseuspkg/perseus.c +++ b/perseuspkg/perseus.c @@ -297,11 +297,11 @@ static PyObject * open_device(PyObject * self, PyObject * args) main_cleanup: - return PyString_FromString("ERROR"); + return PyUnicode_FromString("ERROR"); exit_success: - return PyString_FromString(buf128); + return PyUnicode_FromString(buf128); } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c00e148 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "quisk" +version = "4.2.53" +requires-python = ">=3.13" +description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.' +license = "GPL-2.0-or-later" +license-files = ["license.txt"] +authors = [ + {name = "James C. Ahlstrom", email="jahlstr@gmail.com"} +] +keywords = ['quisk', 'sdr', 'software defined radio'] +classifiers = [ + 'Development Status :: 6 - Mature', + 'Environment :: X11 Applications', + 'Environment :: Win32 (MS Windows)', + 'Intended Audience :: End Users/Desktop', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python :: 3', + 'Programming Language :: C', + 'Topic :: Communications :: Ham Radio', +] + +[project.gui-scripts] +quisk = "quisk.quisk:main" + +[project.urls] +Homepage = "https://james.ahlstrom.name/quisk/index.html" +Documentation = "https://james.ahlstrom.name/quisk/docs.html" +Repository = "https://github.com/jimahlstrom/quisk" +Changelog = "https://james.ahlstrom.name/quisk/CHANGELOG.txt" + +[tool.setuptools.packages.find] +where = ["."] + + diff --git a/quisk.c b/quisk.c index 743571b..7e657ad 100644 --- a/quisk.c +++ b/quisk.c @@ -3,6 +3,7 @@ #include #include #include // Use native C99 complex type for fftw3 +#include #include #include #include @@ -4027,7 +4028,7 @@ static PyObject * open_rx_udp(PyObject * self, PyObject * args) wVersionRequested = MAKEWORD(2, 2); if (WSAStartup(wVersionRequested, &wsaData) != 0) { sprintf(buf, "Failed to initialize Winsock (WSAStartup)"); - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } else { cleanupWSA = 1; @@ -4076,7 +4077,7 @@ static PyObject * open_rx_udp(PyObject * self, PyObject * args) else { sprintf(buf, "Failed to open socket"); } - return PyString_FromString(buf); + return PyUnicode_FromString(buf); } static PyObject * open_sound(PyObject * self, PyObject * args) @@ -4290,7 +4291,7 @@ static PyObject * mixer_set(PyObject * self, PyObject * args) return NULL; quisk_alsa_mixer_set(card_name, numid, value, err_msg, QUISK_SC_SIZE); - return PyString_FromString(err_msg); + return PyUnicode_FromString(err_msg); } static PyObject * pc_to_hermes(PyObject * self, PyObject * args) @@ -4493,12 +4494,12 @@ static PyObject * ip_interfaces(PyObject * self, PyObject * args) baddr |= ~netmask; tup = PyTuple_New(4); if (name == NULL) - PyTuple_SetItem(tup, 0, PyString_FromString("unnamed")); + PyTuple_SetItem(tup, 0, PyUnicode_FromString("unnamed")); else - PyTuple_SetItem(tup, 0, PyString_FromString(name)); - PyTuple_SetItem(tup, 1, PyString_FromString(Win_NtoA(ipAddr))); - PyTuple_SetItem(tup, 2, PyString_FromString(Win_NtoA(netmask))); - PyTuple_SetItem(tup, 3, PyString_FromString(Win_NtoA(baddr))); + PyTuple_SetItem(tup, 0, PyUnicode_FromString(name)); + PyTuple_SetItem(tup, 1, PyUnicode_FromString(Win_NtoA(ipAddr))); + PyTuple_SetItem(tup, 2, PyUnicode_FromString(Win_NtoA(netmask))); + PyTuple_SetItem(tup, 3, PyUnicode_FromString(Win_NtoA(baddr))); PyList_Append(pylist, tup); Py_DECREF(tup); } @@ -4517,10 +4518,10 @@ static PyObject * ip_interfaces(PyObject * self, PyObject * args) while(p) { if ((p->ifa_addr) && p->ifa_addr->sa_family == AF_INET) { tup = PyTuple_New(4); - PyTuple_SetItem(tup, 0, PyString_FromString(p->ifa_name)); - PyTuple_SetItem(tup, 1, PyString_FromString(Lin_NtoA(p->ifa_addr))); - PyTuple_SetItem(tup, 2, PyString_FromString(Lin_NtoA(p->ifa_netmask))); - PyTuple_SetItem(tup, 3, PyString_FromString(Lin_NtoA(p->ifa_broadaddr))); + PyTuple_SetItem(tup, 0, PyUnicode_FromString(p->ifa_name)); + PyTuple_SetItem(tup, 1, PyUnicode_FromString(Lin_NtoA(p->ifa_addr))); + PyTuple_SetItem(tup, 2, PyUnicode_FromString(Lin_NtoA(p->ifa_netmask))); + PyTuple_SetItem(tup, 3, PyUnicode_FromString(Lin_NtoA(p->ifa_broadaddr))); PyList_Append(pylist, tup); Py_DECREF(tup); } @@ -5660,7 +5661,7 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window) static fftw_complex * samples; // complex data for fft static fftw_plan planF, planB; // fft plan for fftW static double * fft_window; // window function - Py_complex pycx; // Python C complex value + complex double pycx; if (PySequence_Check(pyseq) != 1) { PyErr_SetString (QuiskError, "DFT input data is not a sequence"); @@ -5695,22 +5696,20 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window) for (i = 0; i < size; i++) { obj = PySequence_GetItem(pyseq, j); if (PyComplex_Check(obj)) { - pycx = PyComplex_AsCComplex(obj); + pycx = PyComplex_RealAsDouble(obj) + I * PyComplex_ImagAsDouble(obj); } else if (PyFloat_Check(obj)) { - pycx.real = PyFloat_AsDouble(obj); - pycx.imag = 0; + pycx = PyFloat_AsDouble(obj); } else if (PyInt_Check(obj)) { - pycx.real = PyInt_AsLong(obj); - pycx.imag = 0; + pycx = PyInt_AsLong(obj); } else { Py_XDECREF(obj); PyErr_SetString (QuiskError, "DFT input data is not a complex/float/int number"); return NULL; } - samples[i] = pycx.real + I * pycx.imag; + samples[i] = pycx; if (++j >= size) j = 0; Py_XDECREF(obj); @@ -5736,9 +5735,7 @@ static PyObject * Xdft(PyObject * pyseq, int inverse, int window) pyseq = PyList_New(fft_size); j = (size - 1) / 2; // zero frequency in input for (i = 0; i < fft_size; i++) { - pycx.real = creal(samples[i]); - pycx.imag = cimag(samples[i]); - PyList_SetItem(pyseq, j, PyComplex_FromCComplex(pycx)); + PyList_SetItem(pyseq, j, PyComplex_FromDoubles(creal(samples[i]), cimag(samples[i]))); if (++j >= size) j = 0; } diff --git a/quisk.h b/quisk.h index 9df9b97..169fefd 100644 --- a/quisk.h +++ b/quisk.h @@ -29,6 +29,8 @@ #define QUISK_CWKEY_DOWN (quisk_hardware_cwkey || quisk_serial_key_down || quisk_midi_cwkey || quisk_remote_cwkey) +#include +#include #ifdef MS_WINDOWS #define QUISK_SHUT_RD SD_RECEIVE #define QUISK_SHUT_BOTH SD_BOTH @@ -264,6 +266,7 @@ extern void send_graph_data(double * fft_avg, int fft_size, double zoom, double PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * keywds); PyObject * quisk_tci_get_params(PyObject * self, PyObject * args); void tci_send_audio(complex double * cSamples, int nSamples); +void tci_send_iq(complex double * cSamples, int nSamples); int tci_get_mic(complex double * cSamples, int mic_count); extern uint64_t tci_tx_audio_client; // This is ws_cli_conn_t defined in ws.h. diff --git a/quisk.py b/quisk.py index cf44bec..96b6352 100644 --- a/quisk.py +++ b/quisk.py @@ -4030,7 +4030,9 @@ class App(wx.App): self.file_name_play_cq = '' self.midiControls = {} # Control object and associated function for Midi control name self.tci_vfo = 0 - self.tci_started = False + self.tci_dds = 0 + self.tci_modulation = '' + self.tci_split_enable = 0 # get the screen size - thanks to Lucian Langa x, y, self.screen_width, self.screen_height = wx.Display().GetGeometry() # Using display index 0 self.Bind(wx.EVT_IDLE, self.OnIdle) @@ -4361,12 +4363,12 @@ class App(wx.App): value = getattr(conf, "pulse_audio_verbose_output") QS.tci_set_params(verbose=value) QS.tci_set_params(start=1); - self.tci_started = True - QS.tci_set_params(tci_dds=self.VFO) - QS.tci_set_params(tci_if=self.txFreq) - self.tci_vfo = self.txFreq + self.VFO + self.tci_dds = self.VFO + QS.tci_set_params(tci_dds=self.tci_dds) + self.tci_vfo = self.rxFreq + self.VFO QS.tci_set_params(tci_vfo=self.tci_vfo) - QS.tci_set_params(tci_modulation=self.mode) + self.tci_modulation = self.mode + QS.tci_set_params(tci_modulation=self.tci_modulation) QS.tci_set_params(tci_trx=0) QS.tci_set_params(tci_split_enable=0) return True @@ -4693,6 +4695,8 @@ class App(wx.App): if msg: print(msg, end='') QS.close_key() + if QS.tci_get_params("tci_clients_count") > 0: + QS.tci_set_params(close=1) QS.set_file_name(record_button=0) # Turn off file recording time.sleep(0.1) if self.sound_thread: @@ -5782,7 +5786,6 @@ class App(wx.App): QS.set_sidetone(self.sidetone_volume, self.sidetone_0to1, self.ritFreq, conf.keyupDelay) def OnBtnSplit(self, event): # Called when the Split check button is pressed self.split_rxtx = self.splitButton.GetValue() - QS.tci_set_params(tci_split_enable=self.split_rxtx) if self.split_rxtx: if self.split_offset == 0: if self.mode in ("CWL", "CWU"): @@ -5796,7 +5799,7 @@ class App(wx.App): QS.set_split_rxtx(0) self.split_offset = self.txFreq - self.rxFreq self.txFreq = self.rxFreq - self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event) + self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event, rx_freq=self.rxFreq) self.screen.SetTxFreq(self.txFreq, self.rxFreq) def OnMenuSplitPlay1(self, event): self.split_rxtx_play = 1 @@ -6168,7 +6171,6 @@ class App(wx.App): self.ChangeRxTxFrequency(self.rxFreq + delta + self.VFO, self.txFreq + delta + self.VFO) Hardware.ChangeMode(mode) self.mode = mode - QS.tci_set_params(tci_modulation=mode) self.MakeFilterButtons(self.Mode2Filters(mode)) QS.set_rx_mode(Mode2Index.get(mode, 3)) if mode == 'CWL': @@ -6829,22 +6831,56 @@ class App(wx.App): if self.timer - self.clip_time0 > 1.0: self.clip_time0 = 0 self.freqDisplay.Clip(0) + if QS.tci_get_params("tci_clients_count") > 0: + # These are change requests from TCI clients. All must be echoed with the new value from Quisk. + # modulation + mode = QS.tci_get_params("tci_modulation") + if mode is not None: # TCI set new modulation + self.modeButns.SetLabel(mode, True) + self.tci_modulation = self.mode + QS.tci_set_params(tci_modulation=self.tci_modulation) + # split + split = QS.tci_get_params("tci_split_enable") + if split is not None: # TCI set split_enable + self.splitButton.SetValue(split, True) + self.tci_split_enable = self.split_rxtx + QS.tci_set_params(tci_split_enable=self.tci_split_enable) + # tci dds == self.VFO + tci_dds = QS.tci_get_params("tci_dds") + if tci_dds is not None: + self.ChangeHwFrequency(0, tci_dds, 'TCIrequest') + self.tci_dds = self.VFO + QS.tci_set_params(tci_dds=self.tci_dds) + # tci_vfo == self.VFO + self.rxFreq + tci_vfo = QS.tci_get_params("tci_vfo") + if tci_vfo is not None: + self.ChangeRxTxFrequency(tci_vfo, None) + self.tci_vfo = self.VFO + self.rxFreq + QS.tci_set_params(tci_vfo=self.tci_vfo) + # tci_trx handled elsewhere if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks: self.heart_time0 = self.timer Hardware.HeartBeat() - if self.tci_started: - if self.tci_vfo != self.txFreq + self.VFO: # limit the speed of frequency updates - self.tci_vfo = self.txFreq + self.VFO + if QS.tci_get_params("tci_clients_count") > 0: # limit the speed of frequency updates + # these are changes made in Quisk that must be broadcast to all clients. + # modulation + if self.tci_modulation != self.mode: + self.tci_modulation = self.mode + QS.tci_set_params(tci_modulation=self.tci_modulation) + # split + if self.tci_split_enable != self.split_rxtx: + self.tci_split_enable = self.split_rxtx + QS.tci_set_params(tci_split_enable=self.tci_split_enable) + # tci dds == self.VFO + if self.tci_dds != self.VFO: + self.tci_dds = self.VFO + QS.tci_set_params(tci_dds=self.tci_dds) + # tci_vfo == self.VFO + self.rxFreq + tci_vfo = self.VFO + self.rxFreq + if self.tci_vfo != tci_vfo: + self.tci_vfo = tci_vfo QS.tci_set_params(tci_vfo=self.tci_vfo) - freq = QS.tci_get_params("tci_vfo") - if freq is not None: # TCI frequency - self.ChangeRxTxFrequency(freq, None) - mode = QS.tci_get_params("tci_modulation") - if mode is not None: # TCI mode - self.modeButns.SetLabel(mode, True) - split = QS.tci_get_params("tci_split_enable") - if split is not None: # TCI split_enable - self.splitButton.SetValue(split, True) + # tci_trx handled elsewhere if self.is_HermesLite2: self.tx_inhibit = QS.get_params('quisk_tx_inhibit') else: diff --git a/quisk_wdsp.c b/quisk_wdsp.c index f15b44b..34cf4d8 100644 --- a/quisk_wdsp.c +++ b/quisk_wdsp.c @@ -1,7 +1,9 @@ // This module provides C access to the WDSP SDR library. #include +#include #include +#include #include #define CLIP32 2147483647 diff --git a/sdrtrxpkg/__init__.py b/sdrtrxpkg/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/sdrtrxpkg/__init__.py @@ -0,0 +1 @@ +# diff --git a/sdrtrxpkg/quisk_hardware.py b/sdrtrxpkg/quisk_hardware.py new file mode 100644 index 0000000..13df38f --- /dev/null +++ b/sdrtrxpkg/quisk_hardware.py @@ -0,0 +1,569 @@ +# SDR-TRX Quisk Configuration File +# This file is to integrate the function of control of the SDR-TRX wint Quisk and with WSJT-X. +# This is for control of the SDR-TRX with using the UDP port port 12346. WSJT-X interfaces with this +# locally using UDP on port 2237. Capital letters begin commands to the SDR-TRX from Quisk, +# and lowercase letters are for commands coming from WSJT-X (through this Quisk interface). +# This is for usinng the SDR-TRX without a 3.5 mm audio card to the soundcard of the computer, +# and the WSJT-X software using UDP, and the data coming over UDP part 12345 too. +# It requires the libldl/ directory from weakmon to be in the directory you run this from. +# It also requires the WSJTXClass.py file from wsjtx_transceiver_interface to be in the lib directory +# and the version of that class. I have supplied a pruned down versions of ft8.py and ft4.45, because we don't need much +# other than FT8Send and FT4Send from those files. You need to edit transceiver_config.yml to have your callsign and grid, etc. + +import socket +import time +import yaml +import struct +import datetime +import sys +import os +from ft8 import FT8Send +from ft4 import FT4Send +from quisk_hardware_model import Hardware as BaseHardware +import struct +import collections +import lib.WSJTXClass as WSJTXClass # To use encoders + +sys.path.append(os.path.expandvars('$WEAKMON')) + +DEBUG_ON = True + +# +# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py): +# + +################ Receivers SdrTrx, The SdrTrx radio by Rob Frohne, KL7NA +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'sdrtrxpkg/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +## si570_xtal_freq Si570 crystal frequency, integer +# This is the Si570 startup frequency in Hz. 114.285MHz is the typical +# value from the data sheet; you can use 'usbsoftrock calibrate' to find +# the value for your device. +si570_xtal_freq = 114285000 + +## rx_max_amplitude_correct Max ampl correct, number +# If you get your I/Q samples from a sound card, you will need to correct the +# amplitude and phase for inaccuracies in the analog hardware. The correction is +# entered using the controls from the "Rx Phase" button on the config screen. +# You must enter a positive number. This controls the range of the control. +rx_max_amplitude_correct = 0.2 + +## rx_max_phase_correct Max phase correct, number +# If you get your I/Q samples from a sound card, you will need to correct the +# amplitude and phase for inaccuracies in the analog hardware. The correction is +# entered using the controls from the "Rx Phase" button on the config screen. +# You must enter a positive number. This controls the range of the control in degrees. +rx_max_phase_correct = 10.0 + + +# SDR-TRX Hardware Control Class +# + +class Hardware(BaseHardware): + # These are "static variable substitutes" since python doesn't have them. + # They are shared by all instances of the class and don't get redefinede each time a method is called. + # Move these into the Hardware class. + # Uncomment these lines if you wish to use Pulseaudio + name_of_sound_play = "pulse" + + # Radio's Frequency limits. + radio_lower = 3500000 + radio_upper = 30000000 + PACKET_SIZE = 1444 # 4 bytes for packet number + 360 * 4 bytes for int32_t pairs + # Using 16 bit samples, so 2 bytes per sample. + + # Set the number of Hz the signal is tuned to above the center frequency to avoid 1/f noise. + vfo_Center_Offset = 10000 + + tx_ready_wsjtx = False + tx_ready_wsjtx_sent = False + tx_now = False + + # This is the open code for the WSJT server. + ft8_encoder = FT8Send() + ft4_encoder = FT4Send() + + class Packet: + def __init__(self, data): + self.number = struct.unpack(' self.radio_upper: + vfo = self.radio_upper + print("Outside range! Setting to %d" % self.radio_upper) + + # print("sample_rate =", sample_rate) # I believe sample_rate comes from quisk. + # If the tune frequency is outside the RX bandwidth, set it to somewhere within that bandwidth. + if tune > (vfo + sample_rate / 2) or tune < (vfo - sample_rate / 2): + tune = vfo + self.vfo_Center_Offset + print("Bringing tune frequency back into the RX bandwidth.") + + # success = self.set_parameter("FREQ",str(vfo)) + self.set_parameter("FREQ", str(vfo)) + self.set_parameter("TX_FREQ", str(tune)) + + return tune, vfo + + def OnButtonPTT(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + self.get_parameter("TX") + print("TX") + else: + self.get_parameter("RX") + print("RX") + return BaseHardware.OnButtonPTT(self, event) + + + + def establish_connection(self): + while not self.isConnected: + print("isConnected is: ", self.isConnected) + # Check if a connection is established + # Wait for data to arrive and set PICO_UDP_IP to the sender's IP + while self.PICO_UDP_IP is None: # This needs to be fixed. It continues to send broadcast packets. + try: + print("Sending broadcast message...") + self.broadcast_sock.sendto(self.broadcast_message.encode(), ('', self.BROADCAST_PORT)) + time.sleep(1) # Wait for 1 second before sending the next broadcast message + except Exception as e: + print("Broadcast message did not work: {0}".format(e)) + break + try: + data, addr = self.data_sock.recvfrom(self.PACKET_SIZE) # Adjust the buffer size as needed + self.PICO_UDP_IP = addr[0] + print("PICO_UDP_IP set to:", self.PICO_UDP_IP) + # if data == self.broadcast_message: + self.isConnected = True + except socket.error: + if self.PICO_UDP_IP is not None: + print("Connection established with Pico W at IP:", self.PICO_UDP_IP, "but exception occurred.") + break # Exit the loop if self.PICO_UDP_IP is not None + pass # No data available yet + + def GetRxSamples(self): + # Initialize packet counter and time marker + # This routine is made for FT8 and FT4, where we need to send a packet every 15 seconds. + # The Pico W sending the data has a sample rate of 48046 samples per second > 48 ks/s. + # We made the packets so they carry 180 samples per packet. That gives 4000 packets + # per 15 seconds. We discard any packets after that four times a minute at 59, 14, 29, + # and 44 seconds. This should make four ticks per minute, and we should be able to ignore + # # them if we are doing FT8 or FT4. Something else might be more optimum for other modes. + + packet_counter = 0 + next_time_marker = 0 + send_packets = False + + while True: + # Check if we've reached the next time marker + current_time = time.time() + current_seconds = current_time % 60 + + # Define the time markers + if 59 <= current_seconds < 60: + next_time_marker = current_time + 1 - current_seconds + send_packets = True + elif 14 <= current_seconds < 15: + next_time_marker = current_time + 1 - (current_seconds - 14) + send_packets = True + elif 29 <= current_seconds < 30: + next_time_marker = current_time + 1 - (current_seconds - 29) + send_packets = True + elif 44 <= current_seconds < 45: + next_time_marker = current_time + 1 - (current_seconds - 44) + send_packets = True + + # If we've reached the next time marker, reset the packet counter + if current_time >= next_time_marker and send_packets: + packet_counter = 0 + send_packets = False + + try: + data, addr = self.data_sock.recvfrom(self.PACKET_SIZE) + self.no_data_repeat = 0 + except socket.error: + # Send a broadcast message to the Pico W to get the IP address in case it has reset. + self.broadcast_sock.sendto(self.broadcast_message.encode(), ('', self.BROADCAST_PORT)) + break # No more packets available + + packet = self.Packet(data) + self.queue.add_packet(packet) + + # Check for missing packets and fill them in + self.queue.check_missing_packets() + + # Get the packets and pass them to the processing thread + packets = self.queue.get_packets() + + for packet in packets: + data = b''.join(struct.pack('= 4000: + continue + + self.AddRxSamples(data) + packet_counter += 1 + + # + # UDP comms functions, to communicate with the Raspberry Pi Pico W board used in the SDR-TRX. + # + + def get_parameter(self, string): + string = string + "\n" + self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + return self.get_argument() + + def set_parameter(self, string, arg): + string = string + "," + arg + "\r" + "\n" + self.command_sock.sendto(string.encode(), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + print('arg is: ', arg) + temp_arg = self.get_argument() + print('temp_arg is: ', temp_arg) + return True + + def get_argument(self): + self.command_sock.setblocking(True) + try: + data1 = self.command_sock.recv(1024) + print('Received on line 178: ', data1) + except Exception as e: + print("Exception occurred: {0}".format(e)) + return -1 + self.command_sock.setblocking(False) + if not data1.startswith(b'OK'): + print('Received on line 185: ', data1) + # Maybe we didn't catch an OK line? + else: + while True: + try: + data1 = self.command_sock.recv(1024) + print('Received on line 190: ', data1) + break + except BlockingIOError: + pass + # Check to see if we have a comma in the string. If not, there is no argument. + if data1.find(b',') == -1: + return -1 + + data1 = data1.split(b',')[1].rstrip(b'\r\n') + print("data1 =") + print(data1) + + # Check for the OK string. Should we wait for the OK string? + try: + data2 = self.command_sock.recv(1024) + if data2.startswith(b'OK'): + return data2 + except BlockingIOError: + return -1 + + # We might want to integrate the UDP reads below into the stuff above. + + # The code below is to interface WSJT-X with SDR-TRX. + + def encode_ft8(self, msg): + try: + a77 = self.ft8_encoder.pack(msg, 1) + symbols = self.ft8_encoder.make_symbols(a77) + except Exception as e: + print("FT8 encoder error, check message!") + print(f"Exception type: {type(e).__name__}") + print(f"Exception message: {str(e)}") + symbols = None + time.sleep(3) + return symbols + + def encode_ft4(self, msg): + try: + a77 = self.ft4_encoder.pack(msg, 1) + symbols = self.ft4_encoder.make_symbols(a77) + except: + print("FT4 encoder error, check message!") + symbols = None + time.sleep(3) + return symbols + + def load_symbols(self, symbols): + print("Load symbols into transmitter..") + self.command_sock.sendto(b'm', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + count = 0 + for symbol in symbols: + self.command_sock.sendto(struct.pack('>B', symbol), (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + count += 1 + # Wait to avoid Arduino serial buffer overflow. This may not be needed with the Pico. + if count % 50 == 0: + time.sleep(0.05) + self.command_sock.sendto(b'\0', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + resp = self.command_sock.recv(1) + if resp == b'm': + print("Load OK") + else: + print(resp) + + def change_freq(self, new_freq): + #global self.tx_freq + self.command_sock.setblocking(True) + print("Change TX frequency to:", new_freq) + self.command_sock.sendto(b'o', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + self.command_sock.sendto(struct.pack(' 1 and rm < time_window - 1: # Within one second of exact time for FT8. + return False + else: + return True + + # See if HeartBeat will work for this: + def HeartBeat(self): # Should be called at 10 Hz from the main. + # Check transmitter is initialized + #raise Exception("In HeartBeat") + + # print("\n\nWait for transmitter ready...") + if self.tx_ready_wsjtx == False: + if self.tx_ready_wsjtx_sent == False: + self.command_sock.sendto(b'r', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + time.sleep(0.05) + self.tx_ready_wsjtx_sent = True + while True: + try: + x = self.command_sock.recv(1) + if x == b'r': + print("Transmitter ready!") + self.tx_ready_wsjtx = True + break + except BlockingIOError: + pass + if self.tx_ready_wsjtx == True: + try: + fileContent = self.wsjtx_sock.recv(1024) + except Exception as e: + if e == BlockingIOError: # This is our way of pollig the socket. + # Maybe use select to stop the errors printing after we quit. + return BaseHardware.HeartBeat(self) + else: + # print("Error in HeartBeat") + # print(f"Exception type: {type(e).__name__}") + # print(f"Exception message: {str(e)}") + pass + try: + NewPacket = WSJTXClass.WSJTX_Packet(fileContent, 0) + NewPacket.Decode() + + if NewPacket.PacketType == 1: + # print('New Packet is type 1') + StatusPacket = WSJTXClass.WSJTX_Status(fileContent, NewPacket.index) + StatusPacket.Decode() + + # Check TX frequency and update transceiver + new_freq = StatusPacket.TxDF + new_mode = StatusPacket.TxMode.strip() + + + if new_freq != self.tx_freq: + # time.sleep(0.03) + print('Changing frequency from {0} to {1}'.format(self.tx_freq, new_freq)) + try: + self.change_freq(new_freq) + except Exception as e: + print(f"Exception occurred: {e}") + print ('It is now ', self.tx_freq) + + # print('new_mode', new_mode, ' self.mode is: ', self.mode) + if new_mode != self.mode: + print("Mode before: {0}".format(self.mode)) + self.set_mode(new_mode) + print("New mode after: {0}".format(self.mode)) + + # Check if TX is enabled + if StatusPacket.Transmitting == 1: + # Check time, avoid transmitting out of the time slot + current_time = datetime.datetime.now() + utc_time = current_time.astimezone(datetime.timezone.utc) + self.tx_now = self.check_time_window(utc_time) + if self.tx_now: + self.command_sock.sendto(b'p', (self.PICO_UDP_IP, self.COMMAND_UDP_PORT)) + message = StatusPacket.TxMessage + message = message.replace('<', '') + message = message.replace('>', '') + print('Message is: ', message) + self.new_msg(message.strip()) + print("Encoded message is: ", self.current_msg) + # print("tx_now is: ", self.tx_now) + if self.tx_now: + print('Transmitting') + self.transmit() + print("Time: {0}:{1}:{2}".format(utc_time.hour, utc_time.minute, utc_time.second)) + else: + pass + # print('Not Transmitting') + return BaseHardware.HeartBeat(self) + + except Exception as e: + # print("Error in HeartBeat") + # print(f"Exception type: {type(e).__name__}") + # print(f"Exception message: {str(e)}") + # traceback.print_exc() + return BaseHardware.HeartBeat(self) diff --git a/setup.py b/setup.py index 505166e..e06a76f 100644 --- a/setup.py +++ b/setup.py @@ -4,14 +4,8 @@ import os # This file is used to build the Linux and Mac versions of Quisk. Windows builds are not included. -# 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.52' - -fp = open("__init__.py", "w") # write title string -fp.write("#Quisk version %s\n" % Version) -fp.write("from .quisk import main\n") +fp = open('__init__.py') # Read in the version +Version = fp.readline().strip()[15:] fp.close() sources = ['quisk.c', 'sound.c', 'is_key_down.c', 'microphone.c', 'utility.c', diff --git a/soapypkg/makefile b/soapypkg/makefile index 5847e6a..8b08c9c 100644 --- a/soapypkg/makefile +++ b/soapypkg/makefile @@ -1,7 +1,24 @@ +MODULE_NAME := soapy +OBJDIR := ../build/local -soapy2: - python2 setup.py build_ext --force --inplace +CFLAGS := $(shell python3 ../makefil.py --cflags) +LDFLAGS := $(shell python3 ../makefil.py --ldflags) +EXTENSION_SUFFIX := $(shell python3 ../makefil.py --suffix) +TARGET := $(MODULE_NAME)$(EXTENSION_SUFFIX) -soapy3: +# Default target: Link the shared library object +$(TARGET): soapy.o $(OBJDIR)/import_quisk_api.o + $(CC) -o $@ soapy.o $(LDFLAGS) -lSoapySDR $(OBJDIR)/import_quisk_api.o + +# Compile the C source files into object files +soapy.o: soapy.c ../quisk.h ../filter.h + $(CC) -o $@ -c $< $(CFLAGS) -I.. + +.PHONY: clean +clean: + -rm -f *.o + -rm -f $(MODULE_NAME)*.so + +.PHONY: soapy-old +soapy-old: python3 setup.py build_ext --force --inplace - diff --git a/soapypkg/soapy.c b/soapypkg/soapy.c index d769b88..e035fc1 100644 --- a/soapypkg/soapy.c +++ b/soapypkg/soapy.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -223,7 +224,7 @@ static PyObject * open_device(PyObject * self, PyObject * args) else { snprintf(buf128, 128, "SoapySDRDevice_make fail: %s", SoapySDRDevice_lastError()); } - return PyString_FromString(buf128); + return PyUnicode_FromString(buf128); } static void get_direc_len(const char * name, int * direction, int * length) @@ -254,7 +255,7 @@ static PyObject * get_device_list(PyObject * self, PyObject * args) // Called fr for (i = 0; i < length; i++) { dict = PyDict_New(); for (j = 0; j < results[i].size; j++) - PyDict_SetItemString(dict, results[i].keys[j], PyString_FromString(results[i].vals[j])); + PyDict_SetItemString(dict, results[i].keys[j], PyUnicode_FromString(results[i].vals[j])); PyList_Append(devices, dict); Py_DECREF(dict); } @@ -329,7 +330,7 @@ static PyObject * set_parameter(PyObject * self, PyObject * args) // Called from } } if (msg200[0]) - return PyString_FromString(msg200); + return PyUnicode_FromString(msg200); Py_INCREF (Py_None); return Py_None; } @@ -377,7 +378,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from pylist = PyList_New(0); names = SoapySDRDevice_listAntennas(sdev, direction, 0, &len_list); for (i = 0; i < len_list; i++) { - pyobj = PyString_FromString(names[i]); + pyobj = PyUnicode_FromString(names[i]); PyList_Append(pylist, pyobj); Py_DECREF(pyobj); } @@ -440,7 +441,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from pylist = PyList_New(0); names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list); for (i = 0; i < len_list; i++) { - pyobj = PyString_FromString(names[i]); + pyobj = PyUnicode_FromString(names[i]); PyList_Append(pylist, pyobj); Py_DECREF(pyobj); } @@ -450,7 +451,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from else if ( ! strncmp(name, "soapy_listGainsValues", length)) { pylist = PyList_New(0); pylst2 = PyList_New(0); // First element is the total gain - pyobj = PyString_FromString("total"); + pyobj = PyUnicode_FromString("total"); PyList_Append(pylst2, pyobj); Py_DECREF(pyobj); range = SoapySDRDevice_getGainRange(sdev, direction, 0); @@ -460,7 +461,7 @@ static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list); for (i = 0; i < len_list; i++) { pylst2 = PyList_New(0); - pyobj = PyString_FromString(names[i]); + pyobj = PyUnicode_FromString(names[i]); PyList_Append(pylst2, pyobj); Py_DECREF(pyobj); range = SoapySDRDevice_getGainElementRange(sdev, direction, 0, names[i]); diff --git a/sound.c b/sound.c index 153e214..05a4857 100644 --- a/sound.c +++ b/sound.c @@ -4,6 +4,7 @@ #define PY_SSIZE_T_CLEAN #include #include +#include #include #include #include @@ -986,6 +987,8 @@ int quisk_read_sound(void) // Called from sound thread // Perhaps replace the samples with samples from a file if (quisk_record_state == FILE_PLAY_SAMPLES) quisk_play_samples(cSamples, nSamples); + // Send radio IQ samples to TCI + tci_send_iq(cSamples, nSamples); #if ! DEBUG_MIC nSamples = quisk_process_samples(cSamples, nSamples); #endif diff --git a/sound_alsa.c b/sound_alsa.c index 403918a..c472cd5 100644 --- a/sound_alsa.c +++ b/sound_alsa.c @@ -2,6 +2,7 @@ * This modue provides sound access for QUISK using the ALSA * library for Linux. */ +#include #ifdef QUISK_HAVE_ALSA #include #include @@ -454,7 +455,7 @@ static int device_list(PyObject * py, snd_pcm_stream_t stream, char * name) } snprintf(buf100, 100, "%s %s (hw:%d,%d)", card_text, pcm_text, card, dev); if (py) { // add to list of devices - PyList_Append(py, PyString_FromString(buf100)); + PyList_Append(py, PyUnicode_FromString(buf100)); } if (name) { // return the "hw:" name if (strstr(buf100, name)) { @@ -1129,10 +1130,10 @@ static void midi_in_devices(PyObject * pylist, int just_names) } else { pytup = PyTuple_New(2); - PyList_Append(pylist, pytup); - PyTuple_SET_ITEM(pytup, 0, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace")); + PyTuple_SetItem(pytup, 0, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace")); snprintf(card_name, 32, "hw:%d,%d,%d", card, device, sub); - PyTuple_SET_ITEM(pytup, 1, PyUnicode_DecodeUTF8(card_name, strlen(card_name), "replace")); + PyTuple_SetItem(pytup, 1, PyUnicode_DecodeUTF8(card_name, strlen(card_name), "replace")); + PyList_Append(pylist, pytup); } } } diff --git a/sound_directx.c b/sound_directx.c index 34b9894..0fdefef 100644 --- a/sound_directx.c +++ b/sound_directx.c @@ -1,3 +1,5 @@ +#include + #ifdef QUISK_HAVE_DIRECTX #include @@ -55,14 +57,7 @@ static int match_name(LPCTSTR lpszDesc, const char * name) Py_DECREF(py_unicode); return 0; } -#if PY_MAJOR_VERSION >= 3 - if (PyUnicode_READY(py_unicode) == 0) - length = PyUnicode_GET_LENGTH(py_unicode); - else - length = 0; -#else - length = PyUnicode_GET_SIZE(py_unicode); -#endif + length = PyUnicode_GetLength(py_unicode); if (length <= 0) { Py_DECREF(py_unicode); Py_DECREF(py_substring); diff --git a/sound_portaudio.c b/sound_portaudio.c index 466960f..53a44f8 100644 --- a/sound_portaudio.c +++ b/sound_portaudio.c @@ -1,6 +1,8 @@ /* * This module provides sound access for QUISK using the portaudio library. */ +#include + #ifdef QUISK_HAVE_PORTAUDIO #include @@ -475,7 +477,7 @@ static int device_list(PyObject * py, int input) // found one if (py) { snprintf(buf100, 100, "%s", info->name); - PyList_Append(py, PyString_FromString(buf100)); + PyList_Append(py, PyUnicode_FromString(buf100)); } } } diff --git a/sound_pulseaudio.c b/sound_pulseaudio.c index 6b38c00..c936bda 100644 --- a/sound_pulseaudio.c +++ b/sound_pulseaudio.c @@ -34,6 +34,8 @@ * 2020 Apr by N2ADR Changes in printf's */ +#include + #ifdef QUISK_HAVE_PULSEAUDIO #include @@ -1030,20 +1032,20 @@ static void source_sink(const char * name, const char * descr, pa_proplist * pro PyObject * pytup; pytup = PyTuple_New(3); - PyList_Append(pylist, pytup); - PyTuple_SET_ITEM(pytup, 0, PyString_FromString(name)); - PyTuple_SET_ITEM(pytup, 1, PyString_FromString(descr)); + PyTuple_SetItem(pytup, 0, PyUnicode_FromString(name)); + PyTuple_SetItem(pytup, 1, PyUnicode_FromString(descr)); value = pa_proplist_gets(props, "device.api"); if (value && ! strcmp(value, "alsa")) { snprintf(buf300, 300, "%s %s (hw:%s,%s)", pa_proplist_gets(props, "alsa.card_name"), pa_proplist_gets(props, "alsa.name"), pa_proplist_gets(props, "alsa.card"), pa_proplist_gets(props, "alsa.device")); - PyTuple_SET_ITEM(pytup, 2, PyString_FromString(buf300)); + PyTuple_SetItem(pytup, 2, PyUnicode_FromString(buf300)); } else { - PyTuple_SET_ITEM(pytup, 2, PyString_FromString("")); + PyTuple_SetItem(pytup, 2, PyUnicode_FromString("")); } + PyList_Append(pylist, pytup); } @@ -1074,7 +1076,7 @@ static void index_callback(pa_context *c, uint32_t idx, void *userdata) { //printf("%u\n", idx); } -PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args) +PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args) // Called from the GUI thread { // Return a list of PulseAudio device names [pycapt, pyplay] PyObject * pylist, * pycapt, * pyplay; pa_mainloop *pa_names_ml; diff --git a/sound_wasapi.c b/sound_wasapi.c index 85ca232..779d5b6 100644 --- a/sound_wasapi.c +++ b/sound_wasapi.c @@ -7,6 +7,9 @@ * Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! */ + +#include + #ifdef QUISK_HAVE_WASAPI #define UNICODE @@ -1125,10 +1128,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call EXIT_ON_ERROR(hr) // data items are (name, id, is_raw) pytup = PyTuple_New(3); + PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1)); + PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1)); + PyTuple_SetItem(pytup, 2, PyInt_FromLong(1)); PyList_Append(pyplay, pytup); - PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1)); - PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1)); - PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1)); CoTaskMemFree(pwszID); pwszID = NULL; PropVariantClear(&varName); @@ -1155,10 +1158,10 @@ PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Call EXIT_ON_ERROR(hr) // data items are (name, id, is_raw) pytup = PyTuple_New(3); + PyTuple_SetItem(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1)); + PyTuple_SetItem(pytup, 1, PyUnicode_FromWideChar(pwszID, -1)); + PyTuple_SetItem(pytup, 2, PyInt_FromLong(1)); PyList_Append(pycapt, pytup); - PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1)); - PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1)); - PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1)); CoTaskMemFree(pwszID); pwszID = NULL; PropVariantClear(&varName); diff --git a/tci.c b/tci.c index 2457486..fc08691 100644 --- a/tci.c +++ b/tci.c @@ -2,36 +2,37 @@ #include #include #include +#include #include "ws.h" #include #include #include #include "quisk.h" +#include "filter.h" #define TCI_STREAM_DATA_BYTES 16384 #define TCI_RX_BUF_SIZE 1024 -#define TCI_COMMAND_SIZE 32 +#define TCI_COMMAND_SIZE 64 +#define BAD_TCI_IF 999999999 static int verbose; // non-zero for verbose log messages static int tci_port; // Implement the ExpertSDR2 Version 1.4 protocol: -// The only sample rate is 48000. The Tx audio parameters are copied from the Rx audio stream. +// The Tx audio parameters are copied from the Rx audio stream. // The only sample type is FLOAT32 type 3. -// WSJT-X RX_AUDIO_STREAM must be two channels and Stream.length is the number of floats (not samples). -// WSJT-X TX_AUDIO_STREAM always returns two channels and Stream.length is the number of floats (not samples). -// WSJT-X TX_AUDIO_STREAM is twice as long, but the data is in the first half and garbage is in the second half. -// WSJT-X TX_CHRONO Stream.length is the number of floats, and the returned TX_AUDIO_STREAM length is equal to it. // It is unclear whether Stream.length is the number of floats, or half that for two channels. And version 1.4 lacks Stream.channels. // The protocol name for TCI version 1.4 is "protocol:ESDR,1.4;" and the port is 40001. // The protocol name for TCI version 2.0 is "protocol:ExpertSDR3,2.0;" and the port is 50001. // The TCI modulations are: am, sam, dsb, lsb, usb, cw, nfm, wfm, spec, digl, digu, drm. // These are TCI parameters: -//static int64_t quisk_dds; -//static int quisk_if; +static long long quisk_dds; +static long long client_dds=-1; static long long quisk_vfo; static long long client_vfo=-1; +static int quisk_if; +static int client_if=BAD_TCI_IF; static int quisk_trx; static int client_trx=-1; static int quisk_split_enable; @@ -79,6 +80,11 @@ struct ClientData { // data for each client int audio_stream_channels; int audio_stream_samples; int audio_stream_bytes_per_sample; + struct quisk_cHB45Filter HalfBand1; + struct quisk_cFilter filtDecim3; + struct quisk_cFilter filtDecim48to24; + int send_IQ_stream; + int IQ_stream_samplerate; } ; struct _Stream { @@ -132,11 +138,11 @@ struct _StreamTypes StreamTypes[NUM_STREAM_TYPES] = { {"lineout ", 0} } ; -static void PrintStream(const char * msg, size_t size) +static void PrintStream(const char * msg, size_t size, int always) { struct _Stream * pt = (struct _Stream *)msg; - if (time(NULL) - StreamTypes[pt->type].when > 5) { + if (always || time(NULL) - StreamTypes[pt->type].when > 5) { StreamTypes[pt->type].when = time(NULL); if (pt->type == TX_AUDIO_STREAM) QuiskPrintf("TCI Receiving %slength %5d:", StreamTypes[pt->type].name, (int)size); @@ -164,7 +170,7 @@ static int sendframe_txt_bcast(uint16_t port, const char * msg) static size_t sendframe_bin(ws_cli_conn_t client, const char * msg, size_t size) { if (verbose) - PrintStream(msg, size); + PrintStream(msg, size, 0); return ws_sendframe_bin(client, msg, size); } @@ -177,8 +183,12 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx) if (verbose) QuiskPrintf("TCI Received text %s\n", ctx->msg_buf); - for (i = 0; ctx->msg_buf[i]; i++) + //QuiskPrintf("TCI Received text %s %20.6lf\n", ctx->msg_buf, QuiskTimeSec()); + for (i = 0; ctx->msg_buf[i]; i++) { + char_buf[i] = ctx->msg_buf[i]; // make a copy of the command ctx->msg_buf[i] = tolower(ctx->msg_buf[i]); + } + char_buf[i] = 0; command = strtok_r(ctx->msg_buf, ":", &saveptr); if (command == NULL) return 0; @@ -193,10 +203,19 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx) for (i = 0; i < tci_clients_count; i++) { if (tci_clients_list[i] == client) { ctx->send_Rx_audio_stream = 1; + memset(&ctx->HalfBand1, + 0, sizeof(struct quisk_cHB45Filter)); + quisk_filt_cInit(&ctx->filtDecim3, + quiskFilt144D3Coefs, sizeof(quiskFilt144D3Coefs)/sizeof(double)); + quisk_filt_cInit(&ctx->filtDecim48to24, + quiskFilt48dec24Coefs, sizeof(quiskFilt48dec24Coefs)/sizeof(double)); + break; } } pthread_mutex_unlock(&clients_list_mutex); + sendframe_txt(client, char_buf); + return 0; } else if (strcmp(command, "audio_stop") == 0) { pthread_mutex_lock(&clients_list_mutex); @@ -207,19 +226,24 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx) } } pthread_mutex_unlock(&clients_list_mutex); + sendframe_txt(client, char_buf); + return 0; } else if (strcmp(command, "audio_stream_sample_type") == 0) { if (strcmp(arg1, "float32") == 0) { ctx->audio_stream_sample_type = TCI_FLOAT32; ctx->audio_stream_bytes_per_sample = 4; + sendframe_txt(client, char_buf); } - else { - return 0; - } + return 0; } else if (strcmp(command, "audio_samplerate") == 0) { - if (atoi(arg1) != 48000) + i = atoi(arg1); + if (i != 48000 && i != 24000 && i != 12000 && i != 8000) return 0; + ctx->audio_stream_samplerate = i; + sendframe_txt(client, char_buf); + return 0; } else if (strcmp(command, "audio_stream_channels") == 0) { if (strcmp(arg1, "1") == 0) @@ -228,23 +252,74 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx) ctx->audio_stream_channels = 2; else return 0; + sendframe_txt(client, char_buf); + return 0; } else if (strcmp(command, "audio_stream_samples") == 0) { return 0; // do not send } break; + case 'd': + if (strcmp(command, "dds") == 0) { + if (arg2) { + client_dds = atoll(arg2); + // Echo comes from quisk_tci_set_params() + } + else { + snprintf(char_buf, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds); + sendframe_txt(client, char_buf); + } + return 0; + } + break; case 'i': - if (strcmp(command, "iq_start") == 0) - return 0; // do not send - else if (strcmp(command, "iq_stop") == 0) - return 0; // do not send - else if (strcmp(command, "iq_samplerate") == 0) - return 0; // do not send + if (strcmp(command, "iq_start") == 0) { + pthread_mutex_lock(&clients_list_mutex); + for (i = 0; i < tci_clients_count; i++) { + if (tci_clients_list[i] == client) { + ctx->send_IQ_stream = 1; + break; + } + } + pthread_mutex_unlock(&clients_list_mutex); + sendframe_txt(client, char_buf); + return 0; + } + else if (strcmp(command, "iq_stop") == 0) { + pthread_mutex_lock(&clients_list_mutex); + for (i = 0; i < tci_clients_count; i++) { + if (tci_clients_list[i] == client) { + ctx->send_IQ_stream = 0; + break; + } + } + pthread_mutex_unlock(&clients_list_mutex); + sendframe_txt(client, char_buf); + return 0; + } + else if (strcmp(command, "iq_samplerate") == 0) { + ctx->IQ_stream_samplerate = quisk_sound_state.sample_rate; + snprintf(char_buf, TCI_COMMAND_SIZE, "iq_samplerate:%d;", quisk_sound_state.sample_rate); + sendframe_txt(client, char_buf); + return 0; + } + else if (strcmp(command, "if") == 0) { + if (arg3) { + client_if = atoi(arg3); + // Echo comes from quisk_tci_set_params() UNIMPLEMENTED + } + else { + snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if); + sendframe_txt(client, char_buf); + } + return 0; + } break; case 'm': if (strcmp(command, "modulation") == 0) { if (arg2) { strncpy(client_modulation, arg2, TCI_COMMAND_SIZE - 1); + // Echo comes from quisk_tci_set_params() } else { snprintf(char_buf, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation); @@ -260,6 +335,7 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx) client_split_enable = 1; else client_split_enable = 0; + // Echo comes from quisk_tci_set_params() } else { if (quisk_split_enable) @@ -291,6 +367,7 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx) client_trx = 0; tci_tx_audio_client = 0; } + // Echo comes from quisk_tci_set_params() } else { if (quisk_trx) @@ -308,6 +385,7 @@ static int text_message(ws_cli_conn_t client, struct ClientData * ctx) if (strcmp(command, "vfo") == 0) { if (arg3) { client_vfo = atoll(arg3); + // Echo comes from quisk_tci_set_params() } else { snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo); @@ -330,7 +408,9 @@ static void onopen(ws_cli_conn_t client) ctx = malloc(sizeof(struct ClientData)); ctx->msg_length = 0; ctx->send_Rx_audio_stream = 0; + ctx->send_IQ_stream = 0; ctx->audio_stream_samplerate = 48000; + ctx->IQ_stream_samplerate = quisk_sound_state.sample_rate; ctx->audio_stream_sample_type = TCI_FLOAT32; ctx->audio_stream_channels = 2; ctx->audio_stream_samples = 0; @@ -348,7 +428,9 @@ static void onopen(ws_cli_conn_t client) sendframe_txt(client, "protocol:ESDR,1.4;"); //sendframe_txt(client, "vfo_limits:30000,30000000;"); Not known due to transverters - //sendframe_txt(client, "if_limits:-48000,48000;"); + int if_limit = quisk_sound_state.sample_rate / 2; + snprintf(command, TCI_COMMAND_SIZE, "if_limits:%d,%d;", - if_limit, if_limit); + sendframe_txt(client, command); sendframe_txt(client, "trx_count:1;"); sendframe_txt(client, "channel_count:1;"); sendframe_txt(client, "device:QuiskSDR;"); @@ -358,10 +440,10 @@ static void onopen(ws_cli_conn_t client) sendframe_txt(client, "start;"); sendframe_txt(client, "tx_enable:0,true;"); - //snprintf(command, TCI_COMMAND_SIZE, "dds:0,%d;", quisk_dds); - //sendframe_txt(client, command); - //snprintf(command, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if); - //sendframe_txt(client, command); + snprintf(command, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds); + sendframe_txt(client, command); + snprintf(command, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if); + sendframe_txt(client, command); snprintf(command, TCI_COMMAND_SIZE, "modulation:0,%.9s;", quisk_modulation); sendframe_txt(client, command); snprintf(command, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo); @@ -471,7 +553,7 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s break; case TCI_FLOAT32: // Version 1.4 does not return the number of channels. We assume two channels. - // pt->length is the number of floats in WSJT-X. + // pt->length is the number of floats while (vpt < vpt_end && count < pt->length) { re = *(float *)vpt; vpt += sizeof(float); @@ -499,7 +581,7 @@ static void onmessage(ws_cli_conn_t client, const unsigned char *msg, uint64_t s } } if (verbose) - PrintStream((const char *)msg, size); + PrintStream((const char *)msg, size, 0); } @@ -529,11 +611,37 @@ static void tci_startup(void) return; } +static int decimate_audio_stream(complex double * cSamples, int nSamples, struct ClientData * ctx) +{ + switch(ctx->audio_stream_samplerate) { // Starting audio rate is 48000 + case 48000: + break; + case 24000: + nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim48to24, 2); + break; + case 12000: + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &ctx->HalfBand1); + nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim48to24, 2); + break; + case 8000: + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &ctx->HalfBand1); + nSamples = quisk_cDecimate(cSamples, nSamples, &ctx->filtDecim3, 3); + break; + default: + nSamples = 0; + break; + } + return nSamples; +} + void tci_send_audio(complex double * cSamples, int nSamples) // called from the sound thread { int i, n; struct _Stream stream; struct ClientData * ctx; + static complex double * cCopy = NULL; + static int cCopy_size = 0; + complex double * cAudio; if (tci_clients_count <= 0) return; @@ -555,15 +663,29 @@ void tci_send_audio(complex double * cSamples, int nSamples) // called from the int bytes_per_sample = ctx->audio_stream_bytes_per_sample; void * vpt = &stream.data; void * vpt_end = vpt + TCI_STREAM_DATA_BYTES; + if (ctx->audio_stream_samplerate != 48000) { // decimation must not change cSamples + if (cCopy_size < nSamples) { + if (cCopy) + free(cCopy); + cCopy_size = nSamples * 2; + cCopy = (complex double *)malloc(cCopy_size * sizeof(complex double)); + } + memcpy(cCopy, cSamples, nSamples * sizeof(complex double)); + nSamples = decimate_audio_stream(cCopy, nSamples, ctx); + cAudio = cCopy; + } + else { + cAudio = cSamples; + } switch (ctx->audio_stream_sample_type) { case TCI_FLOAT32: stream.length = 0; // make a frame for (i = 0; i < nSamples; i++) { - *(float *)vpt = (float)(creal(cSamples[i]) * (1.0 / 2147483648.0 / 2)); + *(float *)vpt = (float)(creal(cAudio[i]) * (1.0 / 2147483648.0 / 2)); vpt += bytes_per_sample; stream.length++; if (two_channels) { - *(float *)vpt = (float)(cimag(cSamples[i]) * (1.0 / 2147483648.0 / 2)); + *(float *)vpt = (float)(cimag(cAudio[i]) * (1.0 / 2147483648.0 / 2)); vpt += bytes_per_sample; stream.length++; } @@ -571,6 +693,7 @@ void tci_send_audio(complex double * cSamples, int nSamples) // called from the sendframe_bin(tci_clients_list[n], (const char *)&stream, (16 * sizeof(uint32_t) + stream.length * bytes_per_sample)); stream.length = 0; + vpt = &stream.data; } } break; @@ -579,6 +702,58 @@ void tci_send_audio(complex double * cSamples, int nSamples) // called from the pthread_mutex_unlock(&clients_list_mutex); } +void tci_send_iq(complex double * cSamples, int nSamples) // called from the sound thread +{ + int i, n; + struct _Stream stream; + struct ClientData * ctx; + + if (tci_clients_count <= 0) + return; + + if (nSamples <= 0) + return; + + pthread_mutex_lock(&clients_list_mutex); + for (n = 0; n < tci_clients_count; n++) { + ctx = ws_get_connection_context(tci_clients_list[n]); + if (ctx->send_IQ_stream == 0) + continue; + memset(&stream, 0, 16 * sizeof(uint32_t)); + stream.sample_rate = ctx->IQ_stream_samplerate; + stream.format = ctx->audio_stream_sample_type; + stream.type = IQ_STREAM; + stream.channels = 2; + int bytes_per_sample = ctx->audio_stream_bytes_per_sample; + void * vpt = &stream.data; + void * vpt_end = vpt + TCI_STREAM_DATA_BYTES; + int original_length = nSamples; + switch (ctx->audio_stream_sample_type) { + case TCI_FLOAT32: + stream.length = 0; // make a frame + for (i = 0; i < nSamples; i++) { + *(float *)vpt = (float)(creal(cSamples[i]) * (1.0 / 2147483648.0 / 2)); + vpt += bytes_per_sample; + stream.length++; + *(float *)vpt = (float)(cimag(cSamples[i]) * (1.0 / 2147483648.0 / 2)); + vpt += bytes_per_sample; + stream.length++; + if (vpt >= vpt_end || i == nSamples - 1) { + sendframe_bin(tci_clients_list[n], (const char *)&stream, + (16 * sizeof(uint32_t) + stream.length * bytes_per_sample)); + original_length -= stream.length / 2; + stream.length = 0; + vpt = &stream.data; + } + } + break; + } + if (verbose && original_length != 0) + QuiskPrintf("Error in tci_send_iq: Samples remain %d\n", original_length); + } + pthread_mutex_unlock(&clients_list_mutex); +} + int tci_get_mic(complex double * cSamples, int mic_count) // called from the sound thread { if (tci_tx_audio_client) { @@ -608,16 +783,17 @@ int tci_get_mic(complex double * cSamples, int mic_count) // called from the sou PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * keywds) // called from the GUI thread { /* Call with keyword arguments ONLY. Sent from Quisk when parameters change. Broadcast the change to clients.*/ - static char * kwlist[] = {"start", "verbose", "tci_dds", "tci_if", "tci_vfo", "tci_trx", "tci_split_enable", + static char * kwlist[] = {"start", "close", "verbose", "tci_dds", "tci_if", "tci_vfo", "tci_trx", "tci_split_enable", "tci_modulation", NULL} ; long long new_vfo=-1; long long new_dds=-1; - int start=-1, new_if=-1, new_trx=-1, new_split_enable=-1; + int start=-1, close=-1, new_trx=-1, new_split_enable=-1; + int new_if = BAD_TCI_IF; char * mode=NULL; char char_buf[TCI_COMMAND_SIZE]; - if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiLiLiis", kwlist, - &start, &verbose, &new_dds, &new_if, &new_vfo, &new_trx, &new_split_enable, + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiLiLiis", kwlist, + &start, &close, &verbose, &new_dds, &new_if, &new_vfo, &new_trx, &new_split_enable, &mode)) return NULL; if (start == 1) { @@ -628,6 +804,19 @@ PyObject * quisk_tci_set_params(PyObject * self, PyObject * args, PyObject * key Py_INCREF (Py_None); return Py_None; } + if (close != -1) { + sendframe_txt_bcast(tci_port, "stop;"); + } + if (new_dds != -1) { + quisk_dds = new_dds; + snprintf(char_buf, TCI_COMMAND_SIZE, "dds:0,%lld;", quisk_dds); + sendframe_txt_bcast(tci_port, char_buf); + } + if (new_if != BAD_TCI_IF) { + quisk_if = new_if; + snprintf(char_buf, TCI_COMMAND_SIZE, "if:0,0,%d;", quisk_if); + sendframe_txt_bcast(tci_port, char_buf); + } if (new_vfo != -1) { quisk_vfo = new_vfo; snprintf(char_buf, TCI_COMMAND_SIZE, "vfo:0,0,%lld;", quisk_vfo); @@ -683,6 +872,10 @@ PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from if (!PyArg_ParseTuple (args, "s", &name)) return NULL; + + if (strcmp(name, "tci_clients_count") == 0) + return PyLong_FromLong(tci_clients_count); + if (tci_started == 0) { } else if (strcmp(name, "tci_vfo") == 0 && client_vfo >= 0) { @@ -690,6 +883,16 @@ PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from client_vfo = -1; return PyLong_FromLongLong(ll); } + else if (strcmp(name, "tci_dds") == 0 && client_dds >= 0) { + ll = client_dds; + client_dds = -1; + return PyLong_FromLongLong(ll); + } + else if (strcmp(name, "tci_if") == 0 && client_if != BAD_TCI_IF) { + ii = client_if; + client_if = BAD_TCI_IF; + return PyLong_FromLong(ii); + } else if (strcmp(name, "tci_split_enable") == 0 && client_split_enable >= 0) { ii = client_split_enable; client_split_enable = -1; @@ -718,7 +921,7 @@ PyObject * quisk_tci_get_params(PyObject * self, PyObject * args) // called from strcpy(char_buf, "FM"); client_modulation[0] = 0; if (char_buf[0]) - return PyString_FromString(char_buf); + return PyUnicode_FromString(char_buf); } Py_INCREF (Py_None); return Py_None; diff --git a/utility.c b/utility.c index 4aa0021..7505bbc 100644 --- a/utility.c +++ b/utility.c @@ -6,6 +6,7 @@ #include #endif #include +#include #include "quisk.h" // Access to config file attributes. @@ -71,40 +72,27 @@ double QuiskGetConfigDouble(const char * name, double deflt) char * QuiskGetConfigString(const char * name, char * deflt) { // Return the UTF-8 configuration string. Return deflt for failure. - char * res; - PyObject * attr; -#if PY_MAJOR_VERSION < 3 - static char retbuf[QUISK_SC_SIZE]; -#endif + Py_ssize_t size; - if (!quisk_pyConfig || PyErr_Occurred()) - return deflt; - attr = PyObject_GetAttrString(quisk_pyConfig, name); - if (attr) { -#if PY_MAJOR_VERSION >= 3 - res = (char *)PyUnicode_AsUTF8(attr); -#else - if (PyUnicode_Check(attr)) { - PyObject * pystr = PyUnicode_AsUTF8String(attr); - strMcpy(retbuf, PyString_AsString(pystr), QUISK_SC_SIZE); - retbuf[QUISK_SC_SIZE - 1] = 0; - res = retbuf; - Py_DECREF(pystr); - } - else { - res = PyString_AsString(attr); - } -#endif - Py_DECREF(attr); - if (res) - return res; // success - else - PyErr_Clear(); - } - else { - PyErr_Clear(); - } - return deflt; // failure + if (!quisk_pyConfig || PyErr_Occurred()) + return deflt; // failure + PyObject * attr = PyObject_GetAttrString(quisk_pyConfig, name); + if ( ! attr) { + PyErr_Clear(); + return deflt; // failure + } + if ( ! PyUnicode_Check(attr)) { + Py_DECREF(attr); + return deflt; // failure + } + const char * utf8_str = PyUnicode_AsUTF8AndSize(attr, &size); + if ( ! utf8_str) { + Py_DECREF(attr); + PyErr_Clear(); + return deflt; // failure + } + Py_DECREF(attr); + return (char *)utf8_str; } double QuiskTimeSec(void)