Merge remote-tracking branch 'origin/pep8_improvements' into pep8_improvements

This commit is contained in:
DJ2LS 2022-05-23 13:12:24 +02:00
commit 1bab085ca8
40 changed files with 3707 additions and 1811 deletions

View file

@ -2,7 +2,7 @@ name: Linux
on:
push:
tags:
- 'v*'
- "v*"
jobs:
build_linux_release:
@ -20,15 +20,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: main
ref: main
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-20.04'
run: |
@ -44,7 +42,6 @@ jobs:
pip3 install structlog
pip3 install sounddevice
#- name: Build Hamlib Python Binding
# if: matrix.os == 'ubuntu-latest'
# working-directory: tnc
@ -58,19 +55,15 @@ jobs:
# make
# make install
- name: Build codec2 Linux
if: matrix.os == 'ubuntu-20.04'
working-directory: tnc
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2 && mkdir build_linux && cd build_linux
cd codec2 && git checkout master && mkdir build_linux && cd build_linux
cmake ../
make
- name: Build Linux Daemon
if: matrix.os == 'ubuntu-20.04'
working-directory: tnc
@ -82,7 +75,6 @@ jobs:
run: |
ls -R
- name: Compress Linux
shell: bash
run: |
@ -96,8 +88,6 @@ jobs:
name: tnc-artifact
path: ./tnc/dist/compressed/*
- name: Copy TNC to GUI Linux
if: matrix.os == 'ubuntu-20.04'
run: |
@ -124,6 +114,3 @@ jobs:
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
args: "-p always"

View file

@ -2,7 +2,7 @@ name: macOS
on:
push:
tags:
- 'v*'
- "v*"
jobs:
build_linux_release:
@ -20,15 +20,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: main
ref: main
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install macOS dependencies
if: matrix.os == 'macos-10.15'
run: |
@ -45,20 +43,18 @@ jobs:
- name: Install Portaudio
if: matrix.os == 'macos-10.15'
run: |
brew install portaudio
pip3 install pyaudio
brew install portaudio
pip3 install pyaudio
- name: Build codec2 macOS
if: matrix.os == 'macos-10.15'
working-directory: tnc
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2 && mkdir build_mac && cd build_mac
cd codec2 && git checkout master && mkdir build_mac && cd build_mac
cmake ../
make
- name: Build macOS pyinstaller
if: matrix.os == 'macos-10.15'
working-directory: tnc
@ -70,7 +66,6 @@ jobs:
run: |
ls -R
- name: Compress
shell: bash
run: |
@ -84,9 +79,6 @@ jobs:
name: tnc-artifact
path: ./tnc/dist/compressed/*
- name: Copy TNC to GUI
if: matrix.os == 'macos-10.15'
run: |
@ -113,6 +105,3 @@ jobs:
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
args: "-p always"

View file

@ -11,26 +11,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2
- name: Install packages
shell: bash
run: |
sudo apt-get update
sudo apt-get install octave octave-common octave-signal sox python3 python3-pip portaudio19-dev python3-pyaudio
pip3 install psutil crcengine ujson pyserial numpy structlog miniaudio sounddevice
- name: Install packages
shell: bash
run: |
sudo apt-get update
sudo apt-get install octave octave-common octave-signal sox python3 python3-pip portaudio19-dev python3-pyaudio
pip3 install psutil crcengine ujson pyserial numpy structlog miniaudio sounddevice pytest
- name: Build codec2
shell: bash
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2 && git checkout dr-tnc && git pull
mkdir -p build_linux && cd build_linux && cmake .. && make
- name: Build codec2
shell: bash
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2 && git checkout master # This should be pinned to a release
mkdir -p build_linux && cd build_linux && cmake .. && make
- name: run ctests
shell: bash
working-directory: ${{github.workspace}}
run: |
mkdir build && cd build
cmake -DCODEC2_BUILD_DIR=$GITHUB_WORKSPACE/codec2/build_linux ..
ctest --output-on-failure
- name: run ctests
shell: bash
working-directory: ${{github.workspace}}
run: |
mkdir build && cd build
cmake -DCODEC2_BUILD_DIR=$GITHUB_WORKSPACE/codec2/build_linux ..
ctest --output-on-failure

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ tnc/codec2
**/Testing
package-lock.json
.DS_Store

View file

@ -24,16 +24,80 @@ set(TESTFRAMES 3)
add_test(NAME audio_buffer
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
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;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 t48_8_short.py")
python3 test_resample_48_8.py")
set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
add_test(NAME tnc_state_machine
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tnc_states.py")
set_tests_properties(tnc_state_machine PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME tnc_irs_iss
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tnc.py")
set_tests_properties(tnc_irs_iss PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME helper_routines
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_helpers.py")
set_tests_properties(helper_routines PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME py_highsnr_stdio_P_P_multi
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_P_multi.py")
set_tests_properties(py_highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
add_test(NAME py_highsnr_stdio_P_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_P_datacx.py")
set_tests_properties(py_highsnr_stdio_P_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME py_highsnr_stdio_P_C_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_C_datacx.py")
set_tests_properties(py_highsnr_stdio_P_C_datacx PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
add_test(NAME py_highsnr_stdio_C_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
export TESTFRAMES=${TESTFRAMES};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_C_P_datacx.py")
set_tests_properties(py_highsnr_stdio_C_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
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;
@ -59,7 +123,7 @@ add_test(NAME highsnr_stdio_P_P_single
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
python3 test_rx.py --debug --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
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;
@ -68,7 +132,6 @@ add_test(NAME highsnr_stdio_P_P_multi
python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20")
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 as we don't have a virtual sound card
if(NOT DEFINED ENV{GITHUB_RUN_ID})
@ -127,7 +190,7 @@ add_test(NAME highsnr_virtual5_P_P_multi_callback_outside
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")
# ARQ test short
add_test(NAME highsnr_ARQ_short
@ -135,11 +198,11 @@ add_test(NAME highsnr_ARQ_short
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_arq_short.py")
set_tests_properties(highsnr_ARQ_short PROPERTIES PASS_REGULAR_EXPRESSION "ARQ | TX | DATA TRANSMITTED!")
endif()

View file

@ -30,4 +30,3 @@ Download the latest developer release from the releases section, unpack it and j
## Installation
Please check the [wiki](https://wiki.freedata.app) for installation instructions

View file

@ -1,6 +1,6 @@
{
"name": "FreeDATA",
"version": "0.4.0-alpha.8",
"version": "0.4.0-alpha.9",
"description": "FreeDATA ",
"main": "main.js",
"scripts": {

View file

@ -511,8 +511,9 @@ update_chat = function(obj) {
`;
var controlarea_receive = '';
}
} catch {
} catch (err) {
console.log("error with database parsing...")
console.log(err)
}
// CALLSIGN LIST
if (!(document.getElementById('chat-' + dxcallsign + '-list'))) {

View file

@ -1,4 +1,52 @@
# Unit Test Menu
The following `CTest` tests cover some TNC functionality and the interface to codec2:
1. Name: `audio_buffer`
Tests the thread safety of the audio buffer routines.
1. Name: `resampler`
Tests FreeDATA audio resampling from 48KHz to 8KHz.
1. Name: `tnc_state_machine`
Tests TNC transitions between states.
1. Name: `helper_routines`
Tests various helper routines.
1. Name: `py_highsnr_stdio_P_P_multi`
Tests a high signal-to-noise ratio (good quality) audio path using multiple codecs. (Pure python.)
1. Name: `py_highsnr_stdio_P_P_datacx`
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
1. Name: `py_highsnr_stdio_P_C_datacx`
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
1. Name: `py_highsnr_stdio_C_P_datacx`
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
1. Name: `highsnr_stdio_P_C_single`
Tests compatibility with FreeDATA's transmit and freedv's raw data receive.
1. Name: `highsnr_stdio_C_P_single`
Tests compatibility with freedv's raw data transmit and FreeDATA's receive.
1. Name: `highsnr_stdio_P_P_single`
Tests a high signal-to-noise ratio audio path using multiple codecs. (Requires POSIX system.)
1. Name: `highsnr_stdio_P_P_multi`
Tests a high signal-to-noise ratio audio path using multiple codecs. (Requires POSIX system.)
The following tests can not currently be run with GitHub's pipeline as they require the ALSA dummy device
kernel module to be installed. They also do not perform reliably. These tests are slowly being
replaced with equivalent pipeline-compatible tests.
1. Name: `highsnr_virtual1_P_P_single_alsa`
Tests a high signal-to-noise ratio audio path using a single codec directly over an ALSA dummy device.
1. Name: `highsnr_virtual2_P_P_single`
Tests a high signal-to-noise ratio audio path using a single codec over an ALSA dummy device.
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
1. Name: `highsnr_virtual3_P_P_multi`
Tests a high signal-to-noise ratio audio path using multiple codecs over an ALSA dummy device.
1. Name: `highsnr_virtual4_P_P_single_callback`
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
1. Name: `highsnr_virtual4_P_P_single_callback_outside`
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
1. Name: `highsnr_virtual5_P_P_multi_callback`
1. Name: `highsnr_virtual5_P_P_multi_callback_outside`
1. Name: `highsnr_ARQ_short`
**Not functional**, it is an obsolete or not yet completed test.
# Instructions
1. Install:
@ -58,7 +106,7 @@ The virtual audio devices are great for testing, but they are also a little bit
1. Create virtual audio devices. Note: This command needs to be run again after every reboot
```
sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2
sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2
```
1. Check if devices have been created
@ -81,7 +129,7 @@ The virtual audio devices are great for testing, but they are also a little bit
Sub-Geräte: 1/1
Sub-Gerät #0: subdevice #0
```
1. Determine the audio device number you would like to use:
```
python3 test_rx.py --list

View file

@ -1,113 +1,117 @@
#!/usr/bin/env python3
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import ctypes
from ctypes import *
import pathlib
import pyaudio
import time
import threading
import argparse
import sys
import ctypes
import pathlib
import threading
import time
#--------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
parser.add_argument('--bursts', dest="N_BURSTS", default=0, type=int)
parser.add_argument('--frames', dest="N_FRAMES_PER_BURST", default=0, type=int)
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=0, type=int)
parser.add_argument('--txmode', dest="FREEDV_TX_MODE", default=0, type=int)
parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int)
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
import pyaudio
args = parser.parse_args()
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
parser.add_argument("--bursts", dest="N_BURSTS", default=0, type=int)
parser.add_argument("--frames", dest="N_FRAMES_PER_BURST", default=0, type=int)
parser.add_argument("--delay", dest="DELAY_BETWEEN_BURSTS", default=0, type=int)
parser.add_argument("--txmode", dest="FREEDV_TX_MODE", default=0, type=int)
parser.add_argument("--rxmode", dest="FREEDV_RX_MODE", default=0, type=int)
parser.add_argument("--audiooutput", dest="AUDIO_OUTPUT", default=0, type=int)
parser.add_argument("--audioinput", dest="AUDIO_INPUT", default=0, type=int)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
args, _ = parser.parse_known_args()
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT
# 1024 good for mode 6
AUDIO_FRAMES_PER_BUFFER = 2048
AUDIO_FRAMES_PER_BUFFER = 2048
MODEM_SAMPLE_RATE = 8000
FREEDV_TX_MODE = args.FREEDV_TX_MODE
FREEDV_RX_MODE = args.FREEDV_RX_MODE
DEBUGGING_MODE = args.DEBUGGING_MODE
#-------------------------------------------- LOAD FREEDV
# -------------------------------------------- LOAD FREEDV
libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so"
c_lib = ctypes.CDLL(libname)
c_lib = ctypes.CDLL(str(libname))
#--------------------------------------------CREATE PYAUDIO INSTANCE
# --------------------------------------------CREATE PYAUDIO INSTANCE
p = pyaudio.PyAudio()
#--------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
#AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
#AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
# --------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
# AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
# AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
AUDIO_SAMPLE_RATE_TX = 8000
AUDIO_SAMPLE_RATE_RX = 8000
#--------------------------------------------OPEN AUDIO CHANNEL TX
AUDIO_SAMPLE_RATE_RX = 8000
# --------------------------------------------OPEN AUDIO CHANNEL TX
stream_tx = p.open(
format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples
output=True,
output_device_index=AUDIO_OUTPUT_DEVICE,
)
stream_rx = p.open(
format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
input=True,
input_device_index=AUDIO_INPUT_DEVICE,
)
stream_tx = p.open(format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples
output=True,
output_device_index=AUDIO_OUTPUT_DEVICE,
)
stream_rx = p.open(format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
input=True,
input_device_index=AUDIO_INPUT_DEVICE,
)
def receive():
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
freedv = c_lib.freedv_open(FREEDV_RX_MODE)
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
payload_per_frame = bytes_per_frame -2
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_per_frame = bytes_per_frame - 2
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(
freedv
) # get n_tx_modem_samples which defines the size of the modulation object # --> *2
bytes_out = ctypes.c_ubyte * bytes_per_frame # bytes_per_frame
bytes_out = bytes_out() # get pointer from bytes_out
bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame
bytes_out = bytes_out() #get pointer from bytes_out
total_n_bytes = 0
rx_total_frames = 0
rx_frames = 0
rx_bursts = 0
receive = True
while receive == True:
while receive:
time.sleep(0.01)
nin = c_lib.freedv_nin(freedv)
nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE))
if DEBUGGING_MODE == True:
nin_converted = int(nin * (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE))
if DEBUGGING_MODE:
print("-----------------------------")
print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]")
data_in = stream_rx.read(nin_converted, exception_on_overflow = False)
data_in = data_in.rstrip(b'\x00')
c_lib.freedv_rawdatarx.argtype = [ctypes.POINTER(ctypes.c_ubyte), bytes_out, data_in] # check if really neccessary
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
data_in = stream_rx.read(nin_converted, exception_on_overflow=False)
data_in = data_in.rstrip(b"\x00")
c_lib.freedv_rawdatarx.argtype = [
ctypes.POINTER(ctypes.c_ubyte),
bytes_out,
data_in,
] # check if really neccessary
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
total_n_bytes = total_n_bytes + nbytes
if DEBUGGING_MODE == True:
if DEBUGGING_MODE:
print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv)))
if nbytes == bytes_per_frame:
rx_total_frames = rx_total_frames + 1
rx_frames = rx_frames + 1
@ -115,84 +119,111 @@ def receive():
if rx_frames == N_FRAMES_PER_BURST:
rx_frames = 0
rx_bursts = rx_bursts + 1
c_lib.freedv_set_sync(freedv,0)
c_lib.freedv_set_sync(freedv, 0)
burst = bytes_out[0]
n_total_burst = bytes_out[1]
frame = bytes_out[2]
n_total_frame = bytes_out[3]
print("RX | PONG | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "]")
print(
"RX | PONG | BURST ["
+ str(burst)
+ "/"
+ str(n_total_burst)
+ "] FRAME ["
+ str(frame)
+ "/"
+ str(n_total_frame)
+ "]"
)
print("-----------------------------------------------------------------")
c_lib.freedv_set_sync(freedv,0)
c_lib.freedv_set_sync(freedv, 0)
if rx_bursts == N_BURSTS:
receive = False
receive = False
RECEIVE = threading.Thread(target=receive, name="RECEIVE THREAD")
RECEIVE.start()
RECEIVE.start()
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
freedv = c_lib.freedv_open(FREEDV_TX_MODE)
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
payload_per_frame = bytes_per_frame -2
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_per_frame = bytes_per_frame - 2
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(
freedv
) # get n_tx_modem_samples which defines the size of the modulation object # --> *2
mod_out = ctypes.c_short * n_tx_modem_samples
mod_out = mod_out()
mod_out_preamble = ctypes.c_short * (1760*2) #1760 for mode 10,11,12 #4000 for mode 9
mod_out_preamble = ctypes.c_short * (
1760 * 2
) # 1760 for mode 10,11,12 #4000 for mode 9
mod_out_preamble = mod_out_preamble()
print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST) )
print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST))
print("-----------------------------------------------------------------")
for i in range(0,N_BURSTS):
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble);
for i in range(N_BURSTS):
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytearray()
txbuffer += bytes(mod_out_preamble)
for n in range(0,N_FRAMES_PER_BURST):
data_out = bytearray()
data_out += bytes([i+1])
data_out += bytes([N_BURSTS])
data_out += bytes([n+1])
data_out += bytes([N_FRAMES_PER_BURST])
buffer = bytearray(payload_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
buffer[:len(data_out)] = data_out # set buffersize to length of data which will be send
for n in range(N_FRAMES_PER_BURST):
data_out = bytearray()
data_out += bytes([i + 1])
data_out += bytes([N_BURSTS])
data_out += bytes([n + 1])
data_out += bytes([N_FRAMES_PER_BURST])
buffer = bytearray(
payload_per_frame
) # use this if CRC16 checksum is required ( DATA1-3)
buffer[
: len(data_out)
] = data_out # set buffersize to length of data which will be send
crc = ctypes.c_ushort(
c_lib.freedv_gen_crc16(bytes(buffer), payload_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
crc = ctypes.c_ushort(c_lib.freedv_gen_crc16(bytes(buffer), payload_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)
c_lib.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and safe it into mod_out pointer
c_lib.freedv_rawdatatx(
freedv, mod_out, data
) # modulate DATA and safe it into mod_out pointer
txbuffer += bytes(mod_out)
print("TX | PING | BURST [" + str(i+1) + "/" + str(N_BURSTS) + "] FRAME [" + str(n+1) + "/" + str(N_FRAMES_PER_BURST) + "]")
print(
"TX | PING | BURST ["
+ str(i + 1)
+ "/"
+ str(N_BURSTS)
+ "] FRAME ["
+ str(n + 1)
+ "/"
+ str(N_FRAMES_PER_BURST)
+ "]"
)
stream_tx.write(bytes(txbuffer))
ACK_TIMEOUT = time.time() + 3
txbuffer = bytearray()
#time.sleep(DELAY_BETWEEN_BURSTS)
# time.sleep(DELAY_BETWEEN_BURSTS)
# WAIT UNTIL WE RECEIVD AN ACK/DATAC0 FRAME
while ACK_TIMEOUT >= time.time():
time.sleep(0.01)
time.sleep(1)
stream_tx.close()

View file

@ -17,17 +17,17 @@ import threading
import sys
import argparse
#--------------------------------------------GET PARAMETER INPUTS
#--------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
parser.add_argument('--bursts', dest="N_BURSTS", default=0, type=int)
parser.add_argument('--frames', dest="N_FRAMES_PER_BURST", default=0, type=int)
parser.add_argument('--txmode', dest="FREEDV_TX_MODE", default=0, type=int)
parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int)
parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int)
parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
args = parser.parse_args()
args, _ = parser.parse_known_args()
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -41,20 +41,20 @@ FREEDV_RX_MODE = args.FREEDV_RX_MODE
DEBUGGING_MODE = args.DEBUGGING_MODE
# 1024 good for mode 6
AUDIO_FRAMES_PER_BUFFER = 2048
AUDIO_FRAMES_PER_BUFFER = 2048
MODEM_SAMPLE_RATE = 8000
#-------------------------------------------- LOAD FREEDV
#-------------------------------------------- LOAD FREEDV
libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so"
c_lib = ctypes.CDLL(libname)
#--------------------------------------------CREATE PYAUDIO INSTANCE
#--------------------------------------------CREATE PYAUDIO INSTANCE
p = pyaudio.PyAudio()
#--------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
#AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
#AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
#AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
AUDIO_SAMPLE_RATE_TX = 8000
AUDIO_SAMPLE_RATE_RX = 8000
AUDIO_SAMPLE_RATE_RX = 8000
#--------------------------------------------OPEN AUDIO CHANNEL RX
stream_tx = p.open(format=pyaudio.paInt16,
@ -62,22 +62,22 @@ stream_tx = p.open(format=pyaudio.paInt16,
rate=AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples
output=True,
output_device_index=AUDIO_OUTPUT_DEVICE,
)
stream_rx = p.open(format=pyaudio.paInt16,
output_device_index=AUDIO_OUTPUT_DEVICE,
)
stream_rx = p.open(format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
input=True,
input_device_index=AUDIO_INPUT_DEVICE,
)
)
# GENERAL PARAMETERS
# GENERAL PARAMETERS
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
def send_pong(burst,n_total_burst,frame,n_total_frame):
data_out = bytearray()
@ -85,47 +85,47 @@ def send_pong(burst,n_total_burst,frame,n_total_frame):
data_out[1:2] = bytes([n_total_burst])
data_out[2:3] = bytes([frame])
data_out[4:5] = bytes([n_total_frame])
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
freedv = c_lib.freedv_open(FREEDV_TX_MODE)
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
payload_per_frame = bytes_per_frame -2
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2
mod_out = ctypes.c_short * n_tx_modem_samples
mod_out = mod_out()
mod_out_preamble = ctypes.c_short * (1760*2) #1760 for mode 10,11,12 #4000 for mode 9
mod_out_preamble = mod_out_preamble()
buffer = bytearray(payload_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
buffer[:len(data_out)] = data_out # set buffersize to length of data which will be send
crc = ctypes.c_ushort(c_lib.freedv_gen_crc16(bytes(buffer), payload_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
buffer += crc # append crc16 to buffer
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble);
txbuffer = bytearray()
txbuffer += bytes(mod_out_preamble)
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
c_lib.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and safe it into mod_out pointer
c_lib.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and safe it into mod_out pointer
txbuffer += bytes(mod_out)
stream_tx.write(bytes(txbuffer))
txbuffer = bytearray()
txbuffer = bytearray()
# DATA CHANNEL INITIALISATION
freedv = c_lib.freedv_open(FREEDV_RX_MODE)
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
n_max_modem_samples = c_lib.freedv_get_n_max_modem_samples(freedv)
n_max_modem_samples = c_lib.freedv_get_n_max_modem_samples(freedv)
bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame
bytes_out = bytes_out() #get pointer from bytes_out
@ -136,31 +136,31 @@ while receive == True:
time.sleep(0.01)
data_in = b''
nin = c_lib.freedv_nin(freedv)
nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE))
if DEBUGGING_MODE == True:
print("-----------------------------")
print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]")
data_in = stream_rx.read(nin_converted, exception_on_overflow = False)
data_in = stream_rx.read(nin_converted, exception_on_overflow = False)
data_in = data_in.rstrip(b'\x00')
c_lib.freedv_rawdatarx.argtype = [ctypes.POINTER(ctypes.c_ubyte), bytes_out, data_in] # check if really neccessary
c_lib.freedv_rawdatarx.argtype = [ctypes.POINTER(ctypes.c_ubyte), bytes_out, data_in] # check if really neccessary
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
if DEBUGGING_MODE == True:
print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv)))
if nbytes == bytes_per_frame:
burst = bytes_out[0]
n_total_burst = bytes_out[1]
frame = bytes_out[2]
n_total_frame = bytes_out[3]
print("RX | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "] >>> SENDING PONG")
TRANSMIT_PONG = threading.Thread(target=send_pong, args=[burst,n_total_burst,frame,n_total_frame], name="SEND PONG")
TRANSMIT_PONG.start()
c_lib.freedv_set_sync(freedv,0)

View file

@ -1,100 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Unit test for FreeDV API resampler functions, from
# codec2/unittest/t48_8_short.c - generate a sine wave at 8 KHz,
# upsample to 48 kHz, add an interferer, then downsample back to 8 kHz
#
# You can listen to the output files with:
#
# aplay -f S16_LE in8.raw
# aplay -r 48000 -f S16_LE out48.raw
# aplay -f S16_LE out8.raw
#
# They should sound like clean sine waves
import ctypes
from ctypes import *
import pathlib
import argparse
import sys
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np
# dig some constants out
FDMDV_OS_48 = codec2.api.FDMDV_OS_48
FDMDV_OS_TAPS_48K = codec2.api.FDMDV_OS_TAPS_48K
FDMDV_OS_TAPS_48_8K = codec2.api.FDMDV_OS_TAPS_48_8K
N8 = int(180) # processing buffer size at 8 kHz
N48 = int(N8*FDMDV_OS_48) # processing buffer size at 48 kHz
MEM8 = FDMDV_OS_TAPS_48_8K # 8kHz signal filter memory
MEM48 = FDMDV_OS_TAPS_48K # 48kHz signal filter memory
FRAMES = int(50) # number of frames to test
FS8 = 8000
FS48 = 48000
AMP = 16000 # sine wave amplitude
FTEST8 = 800 # input test frequency at FS=8kHz
FINTER48 = 10000 # interferer frequency at FS=48kHz
# Due to the design of these resamplers, the processing buffer (at 8kHz)
# must be an integer multiple of oversampling ratio
assert N8 % FDMDV_OS_48 == 0
# time indexes, we advance every frame
t = 0
t1 = 0
# output files to listen to/evaluate result
fin8 = open("in8.raw", mode='wb')
f48 = open("out48.raw", mode='wb')
fout8 = open("out8.raw", mode='wb')
resampler = codec2.resampler()
for f in range(FRAMES):
sine_in8k = (AMP*np.cos(2*np.pi*np.arange(t,t+N8)*FTEST8/FS8)).astype(np.int16)
t += N8
sine_in8k.tofile(fin8)
sine_out48k = resampler.resample8_to_48(sine_in8k)
sine_out48k.tofile(f48)
# add interfering sine wave (down sampling filter should remove)
sine_in48k = (sine_out48k + (AMP/2)*np.cos(2*np.pi*np.arange(t1,t1+N48)*FINTER48/FS48)).astype(np.int16)
t1 += N48
sine_out8k = resampler.resample48_to_8(sine_in48k)
sine_out8k.tofile(fout8)
fin8.close()
f48.close()
fout8.close()
# Automated test evaluation --------------------------------------------
# The input and output signals will not be time aligned due to the filter
# delays, so compare the magnitude spectrum
in8k = np.fromfile("in8.raw", dtype=np.int16)
out8k = np.fromfile("out8.raw", dtype=np.int16)
assert len(in8k) == len(out8k)
n = len(in8k)
h = np.hanning(len(in8k))
S1 = np.abs(np.fft.fft(in8k * h))
S2 = np.abs(np.fft.fft(out8k * h))
error = S1-S2
error_energy = np.dot(error,error)
ratio = error_energy/np.dot(S1,S1)
ratio_dB = 10*np.log10(ratio);
print("ratio_dB: %4.2f" % (ratio_dB));
threshdB = -40
if ratio_dB < threshdB:
print("PASS")
else:
print("FAIL")

View file

@ -7,36 +7,64 @@ Created on Wed Dec 23 07:04:24 2020
"""
import sys
sys.path.insert(0,'..')
sys.path.insert(0,'../tnc')
import data_handler
import argparse
import codec2
import data_handler
import modem
parser = argparse.ArgumentParser(description='ARQ TEST')
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
args = parser.parse_args()
import pytest
import static
bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}'
# start data handler
data_handler.DATA()
# start modem
modem = modem.RF()
mode = codec2.freedv_get_mode(args.FREEDV_MODE)
print(mode)
n_frames_per_burst = args.N_FRAMES_PER_BURST
# enable testmode
data_handler.TESTMODE = True
@pytest.mark.parametrize("freedv_mode", ["datac0", "datac1", "datac3"])
@pytest.mark.parametrize("n_frames_per_burst", [1, 2, 3])
def test_highsnr_arq_short(freedv_mode: str, n_frames_per_burst: int):
t_mode = t_repeats = t_repeat_delay = 0
t_frames = []
# add command to data qeue
data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_FILE', bytes_out, mode, n_frames_per_burst])
def t_tx_dummy(mode, repeats, repeat_delay, frames):
"""Replacement function for transmit"""
print(f"t_tx_dummy: In transmit({mode}, {repeats}, {repeat_delay}, {frames})")
nonlocal t_mode, t_repeats, t_repeat_delay, t_frames
t_mode = mode
t_repeats = repeats
t_repeat_delay = repeat_delay
t_frames = frames[:]
static.TRANSMITTING = False
# Enable testmode
modem.TESTMODE = True
# Set some inner variables so the modules don't throw exceptions.
modem.RXCHANNEL = "/tmp/rxpipe"
modem.TXCHANNEL = "/tmp/txpipe"
data_handler.TESTMODE = True
static.HAMLIB_RADIOCONTROL = "disabled"
# start data handler
data_handler.DATA()
# start modem
t_modem = modem.RF()
# Replace transmit routine with our own, an effective No-Op.
t_modem.transmit = t_tx_dummy
mode = codec2.freedv_get_mode_value_by_name(freedv_mode)
print(mode)
# add command to data qeue
data_handler.DATA_QUEUE_TRANSMIT.put(
["ARQ_FILE", bytes_out, mode, n_frames_per_burst]
)
# This test isn't complete yet, or is obsolete.
assert False
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -3,57 +3,86 @@
#
# tests audio buffer thread safety
# pylint: disable=global-statement, invalid-name
import sys
sys.path.insert(0,'..')
from tnc import codec2
import threading
import numpy as np
from time import sleep
import codec2
import numpy as np
import pytest
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
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)
n_write = 0
def writer():
def t_writer():
"""
Subprocess to handle writes to the NumPY audio "device."
"""
global n_write
print("writer starting")
n = int(0)
n = 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;
for index in range(WRITE_SZ):
buf[index] = 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))
def test_audiobuffer():
"""
Test for the audiobuffer
"""
global running
# Start the writer in a new thread.
writer_thread = threading.Thread(target=t_writer)
writer_thread.start()
n_out = n_read = errors = 0
for _ in range(NTESTS):
while audio_buffer.nbuffer < READ_SZ:
sleep(0.001)
for i in range(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)
print(f"n_write: {n_write} n_read: {n_read} errors: {errors} ")
# Indirectly stop the thread
running = False
sleep(0.1)
assert not writer_thread.is_alive()
assert n_write - n_read < BUFFER_SZ
assert errors == 0
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -6,41 +6,54 @@ 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 ctypes
import sys
import threading
import time
import numpy as np
sys.path.insert(0,'..')
import pyaudio
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")
# --------------------------------------------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()
args, _ = parser.parse_known_args()
if args.LIST:
p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()):
for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
sys.exit()
class Test():
class Test:
def __init__(self):
self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -49,65 +62,89 @@ class Test():
self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
# v-- consider increasing if you get nread_exceptions > 0
self.AUDIO_FRAMES_PER_BUFFER = 2400 * 2
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
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:
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"]:
for dev in range(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
# 0 = RX 1 = TX
self.AUDIO_INPUT_DEVICE = loopback_list[0]
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
)
sys.exit()
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,
)
else:
print("test_callback_multimode_rx: Not written for STDIN usage.")
print("Exiting.")
sys.exit()
# 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)
# open codec2 instance
self.datac0_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.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 = ctypes.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 = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), ctypes.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 = ctypes.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 = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), ctypes.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 = ctypes.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
@ -127,36 +164,38 @@ class Test():
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.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 = 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)
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:
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)
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:
@ -167,10 +206,13 @@ class Test():
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)
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:
@ -181,10 +223,13 @@ class Test():
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)
# 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:
@ -193,39 +238,59 @@ class Test():
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
self.rx_frames_datac3 = 0
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
self.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:
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.datac0_rxstatus = codec2.api.freedv_get_rx_status(
self.datac0_freedv
)
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac0_rxstatus
]
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv)
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus]
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus]
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(
self.datac1_freedv
)
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac1_rxstatus
]
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(
self.datac3_freedv
)
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac3_rxstatus
]
print(
"NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s"
% (
self.datac0_nin,
self.datac0_rxstatus,
self.datac1_nin,
self.datac1_rxstatus,
self.datac3_nin,
self.datac3_rxstatus,
),
file=sys.stderr,
)
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus),
file=sys.stderr)
def run_audio(self):
try:
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)
print(f"pyAudio error: {e}", file=sys.stderr)
while self.receive and time.time() < self.timeout:
time.sleep(1)
@ -233,18 +298,24 @@ class Test():
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)
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()
test.run_audio()

View file

@ -6,41 +6,52 @@ 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 ctypes
import sys
import time
import numpy as np
sys.path.insert(0,'..')
import pyaudio
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")
# --------------------------------------------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()
args, _ = parser.parse_known_args()
if args.LIST:
p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()):
for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
class Test():
class Test:
def __init__(self):
self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -49,63 +60,91 @@ class Test():
self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
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
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:
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)
loopback_list = [
dev
for dev in range(self.p.get_device_count())
if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]
]
if len(loopback_list) >= 2:
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
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
)
sys.exit()
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,
)
else:
print("test_callback_multimode_rx_outside: Not written for STDIN usage.")
print("Exiting.")
sys.exit()
# 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)
# open codec2 instance
self.datac0_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.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 = ctypes.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 = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), ctypes.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 = ctypes.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 = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), ctypes.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 = ctypes.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
@ -125,62 +164,74 @@ class Test():
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.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)
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.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac0_rxstatus
]
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv)
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus]
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus]
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac1_rxstatus
]
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac3_rxstatus
]
print(
"NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s"
% (
self.datac0_nin,
self.datac0_rxstatus,
self.datac1_nin,
self.datac1_rxstatus,
self.datac3_nin,
self.datac3_rxstatus,
),
file=sys.stderr,
)
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus),
file=sys.stderr)
def run_audio(self):
try:
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)
print(f"pyAudio error: {e}", file=sys.stderr)
while self.receive and time.time() < self.timeout:
while self.datac0_buffer.nbuffer >= self.datac0_nin:
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)
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:
@ -192,10 +243,13 @@ class Test():
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)
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:
@ -206,10 +260,14 @@ class Test():
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)
# 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:
@ -218,27 +276,36 @@ class Test():
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
self.rx_frames_datac3 = 0
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
self.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 (
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)
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()
test.run_audio()

View file

@ -6,182 +6,227 @@ 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 ctypes
import queue
import sys
import time
import numpy as np
sys.path.insert(0,'..')
import pyaudio
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")
# --------------------------------------------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()
args, _ = parser.parse_known_args()
if args.LIST:
p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()):
for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
sys.exit()
class Test():
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
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
# v-- consider increasing if you get nread_exceptions > 0
self.AUDIO_FRAMES_PER_BUFFER = 2400
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
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:
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)
loopback_list = [
dev
for dev in range(self.p.get_device_count())
if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]
]
if len(loopback_list) >= 2:
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
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
)
sys.exit()
print(
f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} "
f"DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} "
f"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,
)
else:
print("test_callback_multimode_tx: Not written for STDOUT usage.")
print("Exiting.")
sys.exit()
# 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')
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!'
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:
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)
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]
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)
freedv = ctypes.cast(codec2.api.freedv_open(m), ctypes.c_void_p)
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = ctypes.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 = ctypes.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 = ctypes.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
buffer[: len(self.data_out)] = self.data_out
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16
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
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):
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):
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
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)
print(
f"GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}",
file=sys.stderr,
)
# append postamble to txbuffer
# 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)
samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS)
mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
txbuffer += bytes(mod_out_silence)
# resample up to 48k (resampler works on np.int16)
@ -189,20 +234,23 @@ class Test():
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
# 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)]
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))
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()
test.run_audio()

View file

@ -6,42 +6,56 @@ 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 ctypes
import sys
import time
import numpy as np
sys.path.insert(0,'..')
import pyaudio
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")
# --------------------------------------------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()
args, _ = parser.parse_known_args()
if args.LIST:
p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()):
for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
sys.exit()
class Test():
class Test:
def __init__(self):
self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -51,51 +65,61 @@ class Test():
self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
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
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:
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"]:
for dev in range(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
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
)
sys.exit()
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 = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
# 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.bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
)
self.bytes_out = ctypes.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
@ -104,44 +128,48 @@ class Test():
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.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')
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)
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)
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)
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 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
@ -149,36 +177,41 @@ class Test():
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:
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)
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)
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()
test.run_audio()

View file

@ -6,42 +6,56 @@ 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 ctypes
import sys
import time
import numpy as np
sys.path.insert(0,'..')
import pyaudio
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")
# --------------------------------------------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()
args, _ = parser.parse_known_args()
if args.LIST:
p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()):
for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
sys.exit()
class Test():
class Test:
def __init__(self):
self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -51,51 +65,61 @@ class Test():
self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
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
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:
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"]:
for dev in range(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
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
)
sys.exit()
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 = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
# 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.bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
)
self.bytes_out = ctypes.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
@ -104,55 +128,59 @@ class Test():
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.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')
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)
x = self.resampler.resample48_to_8(x)
self.audio_buffer.push(x)
return (None, pyaudio.paContinue)
def run_audio(self):
try:
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)
print(f"pyAudio error: {e}", file=sys.stderr)
while self.receive and time.time() < self.timeout:
#time.sleep(1)
# 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)
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)
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 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
@ -160,22 +188,28 @@ class Test():
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)
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()
test.run_audio()

View file

@ -6,43 +6,58 @@ 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 ctypes
import queue
import sys
import time
import numpy as np
sys.path.insert(0,'..')
import pyaudio
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")
# --------------------------------------------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()
args, _ = parser.parse_known_args()
if args.LIST:
p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()):
for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
sys.exit()
class Test():
class Test:
def __init__(self):
self.dataqueue = queue.Queue()
@ -50,180 +65,206 @@ class Test():
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
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
# v-- consider increasing if you get nread_exceptions > 0
self.AUDIO_FRAMES_PER_BUFFER = 2400
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
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:
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"]:
for dev in range( 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
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
)
sys.exit()
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 = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
# 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.bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
)
self.bytes_out = ctypes.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')
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!'
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:
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)
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)
# open codec2 instance
freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.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
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)
mod_out = ctypes.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)
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
freedv
)
mod_out_preamble = ctypes.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)
n_tx_postamble_modem_samples = (
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.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
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
# 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
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)
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):
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):
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
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
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)
samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS)
mod_out_silence = ctypes.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)
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
# 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)]
chunk = [
txbuffer_48k[i : i + self.AUDIO_FRAMES_PER_BUFFER * 2]
for i in range( 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))
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()
test.run_audio()

90
test/test_helpers.py Normal file
View file

@ -0,0 +1,90 @@
"""
Tests for the FreeDATA TNC state machine.
"""
import sys
import helpers
import pytest
import static
@pytest.mark.parametrize("callsign", ["AA1AA", "DE2DE", "E4AWQ-4"])
def test_check_callsign(callsign: str):
"""
Execute test to demonstrate how to create and verify callsign checksums.
"""
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
t_callsign_bytes = helpers.callsign_to_bytes(callsign)
t_callsign = helpers.bytes_to_callsign(t_callsign_bytes)
t_callsign_crc = helpers.get_crc_24(t_callsign)
dxcallsign_bytes = helpers.callsign_to_bytes("ZZ9ZZA-0")
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
assert helpers.check_callsign(t_callsign, t_callsign_crc)[0] is True
assert helpers.check_callsign(t_callsign, dxcallsign_crc)[0] is False
@pytest.mark.parametrize("callsign", ["AA1AA-2", "DE2DE-0", "E4AWQ-4"])
def test_callsign_to_bytes(callsign: str):
"""
Execute test to demonsrate symmetry when converting callsigns to and from byte arrays.
"""
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
t_callsign_crc = helpers.get_crc_24(bytes(callsign, "UTF-8"))
t_callsign_bytes = helpers.callsign_to_bytes(callsign)
t_callsign = helpers.bytes_to_callsign(t_callsign_bytes)
assert helpers.check_callsign(t_callsign, t_callsign_crc)[0] is True
assert helpers.check_callsign(t_callsign, t_callsign_crc)[1] == bytes(
callsign, "UTF-8"
)
@pytest.mark.parametrize("callsign", ["AA1AA-2", "DE2DE-0", "e4awq-4"])
def test_encode_callsign(callsign: str):
"""
Execute test to demonsrate symmetry when encoding and decoding callsigns.
"""
callenc = helpers.encode_call(callsign)
calldec = helpers.decode_call(callenc)
assert callsign.upper() != calldec
@pytest.mark.parametrize("gridsq", ["EM98dc", "DE01GG", "EF42sW"])
def test_encode_grid(gridsq: str):
"""
Execute test to demonsrate symmetry when encoding and decoding grid squares.
"""
gridenc = helpers.encode_grid(gridsq)
griddec = helpers.decode_grid(gridenc)
assert gridsq.upper() == griddec
@pytest.mark.parametrize("gridsq", ["SM98dc", "DE01GZ", "EV42sY"])
@pytest.mark.xfail(reason="Invalid gridsquare provided")
def test_invalid_grid(gridsq: str):
"""
Execute test to demonsrate symmetry when encoding and decoding grid squares.
"""
gridenc = helpers.encode_grid(gridsq)
griddec = helpers.decode_grid(gridenc)
assert gridsq.upper() != griddec
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,123 @@
"""
Tests a high signal-to-noise ratio path with codec2 data formats using codec2 to transmit.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
@pytest.mark.parametrize("testframes", [TESTFRAMES, 2, 1])
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
def test_HighSNR_P_P_DATACx(
bursts: int, frames_per_burst: int, testframes: int, mode: str
):
"""
Test a high signal-to-noise ratio path with DATAC0.
:param bursts: Number of bursts
:type bursts: str
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: str
:param testframes: Number of test frames to transmit
:type testframes: str
"""
tx_side = "freedv_data_raw_tx"
# Facilitate running from main directory as well as inside test/
rx_side = "test_rx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", rx_side)):
rx_side = os.path.join("test", rx_side)
os.environ["PYTHONPATH"] += ":."
with subprocess.Popen(
args=[
tx_side,
mode,
"--testframes",
f"{testframes}",
"--bursts",
f"{bursts}",
"--framesperburst",
f"{frames_per_burst}",
"/dev/zero",
"-",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"sox",
"-t",
".s16",
"-r",
"8000",
"-c",
"1",
"-",
"-t",
".s16",
"-r",
"48000",
"-c",
"1",
"-",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as sox_filter:
with subprocess.Popen(
args=[
"python3",
rx_side,
"--mode",
mode,
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
],
stdin=sox_filter.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
assert receive.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in receive.stdout.readlines()
if "RECEIVED " in str(line, "UTF-8")
]
)
assert f"RECEIVED BURSTS: {bursts}" in lastline
assert f"RECEIVED FRAMES: {frames_per_burst * bursts}" in lastline
assert "RX_ERRORS: 0" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,126 @@
"""
Tests a high signal-to-noise ratio path with codec2 data formats using codec2 to receive.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
def test_HighSNR_P_P_DATACx(bursts: int, frames_per_burst: int, mode: str):
"""
Test a high signal-to-noise ratio path with DATAC0.
:param bursts: Number of bursts
:type bursts: str
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: str
:param testframes: Number of test frames to transmit
:type testframes: str
"""
# Facilitate running from main directory as well as inside test/
tx_side = "test_tx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
tx_side = os.path.join("test", tx_side)
os.environ["PYTHONPATH"] += ":."
rx_side = "freedv_data_raw_rx"
with subprocess.Popen(
args=[
"python3",
tx_side,
"--mode",
mode,
"--delay",
"500",
"--framesperburst",
f"{frames_per_burst}",
"--bursts",
f"{bursts}",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"sox",
"-t",
".s16",
"-r",
"48000",
"-c",
"1",
"-",
"-t",
".s16",
"-r",
"8000",
"-c",
"1",
"-",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as sox_filter:
with subprocess.Popen(
args=[
rx_side,
mode,
"-",
"-",
"--framesperburst",
str(frames_per_burst),
],
stdin=sox_filter.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
with subprocess.Popen(
args=[
"hexdump",
"-C",
],
stdin=receive.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as hexdump:
assert hexdump.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in hexdump.stdout.readlines()
if "HELLO" in str(line, "UTF-8")
]
)
assert "HELLO WORLD!" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,97 @@
"""
Tests a high signal-to-noise ratio path with codec2 data formats.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
def test_HighSNR_P_P_DATACx(bursts: int, frames_per_burst: int, mode: str):
"""
Test a high signal-to-noise ratio path with Codec2 modes.
:param bursts: Number of bursts
:type bursts: str
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: str
"""
# Facilitate running from main directory as well as inside test/
tx_side = "test_tx.py"
rx_side = "test_rx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
tx_side = os.path.join("test", tx_side)
rx_side = os.path.join("test", rx_side)
os.environ["PYTHONPATH"] += ":."
with subprocess.Popen(
args=[
"python3",
tx_side,
"--mode",
mode,
"--delay",
"500",
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"python3",
rx_side,
"--mode",
mode,
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
"--timeout",
"20",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
assert receive.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in receive.stdout.readlines()
if "RECEIVED " in str(line, "UTF-8")
]
)
assert f"RECEIVED BURSTS: {bursts}" in lastline
assert f"RECEIVED FRAMES: {frames_per_burst * bursts}" in lastline
assert "RX_ERRORS: 0" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,92 @@
"""
Tests a high signal-to-noise ratio path with multiple codec2 data formats.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
def test_HighSNR_P_P_Multi(bursts: int, frames_per_burst: int):
"""
Test a high signal-to-noise ratio path with DATAC0, DATAC1 and DATAC3.
:param bursts: Number of bursts
:type bursts: int
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: int
"""
# Facilitate running from main directory as well as inside test/
tx_side = "test_multimode_tx.py"
rx_side = "test_multimode_rx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
tx_side = os.path.join("test", tx_side)
rx_side = os.path.join("test", rx_side)
os.environ["PYTHONPATH"] += ":."
with subprocess.Popen(
args=[
"python3",
tx_side,
"--delay",
"500",
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"python3",
rx_side,
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
"--timeout",
"20",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
assert receive.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in receive.stdout.readlines()
if "DATAC" in str(line, "UTF-8")
]
)
assert f"DATAC0: {bursts}/{frames_per_burst * bursts}" in lastline
assert f"DATAC1: {bursts}/{frames_per_burst * bursts}" in lastline
assert f"DATAC3: {bursts}/{frames_per_burst * bursts}" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

160
test/test_modem.py Normal file
View file

@ -0,0 +1,160 @@
"""
Tests for the FreeDATA modem.
"""
import multiprocessing
import sys
import time
import pytest
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import helpers
import modem
import static
def print_frame(data: bytearray):
"""
Pretty-print the provided frame.
:param data: Frame to be output
:type data: bytearray
"""
print(f"Type : {int(data[0])}")
print(f"DXCRC : {bytes(data[1:4])}")
print(f"CallCRC: {bytes(data[4:7])}")
print(f"Call : {helpers.bytes_to_callsign(data[7:13])}")
def t_create_frame(frame_type: int, mycall: str, dxcall: str) -> bytearray:
"""
Generate the requested frame.
:param frame_type: The numerical type of the desired frame.
:type frame_type: int
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
mycallsign_crc = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
frame = bytearray(14)
frame[:1] = bytes([frame_type])
frame[1:4] = dxcallsign_crc
frame[4:7] = mycallsign_crc
frame[7:13] = mycallsign_bytes
return frame
def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
"""
Generate the session_close frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(223, mycall, dxcall)
def t_create_start_session(mycall: str, dxcall: str) -> bytearray:
"""
Generate the create_session frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(221, mycall, dxcall)
def t_tsh_dummy():
"""Replacement function for transmit"""
print("In t_tsh_dummy")
def t_modem():
"""
Execute test to validate that receiving a session open frame sets the correct machine
state.
"""
t_mode = t_repeats = t_repeat_delay = 0
t_frames = []
# enable testmode
modem.TESTMODE = True
modem.RXCHANNEL = "/tmp/hfchannel1"
modem.TXCHANNEL = "/tmp/hfchannel2"
static.HAMLIB_RADIOCONTROL = "disabled"
def t_tx_dummy(mode, repeats, repeat_delay, frames):
"""Replacement function for transmit"""
print(f"t_tx_dummy: In transmit({mode}, {repeats}, {repeat_delay}, {frames})")
nonlocal t_mode, t_repeats, t_repeat_delay, t_frames
t_mode = mode
t_repeats = repeats
t_repeat_delay = repeat_delay
t_frames = frames[:]
static.TRANSMITTING = False
# Create the modem
local_modem = modem.RF()
# Replace transmit routine with our own, an effective No-Op.
local_modem.transmit = t_tx_dummy
txbuffer = t_create_start_session("AA9AA", "DC2EJ")
# Start the transmission
static.TRANSMITTING = True
modem.MODEM_TRANSMIT_QUEUE.put([14, 5, 250, txbuffer])
while static.TRANSMITTING:
time.sleep(0.1)
# Check that the contents were transferred correctly.
assert t_mode == 14
assert t_repeats == 5
assert t_repeat_delay == 250
assert t_frames == txbuffer
def test_modem_queue():
proc = multiprocessing.Process(target=t_modem, args=())
# print("Starting threads.")
proc.start()
time.sleep(0.5)
# print("Terminating threads.")
proc.terminate()
proc.join()
# print(f"\n{proc.exitcode=}")
assert proc.exitcode == 0
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -1,236 +1,242 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pyaudio
import audioop
import time
import argparse
import sys
import ctypes
from ctypes import *
import pathlib
sys.path.insert(0,'..')
from tnc import codec2
import sys
import time
from typing import List
import numpy as np
import pyaudio
#--------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
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")
sys.path.insert(0, "..")
from tnc import codec2
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()
def test_mm_rx():
# AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400 * 2
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
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
DEBUGGING_MODE = args.DEBUGGING_MODE
TIMEOUT = args.TIMEOUT
# SET COUNTERS
rx_bursts_datac = [0, 0, 0]
rx_frames_datac = [0, 0, 0]
rx_total_frames_datac = [0, 0, 0]
# AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400*2
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
# time meassurement
time_end_datac = [0.0, 0.0, 0.0]
time_needed_datac = [0.0, 0.0, 0.0]
time_start_datac = [0.0, 0.0, 0.0]
# SET COUNTERS
rx_total_frames_datac0 = 0
rx_frames_datac0 = 0
rx_bursts_datac0 = 0
datac_buffer: List[codec2.audio_buffer] = []
datac_bytes_out: List[ctypes.Array] = []
datac_bytes_per_frame = []
datac_freedv: List[ctypes.c_void_p] = []
rx_total_frames_datac1 = 0
rx_frames_datac1 = 0
rx_bursts_datac1 = 0
args = parse_arguments()
rx_total_frames_datac3 = 0
rx_frames_datac3 = 0
rx_bursts_datac3 = 0
if args.LIST:
p_audio = pyaudio.PyAudio()
for dev in range(p_audio.get_device_count()):
print("audiodev: ", dev, p_audio.get_device_info_by_index(dev)["name"])
sys.exit()
# time meassurement
time_start_datac0 = 0
time_end_datac0 = 0
time_start_datac1 = 0
time_end_datac1 = 0
time_start_datac3 = 0
time_end_datac3 = 0
time_needed_datac0 = 0
time_needed_datac1 = 0
time_needed_datac3 = 0
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
DEBUGGING_MODE = args.DEBUGGING_MODE
MAX_TIME = args.TIMEOUT
# open codec2 instance
datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8)
datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST)
datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
# open codec2 instances
for idx in range(3):
datac_freedv.append(
ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
)
)
datac_bytes_per_frame.append(
int(codec2.api.freedv_get_bits_per_modem_frame(datac_freedv[idx]) / 8)
)
datac_bytes_out.append(ctypes.create_string_buffer(datac_bytes_per_frame[idx]))
codec2.api.freedv_set_frames_per_burst(datac_freedv[idx], N_FRAMES_PER_BURST)
datac_buffer.append(codec2.audio_buffer(2 * AUDIO_FRAMES_PER_BUFFER))
datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8)
datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST)
datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
resampler = codec2.resampler()
datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p)
datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8)
datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST)
datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
# check if we want to use an audio device then do an pyaudio init
if AUDIO_INPUT_DEVICE != -1:
p_audio = pyaudio.PyAudio()
# auto search for loopback devices
if AUDIO_INPUT_DEVICE == -2:
loopback_list = [
dev
for dev in range(p_audio.get_device_count())
if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"]
]
resampler = codec2.resampler()
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:
sys.exit()
# 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)
print(
f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE} "
f"DEVICE: {p_audio.get_device_info_by_index(AUDIO_INPUT_DEVICE)['name']} "
f"AUDIO SAMPLE RATE: {AUDIO_SAMPLE_RATE_RX}",
file=sys.stderr,
)
stream_rx = p_audio.open(
format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
input=True,
input_device_index=AUDIO_INPUT_DEVICE,
)
timeout = time.time() + MAX_TIME
print(time.time(), MAX_TIME, timeout)
receive = True
nread_exceptions = 0
# initial nin values
datac_nin = [0, 0, 0]
for idx in range(3):
datac_nin[idx] = codec2.api.freedv_nin(datac_freedv[idx])
def print_stats(time_datac0, time_datac1, time_datac3):
if not DEBUGGING_MODE:
return
time_datac = [time_datac0, time_datac1, time_datac3]
datac_rxstatus = ["", "", ""]
for idx in range(3):
datac_rxstatus[idx] = codec2.api.rx_sync_flags_to_text[
codec2.api.freedv_get_rx_status(datac_freedv[idx])
]
text_out = ""
for idx in range(3):
text_out += f"NIN{idx}: {datac_nin[idx]:5d} "
text_out += f"RX_STATUS{idx}: {datac_rxstatus[idx]:4s} "
text_out += f"TIME: {round(time_datac[idx], 4):.4f} | "
text_out = text_out.rstrip(" ").rstrip("|").rstrip(" ")
print(text_out, file=sys.stderr)
while receive and time.time() < timeout:
if AUDIO_INPUT_DEVICE != -1:
try:
data_in48k = stream_rx.read(
AUDIO_FRAMES_PER_BUFFER, exception_on_overflow=True
)
except OSError as err:
print(err, file=sys.stderr)
if "Input overflowed" in str(err):
nread_exceptions += 1
if "Stream closed" in str(err):
print("Ending....")
receive = False
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,
input_device_index=AUDIO_INPUT_DEVICE
)
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2)
timeout = time.time() + TIMEOUT
print(time.time(),TIMEOUT, timeout)
receive = True
nread_exceptions = 0
# insert samples in buffer
audio_buffer = np.frombuffer(data_in48k, dtype=np.int16)
if len(audio_buffer) != AUDIO_FRAMES_PER_BUFFER:
print("len(x)", len(audio_buffer))
receive = False
audio_buffer = resampler.resample48_to_8(audio_buffer)
# initial nin values
datac0_nin = codec2.api.freedv_nin(datac0_freedv)
datac1_nin = codec2.api.freedv_nin(datac1_freedv)
datac3_nin = codec2.api.freedv_nin(datac3_freedv)
for idx in range(3):
datac_buffer[idx].push(audio_buffer)
while datac_buffer[idx].nbuffer >= datac_nin[idx]:
# demodulate audio
time_start_datac[idx] = time.time()
nbytes = codec2.api.freedv_rawdatarx(
datac_freedv[idx],
datac_bytes_out[idx],
datac_buffer[idx].buffer.ctypes,
)
time_end_datac[idx] = time.time()
datac_buffer[idx].pop(datac_nin[idx])
datac_nin[idx] = codec2.api.freedv_nin(datac_freedv[idx])
if nbytes == datac_bytes_per_frame[idx]:
rx_total_frames_datac[idx] += 1
rx_frames_datac[idx] += 1
def print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3):
if DEBUGGING_MODE:
datac0_rxstatus = codec2.api.freedv_get_rx_status(datac0_freedv)
datac0_rxstatus = codec2.api.rx_sync_flags_to_text[datac0_rxstatus]
if rx_frames_datac[idx] == N_FRAMES_PER_BURST:
rx_frames_datac[idx] = 0
rx_bursts_datac[idx] += 1
time_needed_datac[idx] = time_end_datac[idx] - time_start_datac[idx]
print_stats(
time_needed_datac[0], time_needed_datac[1], time_needed_datac[2]
)
datac1_rxstatus = codec2.api.freedv_get_rx_status(datac1_freedv)
datac1_rxstatus = codec2.api.rx_sync_flags_to_text[datac1_rxstatus]
datac3_rxstatus = codec2.api.freedv_get_rx_status(datac3_freedv)
datac3_rxstatus = codec2.api.rx_sync_flags_to_text[datac3_rxstatus]
if (
rx_bursts_datac[0] == N_BURSTS
and rx_bursts_datac[1] == N_BURSTS
and rx_bursts_datac[2] == N_BURSTS
):
receive = False
print("NIN0: %5d RX_STATUS0: %4s TIME: %4s | NIN1: %5d RX_STATUS1: %4s TIME: %4s | NIN3: %5d RX_STATUS3: %4s TIME: %4s" % \
(datac0_nin, datac0_rxstatus, round(time_needed_datac0, 4), datac1_nin, datac1_rxstatus, round(time_needed_datac1, 4) ,datac3_nin, datac3_rxstatus, round(time_needed_datac3, 4)),
file=sys.stderr)
if nread_exceptions:
print(
f"nread_exceptions {nread_exceptions:d} - receive audio lost! "
"Consider increasing Pyaudio frames_per_buffer...",
file=sys.stderr,
)
# INFO IF WE REACHED TIMEOUT
if time.time() > timeout:
print("TIMEOUT REACHED", file=sys.stderr)
while receive and time.time() < timeout:
if AUDIO_INPUT_DEVICE != -1:
try:
data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True)
except OSError as err:
print(err, file=sys.stderr)
if str(err).find("Input overflowed") != -1:
nread_exceptions += 1
if str(err).find("Stream closed") != -1:
print("Ending....")
receive = False
else:
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2)
# insert samples in buffer
x = np.frombuffer(data_in48k, dtype=np.int16)
if len(x) != AUDIO_FRAMES_PER_BUFFER:
print("len(x)",len(x))
receive = False
x = resampler.resample48_to_8(x)
datac0_buffer.push(x)
datac1_buffer.push(x)
datac3_buffer.push(x)
print_something = False
while datac0_buffer.nbuffer >= datac0_nin:
# demodulate audio
time_start_datac0 = time.time()
nbytes = codec2.api.freedv_rawdatarx(datac0_freedv, datac0_bytes_out, datac0_buffer.buffer.ctypes)
time_end_datac0 = time.time()
datac0_buffer.pop(datac0_nin)
datac0_nin = codec2.api.freedv_nin(datac0_freedv)
if nbytes == datac0_bytes_per_frame:
rx_total_frames_datac0 = rx_total_frames_datac0 + 1
rx_frames_datac0 = rx_frames_datac0 + 1
print(
f"DATAC0: {rx_bursts_datac[0]}/{rx_total_frames_datac[0]} "
f"DATAC1: {rx_bursts_datac[1]}/{rx_total_frames_datac[1]} "
f"DATAC3: {rx_bursts_datac[2]}/{rx_total_frames_datac[2]}",
file=sys.stderr,
)
if rx_frames_datac0 == N_FRAMES_PER_BURST:
rx_frames_datac0 = 0
rx_bursts_datac0 = rx_bursts_datac0 + 1
time_needed_datac0 = time_end_datac0 - time_start_datac0
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
while datac1_buffer.nbuffer >= datac1_nin:
# demodulate audio
time_start_datac1 = time.time()
nbytes = codec2.api.freedv_rawdatarx(datac1_freedv, datac1_bytes_out, datac1_buffer.buffer.ctypes)
time_end_datac1 = time.time()
datac1_buffer.pop(datac1_nin)
datac1_nin = codec2.api.freedv_nin(datac1_freedv)
if nbytes == datac1_bytes_per_frame:
rx_total_frames_datac1 = rx_total_frames_datac1 + 1
rx_frames_datac1 = rx_frames_datac1 + 1
if rx_frames_datac1 == N_FRAMES_PER_BURST:
rx_frames_datac1 = 0
rx_bursts_datac1 = rx_bursts_datac1 + 1
time_needed_datac1 = time_end_datac1 - time_start_datac1
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
while datac3_buffer.nbuffer >= datac3_nin:
# demodulate audio
time_start_datac3 = time.time()
nbytes = codec2.api.freedv_rawdatarx(datac3_freedv, datac3_bytes_out, datac3_buffer.buffer.ctypes)
time_end_datac3 = time.time()
datac3_buffer.pop(datac3_nin)
datac3_nin = codec2.api.freedv_nin(datac3_freedv)
if nbytes == datac3_bytes_per_frame:
rx_total_frames_datac3 = rx_total_frames_datac3 + 1
rx_frames_datac3 = rx_frames_datac3 + 1
if rx_frames_datac3 == N_FRAMES_PER_BURST:
rx_frames_datac3 = 0
rx_bursts_datac3 = rx_bursts_datac3 + 1
time_needed_datac3 = time_end_datac3 - time_start_datac3
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
if rx_bursts_datac0 == N_BURSTS and rx_bursts_datac1 == N_BURSTS and rx_bursts_datac3 == N_BURSTS:
receive = False
if nread_exceptions:
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
nread_exceptions, file=sys.stderr)
# INFO IF WE REACHED TIMEOUT
if time.time() > timeout:
print(f"TIMEOUT REACHED", file=sys.stderr)
if AUDIO_INPUT_DEVICE != -1:
stream_rx.close()
p_audio.terminate()
def parse_arguments():
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
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_known_args()
return args
print(f"DATAC0: {rx_bursts_datac0}/{rx_total_frames_datac0} DATAC1: {rx_bursts_datac1}/{rx_total_frames_datac1} DATAC3: {rx_bursts_datac3}/{rx_total_frames_datac3}", file=sys.stderr)
if AUDIO_INPUT_DEVICE != -1:
stream_rx.close()
p.terminate()
if __name__ == "__main__":
test_mm_rx()

View file

@ -2,150 +2,187 @@
# -*- coding: utf-8 -*-
import ctypes
from ctypes import *
import pathlib
import pyaudio
import time
import threading
import audioop
import argparse
import ctypes
import sys
sys.path.insert(0,'..')
import time
import numpy as np
import pyaudio
sys.path.insert(0, "..")
from tnc import codec2
import numpy as np
# GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='FreeDATA 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")
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
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
# AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
AUDIO_SAMPLE_RATE_TX = 48000
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
def test_mm_tx():
# AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
AUDIO_SAMPLE_RATE_TX = 48000
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
if AUDIO_OUTPUT_DEVICE != -1:
p = pyaudio.PyAudio()
# auto search for loopback devices
if AUDIO_OUTPUT_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_OUTPUT_DEVICE = loopback_list[1] #0 = RX 1 = TX
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else:
quit()
# pyaudio init
stream_tx = p.open(format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples
output=True,
output_device_index=AUDIO_OUTPUT_DEVICE,
)
args = parse_arguments()
resampler = codec2.resampler()
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)
if args.LIST:
p_audio = pyaudio.PyAudio()
for dev in range(p_audio.get_device_count()):
print("audiodev: ", dev, p_audio.get_device_info_by_index(dev)["name"])
sys.exit()
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
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
# data binary string
data_out = b'HELLO WORLD!'
buffer = bytearray(payload_per_frame)
# set buffersize to length of data which will be send
buffer[:len(data_out)] = data_out
resampler = codec2.resampler()
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,N_BURSTS+1):
# Data binary string
data_out = b"HELLO WORLD!"
# write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytes(mod_out_preamble)
modes = [
codec2.api.FREEDV_MODE_DATAC0,
codec2.api.FREEDV_MODE_DATAC1,
codec2.api.FREEDV_MODE_DATAC3,
]
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
for n in range(1,N_FRAMES_PER_BURST+1):
if AUDIO_OUTPUT_DEVICE != -1:
p_audio = pyaudio.PyAudio()
# Auto search for loopback devices
if AUDIO_OUTPUT_DEVICE == -2:
loopback_list = [
dev
for dev in range(p_audio.get_device_count())
if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"]
]
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
if len(loopback_list) >= 2:
AUDIO_OUTPUT_DEVICE = loopback_list[1] # 0 = RX 1 = TX
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else:
sys.exit()
txbuffer += bytes(mod_out)
print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{N_FRAMES_PER_BURST}", file=sys.stderr)
# pyaudio init
stream_tx = p_audio.open(
format=pyaudio.paInt16,
channels=1,
rate=AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples
output=True,
output_device_index=AUDIO_OUTPUT_DEVICE,
)
# append postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
txbuffer += bytes(mod_out_postamble)
for mode in modes:
freedv = ctypes.cast(codec2.api.freedv_open(mode), ctypes.c_void_p)
# append a delay between bursts as audio silence
samples_delay = int(MODEM_SAMPLE_RATE*DELAY_BETWEEN_BURSTS)
mod_out_silence = create_string_buffer(samples_delay*2)
txbuffer += bytes(mod_out_silence)
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = ctypes.create_string_buffer(2 * n_tx_modem_samples)
# resample up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = resampler.resample8_to_48(x)
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
freedv
)
mod_out_preamble = ctypes.create_string_buffer(2 * n_tx_preamble_modem_samples)
# check if we want to use an audio device or stdout
if AUDIO_OUTPUT_DEVICE != -1:
stream_tx.write(txbuffer_48k.tobytes())
else:
# this test needs a lot of time, so we are having a look at times...
starttime = time.time()
# print data to terminal for piping the output to other programs
sys.stdout.buffer.write(txbuffer_48k)
sys.stdout.flush()
n_tx_postamble_modem_samples = (
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
2 * n_tx_postamble_modem_samples
)
# and at least print the needed time to see which time we needed
timeneeded = time.time()-starttime
#print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr)
# and at last check if we had an opened pyaudio instance and close it
if AUDIO_OUTPUT_DEVICE != -1:
time.sleep(stream_tx.get_output_latency())
stream_tx.stop_stream()
stream_tx.close()
p.terminate()
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(data_out)] = data_out
# Generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)
)
# 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 brst in range(1, 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 frm in range(1, N_FRAMES_PER_BURST + 1):
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
# Modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out)
print(
f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{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(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS)
mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
txbuffer += bytes(mod_out_silence)
# Resample up to 48k (resampler works on np.int16)
audio_buffer = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = resampler.resample8_to_48(audio_buffer)
# Check if we want to use an audio device or stdout
if AUDIO_OUTPUT_DEVICE != -1:
stream_tx.write(txbuffer_48k.tobytes())
else:
# This test needs a lot of time, so we are having a look at times...
starttime = time.time()
# Print data to terminal for piping the output to other programs
sys.stdout.buffer.write(txbuffer_48k)
sys.stdout.flush()
# and at least print the needed time to see which time we needed
timeneeded = time.time() - starttime
# print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr)
# and at last check if we had an opened pyaudio instance and close it
if AUDIO_OUTPUT_DEVICE != -1:
time.sleep(stream_tx.get_output_latency())
stream_tx.stop_stream()
stream_tx.close()
p_audio.terminate()
def parse_arguments():
# GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="FreeDATA 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",
)
args, _ = parser.parse_known_args()
return args
if __name__ == "__main__":
test_mm_tx()

View file

@ -3,37 +3,46 @@
#
# Throw away test program to help understand the care and feeding of PyAudio
import pyaudio
import numpy as np
import pyaudio
CHUNK = 1024
FS48 = 48000
FS48 = 48000
FTEST = 800
AMP = 16000
AMP = 16000
# 1. play sine wave out of default sound device
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16,
channels=1,
rate=FS48,
frames_per_buffer=CHUNK,
output=True
)
def test_pa():
# 1. play sine wave out of default sound device
f48 = open("out48.raw", mode='wb')
t = 0;
for f in range(50):
sine_48k = (AMP*np.cos(2*np.pi*np.arange(t,t+CHUNK)*FTEST/FS48)).astype(np.int16)
t += CHUNK
sine_48k.tofile(f48)
stream.write(sine_48k.tobytes())
sil_48k = np.zeros(CHUNK, dtype=np.int16)
for f in range(50):
sil_48k.tofile(f48)
stream.write(sil_48k)
stream.stop_stream()
stream.close()
p.terminate()
f48.close()
p_audio = pyaudio.PyAudio()
stream = p_audio.open(
format=pyaudio.paInt16,
channels=1,
rate=FS48,
frames_per_buffer=CHUNK,
output=True,
)
with open("out48.raw", mode="wb") as f48:
temp = 0
for _ in range(50):
sine_48k = (
AMP * np.cos(2 * np.pi * np.arange(temp, temp + CHUNK) * FTEST / FS48)
).astype(np.int16)
temp += CHUNK
sine_48k.tofile(f48)
stream.write(sine_48k.tobytes())
sil_48k = np.zeros(CHUNK, dtype=np.int16)
for _ in range(50):
sil_48k.tofile(f48)
stream.write(sil_48k)
stream.stop_stream()
stream.close()
p_audio.terminate()
if __name__ == "__main__":
test_pa()

120
test/test_resample_48_8.py Normal file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Unit test for FreeDV API resampler functions, from
# codec2/unittest/t48_8_short.c - generate a sine wave at 8 KHz,
# upsample to 48 kHz, add an interferer, then downsample back to 8 kHz
#
# You can listen to the output files with:
#
# aplay -f S16_LE in8.raw
# aplay -r 48000 -f S16_LE out48.raw
# aplay -f S16_LE out8.raw
#
# They should sound like clean sine waves
# pylint: disable=global-statement, invalid-name, unused-import
import os
import sys
import codec2
import numpy as np
import pytest
# dig some constants out
FDMDV_OS_48 = codec2.api.FDMDV_OS_48
FDMDV_OS_TAPS_48K = codec2.api.FDMDV_OS_TAPS_48K
FDMDV_OS_TAPS_48_8K = codec2.api.FDMDV_OS_TAPS_48_8K
N8 = 180 # processing buffer size at 8 kHz
N48 = N8 * FDMDV_OS_48 # processing buffer size at 48 kHz
MEM8 = FDMDV_OS_TAPS_48_8K # 8kHz signal filter memory
MEM48 = FDMDV_OS_TAPS_48K # 48kHz signal filter memory
FRAMES = 50 # number of frames to test
FS8 = 8000
FS48 = 48000
AMP = 16000 # sine wave amplitude
FTEST8 = 800 # input test frequency at FS=8kHz
FINTER48 = 10000 # interferer frequency at FS=48kHz
# Due to the design of these resamplers, the processing buffer (at 8kHz)
# must be an integer multiple of oversampling ratio
assert N8 % FDMDV_OS_48 == 0
def test_resampler():
"""
Test for the codec2 audio resampling routine
"""
# time indexes, we advance every frame
t = 0
t1 = 0
# output files to listen to/evaluate result
with open("in8.raw", mode="wb") as fin8:
with open("out48.raw", mode="wb") as f48:
with open("out8.raw", mode="wb") as fout8:
resampler = codec2.resampler()
# Generate FRAMES of a sine wave
for _ in range(FRAMES):
# Primary sine wave, which the down-sampling filter should retain.
sine_in8k = (
AMP * np.cos(2 * np.pi * np.arange(t, t + N8) * FTEST8 / FS8)
).astype(np.int16)
t += N8
sine_in8k.tofile(fin8)
sine_out48k = resampler.resample8_to_48(sine_in8k)
sine_out48k.tofile(f48)
# Add an interfering sine wave, which the down-sampling filter should (mostly) remove
sine_in48k = (
sine_out48k
+ (AMP / 2)
* np.cos(2 * np.pi * np.arange(t1, t1 + N48) * FINTER48 / FS48)
).astype(np.int16)
t1 += N48
sine_out8k = resampler.resample48_to_8(sine_in48k)
sine_out8k.tofile(fout8)
# os.unlink("out48.raw")
# Automated test evaluation --------------------------------------------
# The input and output signals will not be time aligned due to the filter
# delays, so compare the magnitude spectrum
# Read the raw audio files
in8k = np.fromfile("in8.raw", dtype=np.int16)
out8k = np.fromfile("out8.raw", dtype=np.int16)
assert len(in8k) == len(out8k)
# os.unlink("in8.raw")
# os.unlink("out8.raw")
# Apply hanning filter to raw input data samples
h = np.hanning(len(in8k))
S1 = np.abs(np.fft.fft(in8k * h))
S2 = np.abs(np.fft.fft(out8k * h))
# Calculate the ratio between signal and noise (error energy).
error = S1 - S2
error_energy = np.dot(error, error)
ratio = error_energy / np.dot(S1, S1)
ratio_dB = 10 * np.log10(ratio)
# Establish -40.0 as the noise ratio ceiling
threshdB = -40.0
print(f"ratio_dB: {ratio_dB:4.2}" % (ratio_dB))
assert ratio_dB < threshdB
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("PASS")
else:
print("FAIL")

View file

@ -6,201 +6,242 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS
"""
import ctypes
from ctypes import *
import pathlib
import sounddevice as sd
import sys
import logging
import time
import threading
import sys
import argparse
import ctypes
import sys
import time
import numpy as np
sys.path.insert(0,'..')
import sounddevice as sd
sys.path.insert(0, "..")
from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
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")
def test_rx():
args = parse_arguments()
args = parser.parse_args()
if args.LIST:
if args.LIST:
devices = sd.query_devices(device=None, kind=None)
index = 0
for device in devices:
print(f"{index} {device['name']}")
index += 1
sd._terminate()
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
# check if we want to use an audio device then do an pyaudio init
if AUDIO_INPUT_DEVICE != -1:
# auto search for loopback devices
if AUDIO_INPUT_DEVICE == -2:
loopback_list = []
devices = sd.query_devices(device=None, kind=None)
index = 0
for device in devices:
if 'Loopback: PCM' in device['name']:
print(index)
loopback_list.append(index)
print(f"{index} {device['name']}")
index += 1
if len(loopback_list) >= 1:
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else:
print("not enough audio loopback devices ready...")
print("you should wait about 30 seconds...")
sd._terminate()
sys.exit()
sd._terminate()
quit()
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr)
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
MAX_TIME = args.TIMEOUT
# audio stream init
stream_rx = sd.RawStream(channels=1, dtype='int16', device=AUDIO_INPUT_DEVICE, samplerate = AUDIO_SAMPLE_RATE_RX, blocksize=4800)
stream_rx.start()
# ----------------------------------------------------------------
# DATA CHANNEL INITIALISATION
# AUDIO PARAMETERS
# v-- consider increasing if you get nread_exceptions > 0
AUDIO_FRAMES_PER_BUFFER = 2400 * 2
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
AUDIO_SAMPLE_RATE_RX = 48000
# open codec2 instance
freedv = cast(codec2.api.freedv_open(MODE), c_void_p)
# make sure our resampler will work
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
# 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)
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()
# time meassurement
time_start = 0
time_end = 0
# Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48.raw
# Corruption of this file is a good way to detect audio card issues
frx = open("rx48.raw", mode='wb')
# initial number of samples we need
nin = codec2.api.freedv_nin(freedv)
while receive and time.time() < timeout:
# check if we want to use an audio device then do an pyaudio init
if AUDIO_INPUT_DEVICE != -1:
try:
#data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True)
data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER)
except OSError as err:
print(err, file=sys.stderr)
#if str(err).find("Input overflowed") != -1:
# nread_exceptions += 1
#if str(err).find("Stream closed") != -1:
# print("Ending...")
# receive = False
else:
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2)
# insert samples in buffer
x = np.frombuffer(data_in48k, dtype=np.int16)
#print(x)
#x = data_in48k
x.tofile(frx)
if len(x) != AUDIO_FRAMES_PER_BUFFER:
receive = False
x = resampler.resample48_to_8(x)
audio_buffer.push(x)
# when we have enough samples call FreeDV Rx
while audio_buffer.nbuffer >= nin:
# start time measurement
time_start = time.time()
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes)
time_end = time.time()
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]
time_needed = time_end - time_start
# auto search for loopback devices
if AUDIO_INPUT_DEVICE == -2:
loopback_list = []
print("nin: %5d rx_status: %4s naudio_buffer: %4d time: %4s" % \
(nin,rx_status,audio_buffer.nbuffer, time_needed), file=sys.stderr)
devices = sd.query_devices(device=None, kind=None)
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
for index, device in enumerate(devices):
if "Loopback: PCM" in device["name"]:
print(index)
loopback_list.append(index)
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()
# and at last check if we had an opened audio instance and close it
if AUDIO_INPUT_DEVICE != -1:
sd._terminate()
if loopback_list:
# 0 = RX 1 = TX
AUDIO_INPUT_DEVICE = loopback_list[0]
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else:
print("not enough audio loopback devices ready...")
print("you should wait about 30 seconds...")
sd._terminate()
sys.exit()
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr)
# audio stream init
stream_rx = sd.RawStream(
channels=1,
dtype="int16",
device=AUDIO_INPUT_DEVICE,
samplerate=AUDIO_SAMPLE_RATE_RX,
blocksize=4800,
)
stream_rx.start()
# ----------------------------------------------------------------
# DATA CHANNEL INITIALISATION
# open codec2 instance
freedv = ctypes.cast(codec2.api.freedv_open(MODE), ctypes.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 = ctypes.create_string_buffer(bytes_per_frame)
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() + MAX_TIME
receive = True
audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER * 2)
resampler = codec2.resampler()
# time meassurement
time_start = 0
time_end = 0
# Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48.raw
# Corruption of this file is a good way to detect audio card issues
frx = open("rx48.raw", mode="wb")
# initial number of samples we need
nin = codec2.api.freedv_nin(freedv)
while receive and time.time() < timeout:
if AUDIO_INPUT_DEVICE != -1:
try:
# data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True)
data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER)
except OSError as err:
print(err, file=sys.stderr)
# if str(err).find("Input overflowed") != -1:
# nread_exceptions += 1
# if str(err).find("Stream closed") != -1:
# print("Ending...")
# receive = False
else:
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2)
# insert samples in buffer
x = np.frombuffer(data_in48k, dtype=np.int16)
# print(x)
# x = data_in48k
x.tofile(frx)
if len(x) != AUDIO_FRAMES_PER_BUFFER:
receive = False
x = resampler.resample48_to_8(x)
audio_buffer.push(x)
# when we have enough samples call FreeDV Rx
while audio_buffer.nbuffer >= nin:
# start time measurement
time_start = time.time()
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(
freedv, bytes_out, audio_buffer.buffer.ctypes
)
time_end = time.time()
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]
time_needed = time_end - time_start
print(
f"nin: {nin:5d} rx_status: {rx_status:4s} "
f"naudio_buffer: {audio_buffer.nbuffer:4d} time: {time_needed:4f}",
file=sys.stderr,
)
if nbytes:
total_n_bytes += nbytes
if nbytes == bytes_per_frame:
rx_total_frames += 1
rx_frames += 1
if rx_frames == N_FRAMES_PER_BURST:
rx_frames = 0
rx_bursts += 1
if rx_bursts == N_BURSTS:
receive = False
if time.time() >= timeout:
print("TIMEOUT REACHED")
if nread_exceptions:
print(
f"nread_exceptions {nread_exceptions:d} - receive audio lost! "
"Consider increasing Pyaudio frames_per_buffer...",
file=sys.stderr,
)
print(
f"RECEIVED BURSTS: {rx_bursts} "
f"RECEIVED FRAMES: {rx_total_frames} "
f"RX_ERRORS: {rx_errors}",
file=sys.stderr,
)
frx.close()
# and at last check if we had an opened audio instance and close it
if AUDIO_INPUT_DEVICE != -1:
sd._terminate()
def parse_arguments():
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
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_known_args()
return args
if __name__ == "__main__":
test_rx()

54
test/test_tnc.py Executable file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import multiprocessing
import os
import sys
import time
import pytest
# pylint: disable=wrong-import-position
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
sys.path.insert(0, "test")
import test_tnc_IRS as irs
import test_tnc_ISS as iss
# These do not update static.INFO.
# "CONNECT", "SEND_TEST_FRAME"
@pytest.mark.parametrize("command", ["CQ", "PING", "BEACON"])
def test_tnc(command):
iss_proc = multiprocessing.Process(target=iss.t_arq_iss, args=[command])
irs_proc = multiprocessing.Process(target=irs.t_arq_irs, args=[command])
# print("Starting threads.")
iss_proc.start()
irs_proc.start()
time.sleep(12)
# print("Terminating threads.")
irs_proc.terminate()
iss_proc.terminate()
irs_proc.join()
iss_proc.join()
for idx in range(2):
try:
os.unlink(f"/tmp/hfchannel{idx+1}")
except FileNotFoundError as fnfe:
print(f"Unlinking pipe: {fnfe}")
assert iss_proc.exitcode == 0, f"Transmit side failed test. {iss_proc}"
assert irs_proc.exitcode == 0, f"Receive side failed test. {irs_proc}"
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-s", "-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

81
test/test_tnc_IRS.py Normal file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS
"""
import os
import sys
import time
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import data_handler
import helpers
import modem
import static
IRS_original_arq_cleanup = object
MESSAGE: str
def irs_arq_cleanup():
"""Replacement for modem.arq_cleanup to detect when to exit process."""
if "TRANSMISSION;STOPPED" in static.INFO:
print(f"{static.INFO=}")
time.sleep(2)
# sys.exit does not terminate threads.
# pylint: disable=protected-access
if f"{MESSAGE};RECEIVING" not in static.INFO:
print(f"{MESSAGE} was not received.")
os._exit(1)
os._exit(0)
IRS_original_arq_cleanup()
def t_arq_irs(*args):
# pylint: disable=global-statement
global IRS_original_arq_cleanup, MESSAGE
MESSAGE = args[0]
# enable testmode
data_handler.TESTMODE = True
modem.TESTMODE = True
modem.RXCHANNEL = "/tmp/hfchannel2"
modem.TXCHANNEL = "/tmp/hfchannel1"
static.HAMLIB_RADIOCONTROL = "disabled"
static.RESPOND_TO_CQ = True
mycallsign = bytes("DN2LS-2", "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
static.MYGRID = bytes("AA12aa", "utf-8")
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# start data handler
tnc = data_handler.DATA()
# Inject a way to exit the TNC infinite loop
IRS_original_arq_cleanup = tnc.arq_cleanup
tnc.arq_cleanup = irs_arq_cleanup
# start modem
t_modem = modem.RF()
# Set timeout
timeout = time.time() + 10
while time.time() < timeout:
time.sleep(0.1)
assert not "TIMEOUT!"
if __name__ == "__main__":
print("This cannot be run as an application.")
sys.exit(1)

118
test/test_tnc_ISS.py Normal file
View file

@ -0,0 +1,118 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS
"""
import os
import sys
import time
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import data_handler
import helpers
import modem
import static
ISS_original_arq_cleanup = object
MESSAGE: str
def iss_arq_cleanup():
"""Replacement for modem.arq_cleanup to detect when to exit process."""
if "TRANSMISSION;STOPPED" in static.INFO:
print(f"{static.INFO=}")
time.sleep(1)
# sys.exit does not terminate threads.
# pylint: disable=protected-access
if f"{MESSAGE};SENDING" not in static.INFO:
print(f"{MESSAGE} was not sent.")
os._exit(1)
os._exit(0)
ISS_original_arq_cleanup()
def t_arq_iss(*args):
# pylint: disable=global-statement
global ISS_original_arq_cleanup, MESSAGE
MESSAGE = args[0]
# enable testmode
data_handler.TESTMODE = True
modem.TESTMODE = True
modem.RXCHANNEL = "/tmp/hfchannel1"
modem.TXCHANNEL = "/tmp/hfchannel2"
static.HAMLIB_RADIOCONTROL = "disabled"
mycallsign = bytes("DJ2LS-2", "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
static.MYGRID = bytes("AA12aa", "utf-8")
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
dxcallsign = b"DN2LS-0"
dxcallsign = helpers.callsign_to_bytes(dxcallsign)
dxcallsign = helpers.bytes_to_callsign(dxcallsign)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN)
bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}'
# start data handler
tnc = data_handler.DATA()
# Inject a way to exit the TNC infinite loop
ISS_original_arq_cleanup = tnc.arq_cleanup
tnc.arq_cleanup = iss_arq_cleanup
# start modem
t_modem = modem.RF()
# mode = codec2.freedv_get_mode_value_by_name(FREEDV_MODE)
# n_frames_per_burst = N_FRAMES_PER_BURST
# add command to data qeue
"""
elif data[0] == 'ARQ_RAW':
# [0] ARQ_RAW
# [1] DATA_OUT bytes
# [2] MODE int
# [3] N_FRAMES_PER_BURST int
# [4] self.transmission_uuid str
# [5] mycallsign with ssid
"""
# data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_RAW', bytes_out, 255, n_frames_per_burst, '123', b'DJ2LS-0'])
# for _ in range(4):
if MESSAGE in ["BEACON"]:
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE, 5, True])
elif MESSAGE in ["PING", "CONNECT"]:
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE, dxcallsign])
else:
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE])
time.sleep(1.5)
# for i in range(4):
# data_handler.DATA_QUEUE_TRANSMIT.put(['PING', b'DN2LS-2'])
data_handler.DATA_QUEUE_TRANSMIT.put(["STOP"])
# Set timeout
timeout = time.time() + 10
while time.time() < timeout:
time.sleep(0.1)
assert not "TIMEOUT!"
if __name__ == "__main__":
print("This cannot be run as an application.")
sys.exit(-1)

225
test/test_tnc_states.py Normal file
View file

@ -0,0 +1,225 @@
"""
Tests for the FreeDATA TNC state machine.
"""
import sys
import pytest
# pylint: disable=wrong-import-position
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import data_handler
import helpers
import static
def print_frame(data: bytearray):
"""
Pretty-print the provided frame.
:param data: Frame to be output
:type data: bytearray
"""
print(f"Type : {int(data[0])}")
print(f"DXCRC : {bytes(data[1:4])}")
print(f"CallCRC: {bytes(data[4:7])}")
print(f"Call : {helpers.bytes_to_callsign(data[7:13])}")
def t_create_frame(frame_type: int, mycall: str, dxcall: str) -> bytearray:
"""
Generate the requested frame.
:param frame_type: The numerical type of the desired frame.
:type frame_type: int
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
mycallsign_crc = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
frame = bytearray(14)
frame[:1] = bytes([frame_type])
frame[1:4] = dxcallsign_crc
frame[4:7] = mycallsign_crc
frame[7:13] = mycallsign_bytes
return frame
def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
"""
Generate the session_close frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(223, mycall, dxcall)
def t_create_start_session(mycall: str, dxcall: str) -> bytearray:
"""
Generate the create_session frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(221, mycall, dxcall)
def t_tsh_dummy():
"""Replacement function for transmit_session_heartbeat"""
print("In transmit_session_heartbeat")
@pytest.mark.parametrize("mycall", ["AA1AA-2", "DE2DE-0", "M4AWQ-4"])
@pytest.mark.parametrize("dxcall", ["AA9AA-1", "DE2ED-0", "F6QWE-3"])
def test_valid_disconnect(mycall: str, dxcall: str):
"""
Execute test to validate that receiving a session open frame sets the correct machine
state.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
# Set the SSIDs we'll use for this test.
static.SSID_LIST = [0, 1, 2, 3, 4]
# Setup the static parameters for the connection.
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
static.MYCALLSIGN = mycallsign
static.MYCALLSIGN_CRC = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_24(dxcallsign)
# Create the TNC
tnc = data_handler.DATA()
# Replace the heartbeat transmit routine with our own, a No-Op.
tnc.transmit_session_heartbeat = t_tsh_dummy
# Create packet to be 'received' by this station.
create_frame = t_create_start_session(mycall=dxcall, dxcall=mycall)
print_frame(create_frame)
tnc.received_session_opener(create_frame)
assert static.ARQ_SESSION is True
assert static.TNC_STATE == "BUSY"
assert static.ARQ_SESSION_STATE == "connecting"
# Create packet to be 'received' by this station.
close_frame = t_create_session_close(mycall=dxcall, dxcall=mycall)
print_frame(close_frame)
tnc.received_session_close(close_frame)
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == mycallsign_bytes
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == dxcallsign_bytes
assert static.ARQ_SESSION is False
assert static.TNC_STATE == "IDLE"
assert static.ARQ_SESSION_STATE == "disconnected"
@pytest.mark.parametrize("mycall", ["AA1AA-2", "DE2DE-0", "E4AWQ-4"])
@pytest.mark.parametrize("dxcall", ["AA9AA-1", "DE2ED-0", "F6QWE-3"])
def test_foreign_disconnect(mycall: str, dxcall: str):
"""
Execute test to validate that receiving a session open frame sets the correct machine
state.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
# Setup the static parameters for the connection.
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
static.MYCALLSIGN = mycallsign
static.MYCALLSIGN_CRC = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_24(dxcallsign)
# Create the TNC
tnc = data_handler.DATA()
# Replace the heartbeat transmit routine with a No-Op.
tnc.transmit_session_heartbeat = t_tsh_dummy
# Create frame to be 'received' by this station.
create_frame = t_create_start_session(mycall=dxcall, dxcall=mycall)
print_frame(create_frame)
tnc.received_session_opener(create_frame)
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == mycallsign_bytes
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == dxcallsign_bytes
assert static.ARQ_SESSION is True
assert static.TNC_STATE == "BUSY"
assert static.ARQ_SESSION_STATE == "connecting"
# Set up a frame from a non-associated station.
foreigncall_bytes = helpers.callsign_to_bytes("ZZ0ZZ-0")
foreigncall = helpers.bytes_to_callsign(foreigncall_bytes)
close_frame = t_create_session_close("ZZ0ZZ-0", "ZZ0ZZ-0")
print_frame(close_frame)
assert (
helpers.check_callsign(static.DXCALLSIGN, bytes(close_frame[4:7]))[0] is False
)
assert helpers.check_callsign(foreigncall, bytes(close_frame[4:7]))[0] is True
# Send the non-associated session close frame to the TNC
tnc.received_session_close(close_frame)
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == helpers.callsign_to_bytes(
mycall
)
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == helpers.callsign_to_bytes(
dxcall
)
assert static.ARQ_SESSION is True
assert static.TNC_STATE == "BUSY"
assert static.ARQ_SESSION_STATE == "connecting"
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -2,174 +2,217 @@
# -*- coding: utf-8 -*-
import ctypes
from ctypes import *
import pathlib
import sounddevice as sd
import time
import argparse
import ctypes
import sys
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np
import sounddevice as sd
# GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
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,
help="delay between bursts in ms")
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 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")
sys.path.insert(0, "..")
from tnc import codec2
args = parser.parse_args()
def test_tx():
args = parse_arguments()
if args.LIST:
devices = sd.query_devices(device=None, kind=None)
index = 0
for device in devices:
print(f"{index} {device['name']}")
index += 1
sd._terminate()
quit()
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
# AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
AUDIO_SAMPLE_RATE_TX = 48000
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
# check if we want to use an audio device then do an pyaudio init
if AUDIO_OUTPUT_DEVICE != -1:
# auto search for loopback devices
if AUDIO_OUTPUT_DEVICE == -2:
loopback_list = []
if args.LIST:
devices = sd.query_devices(device=None, kind=None)
index = 0
for device in devices:
if 'Loopback: PCM' in device['name']:
print(index)
loopback_list.append(index)
index += 1
if len(loopback_list) >= 1:
AUDIO_OUTPUT_DEVICE = loopback_list[len(loopback_list)-1] #0 = RX 1 = TX
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else:
print("not enough audio loopback devices ready...")
print("you should wait about 30 seconds...")
sd._terminate()
quit()
print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr)
# audio stream init
stream_tx = sd.RawStream(channels=1, dtype='int16', device=(0, AUDIO_OUTPUT_DEVICE), samplerate = AUDIO_SAMPLE_RATE_TX, blocksize=4800)
resampler = codec2.resampler()
for index, device in enumerate(devices):
print(f"{index} {device['name']}")
sd._terminate()
sys.exit()
# data binary string
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!'
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
# ----------------------------------------------------------------
# AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
AUDIO_SAMPLE_RATE_TX = 48000
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
# 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
# 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(data_out)] = 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: {N_BURSTS} TOTAL FRAMES_PER_BURST: {N_FRAMES_PER_BURST}", file=sys.stderr)
for i in range(1,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,N_FRAMES_PER_BURST+1):
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
txbuffer += bytes(mod_out)
print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{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(MODEM_SAMPLE_RATE*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: {DELAY_BETWEEN_BURSTS}", file=sys.stderr)
# resample up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = resampler.resample8_to_48(x)
# check if we want to use an audio device or stdout
# check if we want to use an audio device then do an pyaudio init
if AUDIO_OUTPUT_DEVICE != -1:
stream_tx.start()
stream_tx.write(txbuffer_48k)
# auto search for loopback devices
if AUDIO_OUTPUT_DEVICE == -2:
loopback_list = []
devices = sd.query_devices(device=None, kind=None)
for index, device in enumerate(devices):
if "Loopback: PCM" in device["name"]:
print(index)
loopback_list.append(index)
if loopback_list:
# 0 = RX 1 = TX
AUDIO_OUTPUT_DEVICE = loopback_list[-1]
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else:
print("not enough audio loopback devices ready...")
print("you should wait about 30 seconds...")
sd._terminate()
sys.exit()
print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr)
# audio stream init
stream_tx = sd.RawStream(
channels=1,
dtype="int16",
device=(0, AUDIO_OUTPUT_DEVICE),
samplerate=AUDIO_SAMPLE_RATE_TX,
blocksize=4800,
)
resampler = codec2.resampler()
# data binary string
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:
# print data to terminal for piping the output to other programs
sys.stdout.buffer.write(txbuffer_48k)
sys.stdout.flush()
data_out = b"HELLO WORLD!"
# ----------------------------------------------------------------
# Open codec2 instance
freedv = ctypes.cast(codec2.api.freedv_open(MODE), ctypes.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 = ctypes.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 = ctypes.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 = ctypes.create_string_buffer(n_tx_postamble_modem_samples * 2)
# Create buffer for data
# Use this if CRC16 checksum is required (DATA1-3)
buffer = bytearray(payload_bytes_per_frame)
# set buffersize to length of data which will be send
buffer[: len(data_out)] = data_out
# Create CRC for data frame - we are using the CRC function shipped with codec2 to avoid
# CRC algorithm incompatibilities
# generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
)
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: {N_BURSTS} TOTAL FRAMES_PER_BURST: {N_FRAMES_PER_BURST}",
file=sys.stderr,
)
for brst in range(1, 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 frm in range(1, N_FRAMES_PER_BURST + 1):
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
# Modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out)
print(
f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{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(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS)
mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
txbuffer += bytes(mod_out_silence)
# print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {DELAY_BETWEEN_BURSTS}", file=sys.stderr)
# Resample up to 48k (resampler works on np.int16)
np_buffer = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = resampler.resample8_to_48(np_buffer)
# Check if we want to use an audio device or stdout
if AUDIO_OUTPUT_DEVICE != -1:
stream_tx.start()
stream_tx.write(txbuffer_48k)
else:
# Print data to terminal for piping the output to other programs
sys.stdout.buffer.write(txbuffer_48k)
sys.stdout.flush()
# and at last check if we had an opened audio instance and close it
if AUDIO_OUTPUT_DEVICE != -1:
sd._terminate()
# and at last check if we had an opened audio instance and close it
if AUDIO_OUTPUT_DEVICE != -1:
sd._terminate()
def parse_arguments():
# GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
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,
help="delay between bursts in ms",
)
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 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_known_args()
return args
if __name__ == "__main__":
test_tx()

View file

@ -191,7 +191,7 @@ class DAEMON:
options.append('--radiocontrol')
options.append(data[13])
if data[13] != 'rigctld':
if data[13] == 'rigctld':
options.append('--rigctld_ip')
options.append(data[14])

File diff suppressed because it is too large Load diff

View file

@ -647,8 +647,8 @@ class RF:
snr = round(modem_stats_snr, 1)
structlog.get_logger("structlog").info("[MDM] calculate_snr: ", snr=snr)
# print(snr)
static.SNR = np.clip(snr, 0, 255) # limit to max value of 255
# static.SNR = np.clip(snr, 0, 255) # limit to max value of 255
static.SNR = np.clip(snr, -128, 128) # limit to max value of -128/128 as a possible fix of #188
return static.SNR
except Exception as e:
structlog.get_logger("structlog").error(f"[MDM] calculate_snr: Exception: {e}")

View file

@ -51,7 +51,7 @@ class radio():
"""
self.hostname = rigctld_ip
self.port = int(rigctld_port)
if self.connect():
logging.debug("Rigctl intialized")
return True