From 84bf1970dd6b249452f13d59e347c645c88e5fcc Mon Sep 17 00:00:00 2001 From: drowe67 Date: Mon, 20 Dec 2021 09:36:39 +1030 Subject: [PATCH 01/23] first pass callback model rx working --- .../test_callback_rx.py | 150 +++++++++--------- test/001_highsnr_stdio_audio/test_virtual2.sh | 2 +- tnc/codec2.py | 10 +- 3 files changed, 84 insertions(+), 78 deletions(-) diff --git a/test/001_highsnr_stdio_audio/test_callback_rx.py b/test/001_highsnr_stdio_audio/test_callback_rx.py index 9c61d224..edc9f5c7 100644 --- a/test/001_highsnr_stdio_audio/test_callback_rx.py +++ b/test/001_highsnr_stdio_audio/test_callback_rx.py @@ -17,8 +17,8 @@ import threading import sys import argparse import numpy as np -sys.path.insert(0,'..') -import codec2 +sys.path.insert(0,'../..') +from tnc import codec2 #--------------------------------------------GET PARAMETER INPUTS parser = argparse.ArgumentParser(description='FreeDATA audio test') @@ -55,12 +55,82 @@ AUDIO_SAMPLE_RATE_RX = 48000 assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 + +# DATA CHANNEL INITIALISATION + +# open codec2 instance +freedv = cast(codec2.api.freedv_open(MODE), c_void_p) + +# get number of bytes per frame for mode +bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) +payload_bytes_per_frame = bytes_per_frame -2 + +n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv) +bytes_out = create_string_buffer(bytes_per_frame * 2) + +codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST) + +total_n_bytes = 0 +rx_total_frames = 0 +rx_frames = 0 +rx_bursts = 0 +rx_errors = 0 +nread_exceptions = 0 +timeout = time.time() + TIMEOUT +receive = True +audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER*2) +resampler = codec2.resampler() + +# Copy received 48 kHz to a file. Listen to this file with: +# aplay -r 48000 -f S16_LE rx48_callback.raw +# Corruption of this file is a good way to detect audio card issues +frx = open("rx48_callback.raw", mode='wb') + # ------------------------------------------------ PYAUDIO CALLBACK def callback(data_in48k, frame_count, time_info, status): + global total_n_bytes + global rx_total_frames + global rx_frames + global rx_bursts + global receive x = np.frombuffer(data_in48k, dtype=np.int16) - x.tofile(frx) - x = resampler.resample48_to_8(x) - audio_buffer.push(x) + x.tofile(frx) + x = resampler.resample48_to_8(x) + audio_buffer.push(x) + + # when we have enough samples call FreeDV Rx + nin = codec2.api.freedv_nin(freedv) + while audio_buffer.nbuffer >= nin: + + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes) + audio_buffer.pop(nin) + + # call me on every loop! + nin = codec2.api.freedv_nin(freedv) + + rx_status = codec2.api.freedv_get_rx_status(freedv) + if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: + rx_errors = rx_errors + 1 + if DEBUGGING_MODE: + rx_status = codec2.api.rx_sync_flags_to_text[rx_status] + print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ + (nin,rx_status,audio_buffer.nbuffer), file=sys.stderr) + + if nbytes: + total_n_bytes = total_n_bytes + nbytes + + if nbytes == bytes_per_frame: + rx_total_frames = rx_total_frames + 1 + rx_frames = rx_frames + 1 + + if rx_frames == N_FRAMES_PER_BURST: + rx_frames = 0 + rx_bursts = rx_bursts + 1 + + if rx_bursts == N_BURSTS: + receive = False + return (None, pyaudio.paContinue) @@ -96,77 +166,15 @@ if AUDIO_INPUT_DEVICE != -1: stream_rx.start_stream() except Exception as e: print(f"pyAudio error: {e}", file=sys.stderr) + # ---------------------------------------------------------------- - -# DATA CHANNEL INITIALISATION -# open codec2 instance -freedv = cast(codec2.api.freedv_open(MODE), c_void_p) - -# get number of bytes per frame for mode -bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) -payload_bytes_per_frame = bytes_per_frame -2 - -n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv) -bytes_out = create_string_buffer(bytes_per_frame * 2) - -codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST) - -total_n_bytes = 0 -rx_total_frames = 0 -rx_frames = 0 -rx_bursts = 0 -rx_errors = 0 -nread_exceptions = 0 -timeout = time.time() + TIMEOUT -receive = True -audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER*2) -resampler = codec2.resampler() - -# Copy received 48 kHz to a file. Listen to this file with: -# aplay -r 48000 -f S16_LE rx48_callback.raw -# Corruption of this file is a good way to detect audio card issues -frx = open("rx48_callback.raw", mode='wb') - -# initial number of samples we need -nin = codec2.api.freedv_nin(freedv) while receive and time.time() < timeout: + time.sleep(1) - # when we have enough samples call FreeDV Rx - while audio_buffer.nbuffer >= nin: - - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes) - audio_buffer.pop(nin) +if time.time() >= timeout: + print("TIMEOUT REACHED") - # call me on every loop! - nin = codec2.api.freedv_nin(freedv) - - rx_status = codec2.api.freedv_get_rx_status(freedv) - if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: - rx_errors = rx_errors + 1 - if DEBUGGING_MODE: - rx_status = codec2.api.rx_sync_flags_to_text[rx_status] - print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ - (nin,rx_status,audio_buffer.nbuffer), file=sys.stderr) - - if nbytes: - total_n_bytes = total_n_bytes + nbytes - - if nbytes == bytes_per_frame: - rx_total_frames = rx_total_frames + 1 - rx_frames = rx_frames + 1 - - if rx_frames == N_FRAMES_PER_BURST: - rx_frames = 0 - rx_bursts = rx_bursts + 1 - - if rx_bursts == N_BURSTS: - receive = False - - if time.time() >= timeout: - print("TIMEOUT REACHED") - if nread_exceptions: print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ nread_exceptions, file=sys.stderr) diff --git a/test/001_highsnr_stdio_audio/test_virtual2.sh b/test/001_highsnr_stdio_audio/test_virtual2.sh index 5176040a..1eba60d8 100755 --- a/test/001_highsnr_stdio_audio/test_virtual2.sh +++ b/test/001_highsnr_stdio_audio/test_virtual2.sh @@ -16,7 +16,7 @@ check_alsa_loopback # make sure all child processes are killed when we exit trap 'jobs -p | xargs -r kill' EXIT -python3 test_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug & +python3 test_callback_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug & rx_pid=$! sleep 1 python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 diff --git a/tnc/codec2.py b/tnc/codec2.py index 76caf8bf..5b9ab789 100644 --- a/tnc/codec2.py +++ b/tnc/codec2.py @@ -175,9 +175,8 @@ class resampler: in48_mem[:self.MEM48] = self.filter_mem48 in48_mem[self.MEM48:] = in48 - # In C: pin48=&in48[MEM48] - pin48,flag = in48_mem.__array_interface__['data'] - pin48 += 2*self.MEM48 + # In C: pin48=&in48_mem[MEM48] + pin48 = byref(np.ctypeslib.as_ctypes(in48_mem), 2*self.MEM48) n8 = int(len(in48) / api.FDMDV_OS_48) out8 = np.zeros(n8, dtype=np.int16) api.fdmdv_48_to_8_short(out8.ctypes, pin48, n8); @@ -195,9 +194,8 @@ class resampler: in8_mem[:self.MEM8] = self.filter_mem8 in8_mem[self.MEM8:] = in8 - # In C: pin8=&in8[MEM8] - pin8,flag = in8_mem.__array_interface__['data'] - pin8 += 2*self.MEM8 + # In C: pin8=&in8_mem[MEM8] + pin8 = byref(np.ctypeslib.as_ctypes(in8_mem), 2*self.MEM8) out48 = np.zeros(api.FDMDV_OS_48*len(in8), dtype=np.int16) api.fdmdv_8_to_48_short(out48.ctypes, pin8, len(in8)); From 1c4bb7bfbcc80cb724ccdaeea995cc37dc1af269 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 10:22:55 +0100 Subject: [PATCH 02/23] moved code to class less globals and we also need this later --- .../test_callback_rx.py | 269 +++++++++--------- 1 file changed, 133 insertions(+), 136 deletions(-) diff --git a/test/001_highsnr_stdio_audio/test_callback_rx.py b/test/001_highsnr_stdio_audio/test_callback_rx.py index edc9f5c7..a36ee738 100644 --- a/test/001_highsnr_stdio_audio/test_callback_rx.py +++ b/test/001_highsnr_stdio_audio/test_callback_rx.py @@ -38,149 +38,146 @@ if args.LIST: for dev in range(0,p.get_device_count()): print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) quit() - -N_BURSTS = args.N_BURSTS -N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST -AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE -MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value -DEBUGGING_MODE = args.DEBUGGING_MODE -TIMEOUT = args.TIMEOUT - -# AUDIO PARAMETERS -AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 -MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 -AUDIO_SAMPLE_RATE_RX = 48000 - -# make sure our resampler will work -assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 - -# DATA CHANNEL INITIALISATION -# open codec2 instance -freedv = cast(codec2.api.freedv_open(MODE), c_void_p) +class Test(): + def __init__(self): + self.N_BURSTS = args.N_BURSTS + self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE + self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value + self.DEBUGGING_MODE = args.DEBUGGING_MODE + self.TIMEOUT = args.TIMEOUT -# get number of bytes per frame for mode -bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) -payload_bytes_per_frame = bytes_per_frame -2 + # AUDIO PARAMETERS + self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 + self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.AUDIO_SAMPLE_RATE_RX = 48000 -n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv) -bytes_out = create_string_buffer(bytes_per_frame * 2) + # make sure our resampler will work + assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 -codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST) - -total_n_bytes = 0 -rx_total_frames = 0 -rx_frames = 0 -rx_bursts = 0 -rx_errors = 0 -nread_exceptions = 0 -timeout = time.time() + TIMEOUT -receive = True -audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER*2) -resampler = codec2.resampler() - -# Copy received 48 kHz to a file. Listen to this file with: -# aplay -r 48000 -f S16_LE rx48_callback.raw -# Corruption of this file is a good way to detect audio card issues -frx = open("rx48_callback.raw", mode='wb') - -# ------------------------------------------------ PYAUDIO CALLBACK -def callback(data_in48k, frame_count, time_info, status): - global total_n_bytes - global rx_total_frames - global rx_frames - global rx_bursts - global receive - x = np.frombuffer(data_in48k, dtype=np.int16) - x.tofile(frx) - x = resampler.resample48_to_8(x) - audio_buffer.push(x) - - # when we have enough samples call FreeDV Rx - nin = codec2.api.freedv_nin(freedv) - while audio_buffer.nbuffer >= nin: - - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes) - audio_buffer.pop(nin) - - # call me on every loop! - nin = codec2.api.freedv_nin(freedv) - - rx_status = codec2.api.freedv_get_rx_status(freedv) - if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: - rx_errors = rx_errors + 1 - if DEBUGGING_MODE: - rx_status = codec2.api.rx_sync_flags_to_text[rx_status] - print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ - (nin,rx_status,audio_buffer.nbuffer), file=sys.stderr) - - if nbytes: - total_n_bytes = total_n_bytes + nbytes + # check if we want to use an audio device then do an pyaudio init + if self.AUDIO_INPUT_DEVICE != -1: + self.p = pyaudio.PyAudio() + # auto search for loopback devices + if self.AUDIO_INPUT_DEVICE == -2: + loopback_list = [] + for dev in range(0,self.p.get_device_count()): + if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + loopback_list.append(dev) + if len(loopback_list) >= 2: + self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX + print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + else: + quit() + + print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ + AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) - if nbytes == bytes_per_frame: - rx_total_frames = rx_total_frames + 1 - rx_frames = rx_frames + 1 + self.stream_rx = self.p.open(format=pyaudio.paInt16, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback + ) - if rx_frames == N_FRAMES_PER_BURST: - rx_frames = 0 - rx_bursts = rx_bursts + 1 - - if rx_bursts == N_BURSTS: - receive = False - - return (None, pyaudio.paContinue) - - -# check if we want to use an audio device then do an pyaudio init -if AUDIO_INPUT_DEVICE != -1: - p = pyaudio.PyAudio() - # auto search for loopback devices - if AUDIO_INPUT_DEVICE == -2: - loopback_list = [] - for dev in range(0,p.get_device_count()): - if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]: - loopback_list.append(dev) - if len(loopback_list) >= 2: - AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX - print(f"loopback_list rx: {loopback_list}", file=sys.stderr) - else: - quit() - - print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE} DEVICE: {p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['name']} \ - AUDIO SAMPLE RATE: {AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) - - stream_rx = p.open(format=pyaudio.paInt16, - channels=1, - rate=AUDIO_SAMPLE_RATE_RX, - frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, - input=True, - output=False, - input_device_index=AUDIO_INPUT_DEVICE, - stream_callback=callback - ) - try: - print(f"starting pyaudio callback", file=sys.stderr) - stream_rx.start_stream() - except Exception as e: - print(f"pyAudio error: {e}", file=sys.stderr) - -# ---------------------------------------------------------------- - -while receive and time.time() < timeout: - time.sleep(1) - -if time.time() >= timeout: - print("TIMEOUT REACHED") + # open codec2 instance + self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) -if nread_exceptions: - print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ - nread_exceptions, file=sys.stderr) -print(f"RECEIVED BURSTS: {rx_bursts} RECEIVED FRAMES: {rx_total_frames} RX_ERRORS: {rx_errors}", file=sys.stderr) -frx.close() + # get number of bytes per frame for mode + self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8) + + self.bytes_out = create_string_buffer(self.bytes_per_frame * 2) + + codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) + + self.total_n_bytes = 0 + self.rx_total_frames = 0 + self.rx_frames = 0 + self.rx_bursts = 0 + self.rx_errors = 0 + self.nread_exceptions = 0 + self.timeout = time.time() + self.TIMEOUT + self.receive = True + self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2) + self.resampler = codec2.resampler() + + # Copy received 48 kHz to a file. Listen to this file with: + # aplay -r 48000 -f S16_LE rx48_callback.raw + # Corruption of this file is a good way to detect audio card issues + self.frx = open("rx48_callback.raw", mode='wb') + + def callback(self, data_in48k, frame_count, time_info, status): + + x = np.frombuffer(data_in48k, dtype=np.int16) + x.tofile(self.frx) + x = self.resampler.resample48_to_8(x) + self.audio_buffer.push(x) -# cloese pyaudio instance -stream_rx.close() -p.terminate() + # when we have enough samples call FreeDV Rx + nin = codec2.api.freedv_nin(self.freedv) + while self.audio_buffer.nbuffer >= nin: + + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes) + self.audio_buffer.pop(nin) + + # call me on every loop! + nin = codec2.api.freedv_nin(self.freedv) + + rx_status = codec2.api.freedv_get_rx_status(self.freedv) + if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: + self.rx_errors = self.rx_errors + 1 + if self.DEBUGGING_MODE: + rx_status = codec2.api.rx_sync_flags_to_text[rx_status] + print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ + (nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) + + if nbytes: + self.total_n_bytes = self.total_n_bytes + nbytes + + if nbytes == self.bytes_per_frame: + self.rx_total_frames = self.rx_total_frames + 1 + self.rx_frames = self.rx_frames + 1 + + if self.rx_frames == self.N_FRAMES_PER_BURST: + self.rx_frames = 0 + self.rx_bursts = self.rx_bursts + 1 + + if self.rx_bursts == self.N_BURSTS: + self.receive = False + + return (None, pyaudio.paContinue) + + def run_audio(self): + try: + print(f"starting pyaudio callback", file=sys.stderr) + self.stream_rx.start_stream() + except Exception as e: + print(f"pyAudio error: {e}", file=sys.stderr) + + + while self.receive and time.time() < self.timeout: + time.sleep(1) + + if time.time() >= self.timeout: + print("TIMEOUT REACHED") + + if self.nread_exceptions: + print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ + self.nread_exceptions, file=sys.stderr) + print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr) + self.frx.close() + + # cloese pyaudio instance + self.stream_rx.close() + self.p.terminate() + + +test = Test() +test.run_audio() From 9608e3412d84375592ac4bbbf3078b3378c83b14 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 10:33:35 +0100 Subject: [PATCH 03/23] additional test with decoding outside callback added this to see any differences --- CMakeLists.txt | 9 +- .../test_callback_rx_outside.py | 215 ++++++++++++++++++ .../001_highsnr_stdio_audio/test_virtual3a.sh | 23 ++ .../001_highsnr_stdio_audio/test_virtual3b.sh | 23 ++ 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 test/001_highsnr_stdio_audio/test_callback_rx_outside.py create mode 100755 test/001_highsnr_stdio_audio/test_virtual3a.sh create mode 100755 test/001_highsnr_stdio_audio/test_virtual3b.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 56979c0c..636734aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,9 +95,16 @@ add_test(NAME 001_highsnr_virtual4_P_P_SM_CB COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; - ./test_virtual3.sh") + ./test_virtual3a.sh") set_tests_properties(001_highsnr_virtual4_P_P_SM_CB PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") +# let Python do audio I/O via pyaudio callback mode with code outside of callback +add_test(NAME 001_highsnr_virtual4_P_P_SM_CB_OUTSIDE + COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; + PATH=$PATH:${CODEC2_BUILD_DIR}/src; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + ./test_virtual3b.sh") + set_tests_properties(001_highsnr_virtual4_P_P_SM_CB_OUTSIDE PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") endif() diff --git a/test/001_highsnr_stdio_audio/test_callback_rx_outside.py b/test/001_highsnr_stdio_audio/test_callback_rx_outside.py new file mode 100644 index 00000000..f73ea306 --- /dev/null +++ b/test/001_highsnr_stdio_audio/test_callback_rx_outside.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 23 07:04:24 2020 + +@author: DJ2LS +""" + +import ctypes +from ctypes import * +import pathlib +import pyaudio +import sys +import logging +import time +import threading +import sys +import argparse +import numpy as np +sys.path.insert(0,'../..') +from tnc import codec2 + +#--------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description='FreeDATA audio test') +parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) +parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) +parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3']) +parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, + help="audio device number to use, use -2 to automatically select a loopback device") +parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") +parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") +parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") + +args = parser.parse_args() + +if args.LIST: + p = pyaudio.PyAudio() + for dev in range(0,p.get_device_count()): + print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) + quit() + + + +class Test(): + def __init__(self): + self.N_BURSTS = args.N_BURSTS + self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE + self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value + self.DEBUGGING_MODE = args.DEBUGGING_MODE + self.TIMEOUT = args.TIMEOUT + + # AUDIO PARAMETERS + self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 + self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.AUDIO_SAMPLE_RATE_RX = 48000 + + # make sure our resampler will work + assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 + + # check if we want to use an audio device then do an pyaudio init + if self.AUDIO_INPUT_DEVICE != -1: + self.p = pyaudio.PyAudio() + # auto search for loopback devices + if self.AUDIO_INPUT_DEVICE == -2: + loopback_list = [] + for dev in range(0,self.p.get_device_count()): + if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + loopback_list.append(dev) + if len(loopback_list) >= 2: + self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX + print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + else: + quit() + + print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ + AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) + + self.stream_rx = self.p.open(format=pyaudio.paInt16, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback + ) + + # open codec2 instance + self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) + + # get number of bytes per frame for mode + self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8) + + self.bytes_out = create_string_buffer(self.bytes_per_frame * 2) + + codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) + + self.total_n_bytes = 0 + self.rx_total_frames = 0 + self.rx_frames = 0 + self.rx_bursts = 0 + self.rx_errors = 0 + self.nread_exceptions = 0 + self.timeout = time.time() + self.TIMEOUT + self.receive = True + self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2) + self.resampler = codec2.resampler() + + # Copy received 48 kHz to a file. Listen to this file with: + # aplay -r 48000 -f S16_LE rx48_callback.raw + # Corruption of this file is a good way to detect audio card issues + self.frx = open("rx48_callback.raw", mode='wb') + + def callback(self, data_in48k, frame_count, time_info, status): + + x = np.frombuffer(data_in48k, dtype=np.int16) + x.tofile(self.frx) + x = self.resampler.resample48_to_8(x) + self.audio_buffer.push(x) + + ''' + # when we have enough samples call FreeDV Rx + nin = codec2.api.freedv_nin(self.freedv) + while self.audio_buffer.nbuffer >= nin: + + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes) + self.audio_buffer.pop(nin) + + # call me on every loop! + nin = codec2.api.freedv_nin(self.freedv) + + rx_status = codec2.api.freedv_get_rx_status(self.freedv) + if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: + self.rx_errors = self.rx_errors + 1 + if self.DEBUGGING_MODE: + rx_status = codec2.api.rx_sync_flags_to_text[rx_status] + print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ + (nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) + + if nbytes: + self.total_n_bytes = self.total_n_bytes + nbytes + + if nbytes == self.bytes_per_frame: + self.rx_total_frames = self.rx_total_frames + 1 + self.rx_frames = self.rx_frames + 1 + + if self.rx_frames == self.N_FRAMES_PER_BURST: + self.rx_frames = 0 + self.rx_bursts = self.rx_bursts + 1 + + if self.rx_bursts == self.N_BURSTS: + self.receive = False + ''' + return (None, pyaudio.paContinue) + + def run_audio(self): + try: + print(f"starting pyaudio callback", file=sys.stderr) + self.stream_rx.start_stream() + except Exception as e: + print(f"pyAudio error: {e}", file=sys.stderr) + + + while self.receive and time.time() < self.timeout: + #time.sleep(1) + # when we have enough samples call FreeDV Rx + nin = codec2.api.freedv_nin(self.freedv) + while self.audio_buffer.nbuffer >= nin: + + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes) + self.audio_buffer.pop(nin) + + # call me on every loop! + nin = codec2.api.freedv_nin(self.freedv) + + rx_status = codec2.api.freedv_get_rx_status(self.freedv) + if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: + self.rx_errors = self.rx_errors + 1 + if self.DEBUGGING_MODE: + rx_status = codec2.api.rx_sync_flags_to_text[rx_status] + print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ + (nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) + + if nbytes: + self.total_n_bytes = self.total_n_bytes + nbytes + + if nbytes == self.bytes_per_frame: + self.rx_total_frames = self.rx_total_frames + 1 + self.rx_frames = self.rx_frames + 1 + + if self.rx_frames == self.N_FRAMES_PER_BURST: + self.rx_frames = 0 + self.rx_bursts = self.rx_bursts + 1 + + if self.rx_bursts == self.N_BURSTS: + self.receive = False + if time.time() >= self.timeout: + print("TIMEOUT REACHED") + + if self.nread_exceptions: + print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ + self.nread_exceptions, file=sys.stderr) + print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr) + self.frx.close() + + # cloese pyaudio instance + self.stream_rx.close() + self.p.terminate() + + +test = Test() +test.run_audio() diff --git a/test/001_highsnr_stdio_audio/test_virtual3a.sh b/test/001_highsnr_stdio_audio/test_virtual3a.sh new file mode 100755 index 00000000..9a4d5493 --- /dev/null +++ b/test/001_highsnr_stdio_audio/test_virtual3a.sh @@ -0,0 +1,23 @@ +#!/bin/bash -x +# Run a test using the virtual sound cards, Python audio I/O + +function check_alsa_loopback { + lsmod | grep snd_aloop >> /dev/null + if [ $? -eq 1 ]; then + echo "ALSA loopback device not present. Please install with:" + echo + echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2" + exit 1 + fi +} + +check_alsa_loopback + +# make sure all child processes are killed when we exit +trap 'jobs -p | xargs -r kill' EXIT + +python3 test_callback_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug & +rx_pid=$! +#sleep 1 +python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 +wait ${rx_pid} diff --git a/test/001_highsnr_stdio_audio/test_virtual3b.sh b/test/001_highsnr_stdio_audio/test_virtual3b.sh new file mode 100755 index 00000000..40cc495a --- /dev/null +++ b/test/001_highsnr_stdio_audio/test_virtual3b.sh @@ -0,0 +1,23 @@ +#!/bin/bash -x +# Run a test using the virtual sound cards, Python audio I/O + +function check_alsa_loopback { + lsmod | grep snd_aloop >> /dev/null + if [ $? -eq 1 ]; then + echo "ALSA loopback device not present. Please install with:" + echo + echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2" + exit 1 + fi +} + +check_alsa_loopback + +# make sure all child processes are killed when we exit +trap 'jobs -p | xargs -r kill' EXIT + +python3 test_callback_rx_outside.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug & +rx_pid=$! +#sleep 1 +python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 +wait ${rx_pid} From 75e7ef64a1736a634772d3997bfbf9178f83e15f Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 10:35:52 +0100 Subject: [PATCH 04/23] removed old code --- .../test_callback_rx_outside.py | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/test/001_highsnr_stdio_audio/test_callback_rx_outside.py b/test/001_highsnr_stdio_audio/test_callback_rx_outside.py index f73ea306..7f81ad77 100644 --- a/test/001_highsnr_stdio_audio/test_callback_rx_outside.py +++ b/test/001_highsnr_stdio_audio/test_callback_rx_outside.py @@ -119,40 +119,6 @@ class Test(): x = self.resampler.resample48_to_8(x) self.audio_buffer.push(x) - ''' - # when we have enough samples call FreeDV Rx - nin = codec2.api.freedv_nin(self.freedv) - while self.audio_buffer.nbuffer >= nin: - - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes) - self.audio_buffer.pop(nin) - - # call me on every loop! - nin = codec2.api.freedv_nin(self.freedv) - - rx_status = codec2.api.freedv_get_rx_status(self.freedv) - if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: - self.rx_errors = self.rx_errors + 1 - if self.DEBUGGING_MODE: - rx_status = codec2.api.rx_sync_flags_to_text[rx_status] - print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ - (nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) - - if nbytes: - self.total_n_bytes = self.total_n_bytes + nbytes - - if nbytes == self.bytes_per_frame: - self.rx_total_frames = self.rx_total_frames + 1 - self.rx_frames = self.rx_frames + 1 - - if self.rx_frames == self.N_FRAMES_PER_BURST: - self.rx_frames = 0 - self.rx_bursts = self.rx_bursts + 1 - - if self.rx_bursts == self.N_BURSTS: - self.receive = False - ''' return (None, pyaudio.paContinue) def run_audio(self): From 83b420be9ae9ab7ec686fce8d7aa249be761b241 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 11:10:41 +0100 Subject: [PATCH 05/23] changed folder structure removed subdirectories for each test group --- CMakeLists.txt | 20 +++++++++---------- test/{001_highsnr_stdio_audio => }/README.md | 0 test/{002_highsnr_ping_pong => }/ping.py | 0 test/{002_highsnr_ping_pong => }/pong.py | 0 test/{000_resampler => }/t48_8_short.py | 0 .../test_arq_tx.py | 4 ++-- .../test_callback_rx.py | 5 +++-- .../test_callback_rx_outside.py | 2 +- .../test_multimode_rx.py | 2 +- .../test_multimode_tx.py | 2 +- test/{001_highsnr_stdio_audio => }/test_pa.py | 0 test/{001_highsnr_stdio_audio => }/test_rx.py | 2 +- test/{001_highsnr_stdio_audio => }/test_tx.py | 2 +- .../test_virtual1.sh | 0 .../test_virtual1a.sh | 0 .../test_virtual1b.sh | 0 .../test_virtual1c.sh | 0 .../test_virtual2.sh | 0 .../test_virtual3a.sh | 0 .../test_virtual3b.sh | 0 .../test_virtual_mm.sh | 0 21 files changed, 20 insertions(+), 19 deletions(-) rename test/{001_highsnr_stdio_audio => }/README.md (100%) rename test/{002_highsnr_ping_pong => }/ping.py (100%) rename test/{002_highsnr_ping_pong => }/pong.py (100%) rename test/{000_resampler => }/t48_8_short.py (100%) rename test/{003_highsnr_stdio_arq => }/test_arq_tx.py (79%) rename test/{001_highsnr_stdio_audio => }/test_callback_rx.py (99%) rename test/{001_highsnr_stdio_audio => }/test_callback_rx_outside.py (99%) rename test/{001_highsnr_stdio_audio => }/test_multimode_rx.py (99%) rename test/{001_highsnr_stdio_audio => }/test_multimode_tx.py (99%) rename test/{001_highsnr_stdio_audio => }/test_pa.py (100%) rename test/{001_highsnr_stdio_audio => }/test_rx.py (99%) rename test/{001_highsnr_stdio_audio => }/test_tx.py (99%) rename test/{001_highsnr_stdio_audio => }/test_virtual1.sh (100%) rename test/{001_highsnr_stdio_audio => }/test_virtual1a.sh (100%) rename test/{001_highsnr_stdio_audio => }/test_virtual1b.sh (100%) rename test/{001_highsnr_stdio_audio => }/test_virtual1c.sh (100%) rename test/{001_highsnr_stdio_audio => }/test_virtual2.sh (100%) rename test/{001_highsnr_stdio_audio => }/test_virtual3a.sh (100%) rename test/{001_highsnr_stdio_audio => }/test_virtual3b.sh (100%) rename test/{001_highsnr_stdio_audio => }/test_virtual_mm.sh (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 636734aa..e8f70883 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,14 +24,14 @@ set(TESTFRAMES 3) add_test(NAME 000_resampler COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/000_resampler; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 t48_8_short.py") set_tests_properties(000_resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS") add_test(NAME 001_highsnr_stdio_P_C_SM COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} | sox -t .s16 -r 48000 -c 1 - -t .s16 -r 8000 -c 1 - | freedv_data_raw_rx datac0 - - --framesperburst ${FRAMESPERBURST} | hexdump -C") @@ -40,7 +40,7 @@ add_test(NAME 001_highsnr_stdio_P_C_SM add_test(NAME 001_highsnr_stdio_C_P_SM COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; freedv_data_raw_tx datac0 --testframes ${TESTFRAMES} --bursts ${BURSTS} --framesperburst ${FRAMESPERBURST} /dev/zero - | sox -t .s16 -r 8000 -c 1 - -t .s16 -r 48000 -c 1 - | python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}") @@ -49,7 +49,7 @@ add_test(NAME 001_highsnr_stdio_C_P_SM add_test(NAME 001_highsnr_stdio_P_P_SM COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} | python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}") set_tests_properties(001_highsnr_stdio_P_P_SM PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}") @@ -57,7 +57,7 @@ add_test(NAME 001_highsnr_stdio_P_P_SM add_test(NAME 001_highsnr_stdio_P_P_MM COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_multimode_tx.py --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} | python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20") set_tests_properties(001_highsnr_stdio_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}") @@ -70,7 +70,7 @@ if(NOT DEFINED ENV{GITHUB_RUN_ID}) add_test(NAME 001_highsnr_virtual1_P_P COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual1.sh") set_tests_properties(001_highsnr_virtual1_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0") @@ -78,7 +78,7 @@ add_test(NAME 001_highsnr_virtual1_P_P add_test(NAME 001_highsnr_virtual2_P_P COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual2.sh") set_tests_properties(001_highsnr_virtual2_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") @@ -86,7 +86,7 @@ add_test(NAME 001_highsnr_virtual2_P_P add_test(NAME 001_highsnr_virtual3_P_P_MM COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual_mm.sh") set_tests_properties(001_highsnr_virtual3_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") @@ -94,7 +94,7 @@ add_test(NAME 001_highsnr_virtual3_P_P_MM add_test(NAME 001_highsnr_virtual4_P_P_SM_CB COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual3a.sh") set_tests_properties(001_highsnr_virtual4_P_P_SM_CB PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") @@ -102,7 +102,7 @@ add_test(NAME 001_highsnr_virtual4_P_P_SM_CB add_test(NAME 001_highsnr_virtual4_P_P_SM_CB_OUTSIDE COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; - cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual3b.sh") set_tests_properties(001_highsnr_virtual4_P_P_SM_CB_OUTSIDE PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") diff --git a/test/001_highsnr_stdio_audio/README.md b/test/README.md similarity index 100% rename from test/001_highsnr_stdio_audio/README.md rename to test/README.md diff --git a/test/002_highsnr_ping_pong/ping.py b/test/ping.py similarity index 100% rename from test/002_highsnr_ping_pong/ping.py rename to test/ping.py diff --git a/test/002_highsnr_ping_pong/pong.py b/test/pong.py similarity index 100% rename from test/002_highsnr_ping_pong/pong.py rename to test/pong.py diff --git a/test/000_resampler/t48_8_short.py b/test/t48_8_short.py similarity index 100% rename from test/000_resampler/t48_8_short.py rename to test/t48_8_short.py diff --git a/test/003_highsnr_stdio_arq/test_arq_tx.py b/test/test_arq_tx.py similarity index 79% rename from test/003_highsnr_stdio_arq/test_arq_tx.py rename to test/test_arq_tx.py index d4915750..4554282e 100644 --- a/test/003_highsnr_stdio_arq/test_arq_tx.py +++ b/test/test_arq_tx.py @@ -7,8 +7,8 @@ Created on Wed Dec 23 07:04:24 2020 """ import sys -sys.path.insert(0,'../..') -sys.path.insert(0,'../../tnc') +sys.path.insert(0,'..') +sys.path.insert(0,'../tnc') import data_handler diff --git a/test/001_highsnr_stdio_audio/test_callback_rx.py b/test/test_callback_rx.py similarity index 99% rename from test/001_highsnr_stdio_audio/test_callback_rx.py rename to test/test_callback_rx.py index a36ee738..05470e53 100644 --- a/test/001_highsnr_stdio_audio/test_callback_rx.py +++ b/test/test_callback_rx.py @@ -17,7 +17,7 @@ import threading import sys import argparse import numpy as np -sys.path.insert(0,'../..') +sys.path.insert(0,'..') from tnc import codec2 #--------------------------------------------GET PARAMETER INPUTS @@ -119,6 +119,7 @@ class Test(): x = self.resampler.resample48_to_8(x) self.audio_buffer.push(x) + # when we have enough samples call FreeDV Rx nin = codec2.api.freedv_nin(self.freedv) while self.audio_buffer.nbuffer >= nin: @@ -151,7 +152,7 @@ class Test(): if self.rx_bursts == self.N_BURSTS: self.receive = False - + return (None, pyaudio.paContinue) def run_audio(self): diff --git a/test/001_highsnr_stdio_audio/test_callback_rx_outside.py b/test/test_callback_rx_outside.py similarity index 99% rename from test/001_highsnr_stdio_audio/test_callback_rx_outside.py rename to test/test_callback_rx_outside.py index 7f81ad77..cf908ca4 100644 --- a/test/001_highsnr_stdio_audio/test_callback_rx_outside.py +++ b/test/test_callback_rx_outside.py @@ -17,7 +17,7 @@ import threading import sys import argparse import numpy as np -sys.path.insert(0,'../..') +sys.path.insert(0,'..') from tnc import codec2 #--------------------------------------------GET PARAMETER INPUTS diff --git a/test/001_highsnr_stdio_audio/test_multimode_rx.py b/test/test_multimode_rx.py similarity index 99% rename from test/001_highsnr_stdio_audio/test_multimode_rx.py rename to test/test_multimode_rx.py index 8a0ffa94..ae2b20bd 100755 --- a/test/001_highsnr_stdio_audio/test_multimode_rx.py +++ b/test/test_multimode_rx.py @@ -9,7 +9,7 @@ import sys import ctypes from ctypes import * import pathlib -sys.path.insert(0,'../..') +sys.path.insert(0,'..') from tnc import codec2 import numpy as np diff --git a/test/001_highsnr_stdio_audio/test_multimode_tx.py b/test/test_multimode_tx.py similarity index 99% rename from test/001_highsnr_stdio_audio/test_multimode_tx.py rename to test/test_multimode_tx.py index 22f77004..8cb919ff 100644 --- a/test/001_highsnr_stdio_audio/test_multimode_tx.py +++ b/test/test_multimode_tx.py @@ -11,7 +11,7 @@ import threading import audioop import argparse import sys -sys.path.insert(0,'../..') +sys.path.insert(0,'..') from tnc import codec2 import numpy as np diff --git a/test/001_highsnr_stdio_audio/test_pa.py b/test/test_pa.py similarity index 100% rename from test/001_highsnr_stdio_audio/test_pa.py rename to test/test_pa.py diff --git a/test/001_highsnr_stdio_audio/test_rx.py b/test/test_rx.py similarity index 99% rename from test/001_highsnr_stdio_audio/test_rx.py rename to test/test_rx.py index f29ea33d..cea12118 100644 --- a/test/001_highsnr_stdio_audio/test_rx.py +++ b/test/test_rx.py @@ -17,7 +17,7 @@ import threading import sys import argparse import numpy as np -sys.path.insert(0,'../..') +sys.path.insert(0,'..') from tnc import codec2 diff --git a/test/001_highsnr_stdio_audio/test_tx.py b/test/test_tx.py similarity index 99% rename from test/001_highsnr_stdio_audio/test_tx.py rename to test/test_tx.py index 581eae2d..60b314bc 100644 --- a/test/001_highsnr_stdio_audio/test_tx.py +++ b/test/test_tx.py @@ -9,7 +9,7 @@ import pyaudio import time import argparse import sys -sys.path.insert(0,'../..') +sys.path.insert(0,'..') from tnc import codec2 import numpy as np diff --git a/test/001_highsnr_stdio_audio/test_virtual1.sh b/test/test_virtual1.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual1.sh rename to test/test_virtual1.sh diff --git a/test/001_highsnr_stdio_audio/test_virtual1a.sh b/test/test_virtual1a.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual1a.sh rename to test/test_virtual1a.sh diff --git a/test/001_highsnr_stdio_audio/test_virtual1b.sh b/test/test_virtual1b.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual1b.sh rename to test/test_virtual1b.sh diff --git a/test/001_highsnr_stdio_audio/test_virtual1c.sh b/test/test_virtual1c.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual1c.sh rename to test/test_virtual1c.sh diff --git a/test/001_highsnr_stdio_audio/test_virtual2.sh b/test/test_virtual2.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual2.sh rename to test/test_virtual2.sh diff --git a/test/001_highsnr_stdio_audio/test_virtual3a.sh b/test/test_virtual3a.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual3a.sh rename to test/test_virtual3a.sh diff --git a/test/001_highsnr_stdio_audio/test_virtual3b.sh b/test/test_virtual3b.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual3b.sh rename to test/test_virtual3b.sh diff --git a/test/001_highsnr_stdio_audio/test_virtual_mm.sh b/test/test_virtual_mm.sh similarity index 100% rename from test/001_highsnr_stdio_audio/test_virtual_mm.sh rename to test/test_virtual_mm.sh From eaa26d3672e5353f8160ff6330135252c934e410 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 11:15:32 +0100 Subject: [PATCH 06/23] updated to new folder structure Ooops, I forgot this file...where's my brain... --- test/t48_8_short.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/t48_8_short.py b/test/t48_8_short.py index f76cd5b2..480062b8 100644 --- a/test/t48_8_short.py +++ b/test/t48_8_short.py @@ -18,7 +18,7 @@ from ctypes import * import pathlib import argparse import sys -sys.path.insert(0,'../..') +sys.path.insert(0,'..') from tnc import codec2 import numpy as np From 2bca7b26cdf6035a10df87f521976a0bb9872bf2 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 12:15:35 +0100 Subject: [PATCH 07/23] multimode callback tests first working version --- CMakeLists.txt | 17 ++ test/test_callback_multimode_rx.py | 244 +++++++++++++++++++++ test/test_callback_multimode_rx_outside.py | 244 +++++++++++++++++++++ test/test_virtual4a.sh | 32 +++ test/test_virtual4b.sh | 32 +++ 5 files changed, 569 insertions(+) create mode 100644 test/test_callback_multimode_rx.py create mode 100644 test/test_callback_multimode_rx_outside.py create mode 100755 test/test_virtual4a.sh create mode 100755 test/test_virtual4b.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index e8f70883..4983d863 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,5 +106,22 @@ add_test(NAME 001_highsnr_virtual4_P_P_SM_CB_OUTSIDE ./test_virtual3b.sh") set_tests_properties(001_highsnr_virtual4_P_P_SM_CB_OUTSIDE PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") +# let Python do audio I/O via pyaudio callback mode with code outside of callback +add_test(NAME 001_highsnr_virtual5_P_P_MM_CB + COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; + PATH=$PATH:${CODEC2_BUILD_DIR}/src; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; + ./test_virtual4a.sh") + set_tests_properties(001_highsnr_virtual5_P_P_MM_CB PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") + +# let Python do audio I/O via pyaudio callback mode with code outside of callback +add_test(NAME 001_highsnr_virtual5_P_P_MM_CB_OUTSIDE + COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; + PATH=$PATH:${CODEC2_BUILD_DIR}/src; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; + ./test_virtual4b.sh") + set_tests_properties(001_highsnr_virtual5_P_P_MM_CB_OUTSIDE PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") + + endif() diff --git a/test/test_callback_multimode_rx.py b/test/test_callback_multimode_rx.py new file mode 100644 index 00000000..94aa39b7 --- /dev/null +++ b/test/test_callback_multimode_rx.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 23 07:04:24 2020 + +@author: DJ2LS +""" + +import ctypes +from ctypes import * +import pathlib +import pyaudio +import sys +import logging +import time +import threading +import sys +import argparse +import numpy as np +sys.path.insert(0,'..') +from tnc import codec2 + +#--------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description='FreeDATA audio test') +parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) +parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) +parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, help="audio device number to use") +parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") +parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") +parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") + + +args = parser.parse_args() + +if args.LIST: + p = pyaudio.PyAudio() + for dev in range(0,p.get_device_count()): + print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) + quit() + + + +class Test(): + def __init__(self): + self.N_BURSTS = args.N_BURSTS + self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE + self.DEBUGGING_MODE = args.DEBUGGING_MODE + self.TIMEOUT = args.TIMEOUT + + # AUDIO PARAMETERS + self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 + self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.AUDIO_SAMPLE_RATE_RX = 48000 + + # make sure our resampler will work + assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 + + # check if we want to use an audio device then do an pyaudio init + if self.AUDIO_INPUT_DEVICE != -1: + self.p = pyaudio.PyAudio() + # auto search for loopback devices + if self.AUDIO_INPUT_DEVICE == -2: + loopback_list = [] + for dev in range(0,self.p.get_device_count()): + if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + loopback_list.append(dev) + if len(loopback_list) >= 2: + self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX + print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + else: + quit() + + print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ + AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) + + self.stream_rx = self.p.open(format=pyaudio.paInt16, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback + ) + + + + # open codec2 instance + self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) + self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) + self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST) + self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) + + self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) + self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) + self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST) + self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) + + self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) + self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8) + self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST) + self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) + + + + # SET COUNTERS + self.rx_total_frames_datac0 = 0 + self.rx_frames_datac0 = 0 + self.rx_bursts_datac0 = 0 + + self.rx_total_frames_datac1 = 0 + self.rx_frames_datac1 = 0 + self.rx_bursts_datac1 = 0 + + self.rx_total_frames_datac3 = 0 + self.rx_frames_datac3 = 0 + self.rx_bursts_datac3 = 0 + + self.rx_errors = 0 + self.nread_exceptions = 0 + self.timeout = time.time() + self.TIMEOUT + self.receive = True + self.resampler = codec2.resampler() + + # Copy received 48 kHz to a file. Listen to this file with: + # aplay -r 48000 -f S16_LE rx48_callback.raw + # Corruption of this file is a good way to detect audio card issues + self.frx = open("rx48_callback_multimode.raw", mode='wb') + + + # initial nin values + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) + + + def callback(self, data_in48k, frame_count, time_info, status): + + x = np.frombuffer(data_in48k, dtype=np.int16) + x.tofile(self.frx) + x = self.resampler.resample48_to_8(x) + + self.datac0_buffer.push(x) + self.datac1_buffer.push(x) + self.datac3_buffer.push(x) + + + while self.datac0_buffer.nbuffer >= self.datac0_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) + self.datac0_buffer.pop(self.datac0_nin) + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + if nbytes == self.datac0_bytes_per_frame: + self.rx_total_frames_datac0 = self.rx_total_frames_datac0 + 1 + self.rx_frames_datac0 = self.rx_frames_datac0 + 1 + + if self.rx_frames_datac0 == self.N_FRAMES_PER_BURST: + self.rx_frames_datac0 = 0 + self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1 + self.print_stats() + + + while self.datac1_buffer.nbuffer >= self.datac1_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) + self.datac1_buffer.pop(self.datac1_nin) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + if nbytes == self.datac1_bytes_per_frame: + self.rx_total_frames_datac1 = self.rx_total_frames_datac1 + 1 + self.rx_frames_datac1 = self.rx_frames_datac1 + 1 + + if self.rx_frames_datac1 == self.N_FRAMES_PER_BURST: + self.rx_frames_datac1 = 0 + self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1 + self.print_stats() + + while self.datac3_buffer.nbuffer >= self.datac3_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) + self.datac3_buffer.pop(self.datac3_nin) + self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) + if nbytes == self.datac3_bytes_per_frame: + self.rx_total_frames_datac3 = self.rx_total_frames_datac3 + 1 + self.rx_frames_datac3 = self.rx_frames_datac3 + 1 + + if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST: + self.rx_frames_datac3 = 0 + self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 + self.print_stats() + + if self.rx_bursts_datac0 == self.N_BURSTS and self.rx_bursts_datac1 == self.N_BURSTS and self.rx_bursts_datac3 == self.N_BURSTS: + self.receive = False + + + return (None, pyaudio.paContinue) + + def print_stats(self): + if self.DEBUGGING_MODE: + self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv) + self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus] + + self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv) + self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus] + + self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv) + self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus] + + print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \ + (self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus), + file=sys.stderr) + + + def run_audio(self): + try: + print(f"starting pyaudio callback", file=sys.stderr) + self.stream_rx.start_stream() + except Exception as e: + print(f"pyAudio error: {e}", file=sys.stderr) + + + while self.receive and time.time() < self.timeout: + time.sleep(1) + + if time.time() >= self.timeout: + print("TIMEOUT REACHED") + + if self.nread_exceptions: + print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ + self.nread_exceptions, file=sys.stderr) + + print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr) + self.frx.close() + + # cloese pyaudio instance + self.stream_rx.close() + self.p.terminate() + + +test = Test() +test.run_audio() diff --git a/test/test_callback_multimode_rx_outside.py b/test/test_callback_multimode_rx_outside.py new file mode 100644 index 00000000..2c5d8d11 --- /dev/null +++ b/test/test_callback_multimode_rx_outside.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 23 07:04:24 2020 + +@author: DJ2LS +""" + +import ctypes +from ctypes import * +import pathlib +import pyaudio +import sys +import logging +import time +import threading +import sys +import argparse +import numpy as np +sys.path.insert(0,'..') +from tnc import codec2 + +#--------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description='FreeDATA audio test') +parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) +parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) +parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, help="audio device number to use") +parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") +parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") +parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") + + +args = parser.parse_args() + +if args.LIST: + p = pyaudio.PyAudio() + for dev in range(0,p.get_device_count()): + print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) + quit() + + + +class Test(): + def __init__(self): + self.N_BURSTS = args.N_BURSTS + self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE + self.DEBUGGING_MODE = args.DEBUGGING_MODE + self.TIMEOUT = args.TIMEOUT + + # AUDIO PARAMETERS + self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 + self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.AUDIO_SAMPLE_RATE_RX = 48000 + + # make sure our resampler will work + assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 + + # check if we want to use an audio device then do an pyaudio init + if self.AUDIO_INPUT_DEVICE != -1: + self.p = pyaudio.PyAudio() + # auto search for loopback devices + if self.AUDIO_INPUT_DEVICE == -2: + loopback_list = [] + for dev in range(0,self.p.get_device_count()): + if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + loopback_list.append(dev) + if len(loopback_list) >= 2: + self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX + print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + else: + quit() + + print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ + AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) + + self.stream_rx = self.p.open(format=pyaudio.paInt16, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback + ) + + + + # open codec2 instance + self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) + self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) + self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST) + self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) + + self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) + self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) + self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST) + self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) + + self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) + self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8) + self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST) + self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) + + + + # SET COUNTERS + self.rx_total_frames_datac0 = 0 + self.rx_frames_datac0 = 0 + self.rx_bursts_datac0 = 0 + + self.rx_total_frames_datac1 = 0 + self.rx_frames_datac1 = 0 + self.rx_bursts_datac1 = 0 + + self.rx_total_frames_datac3 = 0 + self.rx_frames_datac3 = 0 + self.rx_bursts_datac3 = 0 + + self.rx_errors = 0 + self.nread_exceptions = 0 + self.timeout = time.time() + self.TIMEOUT + self.receive = True + self.resampler = codec2.resampler() + + # Copy received 48 kHz to a file. Listen to this file with: + # aplay -r 48000 -f S16_LE rx48_callback.raw + # Corruption of this file is a good way to detect audio card issues + self.frx = open("rx48_callback_multimode.raw", mode='wb') + + + # initial nin values + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) + + + def callback(self, data_in48k, frame_count, time_info, status): + + x = np.frombuffer(data_in48k, dtype=np.int16) + x.tofile(self.frx) + x = self.resampler.resample48_to_8(x) + + self.datac0_buffer.push(x) + self.datac1_buffer.push(x) + self.datac3_buffer.push(x) + + + + + return (None, pyaudio.paContinue) + + def print_stats(self): + if self.DEBUGGING_MODE: + self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv) + self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus] + + self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv) + self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus] + + self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv) + self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus] + + print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \ + (self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus), + file=sys.stderr) + + + def run_audio(self): + try: + print(f"starting pyaudio callback", file=sys.stderr) + self.stream_rx.start_stream() + except Exception as e: + print(f"pyAudio error: {e}", file=sys.stderr) + + + while self.receive and time.time() < self.timeout: + while self.datac0_buffer.nbuffer >= self.datac0_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) + self.datac0_buffer.pop(self.datac0_nin) + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + if nbytes == self.datac0_bytes_per_frame: + self.rx_total_frames_datac0 = self.rx_total_frames_datac0 + 1 + self.rx_frames_datac0 = self.rx_frames_datac0 + 1 + + if self.rx_frames_datac0 == self.N_FRAMES_PER_BURST: + self.rx_frames_datac0 = 0 + self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1 + self.print_stats() + + + while self.datac1_buffer.nbuffer >= self.datac1_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) + self.datac1_buffer.pop(self.datac1_nin) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + if nbytes == self.datac1_bytes_per_frame: + self.rx_total_frames_datac1 = self.rx_total_frames_datac1 + 1 + self.rx_frames_datac1 = self.rx_frames_datac1 + 1 + + if self.rx_frames_datac1 == self.N_FRAMES_PER_BURST: + self.rx_frames_datac1 = 0 + self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1 + self.print_stats() + + while self.datac3_buffer.nbuffer >= self.datac3_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) + self.datac3_buffer.pop(self.datac3_nin) + self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) + if nbytes == self.datac3_bytes_per_frame: + self.rx_total_frames_datac3 = self.rx_total_frames_datac3 + 1 + self.rx_frames_datac3 = self.rx_frames_datac3 + 1 + + if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST: + self.rx_frames_datac3 = 0 + self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 + self.print_stats() + + if self.rx_bursts_datac0 == self.N_BURSTS and self.rx_bursts_datac1 == self.N_BURSTS and self.rx_bursts_datac3 == self.N_BURSTS: + self.receive = False + + + if time.time() >= self.timeout: + print("TIMEOUT REACHED") + + if self.nread_exceptions: + print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ + self.nread_exceptions, file=sys.stderr) + + print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr) + self.frx.close() + + # cloese pyaudio instance + self.stream_rx.close() + self.p.terminate() + + +test = Test() +test.run_audio() diff --git a/test/test_virtual4a.sh b/test/test_virtual4a.sh new file mode 100755 index 00000000..feb81fb1 --- /dev/null +++ b/test/test_virtual4a.sh @@ -0,0 +1,32 @@ +#!/bin/bash -x +# Run a test using the virtual sound cards + +function check_alsa_loopback { + lsmod | grep snd_aloop >> /dev/null + if [ $? -eq 1 ]; then + echo "ALSA loopback device not present. Please install with:" + echo + echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2" + exit 1 + fi +} + +myInterruptHandler() +{ + exit 1 +} + +check_alsa_loopback + +RX_LOG=$(mktemp) + +trap myInterruptHandler SIGINT + +# make sure all child processes are killed when we exit +trap 'jobs -p | xargs -r kill' EXIT + +python3 test_callback_multimode_rx.py --timeout 60 --framesperburst 2 --bursts 2 --audiodev -2 --debug & +rx_pid=$! +sleep 1 +python3 test_multimode_tx.py --framesperburst 2 --bursts 2 --audiodev -2 --delay 500 +wait ${rx_pid} diff --git a/test/test_virtual4b.sh b/test/test_virtual4b.sh new file mode 100755 index 00000000..69abd4b5 --- /dev/null +++ b/test/test_virtual4b.sh @@ -0,0 +1,32 @@ +#!/bin/bash -x +# Run a test using the virtual sound cards + +function check_alsa_loopback { + lsmod | grep snd_aloop >> /dev/null + if [ $? -eq 1 ]; then + echo "ALSA loopback device not present. Please install with:" + echo + echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2" + exit 1 + fi +} + +myInterruptHandler() +{ + exit 1 +} + +check_alsa_loopback + +RX_LOG=$(mktemp) + +trap myInterruptHandler SIGINT + +# make sure all child processes are killed when we exit +trap 'jobs -p | xargs -r kill' EXIT + +python3 test_callback_multimode_rx_outside.py --timeout 60 --framesperburst 2 --bursts 2 --audiodev -2 --debug & +rx_pid=$! +sleep 1 +python3 test_multimode_tx.py --framesperburst 2 --bursts 2 --audiodev -2 --delay 500 +wait ${rx_pid} From 5d62952e8a7ff62fa4d00ac98b439a77d970c0b5 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 12:16:03 +0100 Subject: [PATCH 08/23] small cleanup removed unused function --- test/test_multimode_rx.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/test_multimode_rx.py b/test/test_multimode_rx.py index ae2b20bd..af205d83 100755 --- a/test/test_multimode_rx.py +++ b/test/test_multimode_rx.py @@ -60,21 +60,18 @@ rx_bursts_datac3 = 0 # open codec2 instance datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8) -datac0_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac0_freedv) datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame * 2) codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST) datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8) -datac1_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac1_freedv) datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame * 2) codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST) datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8) -datac3_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac3_freedv) datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame * 2) codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST) datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) From 3f33bc9ee3c510bfd5a1478f9160b8f6a7e047df Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 12:43:32 +0100 Subject: [PATCH 09/23] updated codec2 lib path --- tnc/codec2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tnc/codec2.py b/tnc/codec2.py index 5b9ab789..0959f205 100644 --- a/tnc/codec2.py +++ b/tnc/codec2.py @@ -29,8 +29,8 @@ def freedv_get_mode(mode): libname = ["libcodec2.so", \ pathlib.Path("codec2/build_linux/src/libcodec2.so.1.0"), \ pathlib.Path("lib/codec2/linux/libcodec2.so.1.0"), \ - pathlib.Path("../../tnc/codec2/build_linux/src/libcodec2.so.1.0"), \ - pathlib.Path("../../tnc/lib/codec2/linux/libcodec2.so.1.0"), \ + pathlib.Path("../tnc/codec2/build_linux/src/libcodec2.so.1.0"), \ + pathlib.Path("../tnc/lib/codec2/linux/libcodec2.so.1.0"), \ ] # iterate through codec2 search pathes for i in libname: From 8b8dfd5233980c0bc432691159d2de0a774c0d15 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 15:37:46 +0100 Subject: [PATCH 10/23] added option for sending testframes this testframes are used to interact with the TNC --- test/test_tx.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/test_tx.py b/test/test_tx.py index 60b314bc..bffe1ae6 100644 --- a/test/test_tx.py +++ b/test/test_tx.py @@ -23,6 +23,8 @@ parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', ' parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, help="audio output device number to use, use -2 to automatically select a loopback device") parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") +parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="list audio devices by number and exit") + args = parser.parse_args() @@ -75,7 +77,14 @@ if AUDIO_OUTPUT_DEVICE != -1: resampler = codec2.resampler() # data binary string -data_out = b'HELLO WORLD!' +if args.TESTFRAMES: + data_out = bytearray(14) + data_out[:1] = bytes([255]) + data_out[1:2] = bytes([1]) + data_out[2:] = b'HELLO WORLD' + +else: + data_out = b'HELLO WORLD!' # ---------------------------------------------------------------- From e8283b5db00b113eb414f0f53649f59d931279e4 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 15:38:43 +0100 Subject: [PATCH 11/23] integrated multimode callback test & FIFO queue to tnc this is a first test to see how it performs --- tnc/modem.py | 368 +++++++++++++++------------------------------------ 1 file changed, 106 insertions(+), 262 deletions(-) diff --git a/tnc/modem.py b/tnc/modem.py index 75de6897..aff83f2a 100644 --- a/tnc/modem.py +++ b/tnc/modem.py @@ -20,7 +20,7 @@ import helpers import static import data_handler import re - +import queue import codec2 # option for testing miniaudio instead of audioop for sample rate conversion @@ -118,54 +118,36 @@ class RF(): # TODO: we need to change the entire modem module to integrate codec2 module self.c_lib = codec2.api self.resampler = codec2.resampler() - ''' - # -------------------------------------------- LOAD FREEDV - try: - # we check at first for libcodec2 compiled from source - # this happens, if we want to run it beeing build in a dev environment - # libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so.1.0" - libname = pathlib.Path("codec2/build_linux/src/libcodec2.so.1.0") - if libname.is_file(): - self.c_lib = ctypes.CDLL(libname) - structlog.get_logger("structlog").info("[TNC] Codec2 found", path=libname, origin="source") - else: - structlog.get_logger("structlog").critical("[TNC] Codec2 not loaded") - raise UnboundLocalError - except: - # this is the normal behavior. Run codec2 from lib folder - #libname = pathlib.Path().absolute() / "lib/codec2/linux/libcodec2.so.1.0" - libname = pathlib.Path("lib/codec2/linux/libcodec2.so.1.0") - if libname.is_file(): - self.c_lib = ctypes.CDLL(libname) - structlog.get_logger("structlog").info("[TNC] Codec2 found", path=libname, origin="precompiled") - else: - structlog.get_logger("structlog").critical("[TNC] Codec2 not found") - ''' - ''' - # --------------------------------------------CTYPES FUNCTION INIT - # TODO: WE STILL HAVE SOME MISSING FUNCTIONS! + # init FIFO queue to store received frames in + self.dataqueue = queue.Queue() - self.c_lib.freedv_open.argype = [c_int] - self.c_lib.freedv_open.restype = c_void_p - self.c_lib.freedv_nin.argtype = [c_void_p] - self.c_lib.freedv_nin.restype = c_int + # open codec2 instance + self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) + self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) + self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,1) + self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) - self.c_lib.freedv_rawdatarx.argtype = [c_void_p, c_char_p, c_char_p] - self.c_lib.freedv_rawdatarx.restype = c_int + self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) + self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) + self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,1) + self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) - self.c_lib.freedv_get_sync.argtype = [c_void_p] - self.c_lib.freedv_get_sync.restype = c_int + self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) + self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8) + self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame * 2) + codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,1) + self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) - self.c_lib.freedv_get_bits_per_modem_frame.argtype = [c_void_p] - self.c_lib.freedv_get_bits_per_modem_frame.restype = c_int - - self.c_lib.freedv_set_frames_per_burst.argtype = [c_void_p, c_int] - self.c_lib.freedv_set_frames_per_burst.restype = c_int - ''' - - # --------------------------------------------CREATE PYAUDIO INSTANCE + # initial nin values + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) + + # --------------------------------------------CREATE PYAUDIO INSTANCE try: # we need to "try" this, because sometimes libasound.so isn't in the default place # try to supress error messages @@ -175,7 +157,8 @@ class RF(): except: self.p = pyaudio.PyAudio() atexit.register(self.p.terminate) - # --------------------------------------------OPEN AUDIO CHANNEL RX + + # --------------------------------------------OPEN RX AUDIO CHANNEL # optional auto selection of loopback device if using in testmode if static.AUDIO_INPUT_DEVICE == -2: loopback_list = [] @@ -185,16 +168,17 @@ class RF(): if len(loopback_list) >= 2: AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX print(f"loopback_list rx: {loopback_list}", file=sys.stderr) - - + self.stream_rx = self.p.open(format=pyaudio.paInt16, channels=self.AUDIO_CHANNELS, rate=self.AUDIO_SAMPLE_RATE_RX, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX, input=True, - input_device_index=static.AUDIO_INPUT_DEVICE + output=False, + input_device_index=static.AUDIO_INPUT_DEVICE, + stream_callback=self.callback ) - # --------------------------------------------OPEN AUDIO CHANNEL TX + # --------------------------------------------OPEN TX AUDIO CHANNEL # optional auto selection of loopback device if using in testmode if static.AUDIO_OUTPUT_DEVICE == -2: loopback_list = [] @@ -220,8 +204,8 @@ class RF(): DECODER_THREAD = threading.Thread(target=self.receive, name="DECODER_THREAD") DECODER_THREAD.start() - #PLAYBACK_THREAD = threading.Thread(target=self.play_audio, name="PLAYBACK_THREAD") - #PLAYBACK_THREAD.start() + WORKER_THREAD = threading.Thread(target=self.worker, name="WORKER_THREAD") + WORKER_THREAD.start() self.fft_data = bytes() FFT_THREAD = threading.Thread(target=self.calculate_fft, name="FFT_THREAD") @@ -299,8 +283,55 @@ class RF(): structlog.get_logger("structlog").error("[TNC] Hamlib - can't open rig", e=sys.exc_info()[0]) -# -------------------------------------------------------------------------------------------------------- + # -------------------------------------------------------------------------------------------------------- + def callback(self, data_in48k, frame_count, time_info, status): + + x = np.frombuffer(data_in48k, dtype=np.int16) + x = self.resampler.resample48_to_8(x) + self.datac0_buffer.push(x) + self.datac1_buffer.push(x) + self.datac3_buffer.push(x) + + # refill fft_data buffer so we can plot a fft + if len(self.fft_data) < 1024: + self.fft_data += bytes(x) + + + while self.datac0_buffer.nbuffer >= self.datac0_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) + self.datac0_buffer.pop(self.datac0_nin) + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + if nbytes == self.datac0_bytes_per_frame: + self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame]) + self.get_scatter(self.datac0_freedv) + self.calculate_snr(self.datac0_freedv) + + while self.datac1_buffer.nbuffer >= self.datac1_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) + self.datac1_buffer.pop(self.datac1_nin) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + if nbytes == self.datac1_bytes_per_frame: + self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame]) + self.get_scatter(self.datac1_freedv) + self.calculate_snr(self.datac1_freedv) + + while self.datac3_buffer.nbuffer >= self.datac3_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) + self.datac3_buffer.pop(self.datac3_nin) + self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) + if nbytes == self.datac3_bytes_per_frame: + self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame]) + self.get_scatter(self.datac3_freedv) + self.calculate_snr(self.datac3_freedv) + + return (None, pyaudio.paContinue) + + + def ptt_and_wait(self, state): static.PTT_STATE = state @@ -388,7 +419,6 @@ class RF(): # we have a problem with the receiving state - ##static.CHANNEL_STATE = state_before_transmit if state_before_transmit != 'RECEIVING_DATA': static.CHANNEL_STATE = 'RECEIVING_SIGNALLING' else: @@ -475,219 +505,29 @@ class RF(): # -------------------------------------------------------------------------------------------------------- def receive(self): - ''' - freedv_mode_datac0 = 14 - freedv_mode_datac1 = 10 - freedv_mode_datac3 = 12 - ''' - - - # open codec2 instance - datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) - datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8) - datac0_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac0_freedv) - datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame * 2) - codec2.api.freedv_set_frames_per_burst(datac0_freedv,1) - datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) - datac0_modem_stats_snr = c_float() - datac0_modem_stats_sync = c_int() - static.FREEDV_SIGNALLING_BYTES_PER_FRAME = datac0_bytes_per_frame - static.FREEDV_SIGNALLING_PAYLOAD_PER_FRAME = datac0_bytes_per_frame - 2 - - datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) - datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8) - datac1_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac1_freedv) - datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame * 2) - codec2.api.freedv_set_frames_per_burst(datac1_freedv,1) - datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) - datac1_modem_stats_snr = c_float() - datac1_modem_stats_sync = c_int() - - - datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) - datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8) - datac3_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac3_freedv) - datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame * 2) - codec2.api.freedv_set_frames_per_burst(datac3_freedv,1) - datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) - datac3_modem_stats_snr = c_float() - datac3_modem_stats_sync = c_int() - - ''' - # DATAC0 + try: + print(f"starting pyaudio callback", file=sys.stderr) + self.stream_rx.start_stream() + except Exception as e: + print(f"pyAudio error: {e}", file=sys.stderr) + - datac0_freedv = cast(self.c_lib.freedv_open(freedv_mode_datac0), c_void_p) - self.c_lib.freedv_get_bits_per_modem_frame(datac0_freedv) - datac0_bytes_per_frame = int(self.c_lib.freedv_get_bits_per_modem_frame(datac0_freedv)/8) - datac0_n_max_modem_samples = self.c_lib.freedv_get_n_max_modem_samples(datac0_freedv) - datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame * 2) - self.c_lib.freedv_set_frames_per_burst(datac0_freedv, 1) - datac0_modem_stats_snr = c_float() - datac0_modem_stats_sync = c_int() - datac0_buffer = bytes() - - static.FREEDV_SIGNALLING_BYTES_PER_FRAME = datac0_bytes_per_frame - static.FREEDV_SIGNALLING_PAYLOAD_PER_FRAME = datac0_bytes_per_frame - 2 - - # DATAC1 - datac1_freedv = cast(self.c_lib.freedv_open(freedv_mode_datac1), c_void_p) - datac1_bytes_per_frame = int(self.c_lib.freedv_get_bits_per_modem_frame(datac1_freedv)/8) - datac1_n_max_modem_samples = self.c_lib.freedv_get_n_max_modem_samples(datac1_freedv) - datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame * 2) - self.c_lib.freedv_set_frames_per_burst(datac1_freedv, 0) - datac1_modem_stats_snr = c_float() - datac1_modem_stats_sync = c_int() - datac1_buffer = bytes() - - # DATAC3 - datac3_freedv = cast(self.c_lib.freedv_open(freedv_mode_datac3), c_void_p) - datac3_bytes_per_frame = int(self.c_lib.freedv_get_bits_per_modem_frame(datac3_freedv)/8) - datac3_n_max_modem_samples = self.c_lib.freedv_get_n_max_modem_samples(datac3_freedv) - datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame * 2) - self.c_lib.freedv_set_frames_per_burst(datac3_freedv, 0) - datac3_modem_stats_snr = c_float() - datac3_modem_stats_sync = c_int() - datac3_buffer = bytes() - ''' - - fft_buffer = bytes() - receive = True - while receive: - - try: - data_in48k = self.stream_rx.read(self.AUDIO_FRAMES_PER_BUFFER_RX, exception_on_overflow = True) - except OSError as err: - print(err, file=sys.stderr) - if str(err).find("Input overflowed") != -1: - nread_exceptions += 1 - if str(err).find("Stream closed") != -1: - print("Ending...") - receive = False - - - - # insert samples in buffer - x = np.frombuffer(data_in48k, dtype=np.int16) - # x.tofile(frx) - if len(x) != self.AUDIO_FRAMES_PER_BUFFER_RX: - receive = False - x = self.resampler.resample48_to_8(x) - - datac0_buffer.push(x) - datac1_buffer.push(x) - datac3_buffer.push(x) - - # when we have enough samples call FreeDV Rx - while datac0_buffer.nbuffer >= datac0_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(datac0_freedv, datac0_bytes_out, datac0_buffer.buffer.ctypes) - datac0_buffer.pop(datac0_nin) - datac0_nin = codec2.api.freedv_nin(datac0_freedv) - if nbytes == datac0_bytes_per_frame: - datac0_task = threading.Thread(target=self.process_data, args=[datac0_bytes_out, datac0_freedv, datac0_bytes_per_frame]) - datac0_task.start() - - while datac1_buffer.nbuffer >= datac1_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(datac1_freedv, datac1_bytes_out, datac1_buffer.buffer.ctypes) - datac1_buffer.pop(datac1_nin) - datac1_nin = codec2.api.freedv_nin(datac1_freedv) - if nbytes == datac1_bytes_per_frame: - datac1_task = threading.Thread(target=self.process_data, args=[datac1_bytes_out, datac1_freedv, datac1_bytes_per_frame]) - datac1_task.start() - - while datac3_buffer.nbuffer >= datac3_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(datac3_freedv, datac3_bytes_out, datac3_buffer.buffer.ctypes) - datac3_buffer.pop(datac3_nin) - datac3_nin = codec2.api.freedv_nin(datac3_freedv) - if nbytes == datac3_bytes_per_frame: - datac3_task = threading.Thread(target=self.process_data, args=[datac3_bytes_out, datac1_freedv, datac1_bytes_per_frame]) - datac3_task.start() - - - - ''' - data_in = bytes() - data_in = self.stream_rx.read(self.AUDIO_CHUNKS, exception_on_overflow=False) - data_in = audioop.ratecv(data_in, 2, 1, self.AUDIO_SAMPLE_RATE_RX, self.MODEM_SAMPLE_RATE, None) - data_in = data_in[0]#.rstrip(b'\x00') - - # we need to set nin * 2 beause of byte size in array handling - datac0_nin = self.c_lib.freedv_nin(datac0_freedv) * 2 - datac1_nin = self.c_lib.freedv_nin(datac1_freedv) * 2 - datac3_nin = self.c_lib.freedv_nin(datac3_freedv) * 2 - - - datac0_buffer += data_in - datac1_buffer += data_in - datac3_buffer += data_in - - # refill fft_data buffer so we can plot a fft - if len(self.fft_data) < 1024: - self.fft_data += data_in - - # DECODING DATAC0 - if len(datac0_buffer) >= (datac0_nin): - datac0_audio = datac0_buffer[:datac0_nin] - datac0_buffer = datac0_buffer[datac0_nin:] - nbytes = self.c_lib.freedv_rawdatarx(datac0_freedv, datac0_bytes_out, datac0_audio) # demodulate audio - sync = self.c_lib.freedv_get_rx_status(datac0_freedv) - - self.get_scatter(datac0_freedv) - - if sync != 0 and nbytes != 0: - print("----------DECODE----------------") - - # calculate snr and scatter - self.get_scatter(datac0_freedv) - self.calculate_snr(datac0_freedv) - - datac0_task = threading.Thread(target=self.process_data, args=[datac0_bytes_out, datac0_freedv, datac0_bytes_per_frame]) - datac0_task.start() - - - # DECODING DATAC1 - if len(datac1_buffer) >= (datac1_nin): - datac1_audio = datac1_buffer[:datac1_nin] - datac1_buffer = datac1_buffer[datac1_nin:] - nbytes = self.c_lib.freedv_rawdatarx(datac1_freedv, datac1_bytes_out, datac1_audio) # demodulate audio - - sync = self.c_lib.freedv_get_rx_status(datac1_freedv) - if sync != 0 and nbytes != 0: - print("----------DECODE----------------") - frame = int.from_bytes(bytes(datac1_bytes_out[:1]), "big") - 10 - n_frames_per_burst = int.from_bytes(bytes(datac1_bytes_out[1:2]), "big") - print("frame: {0}, N_frames_per_burst: {1}".format(frame, n_frames_per_burst)) - - # calculate snr and scatter - self.get_scatter(datac1_freedv) - self.calculate_snr(datac1_freedv) - - datac1_task = threading.Thread(target=self.process_data, args=[datac1_bytes_out, datac1_freedv, datac1_bytes_per_frame]) - datac1_task.start() - - - # DECODING DATAC3 - if len(datac3_buffer) >= (datac3_nin): - datac3_audio = datac3_buffer[:datac3_nin] - datac3_buffer = datac3_buffer[datac3_nin:] - nbytes = self.c_lib.freedv_rawdatarx(datac3_freedv, datac3_bytes_out, datac3_audio) # demodulate audio - - sync = self.c_lib.freedv_get_rx_status(datac3_freedv) - if sync != 0 and nbytes != 0: - print("----------DECODE----------------") - # calculate snr and scatter - self.get_scatter(datac3_freedv) - self.calculate_snr(datac3_freedv) - - datac3_task = threading.Thread(target=self.process_data, args=[datac3_bytes_out, datac3_freedv, datac3_bytes_per_frame]) - datac3_task.start() - ''' + while self.stream_rx.is_active(): + time.sleep(1) + + # worker for FIFO queue for processing received frames + def worker(self): + while True: + time.sleep(0.01) + data = self.dataqueue.get() + self.process_data(data[0], data[1], data[2]) + self.dataqueue.task_done() + + # forward data only if broadcast or we are the receiver # bytes_out[1:2] == callsign check for signalling frames, # bytes_out[6:7] == callsign check for data frames, - # bytes_out[1:2] == b'\x01' --> broadcasts like CQ + # bytes_out[1:2] == b'\x01' --> broadcasts like CQ with n frames per_burst = 1 # we could also create an own function, which returns True. def process_data(self, bytes_out, freedv, bytes_per_frame): @@ -697,7 +537,7 @@ class RF(): frametype = int.from_bytes(bytes(bytes_out[:1]), "big") frame = frametype - 10 n_frames_per_burst = int.from_bytes(bytes(bytes_out[1:2]), "big") - #self.c_lib.freedv_set_frames_per_burst(freedv, n_frames_per_burst); + self.c_lib.freedv_set_frames_per_burst(freedv, n_frames_per_burst); #frequency_offset = self.get_frequency_offset(freedv) #print("Freq-Offset: " + str(frequency_offset)) @@ -777,6 +617,10 @@ class RF(): logging.debug("BEACON RECEIVED") data_handler.received_beacon(bytes_out[:-2]) + elif frametype == 255: + structlog.get_logger("structlog").debug("TESTFRAME RECEIVED", frame=bytes_out[:]) + + else: structlog.get_logger("structlog").warning("[TNC] ARQ - other frame type", frametype=frametype) From 82619ef0984ee1abb5784ec8e7cefba2e7882a3d Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 15:57:32 +0100 Subject: [PATCH 12/23] reduced bytes_out buffer size factor *2 buffer was too large. this didnt affect the tests, but not that nice... --- test/test_callback_multimode_rx.py | 6 +++--- test/test_callback_multimode_rx_outside.py | 6 +++--- test/test_callback_rx.py | 2 +- test/test_multimode_rx.py | 6 +++--- test/test_rx.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/test_callback_multimode_rx.py b/test/test_callback_multimode_rx.py index 94aa39b7..82dfe683 100644 --- a/test/test_callback_multimode_rx.py +++ b/test/test_callback_multimode_rx.py @@ -89,19 +89,19 @@ class Test(): # open codec2 instance self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) - self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame * 2) + self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST) self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) - self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame * 2) + self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST) self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8) - self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame * 2) + self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST) self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) diff --git a/test/test_callback_multimode_rx_outside.py b/test/test_callback_multimode_rx_outside.py index 2c5d8d11..5756fea2 100644 --- a/test/test_callback_multimode_rx_outside.py +++ b/test/test_callback_multimode_rx_outside.py @@ -89,19 +89,19 @@ class Test(): # open codec2 instance self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) - self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame * 2) + self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST) self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) - self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame * 2) + self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST) self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8) - self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame * 2) + self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST) self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) diff --git a/test/test_callback_rx.py b/test/test_callback_rx.py index 05470e53..1ea86e4a 100644 --- a/test/test_callback_rx.py +++ b/test/test_callback_rx.py @@ -92,7 +92,7 @@ class Test(): # get number of bytes per frame for mode self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8) - self.bytes_out = create_string_buffer(self.bytes_per_frame * 2) + self.bytes_out = create_string_buffer(self.bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) diff --git a/test/test_multimode_rx.py b/test/test_multimode_rx.py index af205d83..bdcc787f 100755 --- a/test/test_multimode_rx.py +++ b/test/test_multimode_rx.py @@ -60,19 +60,19 @@ rx_bursts_datac3 = 0 # open codec2 instance datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8) -datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame * 2) +datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST) datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8) -datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame * 2) +datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST) datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8) -datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame * 2) +datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST) datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) diff --git a/test/test_rx.py b/test/test_rx.py index cea12118..b3d339e8 100644 --- a/test/test_rx.py +++ b/test/test_rx.py @@ -93,7 +93,7 @@ bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) payload_bytes_per_frame = bytes_per_frame -2 n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv) -bytes_out = create_string_buffer(bytes_per_frame * 2) +bytes_out = create_string_buffer(bytes_per_frame) codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST) From b54463965a3d119626483dfca83f0b4b1eba31e1 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 16:21:09 +0100 Subject: [PATCH 13/23] code cleanup this isn't the right place for doing tnc changes, but before I forget to change it... --- tnc/modem.py | 48 ++++++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/tnc/modem.py b/tnc/modem.py index aff83f2a..94607c00 100644 --- a/tnc/modem.py +++ b/tnc/modem.py @@ -126,19 +126,24 @@ class RF(): # open codec2 instance self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) - self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame * 2) + self.datac0_payload_per_frame = self.datac0_bytes_per_frame -2 + self.datac0_n_nom_modem_samples = self.c_lib.freedv_get_n_nom_modem_samples(self.datac0_freedv) + self.datac0_n_tx_modem_samples = self.c_lib.freedv_get_n_tx_modem_samples(self.datac0_freedv) + self.datac0_n_tx_preamble_modem_samples = self.c_lib.freedv_get_n_tx_preamble_modem_samples(self.datac0_freedv) + self.datac0_n_tx_postamble_modem_samples = self.c_lib.freedv_get_n_tx_postamble_modem_samples(self.datac0_freedv) + self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,1) self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) - self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame * 2) + self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,1) self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8) - self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame * 2) + self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame) codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,1) self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX) @@ -304,6 +309,7 @@ class RF(): self.datac0_buffer.pop(self.datac0_nin) self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) if nbytes == self.datac0_bytes_per_frame: + print(len(self.datac0_bytes_out)) self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame]) self.get_scatter(self.datac0_freedv) self.calculate_snr(self.datac0_freedv) @@ -327,6 +333,8 @@ class RF(): self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame]) self.get_scatter(self.datac3_freedv) self.calculate_snr(self.datac3_freedv) + + self.dataqueue.join() return (None, pyaudio.paContinue) @@ -360,38 +368,24 @@ class RF(): state_before_transmit = static.CHANNEL_STATE static.CHANNEL_STATE = 'SENDING_SIGNALLING' - freedv_signalling_mode = 14 + mod_out = create_string_buffer(self.datac0_n_tx_modem_samples * 2) + mod_out_preamble = create_string_buffer(self.datac0_n_tx_preamble_modem_samples * 2) + mod_out_postamble = create_string_buffer(self.datac0_n_tx_postamble_modem_samples * 2) - freedv = cast(self.c_lib.freedv_open(freedv_signalling_mode), c_void_p) - - self.c_lib.freedv_set_clip(freedv, 1) - self.c_lib.freedv_set_tx_bpf(freedv, 1) - - bytes_per_frame = int(self.c_lib.freedv_get_bits_per_modem_frame(freedv) / 8) - payload_per_frame = bytes_per_frame - 2 - n_nom_modem_samples = self.c_lib.freedv_get_n_nom_modem_samples(freedv) - n_tx_modem_samples = self.c_lib.freedv_get_n_tx_modem_samples(freedv) - n_tx_preamble_modem_samples = self.c_lib.freedv_get_n_tx_preamble_modem_samples(freedv) - n_tx_postamble_modem_samples = self.c_lib.freedv_get_n_tx_postamble_modem_samples(freedv) - - mod_out = create_string_buffer(n_tx_modem_samples * 2) - mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2) - mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2) - - buffer = bytearray(payload_per_frame) + buffer = bytearray(self.datac0_payload_per_frame) # set buffersize to length of data which will be send buffer[:len(data_out)] = data_out - crc = ctypes.c_ushort(self.c_lib.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16 + crc = ctypes.c_ushort(self.c_lib.freedv_gen_crc16(bytes(buffer), self.datac0_payload_per_frame)) # generate CRC16 # convert crc to 2 byte hex string crc = crc.value.to_bytes(2, byteorder='big') buffer += crc # append crc16 to buffer - data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + data = (ctypes.c_ubyte * self.datac0_bytes_per_frame).from_buffer_copy(buffer) # modulate DATA and safe it into mod_out pointer - self.c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble) - self.c_lib.freedv_rawdatatx(freedv, mod_out, data) - self.c_lib.freedv_rawdatapostambletx(freedv, mod_out_postamble) + self.c_lib.freedv_rawdatapreambletx(self.datac0_freedv, mod_out_preamble) + self.c_lib.freedv_rawdatatx(self.datac0_freedv, mod_out, data) + self.c_lib.freedv_rawdatapostambletx(self.datac0_freedv, mod_out_postamble) self.streambuffer = bytearray() self.streambuffer += bytes(mod_out_preamble) @@ -423,8 +417,6 @@ class RF(): static.CHANNEL_STATE = 'RECEIVING_SIGNALLING' else: static.CHANNEL_STATE = state_before_transmit - - self.c_lib.freedv_close(freedv) return True # -------------------------------------------------------------------------------------------------------- From 0ecaeaa2bb4c2f2c7661bb79e86837384fcfb0e8 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Mon, 20 Dec 2021 18:59:29 +0100 Subject: [PATCH 14/23] removed audioop support we want to test only the new buffer. Not sure if this is currently working --- tnc/modem.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tnc/modem.py b/tnc/modem.py index 94607c00..17c1c958 100644 --- a/tnc/modem.py +++ b/tnc/modem.py @@ -9,7 +9,6 @@ import sys import ctypes from ctypes import * import pathlib -import audioop #import asyncio import logging, structlog, log_handler import time @@ -391,13 +390,15 @@ class RF(): self.streambuffer += bytes(mod_out_preamble) self.streambuffer += bytes(mod_out) self.streambuffer += bytes(mod_out_postamble) - - converted_audio = audioop.ratecv(self.streambuffer, 2, 1, self.MODEM_SAMPLE_RATE, self.AUDIO_SAMPLE_RATE_TX, None) - self.streambuffer = bytes(converted_audio[0]) + # resample up to 48k (resampler works on np.int16) + x = np.frombuffer(self.streambuffer, dtype=np.int16) + txbuffer_48k = self.resampler.resample8_to_48(x) + + # append frame again with as much as in count defined - for i in range(1, count): - self.streambuffer += bytes(converted_audio[0]) + #for i in range(1, count): + # self.streambuffer += bytes(txbuffer_48k.tobytes()) while self.ptt_and_wait(True): pass @@ -406,8 +407,10 @@ class RF(): static.CHANNEL_STATE = 'SENDING_SIGNALLING' # start writing audio data to audio stream - self.stream_tx.write(self.streambuffer) - + #self.stream_tx.write(self.streambuffer) + self.stream_tx.write(txbuffer_48k.tobytes()) + + # set ptt back to false self.ptt_and_wait(False) @@ -472,8 +475,9 @@ class RF(): self.c_lib.freedv_rawdatapostambletx(freedv, mod_out_postamble) self.streambuffer += bytes(mod_out_postamble) - converted_audio = audioop.ratecv(self.streambuffer, 2, 1, self.MODEM_SAMPLE_RATE, self.AUDIO_SAMPLE_RATE_TX, None) - self.streambuffer = bytes(converted_audio[0]) + # resample up to 48k (resampler works on np.int16) + x = np.frombuffer(self.streambuffer, dtype=np.int16) + txbuffer_48k = self.resampler.resample8_to_48(x) # -------------- transmit audio @@ -484,7 +488,7 @@ class RF(): static.CHANNEL_STATE = 'SENDING_DATA' # write audio to stream - self.stream_tx.write(self.streambuffer) + self.stream_tx.write(txbuffer_48k.tobytes()) static.CHANNEL_STATE = 'RECEIVING_SIGNALLING' From 2cc319c8579980bf914ede1590b1b8bac3d4badc Mon Sep 17 00:00:00 2001 From: drowe67 Date: Tue, 21 Dec 2021 07:22:14 +1030 Subject: [PATCH 15/23] renamed tests --- CMakeLists.txt | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4983d863..e2608e8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,105 +22,105 @@ set(FRAMESPERBURST 3) set(BURSTS 1) set(TESTFRAMES 3) -add_test(NAME 000_resampler +add_test(NAME resampler COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 t48_8_short.py") - set_tests_properties(000_resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS") -add_test(NAME 001_highsnr_stdio_P_C_SM +add_test(NAME highsnr_stdio_P_C_single COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} | sox -t .s16 -r 48000 -c 1 - -t .s16 -r 8000 -c 1 - | freedv_data_raw_rx datac0 - - --framesperburst ${FRAMESPERBURST} | hexdump -C") - set_tests_properties(001_highsnr_stdio_P_C_SM PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD") + set_tests_properties(highsnr_stdio_P_C_single PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD") -add_test(NAME 001_highsnr_stdio_C_P_SM +add_test(NAME highsnr_stdio_C_P_single COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; freedv_data_raw_tx datac0 --testframes ${TESTFRAMES} --bursts ${BURSTS} --framesperburst ${FRAMESPERBURST} /dev/zero - | sox -t .s16 -r 8000 -c 1 - -t .s16 -r 48000 -c 1 - | python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}") - set_tests_properties(001_highsnr_stdio_C_P_SM PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}") + set_tests_properties(highsnr_stdio_C_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}") -add_test(NAME 001_highsnr_stdio_P_P_SM +add_test(NAME highsnr_stdio_P_P_single COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} | python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}") - set_tests_properties(001_highsnr_stdio_P_P_SM PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}") + set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}") -add_test(NAME 001_highsnr_stdio_P_P_MM +add_test(NAME highsnr_stdio_P_P_multi COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_multimode_tx.py --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} | python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20") - set_tests_properties(001_highsnr_stdio_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}") + set_tests_properties(highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}") -# These tests can't run on GitHub actions +# These tests can't run on GitHub actions as we don't have a virtual sound card if(NOT DEFINED ENV{GITHUB_RUN_ID}) # uses aplay/arecord then pipe to Python -add_test(NAME 001_highsnr_virtual1_P_P +add_test(NAME highsnr_virtual1_P_P_single_alsa COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual1.sh") - set_tests_properties(001_highsnr_virtual1_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0") + set_tests_properties(highsnr_virtual1_P_P_single_alsa PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0") # let Python do audio I/O -add_test(NAME 001_highsnr_virtual2_P_P +add_test(NAME highsnr_virtual2_P_P_single COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual2.sh") - set_tests_properties(001_highsnr_virtual2_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") + set_tests_properties(highsnr_virtual2_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") # Multimode test with Python I/O -add_test(NAME 001_highsnr_virtual3_P_P_MM +add_test(NAME highsnr_virtual3_P_P_multi COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual_mm.sh") - set_tests_properties(001_highsnr_virtual3_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") + set_tests_properties(highsnr_virtual3_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") # let Python do audio I/O via pyaudio callback mode -add_test(NAME 001_highsnr_virtual4_P_P_SM_CB +add_test(NAME highsnr_virtual4_P_P_single_callback COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual3a.sh") - set_tests_properties(001_highsnr_virtual4_P_P_SM_CB PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") + set_tests_properties(highsnr_virtual4_P_P_single_callback PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") # let Python do audio I/O via pyaudio callback mode with code outside of callback -add_test(NAME 001_highsnr_virtual4_P_P_SM_CB_OUTSIDE +add_test(NAME highsnr_virtual4_P_P_single_callback_outside COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual3b.sh") - set_tests_properties(001_highsnr_virtual4_P_P_SM_CB_OUTSIDE PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") + set_tests_properties(highsnr_virtual4_P_P_single_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") # let Python do audio I/O via pyaudio callback mode with code outside of callback -add_test(NAME 001_highsnr_virtual5_P_P_MM_CB +add_test(NAME highsnr_virtual5_P_P_multi_callback COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual4a.sh") - set_tests_properties(001_highsnr_virtual5_P_P_MM_CB PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") + set_tests_properties(highsnr_virtual5_P_P_multi_callback PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") # let Python do audio I/O via pyaudio callback mode with code outside of callback -add_test(NAME 001_highsnr_virtual5_P_P_MM_CB_OUTSIDE +add_test(NAME highsnr_virtual5_P_P_multi_callback_outside COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual4b.sh") - set_tests_properties(001_highsnr_virtual5_P_P_MM_CB_OUTSIDE PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") + set_tests_properties(highsnr_virtual5_P_P_multi_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") endif() From 342dd29747f679e854c82d87b8b634c0395d1ef2 Mon Sep 17 00:00:00 2001 From: drowe67 Date: Tue, 21 Dec 2021 08:20:02 +1030 Subject: [PATCH 16/23] audio buffer thread sfaety test --- CMakeLists.txt | 6 ++++ test/test_audiobuffer.py | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 test/test_audiobuffer.py diff --git a/CMakeLists.txt b/CMakeLists.txt index e2608e8e..4e4bf625 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,12 @@ set(FRAMESPERBURST 3) set(BURSTS 1) set(TESTFRAMES 3) +add_test(NAME audio_buffer + COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; + python3 test_audiobuffer.py") + set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0") + add_test(NAME resampler COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; diff --git a/test/test_audiobuffer.py b/test/test_audiobuffer.py new file mode 100644 index 00000000..9ef333ed --- /dev/null +++ b/test/test_audiobuffer.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# tests audio buffer thread safety + +import sys +sys.path.insert(0,'..') +from tnc import codec2 +import threading +import numpy as np +from time import sleep + +BUFFER_SZ = 1024 +N_MAX = 100 # write a repeating sequence of 0..N_MAX-1 +WRITE_SZ = 10 # different read and write sized buffers +READ_SZ = 8 +NTESTS = 10000 + +running = True +audio_buffer = codec2.audio_buffer(BUFFER_SZ) + +n_write = int(0) +n_read = int(0) + +def writer(): + global n_write + print("writer starting") + n = int(0) + buf = np.zeros(WRITE_SZ, dtype=np.int16) + while running: + nfree = audio_buffer.size - audio_buffer.nbuffer + if nfree >= WRITE_SZ: + for i in range(0,WRITE_SZ): + buf[i] = n; + n += 1 + if n == N_MAX: + n = 0 + n_write += 1 + audio_buffer.push(buf) + +x = threading.Thread(target=writer) +x.start() + +n_out = int(0) +errors = int(0) +for tests in range(1,NTESTS): + while audio_buffer.nbuffer < READ_SZ: + sleep(0.001) + for i in range(0,READ_SZ): + if audio_buffer.buffer[i] != n_out: + errors += 1 + n_out += 1 + if n_out == N_MAX: + n_out = 0 + n_read += 1 + audio_buffer.pop(READ_SZ) + +running = False +print("n_write: %d n_read: %d errors: %d " % (n_write, n_read, errors)) From 8a0caf7f43f94341b2bc328b075e1374354e2411 Mon Sep 17 00:00:00 2001 From: drowe67 Date: Tue, 21 Dec 2021 08:27:13 +1030 Subject: [PATCH 17/23] added thread locking to audio buffer to fix corner case --- tnc/codec2.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tnc/codec2.py b/tnc/codec2.py index 0959f205..0ed15a52 100644 --- a/tnc/codec2.py +++ b/tnc/codec2.py @@ -8,7 +8,7 @@ import pathlib from enum import Enum import numpy as np #print("loading codec2 module", file=sys.stderr) - +from threading import Lock # Enum for codec2 modes class FREEDV_MODE(Enum): @@ -135,17 +135,22 @@ class audio_buffer: self.size = size self.buffer = np.zeros(size, dtype=np.int16) self.nbuffer = 0 + self.mutex = Lock() def push(self,samples): + self.mutex.acquire() # add samples at the end of the buffer assert self.nbuffer+len(samples) <= self.size self.buffer[self.nbuffer:self.nbuffer+len(samples)] = samples self.nbuffer += len(samples) + self.mutex.release() def pop(self,size): + self.mutex.acquire() # remove samples from the start of the buffer self.nbuffer -= size; self.buffer[:self.nbuffer] = self.buffer[size:size+self.nbuffer] assert self.nbuffer >= 0 - + self.mutex.release() + # resampler --------------------------------------------------------- api.FDMDV_OS_48 = int(6) # oversampling rate From 3c01ff1301fa99aa6332984858bf2912dfbd2c32 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Tue, 21 Dec 2021 09:25:06 +0100 Subject: [PATCH 18/23] logging in own thread and also a small cleanup --- test/test_callback_multimode_rx.py | 42 ++++++++++++---------- test/test_callback_multimode_rx_outside.py | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/test/test_callback_multimode_rx.py b/test/test_callback_multimode_rx.py index 82dfe683..578f5c38 100644 --- a/test/test_callback_multimode_rx.py +++ b/test/test_callback_multimode_rx.py @@ -52,6 +52,8 @@ class Test(): self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.AUDIO_SAMPLE_RATE_RX = 48000 + + # make sure our resampler will work assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 @@ -136,7 +138,11 @@ class Test(): self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) - + + + self.LOGGER_THREAD = threading.Thread(target=self.print_stats, name="LOGGER_THREAD") + self.LOGGER_THREAD.start() + def callback(self, data_in48k, frame_count, time_info, status): @@ -161,7 +167,6 @@ class Test(): if self.rx_frames_datac0 == self.N_FRAMES_PER_BURST: self.rx_frames_datac0 = 0 self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1 - self.print_stats() while self.datac1_buffer.nbuffer >= self.datac1_nin: @@ -176,7 +181,7 @@ class Test(): if self.rx_frames_datac1 == self.N_FRAMES_PER_BURST: self.rx_frames_datac1 = 0 self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1 - self.print_stats() + while self.datac3_buffer.nbuffer >= self.datac3_nin: # demodulate audio @@ -190,28 +195,29 @@ class Test(): if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST: self.rx_frames_datac3 = 0 self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 - self.print_stats() - if self.rx_bursts_datac0 == self.N_BURSTS and self.rx_bursts_datac1 == self.N_BURSTS and self.rx_bursts_datac3 == self.N_BURSTS: - self.receive = False - + if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS: + self.receive = False + return (None, pyaudio.paContinue) def print_stats(self): - if self.DEBUGGING_MODE: - self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv) - self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus] + while self.receive: + time.sleep(0.01) + if self.DEBUGGING_MODE: + self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv) + self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus] - self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv) - self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus] - - self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv) - self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus] + self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv) + self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus] + + self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv) + self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus] - print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \ - (self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus), - file=sys.stderr) + print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \ + (self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus), + file=sys.stderr) def run_audio(self): diff --git a/test/test_callback_multimode_rx_outside.py b/test/test_callback_multimode_rx_outside.py index 5756fea2..43226610 100644 --- a/test/test_callback_multimode_rx_outside.py +++ b/test/test_callback_multimode_rx_outside.py @@ -221,7 +221,7 @@ class Test(): self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 self.print_stats() - if self.rx_bursts_datac0 == self.N_BURSTS and self.rx_bursts_datac1 == self.N_BURSTS and self.rx_bursts_datac3 == self.N_BURSTS: + if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS: self.receive = False From 05e65018d8e2f04aea4f10f1137083d807d250da Mon Sep 17 00:00:00 2001 From: dj2ls Date: Tue, 21 Dec 2021 10:39:07 +0100 Subject: [PATCH 19/23] added stream.is_active excepotion not sure if we really need this --- test/test_callback_multimode_rx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_callback_multimode_rx.py b/test/test_callback_multimode_rx.py index 578f5c38..cc58e6a8 100644 --- a/test/test_callback_multimode_rx.py +++ b/test/test_callback_multimode_rx.py @@ -145,7 +145,6 @@ class Test(): def callback(self, data_in48k, frame_count, time_info, status): - x = np.frombuffer(data_in48k, dtype=np.int16) x.tofile(self.frx) x = self.resampler.resample48_to_8(x) @@ -231,8 +230,9 @@ class Test(): while self.receive and time.time() < self.timeout: time.sleep(1) - if time.time() >= self.timeout: + if time.time() >= self.timeout and self.stream_rx.is_active(): print("TIMEOUT REACHED") + self.receive = False if self.nread_exceptions: print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ From 24f46204eaea404ccda98c353e61000776e6a83b Mon Sep 17 00:00:00 2001 From: dj2ls Date: Wed, 22 Dec 2021 10:31:21 +0100 Subject: [PATCH 20/23] transmitting in callback mode used a FIFO queue to store modulation...crazy somehow. But its working --- test/test_callback_tx.py | 230 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 test/test_callback_tx.py diff --git a/test/test_callback_tx.py b/test/test_callback_tx.py new file mode 100644 index 00000000..e1d4c04a --- /dev/null +++ b/test/test_callback_tx.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 23 07:04:24 2020 + +@author: DJ2LS +""" + +import ctypes +from ctypes import * +import pathlib +import pyaudio +import sys +import logging +import time +import threading +import sys +import argparse +import queue +import numpy as np +sys.path.insert(0,'..') +from tnc import codec2 + +#--------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description='FreeDATA audio test') +parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) +parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) +parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int) +parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3']) +parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, + help="audio device number to use, use -2 to automatically select a loopback device") +parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") +parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") +parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") +parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes") + +args = parser.parse_args() + +if args.LIST: + p = pyaudio.PyAudio() + for dev in range(0,p.get_device_count()): + print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) + quit() + + + +class Test(): + def __init__(self): + + self.dataqueue = queue.Queue() + self.N_BURSTS = args.N_BURSTS + self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE + self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value + self.DEBUGGING_MODE = args.DEBUGGING_MODE + self.TIMEOUT = args.TIMEOUT + self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000 + + # AUDIO PARAMETERS + self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0 + self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.AUDIO_SAMPLE_RATE_TX = 48000 + + # make sure our resampler will work + assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 + + + self.transmit = True + + self.resampler = codec2.resampler() + + + # check if we want to use an audio device then do an pyaudio init + if self.AUDIO_OUTPUT_DEVICE != -1: + self.p = pyaudio.PyAudio() + # auto search for loopback devices + if self.AUDIO_OUTPUT_DEVICE == -2: + loopback_list = [] + for dev in range(0,self.p.get_device_count()): + if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + loopback_list.append(dev) + if len(loopback_list) >= 2: + self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX + print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + else: + quit() + + print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \ + AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr) + + self.stream_tx = self.p.open(format=pyaudio.paInt16, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_TX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=False, + output=True, + output_device_index=self.AUDIO_OUTPUT_DEVICE, + stream_callback=self.callback + ) + + # open codec2 instance + self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) + + # get number of bytes per frame for mode + self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8) + + self.bytes_out = create_string_buffer(self.bytes_per_frame) + + codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) + + + # Copy received 48 kHz to a file. Listen to this file with: + # aplay -r 48000 -f S16_LE rx48_callback.raw + # Corruption of this file is a good way to detect audio card issues + self.ftx = open("tx48_callback.raw", mode='wb') + + # data binary string + if args.TESTFRAMES: + self.data_out = bytearray(14) + self.data_out[:1] = bytes([255]) + self.data_out[1:2] = bytes([1]) + self.data_out[2:] = b'HELLO WORLD' + + else: + self.data_out = b'HELLO WORLD!' + + + def callback(self, data_in48k, frame_count, time_info, status): + + data_out48k = self.dataqueue.get() + return (data_out48k, pyaudio.paContinue) + + def run_audio(self): + try: + print(f"starting pyaudio callback", file=sys.stderr) + self.stream_tx.start_stream() + except Exception as e: + print(f"pyAudio error: {e}", file=sys.stderr) + + + while self.transmit: + time.sleep(0.1) + + + self.ftx.close() + + # close pyaudio instance + self.stream_tx.close() + self.p.terminate() + + def create_modulation(self): + + # open codec2 instance + freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) + + # get number of bytes per frame for mode + bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) + payload_bytes_per_frame = bytes_per_frame -2 + + # init buffer for data + n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) + mod_out = create_string_buffer(n_tx_modem_samples * 2) + + # init buffer for preample + n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) + mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2) + + # init buffer for postamble + n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) + mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2) + + # create buffer for data + buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3) + buffer[:len(self.data_out)] = self.data_out # set buffersize to length of data which will be send + + # create crc for data frame - we are using the crc function shipped with codec2 to avoid + # crc algorithm incompatibilities + crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16 + crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string + buffer += crc # append crc16 to buffer + + print(f"TOTAL BURSTS: {self.N_BURSTS} TOTAL FRAMES_PER_BURST: {self.N_FRAMES_PER_BURST}", file=sys.stderr) + + for i in range(1,self.N_BURSTS+1): + + # write preamble to txbuffer + codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) + txbuffer = bytes(mod_out_preamble) + + # create modulaton for N = FRAMESPERBURST and append it to txbuffer + for n in range(1,self.N_FRAMES_PER_BURST+1): + + data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer + + txbuffer += bytes(mod_out) + + print(f"TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr) + + # append postamble to txbuffer + codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) + txbuffer += bytes(mod_out_postamble) + + # append a delay between bursts as audio silence + samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS) + mod_out_silence = create_string_buffer(samples_delay*2) + txbuffer += bytes(mod_out_silence) + print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {self.DELAY_BETWEEN_BURSTS}", file=sys.stderr) + + # resample up to 48k (resampler works on np.int16) + x = np.frombuffer(txbuffer, dtype=np.int16) + txbuffer_48k = self.resampler.resample8_to_48(x) + + # split modualted audio to chunks + #https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python + txbuffer_48k = bytes(txbuffer_48k) + chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)] + # add modulated chunks to fifo buffer + for c in chunk: + # if data is shorter than the expcected audio frames per buffer we need to append 0 + # to prevent the callback from stucking/crashing + if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2: + c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c)) + self.dataqueue.put(c) + + + +test = Test() +test.create_modulation() +test.run_audio() From 705600a3b1c1c486980502e89dc654b67d5afccd Mon Sep 17 00:00:00 2001 From: dj2ls Date: Wed, 22 Dec 2021 11:19:27 +0100 Subject: [PATCH 21/23] code cleanup --- test/test_callback_tx.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/test_callback_tx.py b/test/test_callback_tx.py index e1d4c04a..5c0c07fe 100644 --- a/test/test_callback_tx.py +++ b/test/test_callback_tx.py @@ -29,8 +29,6 @@ parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=in parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3']) parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, help="audio device number to use, use -2 to automatically select a loopback device") -parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") -parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes") @@ -52,8 +50,6 @@ class Test(): self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value - self.DEBUGGING_MODE = args.DEBUGGING_MODE - self.TIMEOUT = args.TIMEOUT self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000 # AUDIO PARAMETERS @@ -137,9 +133,12 @@ class Test(): except Exception as e: print(f"pyAudio error: {e}", file=sys.stderr) - + sheeps = 0 while self.transmit: - time.sleep(0.1) + time.sleep(1) + sheeps = sheeps + 1 + print(f"counting sheeps...{sheeps}") + self.ftx.close() @@ -195,7 +194,7 @@ class Test(): txbuffer += bytes(mod_out) - print(f"TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr) + print(f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr) # append postamble to txbuffer codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) From a0486a6b00ff4370b7026abffad1d66b91bc1041 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Wed, 22 Dec 2021 11:19:43 +0100 Subject: [PATCH 22/23] multimode callback TX --- test/test_callback_multimode_tx.py | 208 +++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 test/test_callback_multimode_tx.py diff --git a/test/test_callback_multimode_tx.py b/test/test_callback_multimode_tx.py new file mode 100644 index 00000000..f620a6e4 --- /dev/null +++ b/test/test_callback_multimode_tx.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Dec 23 07:04:24 2020 + +@author: DJ2LS +""" + +import ctypes +from ctypes import * +import pathlib +import pyaudio +import sys +import logging +import time +import threading +import sys +import argparse +import queue +import numpy as np +sys.path.insert(0,'..') +from tnc import codec2 + +#--------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description='FreeDATA audio test') +parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) +parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) +parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int) +parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, help="audio output device number to use") +parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") +parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes") + +args = parser.parse_args() + +if args.LIST: + p = pyaudio.PyAudio() + for dev in range(0,p.get_device_count()): + print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) + quit() + + + +class Test(): + def __init__(self): + + self.dataqueue = queue.Queue() + self.N_BURSTS = args.N_BURSTS + self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE + self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000 + + # AUDIO PARAMETERS + self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0 + self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.AUDIO_SAMPLE_RATE_TX = 48000 + + # make sure our resampler will work + assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 + + + self.transmit = True + + self.resampler = codec2.resampler() + + + # check if we want to use an audio device then do an pyaudio init + if self.AUDIO_OUTPUT_DEVICE != -1: + self.p = pyaudio.PyAudio() + # auto search for loopback devices + if self.AUDIO_OUTPUT_DEVICE == -2: + loopback_list = [] + for dev in range(0,self.p.get_device_count()): + if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + loopback_list.append(dev) + if len(loopback_list) >= 2: + self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX + print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + else: + quit() + + print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \ + AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr) + + self.stream_tx = self.p.open(format=pyaudio.paInt16, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_TX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=False, + output=True, + output_device_index=self.AUDIO_OUTPUT_DEVICE, + stream_callback=self.callback + ) + + # Copy received 48 kHz to a file. Listen to this file with: + # aplay -r 48000 -f S16_LE rx48_callback.raw + # Corruption of this file is a good way to detect audio card issues + self.ftx = open("tx48_callback.raw", mode='wb') + + # data binary string + if args.TESTFRAMES: + self.data_out = bytearray(14) + self.data_out[:1] = bytes([255]) + self.data_out[1:2] = bytes([1]) + self.data_out[2:] = b'HELLO WORLD' + + else: + self.data_out = b'HELLO WORLD!' + + + def callback(self, data_in48k, frame_count, time_info, status): + + data_out48k = self.dataqueue.get() + return (data_out48k, pyaudio.paContinue) + + def run_audio(self): + try: + print(f"starting pyaudio callback", file=sys.stderr) + self.stream_tx.start_stream() + except Exception as e: + print(f"pyAudio error: {e}", file=sys.stderr) + + sheeps = 0 + while self.transmit: + time.sleep(1) + sheeps = sheeps + 1 + print(f"counting sheeps...{sheeps}") + + self.ftx.close() + + # close pyaudio instance + self.stream_tx.close() + self.p.terminate() + + def create_modulation(self): + + modes = [codec2.api.FREEDV_MODE_DATAC0, codec2.api.FREEDV_MODE_DATAC1, codec2.api.FREEDV_MODE_DATAC3] + for m in modes: + + freedv = cast(codec2.api.freedv_open(m), c_void_p) + + n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) + mod_out = create_string_buffer(2*n_tx_modem_samples) + + n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) + mod_out_preamble = create_string_buffer(2*n_tx_preamble_modem_samples) + + n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) + mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples) + + bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) + payload_per_frame = bytes_per_frame - 2 + + buffer = bytearray(payload_per_frame) + # set buffersize to length of data which will be send + buffer[:len(self.data_out)] = self.data_out + + crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16 + # convert crc to 2 byte hex string + crc = crc.value.to_bytes(2, byteorder='big') + buffer += crc # append crc16 to buffer + data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + + for i in range(1,self.N_BURSTS+1): + + # write preamble to txbuffer + codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) + txbuffer = bytes(mod_out_preamble) + + # create modulaton for N = FRAMESPERBURST and append it to txbuffer + for n in range(1,self.N_FRAMES_PER_BURST+1): + + data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer + + txbuffer += bytes(mod_out) + print(f"GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr) + + # append postamble to txbuffer + codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) + txbuffer += bytes(mod_out_postamble) + + # append a delay between bursts as audio silence + samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS) + mod_out_silence = create_string_buffer(samples_delay*2) + txbuffer += bytes(mod_out_silence) + + # resample up to 48k (resampler works on np.int16) + x = np.frombuffer(txbuffer, dtype=np.int16) + txbuffer_48k = self.resampler.resample8_to_48(x) + + # split modulated audio to chunks + #https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python + txbuffer_48k = bytes(txbuffer_48k) + chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)] + # add modulated chunks to fifo buffer + for c in chunk: + # if data is shorter than the expcected audio frames per buffer we need to append 0 + # to prevent the callback from stucking/crashing + if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2: + c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c)) + self.dataqueue.put(c) + + + + +test = Test() +test.create_modulation() +test.run_audio() From f99daf6800408bbafebf737970d5585f72facc98 Mon Sep 17 00:00:00 2001 From: dj2ls Date: Wed, 22 Dec 2021 12:48:49 +0100 Subject: [PATCH 23/23] updated tnc modem code for testing TX and RX in same callback --- tnc/data_handler.py | 62 +++++++----- tnc/modem.py | 235 ++++++++++++++++++++++++++++++-------------- 2 files changed, 194 insertions(+), 103 deletions(-) diff --git a/tnc/data_handler.py b/tnc/data_handler.py index 702a61cc..fd64fafd 100644 --- a/tnc/data_handler.py +++ b/tnc/data_handler.py @@ -159,10 +159,10 @@ def arq_data_received(data_in, bytes_per_frame): # TRANSMIT ACK FRAME FOR BURST----------------------------------------------- helpers.wait(0.3) - while not modem.transmit_signalling(ack_frame, 1): - #while static.CHANNEL_STATE == 'SENDING_SIGNALLING': - time.sleep(0.01) - + + txbuffer = [ack_frame] + modem.transmit('datac0', 1, txbuffer) + static.CHANNEL_STATE = 'RECEIVING_DATA' # clear burst buffer static.RX_BURST_BUFFER = [] @@ -191,8 +191,11 @@ def arq_data_received(data_in, bytes_per_frame): rpt_frame[3:9] = missing_frames # TRANSMIT RPT FRAME FOR BURST----------------------------------------------- - while not modem.transmit_signalling(rpt_frame, 1): - time.sleep(0.01) + txbuffer = [rpt_frame] + modem.transmit('datac0', 1, txbuffer) + + #while not modem.transmit_signalling(rpt_frame, 1): + # time.sleep(0.01) static.CHANNEL_STATE = 'RECEIVING_DATA' # ---------------------------- FRAME MACHINE @@ -270,9 +273,11 @@ def arq_data_received(data_in, bytes_per_frame): # possibly we can remove this later helpers.wait(0.5) + txbuffer = [ack_frame] + modem.transmit('datac0', 1, txbuffer) - while not modem.transmit_signalling(ack_frame, 3): - time.sleep(0.01) + #while not modem.transmit_signalling(ack_frame, 3): + # time.sleep(0.01) calculate_transfer_rate_rx(RX_N_FRAMES_PER_DATA_FRAME, RX_N_FRAME_OF_DATA_FRAME, RX_START_OF_TRANSMISSION, RX_PAYLOAD_PER_ARQ_FRAME) @@ -357,6 +362,7 @@ def arq_transmit(data_out, mode, n_frames_per_burst): # save len of data_out to TOTAL_BYTES for our statistics static.TOTAL_BYTES = len(data_out) # --------------------------------------------- LETS CREATE A BUFFER BY SPLITTING THE FILES INTO PEACES + # https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python TX_BUFFER = [data_out[i:i + TX_PAYLOAD_PER_ARQ_FRAME] for i in range(0, len(data_out), TX_PAYLOAD_PER_ARQ_FRAME)] TX_BUFFER_SIZE = len(TX_BUFFER) static.INFO.append("ARQ;TRANSMITTING") @@ -685,9 +691,12 @@ async def arq_open_data_channel(mode): structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]", attempt=str(attempt) + "/" + str(DATA_CHANNEL_MAX_RETRIES)) - while not modem.transmit_signalling(connection_frame, 1): - time.sleep(0.01) - + + + txbuffer = [connection_frame] + modem.transmit('datac0', 1, txbuffer) + + timeout = time.time() + 3 while time.time() < timeout: time.sleep(0.01) @@ -732,8 +741,8 @@ def arq_received_data_channel_opener(data_in): connection_frame[3:9] = static.MYCALLSIGN connection_frame[12:13] = bytes([mode]) - while not modem.transmit_signalling(connection_frame, 2): - time.sleep(0.01) + txbuffer = [connection_frame] + modem.transmit('datac0', 1, txbuffer) structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=static.SNR, mode=mode) @@ -793,10 +802,9 @@ def transmit_ping(callsign): ping_frame[2:3] = static.MYCALLSIGN_CRC8 ping_frame[3:9] = static.MYCALLSIGN - # wait while sending.... - while not modem.transmit_signalling(ping_frame, 1): - time.sleep(0.01) - + txbuffer = [ping_frame] + modem.transmit('datac0', 1, txbuffer) + def received_ping(data_in, frequency_offset): @@ -815,10 +823,8 @@ def received_ping(data_in, frequency_offset): ping_frame[3:9] = static.MYGRID ping_frame[9:11] = frequency_offset.to_bytes(2, byteorder='big', signed=True) - # wait while sending.... - while not modem.transmit_signalling(ping_frame, 1): - time.sleep(0.01) - + txbuffer = [ping_frame] + modem.transmit('datac0', 1, txbuffer) def received_ping_ack(data_in): @@ -850,9 +856,10 @@ def run_beacon(interval): static.INFO.append("BEACON;SENDING") structlog.get_logger("structlog").info("[TNC] Sending beacon!", interval=interval) - while not modem.transmit_signalling(beacon_frame, 2): - #time.sleep(0.01) - pass + + txbuffer = [beacon_frame] + modem.transmit('datac0', 1, txbuffer) + time.sleep(interval) @@ -882,9 +889,10 @@ def transmit_cq(): cq_frame[2:8] = static.MYCALLSIGN cq_frame[8:14] = static.MYGRID - while not modem.transmit_signalling(cq_frame, 3): - #time.sleep(0.01) - pass + txbuffer = [cq_frame] + modem.transmit('datac0', 1, txbuffer) + #while not modem.transmit('datac0', 1, txbuffer): + # pass def received_cq(data_in): diff --git a/tnc/modem.py b/tnc/modem.py index 17c1c958..2ae2badb 100644 --- a/tnc/modem.py +++ b/tnc/modem.py @@ -106,7 +106,7 @@ class RF(): self.AUDIO_SAMPLE_RATE_TX = 48000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.AUDIO_FRAMES_PER_BUFFER_RX = 2400*2 #8192 - self.AUDIO_FRAMES_PER_BUFFER_TX = 8 #8192 Lets to some tests with very small chunks for TX + self.AUDIO_FRAMES_PER_BUFFER_TX = 2400 #8192 Lets to some tests with very small chunks for TX self.AUDIO_CHUNKS = 48 #8 * (self.AUDIO_SAMPLE_RATE_RX/self.MODEM_SAMPLE_RATE) #48 self.AUDIO_CHANNELS = 1 @@ -121,7 +121,10 @@ class RF(): # init FIFO queue to store received frames in self.dataqueue = queue.Queue() - + # init FIFO queue to store modulation out in + self.modoutqueue = queue.Queue() + + # open codec2 instance self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) @@ -170,43 +173,28 @@ class RF(): if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: loopback_list.append(dev) if len(loopback_list) >= 2: - AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX + static.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX + static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #1 = TX print(f"loopback_list rx: {loopback_list}", file=sys.stderr) - self.stream_rx = self.p.open(format=pyaudio.paInt16, + self.audio_stream = self.p.open(format=pyaudio.paInt16, channels=self.AUDIO_CHANNELS, rate=self.AUDIO_SAMPLE_RATE_RX, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX, input=True, - output=False, + output=True, input_device_index=static.AUDIO_INPUT_DEVICE, + output_device_index=static.AUDIO_OUTPUT_DEVICE, stream_callback=self.callback ) - # --------------------------------------------OPEN TX AUDIO CHANNEL - # optional auto selection of loopback device if using in testmode - if static.AUDIO_OUTPUT_DEVICE == -2: - loopback_list = [] - for dev in range(0,self.p.get_device_count()): - if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: - loopback_list.append(dev) - if len(loopback_list) >= 2: - static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #0 = RX 1 = TX - print(f"loopback_list tx: {loopback_list}", file=sys.stderr) - - self.stream_tx = self.p.open(format=pyaudio.paInt16, - channels=self.AUDIO_CHANNELS, - rate=self.AUDIO_SAMPLE_RATE_TX, - frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_TX, # n_nom_modem_samples - output=True, - output_device_index=static.AUDIO_OUTPUT_DEVICE, # static.AUDIO_OUTPUT_DEVICE - ) - self.streambuffer = bytes(0) - self.audio_writing_to_stream = False + # not needed anymore. + #self.streambuffer = bytes(0) + # --------------------------------------------START DECODER THREAD - DECODER_THREAD = threading.Thread(target=self.receive, name="DECODER_THREAD") - DECODER_THREAD.start() + AUDIO_THREAD = threading.Thread(target=self.audio, name="AUDIO_THREAD") + AUDIO_THREAD.start() WORKER_THREAD = threading.Thread(target=self.worker, name="WORKER_THREAD") WORKER_THREAD.start() @@ -216,14 +204,6 @@ class RF(): FFT_THREAD.start() # --------------------------------------------CONFIGURE HAMLIB - # my_rig.set_ptt(Hamlib.RIG_PTT_RIG,0) - # my_rig.set_ptt(Hamlib.RIG_PTT_SERIAL_DTR,0) - # my_rig.set_ptt(Hamlib.RIG_PTT_SERIAL_RTS,1) - #self.my_rig.set_conf("dtr_state", "OFF") - #my_rig.set_conf("rts_state", "OFF") - #self.my_rig.set_conf("ptt_type", "RTS") - #my_rig.set_conf("ptt_type", "RIG_PTT_SERIAL_RTS") - # try to init hamlib try: Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE) @@ -300,42 +280,14 @@ class RF(): # refill fft_data buffer so we can plot a fft if len(self.fft_data) < 1024: self.fft_data += bytes(x) - - - while self.datac0_buffer.nbuffer >= self.datac0_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) - self.datac0_buffer.pop(self.datac0_nin) - self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) - if nbytes == self.datac0_bytes_per_frame: - print(len(self.datac0_bytes_out)) - self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame]) - self.get_scatter(self.datac0_freedv) - self.calculate_snr(self.datac0_freedv) - - while self.datac1_buffer.nbuffer >= self.datac1_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) - self.datac1_buffer.pop(self.datac1_nin) - self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) - if nbytes == self.datac1_bytes_per_frame: - self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame]) - self.get_scatter(self.datac1_freedv) - self.calculate_snr(self.datac1_freedv) - - while self.datac3_buffer.nbuffer >= self.datac3_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) - self.datac3_buffer.pop(self.datac3_nin) - self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) - if nbytes == self.datac3_bytes_per_frame: - self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame]) - self.get_scatter(self.datac3_freedv) - self.calculate_snr(self.datac3_freedv) + - self.dataqueue.join() - - return (None, pyaudio.paContinue) + if self.modoutqueue.empty(): + data_out48k = bytes(self.AUDIO_FRAMES_PER_BUFFER_TX*2*2) + else: + data_out48k = self.modoutqueue.get() + + return (data_out48k, pyaudio.paContinue) @@ -347,7 +299,8 @@ class RF(): self.my_rig.set_ptt(self.hamlib_ptt_type, 1) # rigctld.ptt_enable() ptt_toggle_timeout = time.time() + 0.5 - while time.time() < ptt_toggle_timeout: + + while time.time() < ptt_toggle_timeout and not self.modoutqueue.empty(): pass else: @@ -363,6 +316,103 @@ class RF(): # -------------------------------------------------------------------------------------------------------- + + def transmit(self, mode, count, frames): + + state_before_transmit = static.CHANNEL_STATE + static.CHANNEL_STATE = 'SENDING_SIGNALLING' + + # open codec2 instance + self.MODE = codec2.FREEDV_MODE[mode].value + freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) + + # get number of bytes per frame for mode + bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) + payload_bytes_per_frame = bytes_per_frame -2 + + # init buffer for data + n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) + mod_out = create_string_buffer(n_tx_modem_samples * 2) + + # init buffer for preample + n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) + mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2) + + # init buffer for postamble + n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) + mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2) + + + + for i in range(1,count+1): + + # write preamble to txbuffer + codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) + txbuffer = bytes(mod_out_preamble) + + # create modulaton for n frames in list + for n in range(0,len(frames)): + + + # create buffer for data + buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3) + buffer[:len(frames[n])] = frames[n] # set buffersize to length of data which will be send + + # create crc for data frame - we are using the crc function shipped with codec2 to avoid + # crc algorithm incompatibilities + crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16 + crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string + buffer += crc # append crc16 to buffer + + data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer + + txbuffer += bytes(mod_out) + + + # append postamble to txbuffer + codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) + txbuffer += bytes(mod_out_postamble) + + # resample up to 48k (resampler works on np.int16) + x = np.frombuffer(txbuffer, dtype=np.int16) + txbuffer_48k = self.resampler.resample8_to_48(x) + + # split modualted audio to chunks + #https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python + txbuffer_48k = bytes(txbuffer_48k) + chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER_RX*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER_RX*2)] + # add modulated chunks to fifo buffer + for c in chunk: + # if data is shorter than the expcected audio frames per buffer we need to append 0 + # to prevent the callback from stucking/crashing + if len(c) < self.AUDIO_FRAMES_PER_BUFFER_RX*2: + c += bytes(self.AUDIO_FRAMES_PER_BUFFER_RX*2 - len(c)) + self.modoutqueue.put(c) + print(len(c)) + + while self.ptt_and_wait(True): + pass + + # set channel state + static.CHANNEL_STATE = 'SENDING_SIGNALLING' + + # set ptt back to false + self.ptt_and_wait(False) + + + # we have a problem with the receiving state + if state_before_transmit != 'RECEIVING_DATA': + static.CHANNEL_STATE = 'RECEIVING_SIGNALLING' + else: + static.CHANNEL_STATE = state_before_transmit + + self.c_lib.freedv_close(freedv) + return True + + + + ''' def transmit_signalling(self, data_out, count): state_before_transmit = static.CHANNEL_STATE static.CHANNEL_STATE = 'SENDING_SIGNALLING' @@ -499,17 +549,50 @@ class RF(): return True # -------------------------------------------------------------------------------------------------------- - - def receive(self): + ''' + def audio(self): try: print(f"starting pyaudio callback", file=sys.stderr) - self.stream_rx.start_stream() + self.audio_stream.start_stream() except Exception as e: print(f"pyAudio error: {e}", file=sys.stderr) - while self.stream_rx.is_active(): - time.sleep(1) + while self.audio_stream.is_active(): + while self.datac0_buffer.nbuffer >= self.datac0_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) + self.datac0_buffer.pop(self.datac0_nin) + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + if nbytes == self.datac0_bytes_per_frame: + print(len(self.datac0_bytes_out)) + self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame]) + self.get_scatter(self.datac0_freedv) + self.calculate_snr(self.datac0_freedv) + + while self.datac1_buffer.nbuffer >= self.datac1_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) + self.datac1_buffer.pop(self.datac1_nin) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + if nbytes == self.datac1_bytes_per_frame: + self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame]) + self.get_scatter(self.datac1_freedv) + self.calculate_snr(self.datac1_freedv) + + while self.datac3_buffer.nbuffer >= self.datac3_nin: + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) + self.datac3_buffer.pop(self.datac3_nin) + self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) + if nbytes == self.datac3_bytes_per_frame: + self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame]) + self.get_scatter(self.datac3_freedv) + self.calculate_snr(self.datac3_freedv) + + + + # worker for FIFO queue for processing received frames def worker(self):