Merge pull request #92 from DJ2LS/dr-resampler

PyAudio rx callback and Python<->C Resampler
This commit is contained in:
DJ2LS 2021-12-23 10:17:51 +01:00 committed by GitHub
commit 1f8296fa5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1762 additions and 340 deletions

View file

@ -22,82 +22,112 @@ set(FRAMESPERBURST 3)
set(BURSTS 1)
set(TESTFRAMES 3)
add_test(NAME 000_resampler
add_test(NAME audio_buffer
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/000_resampler;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_audiobuffer.py")
set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME resampler
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 t48_8_short.py")
set_tests_properties(000_resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
add_test(NAME 001_highsnr_stdio_P_C_SM
add_test(NAME highsnr_stdio_P_C_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
sox -t .s16 -r 48000 -c 1 - -t .s16 -r 8000 -c 1 - |
freedv_data_raw_rx datac0 - - --framesperburst ${FRAMESPERBURST} | hexdump -C")
set_tests_properties(001_highsnr_stdio_P_C_SM PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
set_tests_properties(highsnr_stdio_P_C_single PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
add_test(NAME 001_highsnr_stdio_C_P_SM
add_test(NAME highsnr_stdio_C_P_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
freedv_data_raw_tx datac0 --testframes ${TESTFRAMES} --bursts ${BURSTS} --framesperburst ${FRAMESPERBURST} /dev/zero - |
sox -t .s16 -r 8000 -c 1 - -t .s16 -r 48000 -c 1 - |
python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
set_tests_properties(001_highsnr_stdio_C_P_SM PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
set_tests_properties(highsnr_stdio_C_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME 001_highsnr_stdio_P_P_SM
add_test(NAME highsnr_stdio_P_P_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
python3 test_rx.py --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
set_tests_properties(001_highsnr_stdio_P_P_SM PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME 001_highsnr_stdio_P_P_MM
add_test(NAME highsnr_stdio_P_P_multi
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_multimode_tx.py --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20")
set_tests_properties(001_highsnr_stdio_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
set_tests_properties(highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
# These tests can't run on GitHub actions
# These tests can't run on GitHub actions as we don't have a virtual sound card
if(NOT DEFINED ENV{GITHUB_RUN_ID})
# uses aplay/arecord then pipe to Python
add_test(NAME 001_highsnr_virtual1_P_P
add_test(NAME highsnr_virtual1_P_P_single_alsa
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual1.sh")
set_tests_properties(001_highsnr_virtual1_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0")
set_tests_properties(highsnr_virtual1_P_P_single_alsa PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0")
# let Python do audio I/O
add_test(NAME 001_highsnr_virtual2_P_P
add_test(NAME highsnr_virtual2_P_P_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual2.sh")
set_tests_properties(001_highsnr_virtual2_P_P PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
set_tests_properties(highsnr_virtual2_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
# Multimode test with Python I/O
add_test(NAME 001_highsnr_virtual3_P_P_MM
add_test(NAME highsnr_virtual3_P_P_multi
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual_mm.sh")
set_tests_properties(001_highsnr_virtual3_P_P_MM PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
set_tests_properties(highsnr_virtual3_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
# let Python do audio I/O via pyaudio callback mode
add_test(NAME 001_highsnr_virtual4_P_P_SM_CB
add_test(NAME highsnr_virtual4_P_P_single_callback
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test/001_highsnr_stdio_audio;
./test_virtual3.sh")
set_tests_properties(001_highsnr_virtual4_P_P_SM_CB PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual3a.sh")
set_tests_properties(highsnr_virtual4_P_P_single_callback PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
# let Python do audio I/O via pyaudio callback mode with code outside of callback
add_test(NAME highsnr_virtual4_P_P_single_callback_outside
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual3b.sh")
set_tests_properties(highsnr_virtual4_P_P_single_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
# let Python do audio I/O via pyaudio callback mode with code outside of callback
add_test(NAME highsnr_virtual5_P_P_multi_callback
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual4a.sh")
set_tests_properties(highsnr_virtual5_P_P_multi_callback PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
# let Python do audio I/O via pyaudio callback mode with code outside of callback
add_test(NAME highsnr_virtual5_P_P_multi_callback_outside
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual4b.sh")
set_tests_properties(highsnr_virtual5_P_P_multi_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
endif()

View file

@ -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()

View file

@ -18,7 +18,7 @@ from ctypes import *
import pathlib
import argparse
import sys
sys.path.insert(0,'../..')
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np

View file

@ -7,8 +7,8 @@ Created on Wed Dec 23 07:04:24 2020
"""
import sys
sys.path.insert(0,'../..')
sys.path.insert(0,'../../tnc')
sys.path.insert(0,'..')
sys.path.insert(0,'../tnc')
import data_handler

59
test/test_audiobuffer.py Normal file
View 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))

View 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()

View 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()

View 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
View 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()

View 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
View 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()

View file

@ -9,7 +9,7 @@ import sys
import ctypes
from ctypes import *
import pathlib
sys.path.insert(0,'../..')
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np
@ -60,22 +60,19 @@ rx_bursts_datac3 = 0
# open codec2 instance
datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8)
datac0_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac0_freedv)
datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame * 2)
datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST)
datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8)
datac1_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac1_freedv)
datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame * 2)
datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST)
datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p)
datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8)
datac3_n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(datac3_freedv)
datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame * 2)
datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST)
datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)

View file

@ -11,7 +11,7 @@ import threading
import audioop
import argparse
import sys
sys.path.insert(0,'../..')
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np

View file

@ -17,7 +17,7 @@ import threading
import sys
import argparse
import numpy as np
sys.path.insert(0,'../..')
sys.path.insert(0,'..')
from tnc import codec2
@ -93,7 +93,7 @@ bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
payload_bytes_per_frame = bytes_per_frame -2
n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv)
bytes_out = create_string_buffer(bytes_per_frame * 2)
bytes_out = create_string_buffer(bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST)

View file

@ -9,7 +9,7 @@ import pyaudio
import time
import argparse
import sys
sys.path.insert(0,'../..')
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np
@ -23,6 +23,8 @@ parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', '
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int,
help="audio output device number to use, use -2 to automatically select a loopback device")
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="list audio devices by number and exit")
args = parser.parse_args()
@ -75,7 +77,14 @@ if AUDIO_OUTPUT_DEVICE != -1:
resampler = codec2.resampler()
# data binary string
data_out = b'HELLO WORLD!'
if args.TESTFRAMES:
data_out = bytearray(14)
data_out[:1] = bytes([255])
data_out[1:2] = bytes([1])
data_out[2:] = b'HELLO WORLD'
else:
data_out = b'HELLO WORLD!'
# ----------------------------------------------------------------

View file

@ -16,7 +16,7 @@ check_alsa_loopback
# make sure all child processes are killed when we exit
trap 'jobs -p | xargs -r kill' EXIT
python3 test_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug &
python3 test_callback_rx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2 --debug &
rx_pid=$!
sleep 1
python3 test_tx.py --mode datac0 --frames 2 --bursts 3 --audiodev -2

23
test/test_virtual3a.sh Executable file
View 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
View 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
View 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
View 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}

View file

@ -8,7 +8,7 @@ import pathlib
from enum import Enum
import numpy as np
#print("loading codec2 module", file=sys.stderr)
from threading import Lock
# Enum for codec2 modes
class FREEDV_MODE(Enum):
@ -135,17 +135,22 @@ class audio_buffer:
self.size = size
self.buffer = np.zeros(size, dtype=np.int16)
self.nbuffer = 0
self.mutex = Lock()
def push(self,samples):
self.mutex.acquire()
# add samples at the end of the buffer
assert self.nbuffer+len(samples) <= self.size
self.buffer[self.nbuffer:self.nbuffer+len(samples)] = samples
self.nbuffer += len(samples)
self.mutex.release()
def pop(self,size):
self.mutex.acquire()
# remove samples from the start of the buffer
self.nbuffer -= size;
self.buffer[:self.nbuffer] = self.buffer[size:size+self.nbuffer]
assert self.nbuffer >= 0
self.mutex.release()
# resampler ---------------------------------------------------------
api.FDMDV_OS_48 = int(6) # oversampling rate

View file

@ -159,10 +159,10 @@ def arq_data_received(data_in, bytes_per_frame):
# TRANSMIT ACK FRAME FOR BURST-----------------------------------------------
helpers.wait(0.3)
while not modem.transmit_signalling(ack_frame, 1):
#while static.CHANNEL_STATE == 'SENDING_SIGNALLING':
time.sleep(0.01)
txbuffer = [ack_frame]
modem.transmit('datac0', 1, txbuffer)
static.CHANNEL_STATE = 'RECEIVING_DATA'
# clear burst buffer
static.RX_BURST_BUFFER = []
@ -191,8 +191,11 @@ def arq_data_received(data_in, bytes_per_frame):
rpt_frame[3:9] = missing_frames
# TRANSMIT RPT FRAME FOR BURST-----------------------------------------------
while not modem.transmit_signalling(rpt_frame, 1):
time.sleep(0.01)
txbuffer = [rpt_frame]
modem.transmit('datac0', 1, txbuffer)
#while not modem.transmit_signalling(rpt_frame, 1):
# time.sleep(0.01)
static.CHANNEL_STATE = 'RECEIVING_DATA'
# ---------------------------- FRAME MACHINE
@ -270,9 +273,11 @@ def arq_data_received(data_in, bytes_per_frame):
# possibly we can remove this later
helpers.wait(0.5)
txbuffer = [ack_frame]
modem.transmit('datac0', 1, txbuffer)
while not modem.transmit_signalling(ack_frame, 3):
time.sleep(0.01)
#while not modem.transmit_signalling(ack_frame, 3):
# time.sleep(0.01)
calculate_transfer_rate_rx(RX_N_FRAMES_PER_DATA_FRAME, RX_N_FRAME_OF_DATA_FRAME, RX_START_OF_TRANSMISSION, RX_PAYLOAD_PER_ARQ_FRAME)
@ -357,6 +362,7 @@ def arq_transmit(data_out, mode, n_frames_per_burst):
# save len of data_out to TOTAL_BYTES for our statistics
static.TOTAL_BYTES = len(data_out)
# --------------------------------------------- LETS CREATE A BUFFER BY SPLITTING THE FILES INTO PEACES
# https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
TX_BUFFER = [data_out[i:i + TX_PAYLOAD_PER_ARQ_FRAME] for i in range(0, len(data_out), TX_PAYLOAD_PER_ARQ_FRAME)]
TX_BUFFER_SIZE = len(TX_BUFFER)
static.INFO.append("ARQ;TRANSMITTING")
@ -685,9 +691,12 @@ async def arq_open_data_channel(mode):
structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]", attempt=str(attempt) + "/" + str(DATA_CHANNEL_MAX_RETRIES))
while not modem.transmit_signalling(connection_frame, 1):
time.sleep(0.01)
txbuffer = [connection_frame]
modem.transmit('datac0', 1, txbuffer)
timeout = time.time() + 3
while time.time() < timeout:
time.sleep(0.01)
@ -732,8 +741,8 @@ def arq_received_data_channel_opener(data_in):
connection_frame[3:9] = static.MYCALLSIGN
connection_frame[12:13] = bytes([mode])
while not modem.transmit_signalling(connection_frame, 2):
time.sleep(0.01)
txbuffer = [connection_frame]
modem.transmit('datac0', 1, txbuffer)
structlog.get_logger("structlog").info("[TNC] DATA [" + str(static.MYCALLSIGN, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=static.SNR, mode=mode)
@ -793,10 +802,9 @@ def transmit_ping(callsign):
ping_frame[2:3] = static.MYCALLSIGN_CRC8
ping_frame[3:9] = static.MYCALLSIGN
# wait while sending....
while not modem.transmit_signalling(ping_frame, 1):
time.sleep(0.01)
txbuffer = [ping_frame]
modem.transmit('datac0', 1, txbuffer)
def received_ping(data_in, frequency_offset):
@ -815,10 +823,8 @@ def received_ping(data_in, frequency_offset):
ping_frame[3:9] = static.MYGRID
ping_frame[9:11] = frequency_offset.to_bytes(2, byteorder='big', signed=True)
# wait while sending....
while not modem.transmit_signalling(ping_frame, 1):
time.sleep(0.01)
txbuffer = [ping_frame]
modem.transmit('datac0', 1, txbuffer)
def received_ping_ack(data_in):
@ -850,9 +856,10 @@ def run_beacon(interval):
static.INFO.append("BEACON;SENDING")
structlog.get_logger("structlog").info("[TNC] Sending beacon!", interval=interval)
while not modem.transmit_signalling(beacon_frame, 2):
#time.sleep(0.01)
pass
txbuffer = [beacon_frame]
modem.transmit('datac0', 1, txbuffer)
time.sleep(interval)
@ -882,9 +889,10 @@ def transmit_cq():
cq_frame[2:8] = static.MYCALLSIGN
cq_frame[8:14] = static.MYGRID
while not modem.transmit_signalling(cq_frame, 3):
#time.sleep(0.01)
pass
txbuffer = [cq_frame]
modem.transmit('datac0', 1, txbuffer)
#while not modem.transmit('datac0', 1, txbuffer):
# pass
def received_cq(data_in):

View file

@ -9,7 +9,6 @@ import sys
import ctypes
from ctypes import *
import pathlib
import audioop
#import asyncio
import logging, structlog, log_handler
import time
@ -107,7 +106,7 @@ class RF():
self.AUDIO_SAMPLE_RATE_TX = 48000
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
self.AUDIO_FRAMES_PER_BUFFER_RX = 2400*2 #8192
self.AUDIO_FRAMES_PER_BUFFER_TX = 8 #8192 Lets to some tests with very small chunks for TX
self.AUDIO_FRAMES_PER_BUFFER_TX = 2400 #8192 Lets to some tests with very small chunks for TX
self.AUDIO_CHUNKS = 48 #8 * (self.AUDIO_SAMPLE_RATE_RX/self.MODEM_SAMPLE_RATE) #48
self.AUDIO_CHANNELS = 1
@ -122,7 +121,10 @@ class RF():
# init FIFO queue to store received frames in
self.dataqueue = queue.Queue()
# init FIFO queue to store modulation out in
self.modoutqueue = queue.Queue()
# open codec2 instance
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8)
@ -171,43 +173,28 @@ class RF():
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
loopback_list.append(dev)
if len(loopback_list) >= 2:
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
static.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX
static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
self.stream_rx = self.p.open(format=pyaudio.paInt16,
self.audio_stream = self.p.open(format=pyaudio.paInt16,
channels=self.AUDIO_CHANNELS,
rate=self.AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX,
input=True,
output=False,
output=True,
input_device_index=static.AUDIO_INPUT_DEVICE,
output_device_index=static.AUDIO_OUTPUT_DEVICE,
stream_callback=self.callback
)
# --------------------------------------------OPEN TX AUDIO CHANNEL
# optional auto selection of loopback device if using in testmode
if static.AUDIO_OUTPUT_DEVICE == -2:
loopback_list = []
for dev in range(0,self.p.get_device_count()):
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
loopback_list.append(dev)
if len(loopback_list) >= 2:
static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #0 = RX 1 = TX
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
self.stream_tx = self.p.open(format=pyaudio.paInt16,
channels=self.AUDIO_CHANNELS,
rate=self.AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_TX, # n_nom_modem_samples
output=True,
output_device_index=static.AUDIO_OUTPUT_DEVICE, # static.AUDIO_OUTPUT_DEVICE
)
self.streambuffer = bytes(0)
self.audio_writing_to_stream = False
# not needed anymore.
#self.streambuffer = bytes(0)
# --------------------------------------------START DECODER THREAD
DECODER_THREAD = threading.Thread(target=self.receive, name="DECODER_THREAD")
DECODER_THREAD.start()
AUDIO_THREAD = threading.Thread(target=self.audio, name="AUDIO_THREAD")
AUDIO_THREAD.start()
WORKER_THREAD = threading.Thread(target=self.worker, name="WORKER_THREAD")
WORKER_THREAD.start()
@ -217,14 +204,6 @@ class RF():
FFT_THREAD.start()
# --------------------------------------------CONFIGURE HAMLIB
# my_rig.set_ptt(Hamlib.RIG_PTT_RIG,0)
# my_rig.set_ptt(Hamlib.RIG_PTT_SERIAL_DTR,0)
# my_rig.set_ptt(Hamlib.RIG_PTT_SERIAL_RTS,1)
#self.my_rig.set_conf("dtr_state", "OFF")
#my_rig.set_conf("rts_state", "OFF")
#self.my_rig.set_conf("ptt_type", "RTS")
#my_rig.set_conf("ptt_type", "RIG_PTT_SERIAL_RTS")
# try to init hamlib
try:
Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE)
@ -301,42 +280,14 @@ class RF():
# refill fft_data buffer so we can plot a fft
if len(self.fft_data) < 1024:
self.fft_data += bytes(x)
while self.datac0_buffer.nbuffer >= self.datac0_nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
self.datac0_buffer.pop(self.datac0_nin)
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
if nbytes == self.datac0_bytes_per_frame:
print(len(self.datac0_bytes_out))
self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame])
self.get_scatter(self.datac0_freedv)
self.calculate_snr(self.datac0_freedv)
while self.datac1_buffer.nbuffer >= self.datac1_nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
self.datac1_buffer.pop(self.datac1_nin)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
if nbytes == self.datac1_bytes_per_frame:
self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame])
self.get_scatter(self.datac1_freedv)
self.calculate_snr(self.datac1_freedv)
while self.datac3_buffer.nbuffer >= self.datac3_nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
self.datac3_buffer.pop(self.datac3_nin)
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
if nbytes == self.datac3_bytes_per_frame:
self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame])
self.get_scatter(self.datac3_freedv)
self.calculate_snr(self.datac3_freedv)
self.dataqueue.join()
return (None, pyaudio.paContinue)
if self.modoutqueue.empty():
data_out48k = bytes(self.AUDIO_FRAMES_PER_BUFFER_TX*2*2)
else:
data_out48k = self.modoutqueue.get()
return (data_out48k, pyaudio.paContinue)
@ -348,7 +299,8 @@ class RF():
self.my_rig.set_ptt(self.hamlib_ptt_type, 1)
# rigctld.ptt_enable()
ptt_toggle_timeout = time.time() + 0.5
while time.time() < ptt_toggle_timeout:
while time.time() < ptt_toggle_timeout and not self.modoutqueue.empty():
pass
else:
@ -364,6 +316,103 @@ class RF():
# --------------------------------------------------------------------------------------------------------
def transmit(self, mode, count, frames):
state_before_transmit = static.CHANNEL_STATE
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
# open codec2 instance
self.MODE = codec2.FREEDV_MODE[mode].value
freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
# get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
payload_bytes_per_frame = bytes_per_frame -2
# init buffer for data
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = create_string_buffer(n_tx_modem_samples * 2)
# init buffer for preample
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2)
# init buffer for postamble
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2)
for i in range(1,count+1):
# write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytes(mod_out_preamble)
# create modulaton for n frames in list
for n in range(0,len(frames)):
# create buffer for data
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
buffer[:len(frames[n])] = frames[n] # set buffersize to length of data which will be send
# create crc for data frame - we are using the crc function shipped with codec2 to avoid
# crc algorithm incompatibilities
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
buffer += crc # append crc16 to buffer
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
txbuffer += bytes(mod_out)
# append postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
txbuffer += bytes(mod_out_postamble)
# resample up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = self.resampler.resample8_to_48(x)
# split modualted audio to chunks
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
txbuffer_48k = bytes(txbuffer_48k)
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER_RX*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER_RX*2)]
# add modulated chunks to fifo buffer
for c in chunk:
# if data is shorter than the expcected audio frames per buffer we need to append 0
# to prevent the callback from stucking/crashing
if len(c) < self.AUDIO_FRAMES_PER_BUFFER_RX*2:
c += bytes(self.AUDIO_FRAMES_PER_BUFFER_RX*2 - len(c))
self.modoutqueue.put(c)
print(len(c))
while self.ptt_and_wait(True):
pass
# set channel state
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
# set ptt back to false
self.ptt_and_wait(False)
# we have a problem with the receiving state
if state_before_transmit != 'RECEIVING_DATA':
static.CHANNEL_STATE = 'RECEIVING_SIGNALLING'
else:
static.CHANNEL_STATE = state_before_transmit
self.c_lib.freedv_close(freedv)
return True
'''
def transmit_signalling(self, data_out, count):
state_before_transmit = static.CHANNEL_STATE
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
@ -391,13 +440,15 @@ class RF():
self.streambuffer += bytes(mod_out_preamble)
self.streambuffer += bytes(mod_out)
self.streambuffer += bytes(mod_out_postamble)
converted_audio = audioop.ratecv(self.streambuffer, 2, 1, self.MODEM_SAMPLE_RATE, self.AUDIO_SAMPLE_RATE_TX, None)
self.streambuffer = bytes(converted_audio[0])
# resample up to 48k (resampler works on np.int16)
x = np.frombuffer(self.streambuffer, dtype=np.int16)
txbuffer_48k = self.resampler.resample8_to_48(x)
# append frame again with as much as in count defined
for i in range(1, count):
self.streambuffer += bytes(converted_audio[0])
#for i in range(1, count):
# self.streambuffer += bytes(txbuffer_48k.tobytes())
while self.ptt_and_wait(True):
pass
@ -406,8 +457,10 @@ class RF():
static.CHANNEL_STATE = 'SENDING_SIGNALLING'
# start writing audio data to audio stream
self.stream_tx.write(self.streambuffer)
#self.stream_tx.write(self.streambuffer)
self.stream_tx.write(txbuffer_48k.tobytes())
# set ptt back to false
self.ptt_and_wait(False)
@ -472,8 +525,9 @@ class RF():
self.c_lib.freedv_rawdatapostambletx(freedv, mod_out_postamble)
self.streambuffer += bytes(mod_out_postamble)
converted_audio = audioop.ratecv(self.streambuffer, 2, 1, self.MODEM_SAMPLE_RATE, self.AUDIO_SAMPLE_RATE_TX, None)
self.streambuffer = bytes(converted_audio[0])
# resample up to 48k (resampler works on np.int16)
x = np.frombuffer(self.streambuffer, dtype=np.int16)
txbuffer_48k = self.resampler.resample8_to_48(x)
# -------------- transmit audio
@ -484,7 +538,7 @@ class RF():
static.CHANNEL_STATE = 'SENDING_DATA'
# write audio to stream
self.stream_tx.write(self.streambuffer)
self.stream_tx.write(txbuffer_48k.tobytes())
static.CHANNEL_STATE = 'RECEIVING_SIGNALLING'
@ -495,17 +549,50 @@ class RF():
return True
# --------------------------------------------------------------------------------------------------------
def receive(self):
'''
def audio(self):
try:
print(f"starting pyaudio callback", file=sys.stderr)
self.stream_rx.start_stream()
self.audio_stream.start_stream()
except Exception as e:
print(f"pyAudio error: {e}", file=sys.stderr)
while self.stream_rx.is_active():
time.sleep(1)
while self.audio_stream.is_active():
while self.datac0_buffer.nbuffer >= self.datac0_nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
self.datac0_buffer.pop(self.datac0_nin)
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
if nbytes == self.datac0_bytes_per_frame:
print(len(self.datac0_bytes_out))
self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame])
self.get_scatter(self.datac0_freedv)
self.calculate_snr(self.datac0_freedv)
while self.datac1_buffer.nbuffer >= self.datac1_nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
self.datac1_buffer.pop(self.datac1_nin)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
if nbytes == self.datac1_bytes_per_frame:
self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame])
self.get_scatter(self.datac1_freedv)
self.calculate_snr(self.datac1_freedv)
while self.datac3_buffer.nbuffer >= self.datac3_nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
self.datac3_buffer.pop(self.datac3_nin)
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
if nbytes == self.datac3_bytes_per_frame:
self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame])
self.get_scatter(self.datac3_freedv)
self.calculate_snr(self.datac3_freedv)
# worker for FIFO queue for processing received frames
def worker(self):
@ -724,4 +811,3 @@ class RF():
static.FFT = [0] * 400
else:
pass