diff --git a/CMakeLists.txt b/CMakeLists.txt index 56979c0c..4e4bf625 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,82 +22,112 @@ set(FRAMESPERBURST 3) set(BURSTS 1) set(TESTFRAMES 3) -add_test(NAME 000_resampler +add_test(NAME audio_buffer 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 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; 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/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") - 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/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}") - 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/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}") + 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/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}") + 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/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") + 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/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") + 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/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") + 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/001_highsnr_stdio_audio; - ./test_virtual3.sh") - set_tests_properties(001_highsnr_virtual4_P_P_SM_CB PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0") + cd ${CMAKE_CURRENT_SOURCE_DIR}/test; + ./test_virtual3a.sh") + 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 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(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 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(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 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(highsnr_virtual5_P_P_multi_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") + + endif() diff --git a/test/001_highsnr_stdio_audio/test_callback_rx.py b/test/001_highsnr_stdio_audio/test_callback_rx.py deleted file mode 100644 index 9c61d224..00000000 --- a/test/001_highsnr_stdio_audio/test_callback_rx.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/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,'..') -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() - -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 - - -# ------------------------------------------------ PYAUDIO CALLBACK -def callback(data_in48k, frame_count, time_info, status): - x = np.frombuffer(data_in48k, dtype=np.int16) - x.tofile(frx) - x = resampler.resample48_to_8(x) - audio_buffer.push(x) - 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) -# ---------------------------------------------------------------- - -# 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: - - # 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) - - # 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) -print(f"RECEIVED BURSTS: {rx_bursts} RECEIVED FRAMES: {rx_total_frames} RX_ERRORS: {rx_errors}", file=sys.stderr) -frx.close() - -# cloese pyaudio instance -stream_rx.close() -p.terminate() 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 99% rename from test/000_resampler/t48_8_short.py rename to test/t48_8_short.py index f76cd5b2..480062b8 100644 --- a/test/000_resampler/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 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/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)) diff --git a/test/test_callback_multimode_rx.py b/test/test_callback_multimode_rx.py new file mode 100644 index 00000000..cc58e6a8 --- /dev/null +++ b/test/test_callback_multimode_rx.py @@ -0,0 +1,250 @@ +#!/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) + 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) + 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) + 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) + + + 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): + 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 + + + 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 + + + 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 + + + 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): + 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] + + 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 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..." % \ + 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..43226610 --- /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) + 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) + 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) + 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 and self.rx_bursts_datac1 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_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() diff --git a/test/test_callback_rx.py b/test/test_callback_rx.py new file mode 100644 index 00000000..1ea86e4a --- /dev/null +++ b/test/test_callback_rx.py @@ -0,0 +1,184 @@ +#!/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) + + 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) + + 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/test_callback_rx_outside.py b/test/test_callback_rx_outside.py new file mode 100644 index 00000000..cf908ca4 --- /dev/null +++ b/test/test_callback_rx_outside.py @@ -0,0 +1,181 @@ +#!/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) + + 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/test_callback_tx.py b/test/test_callback_tx.py new file mode 100644 index 00000000..5c0c07fe --- /dev/null +++ b/test/test_callback_tx.py @@ -0,0 +1,229 @@ +#!/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('--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.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) + + 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): + + # 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" 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) + 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() diff --git a/test/001_highsnr_stdio_audio/test_multimode_rx.py b/test/test_multimode_rx.py similarity index 94% rename from test/001_highsnr_stdio_audio/test_multimode_rx.py rename to test/test_multimode_rx.py index 8a0ffa94..bdcc787f 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 @@ -60,22 +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_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) +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_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) +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_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) +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/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 98% rename from test/001_highsnr_stdio_audio/test_rx.py rename to test/test_rx.py index f29ea33d..b3d339e8 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 @@ -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) diff --git a/test/001_highsnr_stdio_audio/test_tx.py b/test/test_tx.py similarity index 94% rename from test/001_highsnr_stdio_audio/test_tx.py rename to test/test_tx.py index 581eae2d..bffe1ae6 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 @@ -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!' # ---------------------------------------------------------------- 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 86% rename from test/001_highsnr_stdio_audio/test_virtual2.sh rename to test/test_virtual2.sh index 5176040a..1eba60d8 100755 --- a/test/001_highsnr_stdio_audio/test_virtual2.sh +++ b/test/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/test/test_virtual3a.sh b/test/test_virtual3a.sh new file mode 100755 index 00000000..9a4d5493 --- /dev/null +++ b/test/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/test_virtual3b.sh b/test/test_virtual3b.sh new file mode 100755 index 00000000..40cc495a --- /dev/null +++ b/test/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} 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} 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 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 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 94607c00..ec60c510 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 @@ -107,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 @@ -122,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) @@ -171,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() @@ -217,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) @@ -301,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) @@ -348,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: @@ -364,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' @@ -391,13 +440,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 +457,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 +525,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 +538,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' @@ -495,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): @@ -724,4 +811,3 @@ class RF(): static.FFT = [0] * 400 else: pass -