mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
Merge pull request #92 from DJ2LS/dr-resampler
PyAudio rx callback and Python<->C Resampler
This commit is contained in:
commit
1f8296fa5f
32 changed files with 1762 additions and 340 deletions
|
@ -22,81 +22,111 @@ set(FRAMESPERBURST 3)
|
||||||
set(BURSTS 1)
|
set(BURSTS 1)
|
||||||
set(TESTFRAMES 3)
|
set(TESTFRAMES 3)
|
||||||
|
|
||||||
add_test(NAME 000_resampler
|
add_test(NAME audio_buffer
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/000_resampler;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
python3 t48_8_short.py")
|
python3 test_audiobuffer.py")
|
||||||
set_tests_properties(000_resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
|
set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
|
||||||
|
|
||||||
add_test(NAME 001_highsnr_stdio_P_C_SM
|
add_test(NAME resampler
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 t48_8_short.py")
|
||||||
|
set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
|
||||||
|
|
||||||
|
add_test(NAME highsnr_stdio_P_C_single
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
||||||
sox -t .s16 -r 48000 -c 1 - -t .s16 -r 8000 -c 1 - |
|
sox -t .s16 -r 48000 -c 1 - -t .s16 -r 8000 -c 1 - |
|
||||||
freedv_data_raw_rx datac0 - - --framesperburst ${FRAMESPERBURST} | hexdump -C")
|
freedv_data_raw_rx datac0 - - --framesperburst ${FRAMESPERBURST} | hexdump -C")
|
||||||
set_tests_properties(001_highsnr_stdio_P_C_SM PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
|
set_tests_properties(highsnr_stdio_P_C_single PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
|
||||||
|
|
||||||
add_test(NAME 001_highsnr_stdio_C_P_SM
|
add_test(NAME highsnr_stdio_C_P_single
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
freedv_data_raw_tx datac0 --testframes ${TESTFRAMES} --bursts ${BURSTS} --framesperburst ${FRAMESPERBURST} /dev/zero - |
|
freedv_data_raw_tx datac0 --testframes ${TESTFRAMES} --bursts ${BURSTS} --framesperburst ${FRAMESPERBURST} /dev/zero - |
|
||||||
sox -t .s16 -r 8000 -c 1 - -t .s16 -r 48000 -c 1 - |
|
sox -t .s16 -r 8000 -c 1 - -t .s16 -r 48000 -c 1 - |
|
||||||
python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
|
python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
|
||||||
set_tests_properties(001_highsnr_stdio_C_P_SM PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
set_tests_properties(highsnr_stdio_C_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
||||||
|
|
||||||
add_test(NAME 001_highsnr_stdio_P_P_SM
|
add_test(NAME highsnr_stdio_P_P_single
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
||||||
python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
|
python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
|
||||||
set_tests_properties(001_highsnr_stdio_P_P_SM PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
||||||
|
|
||||||
add_test(NAME 001_highsnr_stdio_P_P_MM
|
add_test(NAME highsnr_stdio_P_P_multi
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
python3 test_multimode_tx.py --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
python3 test_multimode_tx.py --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
||||||
python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20")
|
python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20")
|
||||||
set_tests_properties(001_highsnr_stdio_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
|
set_tests_properties(highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
|
||||||
|
|
||||||
|
|
||||||
# These tests can't run on GitHub actions
|
# These tests can't run on GitHub actions as we don't have a virtual sound card
|
||||||
if(NOT DEFINED ENV{GITHUB_RUN_ID})
|
if(NOT DEFINED ENV{GITHUB_RUN_ID})
|
||||||
|
|
||||||
# uses aplay/arecord then pipe to Python
|
# uses aplay/arecord then pipe to Python
|
||||||
add_test(NAME 001_highsnr_virtual1_P_P
|
add_test(NAME highsnr_virtual1_P_P_single_alsa
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
./test_virtual1.sh")
|
./test_virtual1.sh")
|
||||||
set_tests_properties(001_highsnr_virtual1_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0")
|
set_tests_properties(highsnr_virtual1_P_P_single_alsa PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0")
|
||||||
|
|
||||||
# let Python do audio I/O
|
# let Python do audio I/O
|
||||||
add_test(NAME 001_highsnr_virtual2_P_P
|
add_test(NAME highsnr_virtual2_P_P_single
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
./test_virtual2.sh")
|
./test_virtual2.sh")
|
||||||
set_tests_properties(001_highsnr_virtual2_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
|
set_tests_properties(highsnr_virtual2_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
|
||||||
|
|
||||||
# Multimode test with Python I/O
|
# Multimode test with Python I/O
|
||||||
add_test(NAME 001_highsnr_virtual3_P_P_MM
|
add_test(NAME highsnr_virtual3_P_P_multi
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
./test_virtual_mm.sh")
|
./test_virtual_mm.sh")
|
||||||
set_tests_properties(001_highsnr_virtual3_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
|
set_tests_properties(highsnr_virtual3_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
|
||||||
|
|
||||||
# let Python do audio I/O via pyaudio callback mode
|
# let Python do audio I/O via pyaudio callback mode
|
||||||
add_test(NAME 001_highsnr_virtual4_P_P_SM_CB
|
add_test(NAME highsnr_virtual4_P_P_single_callback
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
./test_virtual3.sh")
|
./test_virtual3a.sh")
|
||||||
set_tests_properties(001_highsnr_virtual4_P_P_SM_CB PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
|
set_tests_properties(highsnr_virtual4_P_P_single_callback PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
|
||||||
|
|
||||||
|
# let Python do audio I/O via pyaudio callback mode with code outside of callback
|
||||||
|
add_test(NAME highsnr_virtual4_P_P_single_callback_outside
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
./test_virtual3b.sh")
|
||||||
|
set_tests_properties(highsnr_virtual4_P_P_single_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
|
||||||
|
|
||||||
|
# let Python do audio I/O via pyaudio callback mode with code outside of callback
|
||||||
|
add_test(NAME highsnr_virtual5_P_P_multi_callback
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
./test_virtual4a.sh")
|
||||||
|
set_tests_properties(highsnr_virtual5_P_P_multi_callback PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
|
||||||
|
|
||||||
|
# let Python do audio I/O via pyaudio callback mode with code outside of callback
|
||||||
|
add_test(NAME highsnr_virtual5_P_P_multi_callback_outside
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
./test_virtual4b.sh")
|
||||||
|
set_tests_properties(highsnr_virtual5_P_P_multi_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
|
||||||
|
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
Created on Wed Dec 23 07:04:24 2020
|
|
||||||
|
|
||||||
@author: DJ2LS
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
|
||||||
import numpy as np
|
|
||||||
sys.path.insert(0,'..')
|
|
||||||
import codec2
|
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
|
||||||
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
|
||||||
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
|
|
||||||
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int,
|
|
||||||
help="audio device number to use, use -2 to automatically select a loopback device")
|
|
||||||
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
|
||||||
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.LIST:
|
|
||||||
p = pyaudio.PyAudio()
|
|
||||||
for dev in range(0,p.get_device_count()):
|
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
|
||||||
quit()
|
|
||||||
|
|
||||||
N_BURSTS = args.N_BURSTS
|
|
||||||
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
|
||||||
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
|
||||||
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
|
||||||
DEBUGGING_MODE = args.DEBUGGING_MODE
|
|
||||||
TIMEOUT = args.TIMEOUT
|
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
|
||||||
AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
|
||||||
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
|
||||||
AUDIO_SAMPLE_RATE_RX = 48000
|
|
||||||
|
|
||||||
# make sure our resampler will work
|
|
||||||
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------ PYAUDIO CALLBACK
|
|
||||||
def callback(data_in48k, frame_count, time_info, status):
|
|
||||||
x = np.frombuffer(data_in48k, dtype=np.int16)
|
|
||||||
x.tofile(frx)
|
|
||||||
x = resampler.resample48_to_8(x)
|
|
||||||
audio_buffer.push(x)
|
|
||||||
return (None, pyaudio.paContinue)
|
|
||||||
|
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
|
||||||
if AUDIO_INPUT_DEVICE != -1:
|
|
||||||
p = pyaudio.PyAudio()
|
|
||||||
# auto search for loopback devices
|
|
||||||
if AUDIO_INPUT_DEVICE == -2:
|
|
||||||
loopback_list = []
|
|
||||||
for dev in range(0,p.get_device_count()):
|
|
||||||
if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]:
|
|
||||||
loopback_list.append(dev)
|
|
||||||
if len(loopback_list) >= 2:
|
|
||||||
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
quit()
|
|
||||||
|
|
||||||
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE} DEVICE: {p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['name']} \
|
|
||||||
AUDIO SAMPLE RATE: {AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
|
||||||
|
|
||||||
stream_rx = p.open(format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=AUDIO_SAMPLE_RATE_RX,
|
|
||||||
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
|
|
||||||
input=True,
|
|
||||||
output=False,
|
|
||||||
input_device_index=AUDIO_INPUT_DEVICE,
|
|
||||||
stream_callback=callback
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
print(f"starting pyaudio callback", file=sys.stderr)
|
|
||||||
stream_rx.start_stream()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"pyAudio error: {e}", file=sys.stderr)
|
|
||||||
# ----------------------------------------------------------------
|
|
||||||
|
|
||||||
# DATA CHANNEL INITIALISATION
|
|
||||||
|
|
||||||
# open codec2 instance
|
|
||||||
freedv = cast(codec2.api.freedv_open(MODE), c_void_p)
|
|
||||||
|
|
||||||
# get number of bytes per frame for mode
|
|
||||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|
|
||||||
payload_bytes_per_frame = bytes_per_frame -2
|
|
||||||
|
|
||||||
n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv)
|
|
||||||
bytes_out = create_string_buffer(bytes_per_frame * 2)
|
|
||||||
|
|
||||||
codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST)
|
|
||||||
|
|
||||||
total_n_bytes = 0
|
|
||||||
rx_total_frames = 0
|
|
||||||
rx_frames = 0
|
|
||||||
rx_bursts = 0
|
|
||||||
rx_errors = 0
|
|
||||||
nread_exceptions = 0
|
|
||||||
timeout = time.time() + TIMEOUT
|
|
||||||
receive = True
|
|
||||||
audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER*2)
|
|
||||||
resampler = codec2.resampler()
|
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
|
||||||
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
|
||||||
frx = open("rx48_callback.raw", mode='wb')
|
|
||||||
|
|
||||||
# initial number of samples we need
|
|
||||||
nin = codec2.api.freedv_nin(freedv)
|
|
||||||
while receive and time.time() < timeout:
|
|
||||||
|
|
||||||
# when we have enough samples call FreeDV Rx
|
|
||||||
while audio_buffer.nbuffer >= nin:
|
|
||||||
|
|
||||||
# demodulate audio
|
|
||||||
nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes)
|
|
||||||
audio_buffer.pop(nin)
|
|
||||||
|
|
||||||
# call me on every loop!
|
|
||||||
nin = codec2.api.freedv_nin(freedv)
|
|
||||||
|
|
||||||
rx_status = codec2.api.freedv_get_rx_status(freedv)
|
|
||||||
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
|
||||||
rx_errors = rx_errors + 1
|
|
||||||
if DEBUGGING_MODE:
|
|
||||||
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
|
||||||
print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \
|
|
||||||
(nin,rx_status,audio_buffer.nbuffer), file=sys.stderr)
|
|
||||||
|
|
||||||
if nbytes:
|
|
||||||
total_n_bytes = total_n_bytes + nbytes
|
|
||||||
|
|
||||||
if nbytes == bytes_per_frame:
|
|
||||||
rx_total_frames = rx_total_frames + 1
|
|
||||||
rx_frames = rx_frames + 1
|
|
||||||
|
|
||||||
if rx_frames == N_FRAMES_PER_BURST:
|
|
||||||
rx_frames = 0
|
|
||||||
rx_bursts = rx_bursts + 1
|
|
||||||
|
|
||||||
if rx_bursts == N_BURSTS:
|
|
||||||
receive = False
|
|
||||||
|
|
||||||
if time.time() >= timeout:
|
|
||||||
print("TIMEOUT REACHED")
|
|
||||||
|
|
||||||
if nread_exceptions:
|
|
||||||
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
|
||||||
nread_exceptions, file=sys.stderr)
|
|
||||||
print(f"RECEIVED BURSTS: {rx_bursts} RECEIVED FRAMES: {rx_total_frames} RX_ERRORS: {rx_errors}", file=sys.stderr)
|
|
||||||
frx.close()
|
|
||||||
|
|
||||||
# cloese pyaudio instance
|
|
||||||
stream_rx.close()
|
|
||||||
p.terminate()
|
|
|
@ -18,7 +18,7 @@ from ctypes import *
|
||||||
import pathlib
|
import pathlib
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'../..')
|
sys.path.insert(0,'..')
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
|
@ -7,8 +7,8 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'../..')
|
sys.path.insert(0,'..')
|
||||||
sys.path.insert(0,'../../tnc')
|
sys.path.insert(0,'../tnc')
|
||||||
import data_handler
|
import data_handler
|
||||||
|
|
||||||
|
|
59
test/test_audiobuffer.py
Normal file
59
test/test_audiobuffer.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# tests audio buffer thread safety
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0,'..')
|
||||||
|
from tnc import codec2
|
||||||
|
import threading
|
||||||
|
import numpy as np
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
BUFFER_SZ = 1024
|
||||||
|
N_MAX = 100 # write a repeating sequence of 0..N_MAX-1
|
||||||
|
WRITE_SZ = 10 # different read and write sized buffers
|
||||||
|
READ_SZ = 8
|
||||||
|
NTESTS = 10000
|
||||||
|
|
||||||
|
running = True
|
||||||
|
audio_buffer = codec2.audio_buffer(BUFFER_SZ)
|
||||||
|
|
||||||
|
n_write = int(0)
|
||||||
|
n_read = int(0)
|
||||||
|
|
||||||
|
def writer():
|
||||||
|
global n_write
|
||||||
|
print("writer starting")
|
||||||
|
n = int(0)
|
||||||
|
buf = np.zeros(WRITE_SZ, dtype=np.int16)
|
||||||
|
while running:
|
||||||
|
nfree = audio_buffer.size - audio_buffer.nbuffer
|
||||||
|
if nfree >= WRITE_SZ:
|
||||||
|
for i in range(0,WRITE_SZ):
|
||||||
|
buf[i] = n;
|
||||||
|
n += 1
|
||||||
|
if n == N_MAX:
|
||||||
|
n = 0
|
||||||
|
n_write += 1
|
||||||
|
audio_buffer.push(buf)
|
||||||
|
|
||||||
|
x = threading.Thread(target=writer)
|
||||||
|
x.start()
|
||||||
|
|
||||||
|
n_out = int(0)
|
||||||
|
errors = int(0)
|
||||||
|
for tests in range(1,NTESTS):
|
||||||
|
while audio_buffer.nbuffer < READ_SZ:
|
||||||
|
sleep(0.001)
|
||||||
|
for i in range(0,READ_SZ):
|
||||||
|
if audio_buffer.buffer[i] != n_out:
|
||||||
|
errors += 1
|
||||||
|
n_out += 1
|
||||||
|
if n_out == N_MAX:
|
||||||
|
n_out = 0
|
||||||
|
n_read += 1
|
||||||
|
audio_buffer.pop(READ_SZ)
|
||||||
|
|
||||||
|
running = False
|
||||||
|
print("n_write: %d n_read: %d errors: %d " % (n_write, n_read, errors))
|
250
test/test_callback_multimode_rx.py
Normal file
250
test/test_callback_multimode_rx.py
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
import pathlib
|
||||||
|
import pyaudio
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
sys.path.insert(0,'..')
|
||||||
|
from tnc import codec2
|
||||||
|
|
||||||
|
#--------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
||||||
|
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
||||||
|
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, help="audio device number to use")
|
||||||
|
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
||||||
|
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
||||||
|
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.LIST:
|
||||||
|
p = pyaudio.PyAudio()
|
||||||
|
for dev in range(0,p.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Test():
|
||||||
|
def __init__(self):
|
||||||
|
self.N_BURSTS = args.N_BURSTS
|
||||||
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
||||||
|
self.DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
|
# AUDIO PARAMETERS
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# make sure our resampler will work
|
||||||
|
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
|
self.p = pyaudio.PyAudio()
|
||||||
|
# auto search for loopback devices
|
||||||
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
|
loopback_list = []
|
||||||
|
for dev in range(0,self.p.get_device_count()):
|
||||||
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
||||||
|
loopback_list.append(dev)
|
||||||
|
if len(loopback_list) >= 2:
|
||||||
|
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
||||||
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
quit()
|
||||||
|
|
||||||
|
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
||||||
|
|
||||||
|
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
||||||
|
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8)
|
||||||
|
self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
|
||||||
|
self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8)
|
||||||
|
self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p)
|
||||||
|
self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8)
|
||||||
|
self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# SET COUNTERS
|
||||||
|
self.rx_total_frames_datac0 = 0
|
||||||
|
self.rx_frames_datac0 = 0
|
||||||
|
self.rx_bursts_datac0 = 0
|
||||||
|
|
||||||
|
self.rx_total_frames_datac1 = 0
|
||||||
|
self.rx_frames_datac1 = 0
|
||||||
|
self.rx_bursts_datac1 = 0
|
||||||
|
|
||||||
|
self.rx_total_frames_datac3 = 0
|
||||||
|
self.rx_frames_datac3 = 0
|
||||||
|
self.rx_bursts_datac3 = 0
|
||||||
|
|
||||||
|
self.rx_errors = 0
|
||||||
|
self.nread_exceptions = 0
|
||||||
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
|
self.receive = True
|
||||||
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
|
self.frx = open("rx48_callback_multimode.raw", mode='wb')
|
||||||
|
|
||||||
|
|
||||||
|
# initial nin values
|
||||||
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
|
|
||||||
|
|
||||||
|
self.LOGGER_THREAD = threading.Thread(target=self.print_stats, name="LOGGER_THREAD")
|
||||||
|
self.LOGGER_THREAD.start()
|
||||||
|
|
||||||
|
|
||||||
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
|
x.tofile(self.frx)
|
||||||
|
x = self.resampler.resample48_to_8(x)
|
||||||
|
|
||||||
|
self.datac0_buffer.push(x)
|
||||||
|
self.datac1_buffer.push(x)
|
||||||
|
self.datac3_buffer.push(x)
|
||||||
|
|
||||||
|
|
||||||
|
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
|
||||||
|
self.datac0_buffer.pop(self.datac0_nin)
|
||||||
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
|
if nbytes == self.datac0_bytes_per_frame:
|
||||||
|
self.rx_total_frames_datac0 = self.rx_total_frames_datac0 + 1
|
||||||
|
self.rx_frames_datac0 = self.rx_frames_datac0 + 1
|
||||||
|
|
||||||
|
if self.rx_frames_datac0 == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames_datac0 = 0
|
||||||
|
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
|
||||||
|
|
||||||
|
|
||||||
|
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
|
||||||
|
self.datac1_buffer.pop(self.datac1_nin)
|
||||||
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
|
if nbytes == self.datac1_bytes_per_frame:
|
||||||
|
self.rx_total_frames_datac1 = self.rx_total_frames_datac1 + 1
|
||||||
|
self.rx_frames_datac1 = self.rx_frames_datac1 + 1
|
||||||
|
|
||||||
|
if self.rx_frames_datac1 == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames_datac1 = 0
|
||||||
|
self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1
|
||||||
|
|
||||||
|
|
||||||
|
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
|
||||||
|
self.datac3_buffer.pop(self.datac3_nin)
|
||||||
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
|
if nbytes == self.datac3_bytes_per_frame:
|
||||||
|
self.rx_total_frames_datac3 = self.rx_total_frames_datac3 + 1
|
||||||
|
self.rx_frames_datac3 = self.rx_frames_datac3 + 1
|
||||||
|
|
||||||
|
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames_datac3 = 0
|
||||||
|
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
|
||||||
|
|
||||||
|
|
||||||
|
if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS:
|
||||||
|
self.receive = False
|
||||||
|
|
||||||
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
|
def print_stats(self):
|
||||||
|
while self.receive:
|
||||||
|
time.sleep(0.01)
|
||||||
|
if self.DEBUGGING_MODE:
|
||||||
|
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv)
|
||||||
|
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus]
|
||||||
|
|
||||||
|
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv)
|
||||||
|
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus]
|
||||||
|
|
||||||
|
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
|
||||||
|
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus]
|
||||||
|
|
||||||
|
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \
|
||||||
|
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus),
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def run_audio(self):
|
||||||
|
try:
|
||||||
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
|
self.stream_rx.start_stream()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
while self.receive and time.time() < self.timeout:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if time.time() >= self.timeout and self.stream_rx.is_active():
|
||||||
|
print("TIMEOUT REACHED")
|
||||||
|
self.receive = False
|
||||||
|
|
||||||
|
if self.nread_exceptions:
|
||||||
|
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
||||||
|
self.nread_exceptions, file=sys.stderr)
|
||||||
|
|
||||||
|
print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr)
|
||||||
|
self.frx.close()
|
||||||
|
|
||||||
|
# cloese pyaudio instance
|
||||||
|
self.stream_rx.close()
|
||||||
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.run_audio()
|
244
test/test_callback_multimode_rx_outside.py
Normal file
244
test/test_callback_multimode_rx_outside.py
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
import pathlib
|
||||||
|
import pyaudio
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
sys.path.insert(0,'..')
|
||||||
|
from tnc import codec2
|
||||||
|
|
||||||
|
#--------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
||||||
|
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
||||||
|
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, help="audio device number to use")
|
||||||
|
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
||||||
|
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
||||||
|
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.LIST:
|
||||||
|
p = pyaudio.PyAudio()
|
||||||
|
for dev in range(0,p.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Test():
|
||||||
|
def __init__(self):
|
||||||
|
self.N_BURSTS = args.N_BURSTS
|
||||||
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
||||||
|
self.DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
|
# AUDIO PARAMETERS
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
|
# make sure our resampler will work
|
||||||
|
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
|
self.p = pyaudio.PyAudio()
|
||||||
|
# auto search for loopback devices
|
||||||
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
|
loopback_list = []
|
||||||
|
for dev in range(0,self.p.get_device_count()):
|
||||||
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
||||||
|
loopback_list.append(dev)
|
||||||
|
if len(loopback_list) >= 2:
|
||||||
|
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
||||||
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
quit()
|
||||||
|
|
||||||
|
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
||||||
|
|
||||||
|
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
||||||
|
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8)
|
||||||
|
self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
|
||||||
|
self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8)
|
||||||
|
self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p)
|
||||||
|
self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8)
|
||||||
|
self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# SET COUNTERS
|
||||||
|
self.rx_total_frames_datac0 = 0
|
||||||
|
self.rx_frames_datac0 = 0
|
||||||
|
self.rx_bursts_datac0 = 0
|
||||||
|
|
||||||
|
self.rx_total_frames_datac1 = 0
|
||||||
|
self.rx_frames_datac1 = 0
|
||||||
|
self.rx_bursts_datac1 = 0
|
||||||
|
|
||||||
|
self.rx_total_frames_datac3 = 0
|
||||||
|
self.rx_frames_datac3 = 0
|
||||||
|
self.rx_bursts_datac3 = 0
|
||||||
|
|
||||||
|
self.rx_errors = 0
|
||||||
|
self.nread_exceptions = 0
|
||||||
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
|
self.receive = True
|
||||||
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
|
self.frx = open("rx48_callback_multimode.raw", mode='wb')
|
||||||
|
|
||||||
|
|
||||||
|
# initial nin values
|
||||||
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
|
|
||||||
|
|
||||||
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
|
x.tofile(self.frx)
|
||||||
|
x = self.resampler.resample48_to_8(x)
|
||||||
|
|
||||||
|
self.datac0_buffer.push(x)
|
||||||
|
self.datac1_buffer.push(x)
|
||||||
|
self.datac3_buffer.push(x)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
|
def print_stats(self):
|
||||||
|
if self.DEBUGGING_MODE:
|
||||||
|
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv)
|
||||||
|
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus]
|
||||||
|
|
||||||
|
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv)
|
||||||
|
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus]
|
||||||
|
|
||||||
|
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
|
||||||
|
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus]
|
||||||
|
|
||||||
|
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \
|
||||||
|
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus),
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def run_audio(self):
|
||||||
|
try:
|
||||||
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
|
self.stream_rx.start_stream()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
while self.receive and time.time() < self.timeout:
|
||||||
|
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
|
||||||
|
self.datac0_buffer.pop(self.datac0_nin)
|
||||||
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
|
if nbytes == self.datac0_bytes_per_frame:
|
||||||
|
self.rx_total_frames_datac0 = self.rx_total_frames_datac0 + 1
|
||||||
|
self.rx_frames_datac0 = self.rx_frames_datac0 + 1
|
||||||
|
|
||||||
|
if self.rx_frames_datac0 == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames_datac0 = 0
|
||||||
|
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
|
||||||
|
self.print_stats()
|
||||||
|
|
||||||
|
|
||||||
|
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
|
||||||
|
self.datac1_buffer.pop(self.datac1_nin)
|
||||||
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
|
if nbytes == self.datac1_bytes_per_frame:
|
||||||
|
self.rx_total_frames_datac1 = self.rx_total_frames_datac1 + 1
|
||||||
|
self.rx_frames_datac1 = self.rx_frames_datac1 + 1
|
||||||
|
|
||||||
|
if self.rx_frames_datac1 == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames_datac1 = 0
|
||||||
|
self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1
|
||||||
|
self.print_stats()
|
||||||
|
|
||||||
|
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
|
||||||
|
self.datac3_buffer.pop(self.datac3_nin)
|
||||||
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
|
if nbytes == self.datac3_bytes_per_frame:
|
||||||
|
self.rx_total_frames_datac3 = self.rx_total_frames_datac3 + 1
|
||||||
|
self.rx_frames_datac3 = self.rx_frames_datac3 + 1
|
||||||
|
|
||||||
|
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames_datac3 = 0
|
||||||
|
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
|
||||||
|
self.print_stats()
|
||||||
|
|
||||||
|
if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS:
|
||||||
|
self.receive = False
|
||||||
|
|
||||||
|
|
||||||
|
if time.time() >= self.timeout:
|
||||||
|
print("TIMEOUT REACHED")
|
||||||
|
|
||||||
|
if self.nread_exceptions:
|
||||||
|
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
||||||
|
self.nread_exceptions, file=sys.stderr)
|
||||||
|
|
||||||
|
print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr)
|
||||||
|
self.frx.close()
|
||||||
|
|
||||||
|
# cloese pyaudio instance
|
||||||
|
self.stream_rx.close()
|
||||||
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.run_audio()
|
208
test/test_callback_multimode_tx.py
Normal file
208
test/test_callback_multimode_tx.py
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
import pathlib
|
||||||
|
import pyaudio
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import queue
|
||||||
|
import numpy as np
|
||||||
|
sys.path.insert(0,'..')
|
||||||
|
from tnc import codec2
|
||||||
|
|
||||||
|
#--------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
||||||
|
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
||||||
|
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
|
||||||
|
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, help="audio output device number to use")
|
||||||
|
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
||||||
|
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.LIST:
|
||||||
|
p = pyaudio.PyAudio()
|
||||||
|
for dev in range(0,p.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Test():
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.dataqueue = queue.Queue()
|
||||||
|
self.N_BURSTS = args.N_BURSTS
|
||||||
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
||||||
|
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
|
||||||
|
|
||||||
|
# AUDIO PARAMETERS
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
|
self.AUDIO_SAMPLE_RATE_TX = 48000
|
||||||
|
|
||||||
|
# make sure our resampler will work
|
||||||
|
assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
|
||||||
|
self.transmit = True
|
||||||
|
|
||||||
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
|
||||||
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
|
if self.AUDIO_OUTPUT_DEVICE != -1:
|
||||||
|
self.p = pyaudio.PyAudio()
|
||||||
|
# auto search for loopback devices
|
||||||
|
if self.AUDIO_OUTPUT_DEVICE == -2:
|
||||||
|
loopback_list = []
|
||||||
|
for dev in range(0,self.p.get_device_count()):
|
||||||
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
||||||
|
loopback_list.append(dev)
|
||||||
|
if len(loopback_list) >= 2:
|
||||||
|
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
||||||
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
quit()
|
||||||
|
|
||||||
|
print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr)
|
||||||
|
|
||||||
|
self.stream_tx = self.p.open(format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_TX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=False,
|
||||||
|
output=True,
|
||||||
|
output_device_index=self.AUDIO_OUTPUT_DEVICE,
|
||||||
|
stream_callback=self.callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
|
self.ftx = open("tx48_callback.raw", mode='wb')
|
||||||
|
|
||||||
|
# data binary string
|
||||||
|
if args.TESTFRAMES:
|
||||||
|
self.data_out = bytearray(14)
|
||||||
|
self.data_out[:1] = bytes([255])
|
||||||
|
self.data_out[1:2] = bytes([1])
|
||||||
|
self.data_out[2:] = b'HELLO WORLD'
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.data_out = b'HELLO WORLD!'
|
||||||
|
|
||||||
|
|
||||||
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
|
data_out48k = self.dataqueue.get()
|
||||||
|
return (data_out48k, pyaudio.paContinue)
|
||||||
|
|
||||||
|
def run_audio(self):
|
||||||
|
try:
|
||||||
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
|
self.stream_tx.start_stream()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
sheeps = 0
|
||||||
|
while self.transmit:
|
||||||
|
time.sleep(1)
|
||||||
|
sheeps = sheeps + 1
|
||||||
|
print(f"counting sheeps...{sheeps}")
|
||||||
|
|
||||||
|
self.ftx.close()
|
||||||
|
|
||||||
|
# close pyaudio instance
|
||||||
|
self.stream_tx.close()
|
||||||
|
self.p.terminate()
|
||||||
|
|
||||||
|
def create_modulation(self):
|
||||||
|
|
||||||
|
modes = [codec2.api.FREEDV_MODE_DATAC0, codec2.api.FREEDV_MODE_DATAC1, codec2.api.FREEDV_MODE_DATAC3]
|
||||||
|
for m in modes:
|
||||||
|
|
||||||
|
freedv = cast(codec2.api.freedv_open(m), c_void_p)
|
||||||
|
|
||||||
|
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||||
|
mod_out = create_string_buffer(2*n_tx_modem_samples)
|
||||||
|
|
||||||
|
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
|
||||||
|
mod_out_preamble = create_string_buffer(2*n_tx_preamble_modem_samples)
|
||||||
|
|
||||||
|
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||||
|
mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples)
|
||||||
|
|
||||||
|
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||||
|
payload_per_frame = bytes_per_frame - 2
|
||||||
|
|
||||||
|
buffer = bytearray(payload_per_frame)
|
||||||
|
# set buffersize to length of data which will be send
|
||||||
|
buffer[:len(self.data_out)] = self.data_out
|
||||||
|
|
||||||
|
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16
|
||||||
|
# convert crc to 2 byte hex string
|
||||||
|
crc = crc.value.to_bytes(2, byteorder='big')
|
||||||
|
buffer += crc # append crc16 to buffer
|
||||||
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
|
||||||
|
for i in range(1,self.N_BURSTS+1):
|
||||||
|
|
||||||
|
# write preamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
|
txbuffer = bytes(mod_out_preamble)
|
||||||
|
|
||||||
|
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
||||||
|
for n in range(1,self.N_FRAMES_PER_BURST+1):
|
||||||
|
|
||||||
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
||||||
|
|
||||||
|
txbuffer += bytes(mod_out)
|
||||||
|
print(f"GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr)
|
||||||
|
|
||||||
|
# append postamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
|
txbuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
|
# append a delay between bursts as audio silence
|
||||||
|
samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS)
|
||||||
|
mod_out_silence = create_string_buffer(samples_delay*2)
|
||||||
|
txbuffer += bytes(mod_out_silence)
|
||||||
|
|
||||||
|
# resample up to 48k (resampler works on np.int16)
|
||||||
|
x = np.frombuffer(txbuffer, dtype=np.int16)
|
||||||
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
||||||
|
|
||||||
|
# split modulated audio to chunks
|
||||||
|
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
||||||
|
txbuffer_48k = bytes(txbuffer_48k)
|
||||||
|
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)]
|
||||||
|
# add modulated chunks to fifo buffer
|
||||||
|
for c in chunk:
|
||||||
|
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
||||||
|
# to prevent the callback from stucking/crashing
|
||||||
|
if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2:
|
||||||
|
c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c))
|
||||||
|
self.dataqueue.put(c)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.create_modulation()
|
||||||
|
test.run_audio()
|
184
test/test_callback_rx.py
Normal file
184
test/test_callback_rx.py
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
import pathlib
|
||||||
|
import pyaudio
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
sys.path.insert(0,'..')
|
||||||
|
from tnc import codec2
|
||||||
|
|
||||||
|
#--------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
||||||
|
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
||||||
|
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
|
||||||
|
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int,
|
||||||
|
help="audio device number to use, use -2 to automatically select a loopback device")
|
||||||
|
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
||||||
|
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.LIST:
|
||||||
|
p = pyaudio.PyAudio()
|
||||||
|
for dev in range(0,p.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Test():
|
||||||
|
def __init__(self):
|
||||||
|
self.N_BURSTS = args.N_BURSTS
|
||||||
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
||||||
|
self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
||||||
|
self.DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
|
# AUDIO PARAMETERS
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
|
# make sure our resampler will work
|
||||||
|
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
|
self.p = pyaudio.PyAudio()
|
||||||
|
# auto search for loopback devices
|
||||||
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
|
loopback_list = []
|
||||||
|
for dev in range(0,self.p.get_device_count()):
|
||||||
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
||||||
|
loopback_list.append(dev)
|
||||||
|
if len(loopback_list) >= 2:
|
||||||
|
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
||||||
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
quit()
|
||||||
|
|
||||||
|
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
||||||
|
|
||||||
|
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
||||||
|
|
||||||
|
# get number of bytes per frame for mode
|
||||||
|
self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8)
|
||||||
|
|
||||||
|
self.bytes_out = create_string_buffer(self.bytes_per_frame)
|
||||||
|
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
|
||||||
|
self.total_n_bytes = 0
|
||||||
|
self.rx_total_frames = 0
|
||||||
|
self.rx_frames = 0
|
||||||
|
self.rx_bursts = 0
|
||||||
|
self.rx_errors = 0
|
||||||
|
self.nread_exceptions = 0
|
||||||
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
|
self.receive = True
|
||||||
|
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2)
|
||||||
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
|
self.frx = open("rx48_callback.raw", mode='wb')
|
||||||
|
|
||||||
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
|
x.tofile(self.frx)
|
||||||
|
x = self.resampler.resample48_to_8(x)
|
||||||
|
self.audio_buffer.push(x)
|
||||||
|
|
||||||
|
|
||||||
|
# when we have enough samples call FreeDV Rx
|
||||||
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
|
while self.audio_buffer.nbuffer >= nin:
|
||||||
|
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes)
|
||||||
|
self.audio_buffer.pop(nin)
|
||||||
|
|
||||||
|
# call me on every loop!
|
||||||
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
|
|
||||||
|
rx_status = codec2.api.freedv_get_rx_status(self.freedv)
|
||||||
|
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
||||||
|
self.rx_errors = self.rx_errors + 1
|
||||||
|
if self.DEBUGGING_MODE:
|
||||||
|
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
||||||
|
print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \
|
||||||
|
(nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr)
|
||||||
|
|
||||||
|
if nbytes:
|
||||||
|
self.total_n_bytes = self.total_n_bytes + nbytes
|
||||||
|
|
||||||
|
if nbytes == self.bytes_per_frame:
|
||||||
|
self.rx_total_frames = self.rx_total_frames + 1
|
||||||
|
self.rx_frames = self.rx_frames + 1
|
||||||
|
|
||||||
|
if self.rx_frames == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames = 0
|
||||||
|
self.rx_bursts = self.rx_bursts + 1
|
||||||
|
|
||||||
|
if self.rx_bursts == self.N_BURSTS:
|
||||||
|
self.receive = False
|
||||||
|
|
||||||
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
|
def run_audio(self):
|
||||||
|
try:
|
||||||
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
|
self.stream_rx.start_stream()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
while self.receive and time.time() < self.timeout:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if time.time() >= self.timeout:
|
||||||
|
print("TIMEOUT REACHED")
|
||||||
|
|
||||||
|
if self.nread_exceptions:
|
||||||
|
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
||||||
|
self.nread_exceptions, file=sys.stderr)
|
||||||
|
print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr)
|
||||||
|
self.frx.close()
|
||||||
|
|
||||||
|
# cloese pyaudio instance
|
||||||
|
self.stream_rx.close()
|
||||||
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.run_audio()
|
181
test/test_callback_rx_outside.py
Normal file
181
test/test_callback_rx_outside.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
import pathlib
|
||||||
|
import pyaudio
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
sys.path.insert(0,'..')
|
||||||
|
from tnc import codec2
|
||||||
|
|
||||||
|
#--------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
||||||
|
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
||||||
|
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
|
||||||
|
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int,
|
||||||
|
help="audio device number to use, use -2 to automatically select a loopback device")
|
||||||
|
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
||||||
|
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.LIST:
|
||||||
|
p = pyaudio.PyAudio()
|
||||||
|
for dev in range(0,p.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Test():
|
||||||
|
def __init__(self):
|
||||||
|
self.N_BURSTS = args.N_BURSTS
|
||||||
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
self.AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
||||||
|
self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
||||||
|
self.DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
|
# AUDIO PARAMETERS
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
|
# make sure our resampler will work
|
||||||
|
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
|
self.p = pyaudio.PyAudio()
|
||||||
|
# auto search for loopback devices
|
||||||
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
|
loopback_list = []
|
||||||
|
for dev in range(0,self.p.get_device_count()):
|
||||||
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
||||||
|
loopback_list.append(dev)
|
||||||
|
if len(loopback_list) >= 2:
|
||||||
|
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
||||||
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
quit()
|
||||||
|
|
||||||
|
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
||||||
|
|
||||||
|
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
||||||
|
|
||||||
|
# get number of bytes per frame for mode
|
||||||
|
self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8)
|
||||||
|
|
||||||
|
self.bytes_out = create_string_buffer(self.bytes_per_frame * 2)
|
||||||
|
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
|
||||||
|
self.total_n_bytes = 0
|
||||||
|
self.rx_total_frames = 0
|
||||||
|
self.rx_frames = 0
|
||||||
|
self.rx_bursts = 0
|
||||||
|
self.rx_errors = 0
|
||||||
|
self.nread_exceptions = 0
|
||||||
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
|
self.receive = True
|
||||||
|
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2)
|
||||||
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
|
self.frx = open("rx48_callback.raw", mode='wb')
|
||||||
|
|
||||||
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
|
x.tofile(self.frx)
|
||||||
|
x = self.resampler.resample48_to_8(x)
|
||||||
|
self.audio_buffer.push(x)
|
||||||
|
|
||||||
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
|
def run_audio(self):
|
||||||
|
try:
|
||||||
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
|
self.stream_rx.start_stream()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
while self.receive and time.time() < self.timeout:
|
||||||
|
#time.sleep(1)
|
||||||
|
# when we have enough samples call FreeDV Rx
|
||||||
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
|
while self.audio_buffer.nbuffer >= nin:
|
||||||
|
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes)
|
||||||
|
self.audio_buffer.pop(nin)
|
||||||
|
|
||||||
|
# call me on every loop!
|
||||||
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
|
|
||||||
|
rx_status = codec2.api.freedv_get_rx_status(self.freedv)
|
||||||
|
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
||||||
|
self.rx_errors = self.rx_errors + 1
|
||||||
|
if self.DEBUGGING_MODE:
|
||||||
|
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
||||||
|
print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \
|
||||||
|
(nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr)
|
||||||
|
|
||||||
|
if nbytes:
|
||||||
|
self.total_n_bytes = self.total_n_bytes + nbytes
|
||||||
|
|
||||||
|
if nbytes == self.bytes_per_frame:
|
||||||
|
self.rx_total_frames = self.rx_total_frames + 1
|
||||||
|
self.rx_frames = self.rx_frames + 1
|
||||||
|
|
||||||
|
if self.rx_frames == self.N_FRAMES_PER_BURST:
|
||||||
|
self.rx_frames = 0
|
||||||
|
self.rx_bursts = self.rx_bursts + 1
|
||||||
|
|
||||||
|
if self.rx_bursts == self.N_BURSTS:
|
||||||
|
self.receive = False
|
||||||
|
if time.time() >= self.timeout:
|
||||||
|
print("TIMEOUT REACHED")
|
||||||
|
|
||||||
|
if self.nread_exceptions:
|
||||||
|
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
||||||
|
self.nread_exceptions, file=sys.stderr)
|
||||||
|
print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr)
|
||||||
|
self.frx.close()
|
||||||
|
|
||||||
|
# cloese pyaudio instance
|
||||||
|
self.stream_rx.close()
|
||||||
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.run_audio()
|
229
test/test_callback_tx.py
Normal file
229
test/test_callback_tx.py
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import *
|
||||||
|
import pathlib
|
||||||
|
import pyaudio
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import queue
|
||||||
|
import numpy as np
|
||||||
|
sys.path.insert(0,'..')
|
||||||
|
from tnc import codec2
|
||||||
|
|
||||||
|
#--------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
||||||
|
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
||||||
|
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
|
||||||
|
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
|
||||||
|
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int,
|
||||||
|
help="audio device number to use, use -2 to automatically select a loopback device")
|
||||||
|
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
||||||
|
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.LIST:
|
||||||
|
p = pyaudio.PyAudio()
|
||||||
|
for dev in range(0,p.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Test():
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.dataqueue = queue.Queue()
|
||||||
|
self.N_BURSTS = args.N_BURSTS
|
||||||
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
||||||
|
self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
||||||
|
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
|
||||||
|
|
||||||
|
# AUDIO PARAMETERS
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
|
self.AUDIO_SAMPLE_RATE_TX = 48000
|
||||||
|
|
||||||
|
# make sure our resampler will work
|
||||||
|
assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
|
||||||
|
self.transmit = True
|
||||||
|
|
||||||
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
|
||||||
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
|
if self.AUDIO_OUTPUT_DEVICE != -1:
|
||||||
|
self.p = pyaudio.PyAudio()
|
||||||
|
# auto search for loopback devices
|
||||||
|
if self.AUDIO_OUTPUT_DEVICE == -2:
|
||||||
|
loopback_list = []
|
||||||
|
for dev in range(0,self.p.get_device_count()):
|
||||||
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
||||||
|
loopback_list.append(dev)
|
||||||
|
if len(loopback_list) >= 2:
|
||||||
|
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
||||||
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
quit()
|
||||||
|
|
||||||
|
print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr)
|
||||||
|
|
||||||
|
self.stream_tx = self.p.open(format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_TX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=False,
|
||||||
|
output=True,
|
||||||
|
output_device_index=self.AUDIO_OUTPUT_DEVICE,
|
||||||
|
stream_callback=self.callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
||||||
|
|
||||||
|
# get number of bytes per frame for mode
|
||||||
|
self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8)
|
||||||
|
|
||||||
|
self.bytes_out = create_string_buffer(self.bytes_per_frame)
|
||||||
|
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST)
|
||||||
|
|
||||||
|
|
||||||
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
|
self.ftx = open("tx48_callback.raw", mode='wb')
|
||||||
|
|
||||||
|
# data binary string
|
||||||
|
if args.TESTFRAMES:
|
||||||
|
self.data_out = bytearray(14)
|
||||||
|
self.data_out[:1] = bytes([255])
|
||||||
|
self.data_out[1:2] = bytes([1])
|
||||||
|
self.data_out[2:] = b'HELLO WORLD'
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.data_out = b'HELLO WORLD!'
|
||||||
|
|
||||||
|
|
||||||
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
|
data_out48k = self.dataqueue.get()
|
||||||
|
return (data_out48k, pyaudio.paContinue)
|
||||||
|
|
||||||
|
def run_audio(self):
|
||||||
|
try:
|
||||||
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
|
self.stream_tx.start_stream()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
sheeps = 0
|
||||||
|
while self.transmit:
|
||||||
|
time.sleep(1)
|
||||||
|
sheeps = sheeps + 1
|
||||||
|
print(f"counting sheeps...{sheeps}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
self.ftx.close()
|
||||||
|
|
||||||
|
# close pyaudio instance
|
||||||
|
self.stream_tx.close()
|
||||||
|
self.p.terminate()
|
||||||
|
|
||||||
|
def create_modulation(self):
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
||||||
|
|
||||||
|
# get number of bytes per frame for mode
|
||||||
|
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|
||||||
|
payload_bytes_per_frame = bytes_per_frame -2
|
||||||
|
|
||||||
|
# init buffer for data
|
||||||
|
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||||
|
mod_out = create_string_buffer(n_tx_modem_samples * 2)
|
||||||
|
|
||||||
|
# init buffer for preample
|
||||||
|
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
|
||||||
|
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2)
|
||||||
|
|
||||||
|
# init buffer for postamble
|
||||||
|
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||||
|
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2)
|
||||||
|
|
||||||
|
# create buffer for data
|
||||||
|
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
||||||
|
buffer[:len(self.data_out)] = self.data_out # set buffersize to length of data which will be send
|
||||||
|
|
||||||
|
# create crc for data frame - we are using the crc function shipped with codec2 to avoid
|
||||||
|
# crc algorithm incompatibilities
|
||||||
|
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16
|
||||||
|
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
||||||
|
buffer += crc # append crc16 to buffer
|
||||||
|
|
||||||
|
print(f"TOTAL BURSTS: {self.N_BURSTS} TOTAL FRAMES_PER_BURST: {self.N_FRAMES_PER_BURST}", file=sys.stderr)
|
||||||
|
|
||||||
|
for i in range(1,self.N_BURSTS+1):
|
||||||
|
|
||||||
|
# write preamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
|
txbuffer = bytes(mod_out_preamble)
|
||||||
|
|
||||||
|
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
||||||
|
for n in range(1,self.N_FRAMES_PER_BURST+1):
|
||||||
|
|
||||||
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
||||||
|
|
||||||
|
txbuffer += bytes(mod_out)
|
||||||
|
|
||||||
|
print(f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr)
|
||||||
|
|
||||||
|
# append postamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
|
txbuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
|
# append a delay between bursts as audio silence
|
||||||
|
samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS)
|
||||||
|
mod_out_silence = create_string_buffer(samples_delay*2)
|
||||||
|
txbuffer += bytes(mod_out_silence)
|
||||||
|
print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {self.DELAY_BETWEEN_BURSTS}", file=sys.stderr)
|
||||||
|
|
||||||
|
# resample up to 48k (resampler works on np.int16)
|
||||||
|
x = np.frombuffer(txbuffer, dtype=np.int16)
|
||||||
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
||||||
|
|
||||||
|
# split modualted audio to chunks
|
||||||
|
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
||||||
|
txbuffer_48k = bytes(txbuffer_48k)
|
||||||
|
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)]
|
||||||
|
# add modulated chunks to fifo buffer
|
||||||
|
for c in chunk:
|
||||||
|
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
||||||
|
# to prevent the callback from stucking/crashing
|
||||||
|
if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2:
|
||||||
|
c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c))
|
||||||
|
self.dataqueue.put(c)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.create_modulation()
|
||||||
|
test.run_audio()
|
|
@ -9,7 +9,7 @@ import sys
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
import pathlib
|
import pathlib
|
||||||
sys.path.insert(0,'../..')
|
sys.path.insert(0,'..')
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -60,22 +60,19 @@ rx_bursts_datac3 = 0
|
||||||
# open codec2 instance
|
# open codec2 instance
|
||||||
datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
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_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8)
|
||||||
datac0_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac0_freedv)
|
datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame)
|
||||||
datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame * 2)
|
|
||||||
codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST)
|
codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST)
|
||||||
datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
|
datac1_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_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8)
|
||||||
datac1_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac1_freedv)
|
datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame)
|
||||||
datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame * 2)
|
|
||||||
codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST)
|
codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST)
|
||||||
datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p)
|
datac3_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_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8)
|
||||||
datac3_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac3_freedv)
|
datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame)
|
||||||
datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame * 2)
|
|
||||||
codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST)
|
codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST)
|
||||||
datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import threading
|
||||||
import audioop
|
import audioop
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'../..')
|
sys.path.insert(0,'..')
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
|
@ -17,7 +17,7 @@ import threading
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'../..')
|
sys.path.insert(0,'..')
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|
||||||
payload_bytes_per_frame = bytes_per_frame -2
|
payload_bytes_per_frame = bytes_per_frame -2
|
||||||
|
|
||||||
n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv)
|
n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv)
|
||||||
bytes_out = create_string_buffer(bytes_per_frame * 2)
|
bytes_out = create_string_buffer(bytes_per_frame)
|
||||||
|
|
||||||
codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST)
|
codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import pyaudio
|
||||||
import time
|
import time
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'../..')
|
sys.path.insert(0,'..')
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', '
|
||||||
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int,
|
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")
|
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('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
||||||
|
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="list audio devices by number and exit")
|
||||||
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -75,7 +77,14 @@ if AUDIO_OUTPUT_DEVICE != -1:
|
||||||
resampler = codec2.resampler()
|
resampler = codec2.resampler()
|
||||||
|
|
||||||
# data binary string
|
# data binary string
|
||||||
data_out = b'HELLO WORLD!'
|
if args.TESTFRAMES:
|
||||||
|
data_out = bytearray(14)
|
||||||
|
data_out[:1] = bytes([255])
|
||||||
|
data_out[1:2] = bytes([1])
|
||||||
|
data_out[2:] = b'HELLO WORLD'
|
||||||
|
|
||||||
|
else:
|
||||||
|
data_out = b'HELLO WORLD!'
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
|
@ -16,7 +16,7 @@ check_alsa_loopback
|
||||||
# make sure all child processes are killed when we exit
|
# make sure all child processes are killed when we exit
|
||||||
trap 'jobs -p | xargs -r kill' EXIT
|
trap 'jobs -p | xargs -r kill' EXIT
|
||||||
|
|
||||||
python3 test_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug &
|
python3 test_callback_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug &
|
||||||
rx_pid=$!
|
rx_pid=$!
|
||||||
sleep 1
|
sleep 1
|
||||||
python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2
|
python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2
|
23
test/test_virtual3a.sh
Executable file
23
test/test_virtual3a.sh
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash -x
|
||||||
|
# Run a test using the virtual sound cards, Python audio I/O
|
||||||
|
|
||||||
|
function check_alsa_loopback {
|
||||||
|
lsmod | grep snd_aloop >> /dev/null
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
echo "ALSA loopback device not present. Please install with:"
|
||||||
|
echo
|
||||||
|
echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_alsa_loopback
|
||||||
|
|
||||||
|
# make sure all child processes are killed when we exit
|
||||||
|
trap 'jobs -p | xargs -r kill' EXIT
|
||||||
|
|
||||||
|
python3 test_callback_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug &
|
||||||
|
rx_pid=$!
|
||||||
|
#sleep 1
|
||||||
|
python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2
|
||||||
|
wait ${rx_pid}
|
23
test/test_virtual3b.sh
Executable file
23
test/test_virtual3b.sh
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash -x
|
||||||
|
# Run a test using the virtual sound cards, Python audio I/O
|
||||||
|
|
||||||
|
function check_alsa_loopback {
|
||||||
|
lsmod | grep snd_aloop >> /dev/null
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
echo "ALSA loopback device not present. Please install with:"
|
||||||
|
echo
|
||||||
|
echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_alsa_loopback
|
||||||
|
|
||||||
|
# make sure all child processes are killed when we exit
|
||||||
|
trap 'jobs -p | xargs -r kill' EXIT
|
||||||
|
|
||||||
|
python3 test_callback_rx_outside.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug &
|
||||||
|
rx_pid=$!
|
||||||
|
#sleep 1
|
||||||
|
python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2
|
||||||
|
wait ${rx_pid}
|
32
test/test_virtual4a.sh
Executable file
32
test/test_virtual4a.sh
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/bash -x
|
||||||
|
# Run a test using the virtual sound cards
|
||||||
|
|
||||||
|
function check_alsa_loopback {
|
||||||
|
lsmod | grep snd_aloop >> /dev/null
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
echo "ALSA loopback device not present. Please install with:"
|
||||||
|
echo
|
||||||
|
echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
myInterruptHandler()
|
||||||
|
{
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_alsa_loopback
|
||||||
|
|
||||||
|
RX_LOG=$(mktemp)
|
||||||
|
|
||||||
|
trap myInterruptHandler SIGINT
|
||||||
|
|
||||||
|
# make sure all child processes are killed when we exit
|
||||||
|
trap 'jobs -p | xargs -r kill' EXIT
|
||||||
|
|
||||||
|
python3 test_callback_multimode_rx.py --timeout 60 --framesperburst 2 --bursts 2 --audiodev -2 --debug &
|
||||||
|
rx_pid=$!
|
||||||
|
sleep 1
|
||||||
|
python3 test_multimode_tx.py --framesperburst 2 --bursts 2 --audiodev -2 --delay 500
|
||||||
|
wait ${rx_pid}
|
32
test/test_virtual4b.sh
Executable file
32
test/test_virtual4b.sh
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/bash -x
|
||||||
|
# Run a test using the virtual sound cards
|
||||||
|
|
||||||
|
function check_alsa_loopback {
|
||||||
|
lsmod | grep snd_aloop >> /dev/null
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
echo "ALSA loopback device not present. Please install with:"
|
||||||
|
echo
|
||||||
|
echo " sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
myInterruptHandler()
|
||||||
|
{
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_alsa_loopback
|
||||||
|
|
||||||
|
RX_LOG=$(mktemp)
|
||||||
|
|
||||||
|
trap myInterruptHandler SIGINT
|
||||||
|
|
||||||
|
# make sure all child processes are killed when we exit
|
||||||
|
trap 'jobs -p | xargs -r kill' EXIT
|
||||||
|
|
||||||
|
python3 test_callback_multimode_rx_outside.py --timeout 60 --framesperburst 2 --bursts 2 --audiodev -2 --debug &
|
||||||
|
rx_pid=$!
|
||||||
|
sleep 1
|
||||||
|
python3 test_multimode_tx.py --framesperburst 2 --bursts 2 --audiodev -2 --delay 500
|
||||||
|
wait ${rx_pid}
|
|
@ -8,7 +8,7 @@ import pathlib
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import numpy as np
|
import numpy as np
|
||||||
#print("loading codec2 module", file=sys.stderr)
|
#print("loading codec2 module", file=sys.stderr)
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
# Enum for codec2 modes
|
# Enum for codec2 modes
|
||||||
class FREEDV_MODE(Enum):
|
class FREEDV_MODE(Enum):
|
||||||
|
@ -135,16 +135,21 @@ class audio_buffer:
|
||||||
self.size = size
|
self.size = size
|
||||||
self.buffer = np.zeros(size, dtype=np.int16)
|
self.buffer = np.zeros(size, dtype=np.int16)
|
||||||
self.nbuffer = 0
|
self.nbuffer = 0
|
||||||
|
self.mutex = Lock()
|
||||||
def push(self,samples):
|
def push(self,samples):
|
||||||
|
self.mutex.acquire()
|
||||||
# add samples at the end of the buffer
|
# add samples at the end of the buffer
|
||||||
assert self.nbuffer+len(samples) <= self.size
|
assert self.nbuffer+len(samples) <= self.size
|
||||||
self.buffer[self.nbuffer:self.nbuffer+len(samples)] = samples
|
self.buffer[self.nbuffer:self.nbuffer+len(samples)] = samples
|
||||||
self.nbuffer += len(samples)
|
self.nbuffer += len(samples)
|
||||||
|
self.mutex.release()
|
||||||
def pop(self,size):
|
def pop(self,size):
|
||||||
|
self.mutex.acquire()
|
||||||
# remove samples from the start of the buffer
|
# remove samples from the start of the buffer
|
||||||
self.nbuffer -= size;
|
self.nbuffer -= size;
|
||||||
self.buffer[:self.nbuffer] = self.buffer[size:size+self.nbuffer]
|
self.buffer[:self.nbuffer] = self.buffer[size:size+self.nbuffer]
|
||||||
assert self.nbuffer >= 0
|
assert self.nbuffer >= 0
|
||||||
|
self.mutex.release()
|
||||||
|
|
||||||
# resampler ---------------------------------------------------------
|
# resampler ---------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -159,9 +159,9 @@ def arq_data_received(data_in, bytes_per_frame):
|
||||||
|
|
||||||
# TRANSMIT ACK FRAME FOR BURST-----------------------------------------------
|
# TRANSMIT ACK FRAME FOR BURST-----------------------------------------------
|
||||||
helpers.wait(0.3)
|
helpers.wait(0.3)
|
||||||
while not modem.transmit_signalling(ack_frame, 1):
|
|
||||||
#while static.CHANNEL_STATE == 'SENDING_SIGNALLING':
|
txbuffer = [ack_frame]
|
||||||
time.sleep(0.01)
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
|
|
||||||
static.CHANNEL_STATE = 'RECEIVING_DATA'
|
static.CHANNEL_STATE = 'RECEIVING_DATA'
|
||||||
# clear burst buffer
|
# clear burst buffer
|
||||||
|
@ -191,8 +191,11 @@ def arq_data_received(data_in, bytes_per_frame):
|
||||||
rpt_frame[3:9] = missing_frames
|
rpt_frame[3:9] = missing_frames
|
||||||
|
|
||||||
# TRANSMIT RPT FRAME FOR BURST-----------------------------------------------
|
# TRANSMIT RPT FRAME FOR BURST-----------------------------------------------
|
||||||
while not modem.transmit_signalling(rpt_frame, 1):
|
txbuffer = [rpt_frame]
|
||||||
time.sleep(0.01)
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
|
|
||||||
|
#while not modem.transmit_signalling(rpt_frame, 1):
|
||||||
|
# time.sleep(0.01)
|
||||||
static.CHANNEL_STATE = 'RECEIVING_DATA'
|
static.CHANNEL_STATE = 'RECEIVING_DATA'
|
||||||
|
|
||||||
# ---------------------------- FRAME MACHINE
|
# ---------------------------- FRAME MACHINE
|
||||||
|
@ -270,9 +273,11 @@ def arq_data_received(data_in, bytes_per_frame):
|
||||||
# possibly we can remove this later
|
# possibly we can remove this later
|
||||||
helpers.wait(0.5)
|
helpers.wait(0.5)
|
||||||
|
|
||||||
|
txbuffer = [ack_frame]
|
||||||
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
|
|
||||||
while not modem.transmit_signalling(ack_frame, 3):
|
#while not modem.transmit_signalling(ack_frame, 3):
|
||||||
time.sleep(0.01)
|
# time.sleep(0.01)
|
||||||
|
|
||||||
calculate_transfer_rate_rx(RX_N_FRAMES_PER_DATA_FRAME, RX_N_FRAME_OF_DATA_FRAME, RX_START_OF_TRANSMISSION, RX_PAYLOAD_PER_ARQ_FRAME)
|
calculate_transfer_rate_rx(RX_N_FRAMES_PER_DATA_FRAME, RX_N_FRAME_OF_DATA_FRAME, RX_START_OF_TRANSMISSION, RX_PAYLOAD_PER_ARQ_FRAME)
|
||||||
|
|
||||||
|
@ -357,6 +362,7 @@ def arq_transmit(data_out, mode, n_frames_per_burst):
|
||||||
# save len of data_out to TOTAL_BYTES for our statistics
|
# save len of data_out to TOTAL_BYTES for our statistics
|
||||||
static.TOTAL_BYTES = len(data_out)
|
static.TOTAL_BYTES = len(data_out)
|
||||||
# --------------------------------------------- LETS CREATE A BUFFER BY SPLITTING THE FILES INTO PEACES
|
# --------------------------------------------- LETS CREATE A BUFFER BY SPLITTING THE FILES INTO PEACES
|
||||||
|
# https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
||||||
TX_BUFFER = [data_out[i:i + TX_PAYLOAD_PER_ARQ_FRAME] for i in range(0, len(data_out), TX_PAYLOAD_PER_ARQ_FRAME)]
|
TX_BUFFER = [data_out[i:i + TX_PAYLOAD_PER_ARQ_FRAME] for i in range(0, len(data_out), TX_PAYLOAD_PER_ARQ_FRAME)]
|
||||||
TX_BUFFER_SIZE = len(TX_BUFFER)
|
TX_BUFFER_SIZE = len(TX_BUFFER)
|
||||||
static.INFO.append("ARQ;TRANSMITTING")
|
static.INFO.append("ARQ;TRANSMITTING")
|
||||||
|
@ -685,8 +691,11 @@ async def arq_open_data_channel(mode):
|
||||||
|
|
||||||
structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]", attempt=str(attempt) + "/" + str(DATA_CHANNEL_MAX_RETRIES))
|
structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]", attempt=str(attempt) + "/" + str(DATA_CHANNEL_MAX_RETRIES))
|
||||||
|
|
||||||
while not modem.transmit_signalling(connection_frame, 1):
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
txbuffer = [connection_frame]
|
||||||
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
|
|
||||||
|
|
||||||
timeout = time.time() + 3
|
timeout = time.time() + 3
|
||||||
while time.time() < timeout:
|
while time.time() < timeout:
|
||||||
|
@ -732,8 +741,8 @@ def arq_received_data_channel_opener(data_in):
|
||||||
connection_frame[3:9] = static.MYCALLSIGN
|
connection_frame[3:9] = static.MYCALLSIGN
|
||||||
connection_frame[12:13] = bytes([mode])
|
connection_frame[12:13] = bytes([mode])
|
||||||
|
|
||||||
while not modem.transmit_signalling(connection_frame, 2):
|
txbuffer = [connection_frame]
|
||||||
time.sleep(0.01)
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
|
|
||||||
structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=static.SNR, mode=mode)
|
structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=static.SNR, mode=mode)
|
||||||
|
|
||||||
|
@ -793,9 +802,8 @@ def transmit_ping(callsign):
|
||||||
ping_frame[2:3] = static.MYCALLSIGN_CRC8
|
ping_frame[2:3] = static.MYCALLSIGN_CRC8
|
||||||
ping_frame[3:9] = static.MYCALLSIGN
|
ping_frame[3:9] = static.MYCALLSIGN
|
||||||
|
|
||||||
# wait while sending....
|
txbuffer = [ping_frame]
|
||||||
while not modem.transmit_signalling(ping_frame, 1):
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
|
|
||||||
def received_ping(data_in, frequency_offset):
|
def received_ping(data_in, frequency_offset):
|
||||||
|
@ -815,10 +823,8 @@ def received_ping(data_in, frequency_offset):
|
||||||
ping_frame[3:9] = static.MYGRID
|
ping_frame[3:9] = static.MYGRID
|
||||||
ping_frame[9:11] = frequency_offset.to_bytes(2, byteorder='big', signed=True)
|
ping_frame[9:11] = frequency_offset.to_bytes(2, byteorder='big', signed=True)
|
||||||
|
|
||||||
# wait while sending....
|
txbuffer = [ping_frame]
|
||||||
while not modem.transmit_signalling(ping_frame, 1):
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
|
|
||||||
def received_ping_ack(data_in):
|
def received_ping_ack(data_in):
|
||||||
|
|
||||||
|
@ -850,9 +856,10 @@ def run_beacon(interval):
|
||||||
|
|
||||||
static.INFO.append("BEACON;SENDING")
|
static.INFO.append("BEACON;SENDING")
|
||||||
structlog.get_logger("structlog").info("[TNC] Sending beacon!", interval=interval)
|
structlog.get_logger("structlog").info("[TNC] Sending beacon!", interval=interval)
|
||||||
while not modem.transmit_signalling(beacon_frame, 2):
|
|
||||||
#time.sleep(0.01)
|
txbuffer = [beacon_frame]
|
||||||
pass
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
|
|
||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
|
@ -882,9 +889,10 @@ def transmit_cq():
|
||||||
cq_frame[2:8] = static.MYCALLSIGN
|
cq_frame[2:8] = static.MYCALLSIGN
|
||||||
cq_frame[8:14] = static.MYGRID
|
cq_frame[8:14] = static.MYGRID
|
||||||
|
|
||||||
while not modem.transmit_signalling(cq_frame, 3):
|
txbuffer = [cq_frame]
|
||||||
#time.sleep(0.01)
|
modem.transmit('datac0', 1, txbuffer)
|
||||||
pass
|
#while not modem.transmit('datac0', 1, txbuffer):
|
||||||
|
# pass
|
||||||
|
|
||||||
|
|
||||||
def received_cq(data_in):
|
def received_cq(data_in):
|
||||||
|
|
250
tnc/modem.py
250
tnc/modem.py
|
@ -9,7 +9,6 @@ import sys
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
import pathlib
|
import pathlib
|
||||||
import audioop
|
|
||||||
#import asyncio
|
#import asyncio
|
||||||
import logging, structlog, log_handler
|
import logging, structlog, log_handler
|
||||||
import time
|
import time
|
||||||
|
@ -107,7 +106,7 @@ class RF():
|
||||||
self.AUDIO_SAMPLE_RATE_TX = 48000
|
self.AUDIO_SAMPLE_RATE_TX = 48000
|
||||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
self.AUDIO_FRAMES_PER_BUFFER_RX = 2400*2 #8192
|
self.AUDIO_FRAMES_PER_BUFFER_RX = 2400*2 #8192
|
||||||
self.AUDIO_FRAMES_PER_BUFFER_TX = 8 #8192 Lets to some tests with very small chunks for TX
|
self.AUDIO_FRAMES_PER_BUFFER_TX = 2400 #8192 Lets to some tests with very small chunks for TX
|
||||||
self.AUDIO_CHUNKS = 48 #8 * (self.AUDIO_SAMPLE_RATE_RX/self.MODEM_SAMPLE_RATE) #48
|
self.AUDIO_CHUNKS = 48 #8 * (self.AUDIO_SAMPLE_RATE_RX/self.MODEM_SAMPLE_RATE) #48
|
||||||
self.AUDIO_CHANNELS = 1
|
self.AUDIO_CHANNELS = 1
|
||||||
|
|
||||||
|
@ -122,6 +121,9 @@ class RF():
|
||||||
# init FIFO queue to store received frames in
|
# init FIFO queue to store received frames in
|
||||||
self.dataqueue = queue.Queue()
|
self.dataqueue = queue.Queue()
|
||||||
|
|
||||||
|
# init FIFO queue to store modulation out in
|
||||||
|
self.modoutqueue = queue.Queue()
|
||||||
|
|
||||||
|
|
||||||
# open codec2 instance
|
# open codec2 instance
|
||||||
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
||||||
|
@ -171,43 +173,28 @@ class RF():
|
||||||
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
||||||
loopback_list.append(dev)
|
loopback_list.append(dev)
|
||||||
if len(loopback_list) >= 2:
|
if len(loopback_list) >= 2:
|
||||||
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
static.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX
|
||||||
|
static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #1 = TX
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
|
||||||
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
self.audio_stream = self.p.open(format=pyaudio.paInt16,
|
||||||
channels=self.AUDIO_CHANNELS,
|
channels=self.AUDIO_CHANNELS,
|
||||||
rate=self.AUDIO_SAMPLE_RATE_RX,
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX,
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX,
|
||||||
input=True,
|
input=True,
|
||||||
output=False,
|
output=True,
|
||||||
input_device_index=static.AUDIO_INPUT_DEVICE,
|
input_device_index=static.AUDIO_INPUT_DEVICE,
|
||||||
|
output_device_index=static.AUDIO_OUTPUT_DEVICE,
|
||||||
stream_callback=self.callback
|
stream_callback=self.callback
|
||||||
)
|
)
|
||||||
# --------------------------------------------OPEN TX AUDIO CHANNEL
|
|
||||||
# optional auto selection of loopback device if using in testmode
|
|
||||||
if static.AUDIO_OUTPUT_DEVICE == -2:
|
|
||||||
loopback_list = []
|
|
||||||
for dev in range(0,self.p.get_device_count()):
|
|
||||||
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
|
||||||
loopback_list.append(dev)
|
|
||||||
if len(loopback_list) >= 2:
|
|
||||||
static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #0 = RX 1 = TX
|
|
||||||
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
|
|
||||||
|
|
||||||
self.stream_tx = self.p.open(format=pyaudio.paInt16,
|
# not needed anymore.
|
||||||
channels=self.AUDIO_CHANNELS,
|
#self.streambuffer = bytes(0)
|
||||||
rate=self.AUDIO_SAMPLE_RATE_TX,
|
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_TX, # n_nom_modem_samples
|
|
||||||
output=True,
|
|
||||||
output_device_index=static.AUDIO_OUTPUT_DEVICE, # static.AUDIO_OUTPUT_DEVICE
|
|
||||||
)
|
|
||||||
|
|
||||||
self.streambuffer = bytes(0)
|
|
||||||
self.audio_writing_to_stream = False
|
|
||||||
# --------------------------------------------START DECODER THREAD
|
# --------------------------------------------START DECODER THREAD
|
||||||
|
|
||||||
DECODER_THREAD = threading.Thread(target=self.receive, name="DECODER_THREAD")
|
AUDIO_THREAD = threading.Thread(target=self.audio, name="AUDIO_THREAD")
|
||||||
DECODER_THREAD.start()
|
AUDIO_THREAD.start()
|
||||||
|
|
||||||
WORKER_THREAD = threading.Thread(target=self.worker, name="WORKER_THREAD")
|
WORKER_THREAD = threading.Thread(target=self.worker, name="WORKER_THREAD")
|
||||||
WORKER_THREAD.start()
|
WORKER_THREAD.start()
|
||||||
|
@ -217,14 +204,6 @@ class RF():
|
||||||
FFT_THREAD.start()
|
FFT_THREAD.start()
|
||||||
|
|
||||||
# --------------------------------------------CONFIGURE HAMLIB
|
# --------------------------------------------CONFIGURE HAMLIB
|
||||||
# my_rig.set_ptt(Hamlib.RIG_PTT_RIG,0)
|
|
||||||
# my_rig.set_ptt(Hamlib.RIG_PTT_SERIAL_DTR,0)
|
|
||||||
# my_rig.set_ptt(Hamlib.RIG_PTT_SERIAL_RTS,1)
|
|
||||||
#self.my_rig.set_conf("dtr_state", "OFF")
|
|
||||||
#my_rig.set_conf("rts_state", "OFF")
|
|
||||||
#self.my_rig.set_conf("ptt_type", "RTS")
|
|
||||||
#my_rig.set_conf("ptt_type", "RIG_PTT_SERIAL_RTS")
|
|
||||||
|
|
||||||
# try to init hamlib
|
# try to init hamlib
|
||||||
try:
|
try:
|
||||||
Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE)
|
Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE)
|
||||||
|
@ -303,40 +282,12 @@ class RF():
|
||||||
self.fft_data += bytes(x)
|
self.fft_data += bytes(x)
|
||||||
|
|
||||||
|
|
||||||
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
if self.modoutqueue.empty():
|
||||||
# demodulate audio
|
data_out48k = bytes(self.AUDIO_FRAMES_PER_BUFFER_TX*2*2)
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
|
else:
|
||||||
self.datac0_buffer.pop(self.datac0_nin)
|
data_out48k = self.modoutqueue.get()
|
||||||
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
|
||||||
if nbytes == self.datac0_bytes_per_frame:
|
|
||||||
print(len(self.datac0_bytes_out))
|
|
||||||
self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame])
|
|
||||||
self.get_scatter(self.datac0_freedv)
|
|
||||||
self.calculate_snr(self.datac0_freedv)
|
|
||||||
|
|
||||||
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
return (data_out48k, pyaudio.paContinue)
|
||||||
# demodulate audio
|
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
|
|
||||||
self.datac1_buffer.pop(self.datac1_nin)
|
|
||||||
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
|
||||||
if nbytes == self.datac1_bytes_per_frame:
|
|
||||||
self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame])
|
|
||||||
self.get_scatter(self.datac1_freedv)
|
|
||||||
self.calculate_snr(self.datac1_freedv)
|
|
||||||
|
|
||||||
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
|
||||||
# demodulate audio
|
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
|
|
||||||
self.datac3_buffer.pop(self.datac3_nin)
|
|
||||||
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
|
||||||
if nbytes == self.datac3_bytes_per_frame:
|
|
||||||
self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame])
|
|
||||||
self.get_scatter(self.datac3_freedv)
|
|
||||||
self.calculate_snr(self.datac3_freedv)
|
|
||||||
|
|
||||||
self.dataqueue.join()
|
|
||||||
|
|
||||||
return (None, pyaudio.paContinue)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -348,7 +299,8 @@ class RF():
|
||||||
self.my_rig.set_ptt(self.hamlib_ptt_type, 1)
|
self.my_rig.set_ptt(self.hamlib_ptt_type, 1)
|
||||||
# rigctld.ptt_enable()
|
# rigctld.ptt_enable()
|
||||||
ptt_toggle_timeout = time.time() + 0.5
|
ptt_toggle_timeout = time.time() + 0.5
|
||||||
while time.time() < ptt_toggle_timeout:
|
|
||||||
|
while time.time() < ptt_toggle_timeout and not self.modoutqueue.empty():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -364,6 +316,103 @@ class RF():
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def transmit(self, mode, count, frames):
|
||||||
|
|
||||||
|
state_before_transmit = static.CHANNEL_STATE
|
||||||
|
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.MODE = codec2.FREEDV_MODE[mode].value
|
||||||
|
freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
||||||
|
|
||||||
|
# get number of bytes per frame for mode
|
||||||
|
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|
||||||
|
payload_bytes_per_frame = bytes_per_frame -2
|
||||||
|
|
||||||
|
# init buffer for data
|
||||||
|
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||||
|
mod_out = create_string_buffer(n_tx_modem_samples * 2)
|
||||||
|
|
||||||
|
# init buffer for preample
|
||||||
|
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
|
||||||
|
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2)
|
||||||
|
|
||||||
|
# init buffer for postamble
|
||||||
|
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||||
|
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for i in range(1,count+1):
|
||||||
|
|
||||||
|
# write preamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
|
txbuffer = bytes(mod_out_preamble)
|
||||||
|
|
||||||
|
# create modulaton for n frames in list
|
||||||
|
for n in range(0,len(frames)):
|
||||||
|
|
||||||
|
|
||||||
|
# create buffer for data
|
||||||
|
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
||||||
|
buffer[:len(frames[n])] = frames[n] # set buffersize to length of data which will be send
|
||||||
|
|
||||||
|
# create crc for data frame - we are using the crc function shipped with codec2 to avoid
|
||||||
|
# crc algorithm incompatibilities
|
||||||
|
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16
|
||||||
|
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
||||||
|
buffer += crc # append crc16 to buffer
|
||||||
|
|
||||||
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
||||||
|
|
||||||
|
txbuffer += bytes(mod_out)
|
||||||
|
|
||||||
|
|
||||||
|
# append postamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
|
txbuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
|
# resample up to 48k (resampler works on np.int16)
|
||||||
|
x = np.frombuffer(txbuffer, dtype=np.int16)
|
||||||
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
||||||
|
|
||||||
|
# split modualted audio to chunks
|
||||||
|
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
||||||
|
txbuffer_48k = bytes(txbuffer_48k)
|
||||||
|
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER_RX*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER_RX*2)]
|
||||||
|
# add modulated chunks to fifo buffer
|
||||||
|
for c in chunk:
|
||||||
|
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
||||||
|
# to prevent the callback from stucking/crashing
|
||||||
|
if len(c) < self.AUDIO_FRAMES_PER_BUFFER_RX*2:
|
||||||
|
c += bytes(self.AUDIO_FRAMES_PER_BUFFER_RX*2 - len(c))
|
||||||
|
self.modoutqueue.put(c)
|
||||||
|
print(len(c))
|
||||||
|
|
||||||
|
while self.ptt_and_wait(True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# set channel state
|
||||||
|
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
|
||||||
|
|
||||||
|
# set ptt back to false
|
||||||
|
self.ptt_and_wait(False)
|
||||||
|
|
||||||
|
|
||||||
|
# we have a problem with the receiving state
|
||||||
|
if state_before_transmit != 'RECEIVING_DATA':
|
||||||
|
static.CHANNEL_STATE = 'RECEIVING_SIGNALLING'
|
||||||
|
else:
|
||||||
|
static.CHANNEL_STATE = state_before_transmit
|
||||||
|
|
||||||
|
self.c_lib.freedv_close(freedv)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
def transmit_signalling(self, data_out, count):
|
def transmit_signalling(self, data_out, count):
|
||||||
state_before_transmit = static.CHANNEL_STATE
|
state_before_transmit = static.CHANNEL_STATE
|
||||||
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
|
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
|
||||||
|
@ -392,12 +441,14 @@ class RF():
|
||||||
self.streambuffer += bytes(mod_out)
|
self.streambuffer += bytes(mod_out)
|
||||||
self.streambuffer += bytes(mod_out_postamble)
|
self.streambuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
converted_audio = audioop.ratecv(self.streambuffer, 2, 1, self.MODEM_SAMPLE_RATE, self.AUDIO_SAMPLE_RATE_TX, None)
|
# resample up to 48k (resampler works on np.int16)
|
||||||
self.streambuffer = bytes(converted_audio[0])
|
x = np.frombuffer(self.streambuffer, dtype=np.int16)
|
||||||
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
||||||
|
|
||||||
|
|
||||||
# append frame again with as much as in count defined
|
# append frame again with as much as in count defined
|
||||||
for i in range(1, count):
|
#for i in range(1, count):
|
||||||
self.streambuffer += bytes(converted_audio[0])
|
# self.streambuffer += bytes(txbuffer_48k.tobytes())
|
||||||
|
|
||||||
while self.ptt_and_wait(True):
|
while self.ptt_and_wait(True):
|
||||||
pass
|
pass
|
||||||
|
@ -406,7 +457,9 @@ class RF():
|
||||||
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
|
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
|
||||||
|
|
||||||
# start writing audio data to audio stream
|
# start writing audio data to audio stream
|
||||||
self.stream_tx.write(self.streambuffer)
|
#self.stream_tx.write(self.streambuffer)
|
||||||
|
self.stream_tx.write(txbuffer_48k.tobytes())
|
||||||
|
|
||||||
|
|
||||||
# set ptt back to false
|
# set ptt back to false
|
||||||
self.ptt_and_wait(False)
|
self.ptt_and_wait(False)
|
||||||
|
@ -472,8 +525,9 @@ class RF():
|
||||||
self.c_lib.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
self.c_lib.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
self.streambuffer += bytes(mod_out_postamble)
|
self.streambuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
converted_audio = audioop.ratecv(self.streambuffer, 2, 1, self.MODEM_SAMPLE_RATE, self.AUDIO_SAMPLE_RATE_TX, None)
|
# resample up to 48k (resampler works on np.int16)
|
||||||
self.streambuffer = bytes(converted_audio[0])
|
x = np.frombuffer(self.streambuffer, dtype=np.int16)
|
||||||
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
||||||
|
|
||||||
# -------------- transmit audio
|
# -------------- transmit audio
|
||||||
|
|
||||||
|
@ -484,7 +538,7 @@ class RF():
|
||||||
static.CHANNEL_STATE = 'SENDING_DATA'
|
static.CHANNEL_STATE = 'SENDING_DATA'
|
||||||
|
|
||||||
# write audio to stream
|
# write audio to stream
|
||||||
self.stream_tx.write(self.streambuffer)
|
self.stream_tx.write(txbuffer_48k.tobytes())
|
||||||
|
|
||||||
static.CHANNEL_STATE = 'RECEIVING_SIGNALLING'
|
static.CHANNEL_STATE = 'RECEIVING_SIGNALLING'
|
||||||
|
|
||||||
|
@ -495,17 +549,50 @@ class RF():
|
||||||
|
|
||||||
return True
|
return True
|
||||||
# --------------------------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------------------------
|
||||||
|
'''
|
||||||
def receive(self):
|
def audio(self):
|
||||||
try:
|
try:
|
||||||
print(f"starting pyaudio callback", file=sys.stderr)
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
self.stream_rx.start_stream()
|
self.audio_stream.start_stream()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"pyAudio error: {e}", file=sys.stderr)
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
while self.stream_rx.is_active():
|
while self.audio_stream.is_active():
|
||||||
time.sleep(1)
|
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
|
||||||
|
self.datac0_buffer.pop(self.datac0_nin)
|
||||||
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
|
if nbytes == self.datac0_bytes_per_frame:
|
||||||
|
print(len(self.datac0_bytes_out))
|
||||||
|
self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame])
|
||||||
|
self.get_scatter(self.datac0_freedv)
|
||||||
|
self.calculate_snr(self.datac0_freedv)
|
||||||
|
|
||||||
|
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
|
||||||
|
self.datac1_buffer.pop(self.datac1_nin)
|
||||||
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
|
if nbytes == self.datac1_bytes_per_frame:
|
||||||
|
self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame])
|
||||||
|
self.get_scatter(self.datac1_freedv)
|
||||||
|
self.calculate_snr(self.datac1_freedv)
|
||||||
|
|
||||||
|
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
|
||||||
|
self.datac3_buffer.pop(self.datac3_nin)
|
||||||
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
|
if nbytes == self.datac3_bytes_per_frame:
|
||||||
|
self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame])
|
||||||
|
self.get_scatter(self.datac3_freedv)
|
||||||
|
self.calculate_snr(self.datac3_freedv)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# worker for FIFO queue for processing received frames
|
# worker for FIFO queue for processing received frames
|
||||||
def worker(self):
|
def worker(self):
|
||||||
|
@ -724,4 +811,3 @@ class RF():
|
||||||
static.FFT = [0] * 400
|
static.FFT = [0] * 400
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue