mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
Merge remote-tracking branch 'origin/pep8_improvements' into pep8_improvements
This commit is contained in:
commit
1bab085ca8
40 changed files with 3707 additions and 1811 deletions
19
.github/workflows/build-project-linux.yml
vendored
19
.github/workflows/build-project-linux.yml
vendored
|
@ -2,7 +2,7 @@ name: Linux
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_linux_release:
|
build_linux_release:
|
||||||
|
@ -20,15 +20,13 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
run: |
|
run: |
|
||||||
|
@ -44,7 +42,6 @@ jobs:
|
||||||
pip3 install structlog
|
pip3 install structlog
|
||||||
pip3 install sounddevice
|
pip3 install sounddevice
|
||||||
|
|
||||||
|
|
||||||
#- name: Build Hamlib Python Binding
|
#- name: Build Hamlib Python Binding
|
||||||
# if: matrix.os == 'ubuntu-latest'
|
# if: matrix.os == 'ubuntu-latest'
|
||||||
# working-directory: tnc
|
# working-directory: tnc
|
||||||
|
@ -58,19 +55,15 @@ jobs:
|
||||||
# make
|
# make
|
||||||
# make install
|
# make install
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build codec2 Linux
|
- name: Build codec2 Linux
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
working-directory: tnc
|
working-directory: tnc
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/drowe67/codec2.git
|
git clone https://github.com/drowe67/codec2.git
|
||||||
cd codec2 && mkdir build_linux && cd build_linux
|
cd codec2 && git checkout master && mkdir build_linux && cd build_linux
|
||||||
cmake ../
|
cmake ../
|
||||||
make
|
make
|
||||||
|
|
||||||
|
|
||||||
- name: Build Linux Daemon
|
- name: Build Linux Daemon
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
working-directory: tnc
|
working-directory: tnc
|
||||||
|
@ -82,7 +75,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
ls -R
|
ls -R
|
||||||
|
|
||||||
|
|
||||||
- name: Compress Linux
|
- name: Compress Linux
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
@ -96,8 +88,6 @@ jobs:
|
||||||
name: tnc-artifact
|
name: tnc-artifact
|
||||||
path: ./tnc/dist/compressed/*
|
path: ./tnc/dist/compressed/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Copy TNC to GUI Linux
|
- name: Copy TNC to GUI Linux
|
||||||
if: matrix.os == 'ubuntu-20.04'
|
if: matrix.os == 'ubuntu-20.04'
|
||||||
run: |
|
run: |
|
||||||
|
@ -124,6 +114,3 @@ jobs:
|
||||||
# release the app after building
|
# release the app after building
|
||||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
args: "-p always"
|
args: "-p always"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
21
.github/workflows/build-project-mac.yml
vendored
21
.github/workflows/build-project-mac.yml
vendored
|
@ -2,7 +2,7 @@ name: macOS
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- "v*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_linux_release:
|
build_linux_release:
|
||||||
|
@ -20,15 +20,13 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Install macOS dependencies
|
- name: Install macOS dependencies
|
||||||
if: matrix.os == 'macos-10.15'
|
if: matrix.os == 'macos-10.15'
|
||||||
run: |
|
run: |
|
||||||
|
@ -45,20 +43,18 @@ jobs:
|
||||||
- name: Install Portaudio
|
- name: Install Portaudio
|
||||||
if: matrix.os == 'macos-10.15'
|
if: matrix.os == 'macos-10.15'
|
||||||
run: |
|
run: |
|
||||||
brew install portaudio
|
brew install portaudio
|
||||||
pip3 install pyaudio
|
pip3 install pyaudio
|
||||||
|
|
||||||
|
|
||||||
- name: Build codec2 macOS
|
- name: Build codec2 macOS
|
||||||
if: matrix.os == 'macos-10.15'
|
if: matrix.os == 'macos-10.15'
|
||||||
working-directory: tnc
|
working-directory: tnc
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/drowe67/codec2.git
|
git clone https://github.com/drowe67/codec2.git
|
||||||
cd codec2 && mkdir build_mac && cd build_mac
|
cd codec2 && git checkout master && mkdir build_mac && cd build_mac
|
||||||
cmake ../
|
cmake ../
|
||||||
make
|
make
|
||||||
|
|
||||||
|
|
||||||
- name: Build macOS pyinstaller
|
- name: Build macOS pyinstaller
|
||||||
if: matrix.os == 'macos-10.15'
|
if: matrix.os == 'macos-10.15'
|
||||||
working-directory: tnc
|
working-directory: tnc
|
||||||
|
@ -70,7 +66,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
ls -R
|
ls -R
|
||||||
|
|
||||||
|
|
||||||
- name: Compress
|
- name: Compress
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
@ -84,9 +79,6 @@ jobs:
|
||||||
name: tnc-artifact
|
name: tnc-artifact
|
||||||
path: ./tnc/dist/compressed/*
|
path: ./tnc/dist/compressed/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Copy TNC to GUI
|
- name: Copy TNC to GUI
|
||||||
if: matrix.os == 'macos-10.15'
|
if: matrix.os == 'macos-10.15'
|
||||||
run: |
|
run: |
|
||||||
|
@ -113,6 +105,3 @@ jobs:
|
||||||
# release the app after building
|
# release the app after building
|
||||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
args: "-p always"
|
args: "-p always"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
40
.github/workflows/ctest.yml
vendored
40
.github/workflows/ctest.yml
vendored
|
@ -11,26 +11,26 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install octave octave-common octave-signal sox python3 python3-pip portaudio19-dev python3-pyaudio
|
sudo apt-get install octave octave-common octave-signal sox python3 python3-pip portaudio19-dev python3-pyaudio
|
||||||
pip3 install psutil crcengine ujson pyserial numpy structlog miniaudio sounddevice
|
pip3 install psutil crcengine ujson pyserial numpy structlog miniaudio sounddevice pytest
|
||||||
|
|
||||||
- name: Build codec2
|
- name: Build codec2
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/drowe67/codec2.git
|
git clone https://github.com/drowe67/codec2.git
|
||||||
cd codec2 && git checkout dr-tnc && git pull
|
cd codec2 && git checkout master # This should be pinned to a release
|
||||||
mkdir -p build_linux && cd build_linux && cmake .. && make
|
mkdir -p build_linux && cd build_linux && cmake .. && make
|
||||||
|
|
||||||
- name: run ctests
|
- name: run ctests
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: ${{github.workspace}}
|
working-directory: ${{github.workspace}}
|
||||||
run: |
|
run: |
|
||||||
mkdir build && cd build
|
mkdir build && cd build
|
||||||
cmake -DCODEC2_BUILD_DIR=$GITHUB_WORKSPACE/codec2/build_linux ..
|
cmake -DCODEC2_BUILD_DIR=$GITHUB_WORKSPACE/codec2/build_linux ..
|
||||||
ctest --output-on-failure
|
ctest --output-on-failure
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,3 +6,4 @@ tnc/codec2
|
||||||
**/Testing
|
**/Testing
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -24,16 +24,80 @@ set(TESTFRAMES 3)
|
||||||
|
|
||||||
add_test(NAME audio_buffer
|
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;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
python3 test_audiobuffer.py")
|
python3 test_audiobuffer.py")
|
||||||
set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
|
set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
|
||||||
|
|
||||||
add_test(NAME resampler
|
add_test(NAME resampler
|
||||||
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
python3 t48_8_short.py")
|
python3 test_resample_48_8.py")
|
||||||
set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
|
set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
|
||||||
|
|
||||||
|
add_test(NAME tnc_state_machine
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 test_tnc_states.py")
|
||||||
|
set_tests_properties(tnc_state_machine PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
|
||||||
|
|
||||||
|
add_test(NAME tnc_irs_iss
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 test_tnc.py")
|
||||||
|
set_tests_properties(tnc_irs_iss PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
|
||||||
|
|
||||||
|
add_test(NAME helper_routines
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 test_helpers.py")
|
||||||
|
set_tests_properties(helper_routines PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
|
||||||
|
|
||||||
|
add_test(NAME py_highsnr_stdio_P_P_multi
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
|
export BURSTS=${BURSTS};
|
||||||
|
export FRAMESPERBURST=${FRAMESPERBURST};
|
||||||
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 test_highsnr_stdio_P_P_multi.py")
|
||||||
|
set_tests_properties(py_highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
|
||||||
|
|
||||||
|
add_test(NAME py_highsnr_stdio_P_P_datacx
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
|
export BURSTS=${BURSTS};
|
||||||
|
export FRAMESPERBURST=${FRAMESPERBURST};
|
||||||
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 test_highsnr_stdio_P_P_datacx.py")
|
||||||
|
set_tests_properties(py_highsnr_stdio_P_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
||||||
|
|
||||||
|
add_test(NAME py_highsnr_stdio_P_C_datacx
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
|
export BURSTS=${BURSTS};
|
||||||
|
export FRAMESPERBURST=${FRAMESPERBURST};
|
||||||
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 test_highsnr_stdio_P_C_datacx.py")
|
||||||
|
set_tests_properties(py_highsnr_stdio_P_C_datacx PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
|
||||||
|
|
||||||
|
add_test(NAME py_highsnr_stdio_C_P_datacx
|
||||||
|
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
|
||||||
|
export PYTHONPATH=../tnc;
|
||||||
|
export BURSTS=${BURSTS};
|
||||||
|
export FRAMESPERBURST=${FRAMESPERBURST};
|
||||||
|
export TESTFRAMES=${TESTFRAMES};
|
||||||
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
|
python3 test_highsnr_stdio_C_P_datacx.py")
|
||||||
|
set_tests_properties(py_highsnr_stdio_C_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
||||||
|
|
||||||
add_test(NAME highsnr_stdio_P_C_single
|
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;
|
||||||
|
@ -59,7 +123,7 @@ add_test(NAME highsnr_stdio_P_P_single
|
||||||
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
|
||||||
python3 test_rx.py --debug --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
|
python3 test_rx.py --debug --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
|
||||||
set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
|
||||||
|
|
||||||
add_test(NAME highsnr_stdio_P_P_multi
|
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;
|
||||||
|
@ -68,7 +132,6 @@ add_test(NAME highsnr_stdio_P_P_multi
|
||||||
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(highsnr_stdio_P_P_multi 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 as we don't have a virtual sound card
|
# 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})
|
||||||
|
|
||||||
|
@ -127,7 +190,7 @@ add_test(NAME highsnr_virtual5_P_P_multi_callback_outside
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
./test_virtual4b.sh")
|
./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")
|
set_tests_properties(highsnr_virtual5_P_P_multi_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4")
|
||||||
|
|
||||||
# ARQ test short
|
# ARQ test short
|
||||||
|
|
||||||
add_test(NAME highsnr_ARQ_short
|
add_test(NAME highsnr_ARQ_short
|
||||||
|
@ -135,11 +198,11 @@ add_test(NAME highsnr_ARQ_short
|
||||||
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
|
||||||
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
|
||||||
python3 test_arq_short.py")
|
python3 test_arq_short.py")
|
||||||
|
|
||||||
set_tests_properties(highsnr_ARQ_short PROPERTIES PASS_REGULAR_EXPRESSION "ARQ | TX | DATA TRANSMITTED!")
|
set_tests_properties(highsnr_ARQ_short PROPERTIES PASS_REGULAR_EXPRESSION "ARQ | TX | DATA TRANSMITTED!")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
@ -30,4 +30,3 @@ Download the latest developer release from the releases section, unpack it and j
|
||||||
## Installation
|
## Installation
|
||||||
Please check the [wiki](https://wiki.freedata.app) for installation instructions
|
Please check the [wiki](https://wiki.freedata.app) for installation instructions
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "FreeDATA",
|
"name": "FreeDATA",
|
||||||
"version": "0.4.0-alpha.8",
|
"version": "0.4.0-alpha.9",
|
||||||
"description": "FreeDATA ",
|
"description": "FreeDATA ",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -511,8 +511,9 @@ update_chat = function(obj) {
|
||||||
`;
|
`;
|
||||||
var controlarea_receive = '';
|
var controlarea_receive = '';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
console.log("error with database parsing...")
|
console.log("error with database parsing...")
|
||||||
|
console.log(err)
|
||||||
}
|
}
|
||||||
// CALLSIGN LIST
|
// CALLSIGN LIST
|
||||||
if (!(document.getElementById('chat-' + dxcallsign + '-list'))) {
|
if (!(document.getElementById('chat-' + dxcallsign + '-list'))) {
|
||||||
|
|
|
@ -1,4 +1,52 @@
|
||||||
|
|
||||||
|
# Unit Test Menu
|
||||||
|
|
||||||
|
The following `CTest` tests cover some TNC functionality and the interface to codec2:
|
||||||
|
1. Name: `audio_buffer`
|
||||||
|
Tests the thread safety of the audio buffer routines.
|
||||||
|
1. Name: `resampler`
|
||||||
|
Tests FreeDATA audio resampling from 48KHz to 8KHz.
|
||||||
|
1. Name: `tnc_state_machine`
|
||||||
|
Tests TNC transitions between states.
|
||||||
|
1. Name: `helper_routines`
|
||||||
|
Tests various helper routines.
|
||||||
|
1. Name: `py_highsnr_stdio_P_P_multi`
|
||||||
|
Tests a high signal-to-noise ratio (good quality) audio path using multiple codecs. (Pure python.)
|
||||||
|
1. Name: `py_highsnr_stdio_P_P_datacx`
|
||||||
|
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
|
||||||
|
1. Name: `py_highsnr_stdio_P_C_datacx`
|
||||||
|
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
|
||||||
|
1. Name: `py_highsnr_stdio_C_P_datacx`
|
||||||
|
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
|
||||||
|
1. Name: `highsnr_stdio_P_C_single`
|
||||||
|
Tests compatibility with FreeDATA's transmit and freedv's raw data receive.
|
||||||
|
1. Name: `highsnr_stdio_C_P_single`
|
||||||
|
Tests compatibility with freedv's raw data transmit and FreeDATA's receive.
|
||||||
|
1. Name: `highsnr_stdio_P_P_single`
|
||||||
|
Tests a high signal-to-noise ratio audio path using multiple codecs. (Requires POSIX system.)
|
||||||
|
1. Name: `highsnr_stdio_P_P_multi`
|
||||||
|
Tests a high signal-to-noise ratio audio path using multiple codecs. (Requires POSIX system.)
|
||||||
|
|
||||||
|
The following tests can not currently be run with GitHub's pipeline as they require the ALSA dummy device
|
||||||
|
kernel module to be installed. They also do not perform reliably. These tests are slowly being
|
||||||
|
replaced with equivalent pipeline-compatible tests.
|
||||||
|
1. Name: `highsnr_virtual1_P_P_single_alsa`
|
||||||
|
Tests a high signal-to-noise ratio audio path using a single codec directly over an ALSA dummy device.
|
||||||
|
1. Name: `highsnr_virtual2_P_P_single`
|
||||||
|
Tests a high signal-to-noise ratio audio path using a single codec over an ALSA dummy device.
|
||||||
|
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
|
||||||
|
1. Name: `highsnr_virtual3_P_P_multi`
|
||||||
|
Tests a high signal-to-noise ratio audio path using multiple codecs over an ALSA dummy device.
|
||||||
|
1. Name: `highsnr_virtual4_P_P_single_callback`
|
||||||
|
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
|
||||||
|
1. Name: `highsnr_virtual4_P_P_single_callback_outside`
|
||||||
|
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
|
||||||
|
1. Name: `highsnr_virtual5_P_P_multi_callback`
|
||||||
|
1. Name: `highsnr_virtual5_P_P_multi_callback_outside`
|
||||||
|
1. Name: `highsnr_ARQ_short`
|
||||||
|
**Not functional**, it is an obsolete or not yet completed test.
|
||||||
|
|
||||||
|
|
||||||
# Instructions
|
# Instructions
|
||||||
|
|
||||||
1. Install:
|
1. Install:
|
||||||
|
@ -58,7 +106,7 @@ The virtual audio devices are great for testing, but they are also a little bit
|
||||||
|
|
||||||
1. Create virtual audio devices. Note: This command needs to be run again after every reboot
|
1. Create virtual audio devices. Note: This command needs to be run again after every reboot
|
||||||
```
|
```
|
||||||
sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2
|
sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Check if devices have been created
|
1. Check if devices have been created
|
||||||
|
@ -81,7 +129,7 @@ The virtual audio devices are great for testing, but they are also a little bit
|
||||||
Sub-Geräte: 1/1
|
Sub-Geräte: 1/1
|
||||||
Sub-Gerät #0: subdevice #0
|
Sub-Gerät #0: subdevice #0
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Determine the audio device number you would like to use:
|
1. Determine the audio device number you would like to use:
|
||||||
```
|
```
|
||||||
python3 test_rx.py --list
|
python3 test_rx.py --list
|
||||||
|
|
249
test/ping.py
249
test/ping.py
|
@ -1,113 +1,117 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import ctypes
|
||||||
|
import pathlib
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
import pyaudio
|
||||||
parser = argparse.ArgumentParser(description='Simons TEST TNC')
|
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=0, type=int)
|
|
||||||
parser.add_argument('--frames', dest="N_FRAMES_PER_BURST", default=0, type=int)
|
|
||||||
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=0, type=int)
|
|
||||||
parser.add_argument('--txmode', dest="FREEDV_TX_MODE", default=0, type=int)
|
|
||||||
parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int)
|
|
||||||
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
|
|
||||||
parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
|
|
||||||
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description="Simons TEST TNC")
|
||||||
|
parser.add_argument("--bursts", dest="N_BURSTS", default=0, type=int)
|
||||||
|
parser.add_argument("--frames", dest="N_FRAMES_PER_BURST", default=0, type=int)
|
||||||
|
parser.add_argument("--delay", dest="DELAY_BETWEEN_BURSTS", default=0, type=int)
|
||||||
|
parser.add_argument("--txmode", dest="FREEDV_TX_MODE", default=0, type=int)
|
||||||
|
parser.add_argument("--rxmode", dest="FREEDV_RX_MODE", default=0, type=int)
|
||||||
|
parser.add_argument("--audiooutput", dest="AUDIO_OUTPUT", default=0, type=int)
|
||||||
|
parser.add_argument("--audioinput", dest="AUDIO_INPUT", default=0, type=int)
|
||||||
|
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
|
||||||
N_BURSTS = args.N_BURSTS
|
N_BURSTS = args.N_BURSTS
|
||||||
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
|
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
|
||||||
|
|
||||||
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT
|
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT
|
||||||
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT
|
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT
|
||||||
|
|
||||||
# 1024 good for mode 6
|
# 1024 good for mode 6
|
||||||
AUDIO_FRAMES_PER_BUFFER = 2048
|
AUDIO_FRAMES_PER_BUFFER = 2048
|
||||||
MODEM_SAMPLE_RATE = 8000
|
MODEM_SAMPLE_RATE = 8000
|
||||||
|
|
||||||
FREEDV_TX_MODE = args.FREEDV_TX_MODE
|
FREEDV_TX_MODE = args.FREEDV_TX_MODE
|
||||||
FREEDV_RX_MODE = args.FREEDV_RX_MODE
|
FREEDV_RX_MODE = args.FREEDV_RX_MODE
|
||||||
|
|
||||||
DEBUGGING_MODE = args.DEBUGGING_MODE
|
DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
#-------------------------------------------- LOAD FREEDV
|
# -------------------------------------------- LOAD FREEDV
|
||||||
libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so"
|
libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so"
|
||||||
c_lib = ctypes.CDLL(libname)
|
c_lib = ctypes.CDLL(str(libname))
|
||||||
|
|
||||||
#--------------------------------------------CREATE PYAUDIO INSTANCE
|
# --------------------------------------------CREATE PYAUDIO INSTANCE
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
#--------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
|
# --------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
|
||||||
#AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
|
# AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
|
||||||
#AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
|
# AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
|
||||||
AUDIO_SAMPLE_RATE_TX = 8000
|
AUDIO_SAMPLE_RATE_TX = 8000
|
||||||
AUDIO_SAMPLE_RATE_RX = 8000
|
AUDIO_SAMPLE_RATE_RX = 8000
|
||||||
#--------------------------------------------OPEN AUDIO CHANNEL TX
|
# --------------------------------------------OPEN AUDIO CHANNEL TX
|
||||||
|
|
||||||
|
stream_tx = p.open(
|
||||||
|
format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=AUDIO_SAMPLE_RATE_TX,
|
||||||
|
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples
|
||||||
|
output=True,
|
||||||
|
output_device_index=AUDIO_OUTPUT_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
stream_rx = p.open(
|
||||||
|
format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=AUDIO_SAMPLE_RATE_RX,
|
||||||
|
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=True,
|
||||||
|
input_device_index=AUDIO_INPUT_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
stream_tx = p.open(format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=AUDIO_SAMPLE_RATE_TX,
|
|
||||||
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples
|
|
||||||
output=True,
|
|
||||||
output_device_index=AUDIO_OUTPUT_DEVICE,
|
|
||||||
)
|
|
||||||
|
|
||||||
stream_rx = p.open(format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=AUDIO_SAMPLE_RATE_RX,
|
|
||||||
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
|
|
||||||
input=True,
|
|
||||||
input_device_index=AUDIO_INPUT_DEVICE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def receive():
|
def receive():
|
||||||
|
|
||||||
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
||||||
freedv = c_lib.freedv_open(FREEDV_RX_MODE)
|
freedv = c_lib.freedv_open(FREEDV_RX_MODE)
|
||||||
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
|
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||||
payload_per_frame = bytes_per_frame -2
|
payload_per_frame = bytes_per_frame - 2
|
||||||
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
|
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
|
||||||
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2
|
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(
|
||||||
|
freedv
|
||||||
|
) # get n_tx_modem_samples which defines the size of the modulation object # --> *2
|
||||||
|
|
||||||
|
bytes_out = ctypes.c_ubyte * bytes_per_frame # bytes_per_frame
|
||||||
|
bytes_out = bytes_out() # get pointer from bytes_out
|
||||||
|
|
||||||
bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame
|
|
||||||
bytes_out = bytes_out() #get pointer from bytes_out
|
|
||||||
|
|
||||||
total_n_bytes = 0
|
total_n_bytes = 0
|
||||||
rx_total_frames = 0
|
rx_total_frames = 0
|
||||||
rx_frames = 0
|
rx_frames = 0
|
||||||
rx_bursts = 0
|
rx_bursts = 0
|
||||||
receive = True
|
receive = True
|
||||||
while receive == True:
|
while receive:
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
||||||
nin = c_lib.freedv_nin(freedv)
|
nin = c_lib.freedv_nin(freedv)
|
||||||
nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE))
|
nin_converted = int(nin * (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE))
|
||||||
if DEBUGGING_MODE == True:
|
if DEBUGGING_MODE:
|
||||||
print("-----------------------------")
|
print("-----------------------------")
|
||||||
print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]")
|
print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]")
|
||||||
|
|
||||||
data_in = stream_rx.read(nin_converted, exception_on_overflow = False)
|
data_in = stream_rx.read(nin_converted, exception_on_overflow=False)
|
||||||
data_in = data_in.rstrip(b'\x00')
|
data_in = data_in.rstrip(b"\x00")
|
||||||
|
|
||||||
c_lib.freedv_rawdatarx.argtype = [ctypes.POINTER(ctypes.c_ubyte), bytes_out, data_in] # check if really neccessary
|
c_lib.freedv_rawdatarx.argtype = [
|
||||||
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
|
ctypes.POINTER(ctypes.c_ubyte),
|
||||||
|
bytes_out,
|
||||||
|
data_in,
|
||||||
|
] # check if really neccessary
|
||||||
|
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
|
||||||
total_n_bytes = total_n_bytes + nbytes
|
total_n_bytes = total_n_bytes + nbytes
|
||||||
if DEBUGGING_MODE == True:
|
if DEBUGGING_MODE:
|
||||||
print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv)))
|
print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv)))
|
||||||
|
|
||||||
if nbytes == bytes_per_frame:
|
if nbytes == bytes_per_frame:
|
||||||
rx_total_frames = rx_total_frames + 1
|
rx_total_frames = rx_total_frames + 1
|
||||||
rx_frames = rx_frames + 1
|
rx_frames = rx_frames + 1
|
||||||
|
@ -115,84 +119,111 @@ def receive():
|
||||||
if rx_frames == N_FRAMES_PER_BURST:
|
if rx_frames == N_FRAMES_PER_BURST:
|
||||||
rx_frames = 0
|
rx_frames = 0
|
||||||
rx_bursts = rx_bursts + 1
|
rx_bursts = rx_bursts + 1
|
||||||
c_lib.freedv_set_sync(freedv,0)
|
c_lib.freedv_set_sync(freedv, 0)
|
||||||
|
|
||||||
|
|
||||||
burst = bytes_out[0]
|
burst = bytes_out[0]
|
||||||
n_total_burst = bytes_out[1]
|
n_total_burst = bytes_out[1]
|
||||||
frame = bytes_out[2]
|
frame = bytes_out[2]
|
||||||
n_total_frame = bytes_out[3]
|
n_total_frame = bytes_out[3]
|
||||||
|
|
||||||
|
print(
|
||||||
print("RX | PONG | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "]")
|
"RX | PONG | BURST ["
|
||||||
|
+ str(burst)
|
||||||
|
+ "/"
|
||||||
|
+ str(n_total_burst)
|
||||||
|
+ "] FRAME ["
|
||||||
|
+ str(frame)
|
||||||
|
+ "/"
|
||||||
|
+ str(n_total_frame)
|
||||||
|
+ "]"
|
||||||
|
)
|
||||||
print("-----------------------------------------------------------------")
|
print("-----------------------------------------------------------------")
|
||||||
c_lib.freedv_set_sync(freedv,0)
|
c_lib.freedv_set_sync(freedv, 0)
|
||||||
|
|
||||||
|
|
||||||
if rx_bursts == N_BURSTS:
|
if rx_bursts == N_BURSTS:
|
||||||
receive = False
|
receive = False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
RECEIVE = threading.Thread(target=receive, name="RECEIVE THREAD")
|
RECEIVE = threading.Thread(target=receive, name="RECEIVE THREAD")
|
||||||
RECEIVE.start()
|
RECEIVE.start()
|
||||||
|
|
||||||
|
|
||||||
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
||||||
freedv = c_lib.freedv_open(FREEDV_TX_MODE)
|
freedv = c_lib.freedv_open(FREEDV_TX_MODE)
|
||||||
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
|
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||||
payload_per_frame = bytes_per_frame -2
|
payload_per_frame = bytes_per_frame - 2
|
||||||
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
|
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
|
||||||
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2
|
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(
|
||||||
|
freedv
|
||||||
|
) # get n_tx_modem_samples which defines the size of the modulation object # --> *2
|
||||||
|
|
||||||
mod_out = ctypes.c_short * n_tx_modem_samples
|
mod_out = ctypes.c_short * n_tx_modem_samples
|
||||||
mod_out = mod_out()
|
mod_out = mod_out()
|
||||||
mod_out_preamble = ctypes.c_short * (1760*2) #1760 for mode 10,11,12 #4000 for mode 9
|
mod_out_preamble = ctypes.c_short * (
|
||||||
|
1760 * 2
|
||||||
|
) # 1760 for mode 10,11,12 #4000 for mode 9
|
||||||
mod_out_preamble = mod_out_preamble()
|
mod_out_preamble = mod_out_preamble()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST) )
|
print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST))
|
||||||
print("-----------------------------------------------------------------")
|
print("-----------------------------------------------------------------")
|
||||||
|
|
||||||
for i in range(0,N_BURSTS):
|
for i in range(N_BURSTS):
|
||||||
|
|
||||||
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble);
|
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
|
|
||||||
txbuffer = bytearray()
|
txbuffer = bytearray()
|
||||||
txbuffer += bytes(mod_out_preamble)
|
txbuffer += bytes(mod_out_preamble)
|
||||||
|
|
||||||
for n in range(0,N_FRAMES_PER_BURST):
|
for n in range(N_FRAMES_PER_BURST):
|
||||||
|
|
||||||
data_out = bytearray()
|
data_out = bytearray()
|
||||||
data_out += bytes([i+1])
|
data_out += bytes([i + 1])
|
||||||
data_out += bytes([N_BURSTS])
|
data_out += bytes([N_BURSTS])
|
||||||
data_out += bytes([n+1])
|
data_out += bytes([n + 1])
|
||||||
data_out += bytes([N_FRAMES_PER_BURST])
|
data_out += bytes([N_FRAMES_PER_BURST])
|
||||||
|
|
||||||
buffer = bytearray(payload_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
buffer = bytearray(
|
||||||
buffer[:len(data_out)] = data_out # set buffersize to length of data which will be send
|
payload_per_frame
|
||||||
|
) # use this if CRC16 checksum is required ( DATA1-3)
|
||||||
|
buffer[
|
||||||
|
: len(data_out)
|
||||||
|
] = data_out # set buffersize to length of data which will be send
|
||||||
|
|
||||||
|
crc = ctypes.c_ushort(
|
||||||
|
c_lib.freedv_gen_crc16(bytes(buffer), payload_per_frame)
|
||||||
|
) # generate CRC16
|
||||||
|
crc = crc.value.to_bytes(2, byteorder="big") # convert crc to 2 byte hex string
|
||||||
|
buffer += crc # append crc16 to buffer
|
||||||
|
|
||||||
crc = ctypes.c_ushort(c_lib.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16
|
|
||||||
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
|
||||||
buffer += crc # append crc16 to buffer
|
|
||||||
|
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
c_lib.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and safe it into mod_out pointer
|
c_lib.freedv_rawdatatx(
|
||||||
|
freedv, mod_out, data
|
||||||
|
) # modulate DATA and safe it into mod_out pointer
|
||||||
|
|
||||||
txbuffer += bytes(mod_out)
|
txbuffer += bytes(mod_out)
|
||||||
|
|
||||||
print("TX | PING | BURST [" + str(i+1) + "/" + str(N_BURSTS) + "] FRAME [" + str(n+1) + "/" + str(N_FRAMES_PER_BURST) + "]")
|
print(
|
||||||
|
"TX | PING | BURST ["
|
||||||
|
+ str(i + 1)
|
||||||
|
+ "/"
|
||||||
|
+ str(N_BURSTS)
|
||||||
|
+ "] FRAME ["
|
||||||
|
+ str(n + 1)
|
||||||
|
+ "/"
|
||||||
|
+ str(N_FRAMES_PER_BURST)
|
||||||
|
+ "]"
|
||||||
|
)
|
||||||
stream_tx.write(bytes(txbuffer))
|
stream_tx.write(bytes(txbuffer))
|
||||||
ACK_TIMEOUT = time.time() + 3
|
ACK_TIMEOUT = time.time() + 3
|
||||||
txbuffer = bytearray()
|
txbuffer = bytearray()
|
||||||
|
|
||||||
#time.sleep(DELAY_BETWEEN_BURSTS)
|
# time.sleep(DELAY_BETWEEN_BURSTS)
|
||||||
|
|
||||||
# WAIT UNTIL WE RECEIVD AN ACK/DATAC0 FRAME
|
# WAIT UNTIL WE RECEIVD AN ACK/DATAC0 FRAME
|
||||||
while ACK_TIMEOUT >= time.time():
|
while ACK_TIMEOUT >= time.time():
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
stream_tx.close()
|
stream_tx.close()
|
||||||
|
|
86
test/pong.py
86
test/pong.py
|
@ -17,17 +17,17 @@ import threading
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
#--------------------------------------------GET PARAMETER INPUTS
|
||||||
parser = argparse.ArgumentParser(description='Simons TEST TNC')
|
parser = argparse.ArgumentParser(description='Simons TEST TNC')
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=0, type=int)
|
parser.add_argument('--bursts', dest="N_BURSTS", default=0, type=int)
|
||||||
parser.add_argument('--frames', dest="N_FRAMES_PER_BURST", default=0, type=int)
|
parser.add_argument('--frames', dest="N_FRAMES_PER_BURST", default=0, type=int)
|
||||||
parser.add_argument('--txmode', dest="FREEDV_TX_MODE", default=0, type=int)
|
parser.add_argument('--txmode', dest="FREEDV_TX_MODE", default=0, type=int)
|
||||||
parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int)
|
parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int)
|
||||||
parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
|
parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
|
||||||
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
|
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
|
||||||
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
N_BURSTS = args.N_BURSTS
|
N_BURSTS = args.N_BURSTS
|
||||||
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
@ -41,20 +41,20 @@ FREEDV_RX_MODE = args.FREEDV_RX_MODE
|
||||||
DEBUGGING_MODE = args.DEBUGGING_MODE
|
DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
|
|
||||||
# 1024 good for mode 6
|
# 1024 good for mode 6
|
||||||
AUDIO_FRAMES_PER_BUFFER = 2048
|
AUDIO_FRAMES_PER_BUFFER = 2048
|
||||||
MODEM_SAMPLE_RATE = 8000
|
MODEM_SAMPLE_RATE = 8000
|
||||||
|
|
||||||
#-------------------------------------------- LOAD FREEDV
|
#-------------------------------------------- LOAD FREEDV
|
||||||
libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so"
|
libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so"
|
||||||
c_lib = ctypes.CDLL(libname)
|
c_lib = ctypes.CDLL(libname)
|
||||||
#--------------------------------------------CREATE PYAUDIO INSTANCE
|
#--------------------------------------------CREATE PYAUDIO INSTANCE
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
#--------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
|
#--------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
|
||||||
|
|
||||||
#AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
|
#AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
|
||||||
#AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
|
#AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
|
||||||
AUDIO_SAMPLE_RATE_TX = 8000
|
AUDIO_SAMPLE_RATE_TX = 8000
|
||||||
AUDIO_SAMPLE_RATE_RX = 8000
|
AUDIO_SAMPLE_RATE_RX = 8000
|
||||||
#--------------------------------------------OPEN AUDIO CHANNEL RX
|
#--------------------------------------------OPEN AUDIO CHANNEL RX
|
||||||
|
|
||||||
stream_tx = p.open(format=pyaudio.paInt16,
|
stream_tx = p.open(format=pyaudio.paInt16,
|
||||||
|
@ -62,22 +62,22 @@ stream_tx = p.open(format=pyaudio.paInt16,
|
||||||
rate=AUDIO_SAMPLE_RATE_TX,
|
rate=AUDIO_SAMPLE_RATE_TX,
|
||||||
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples
|
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples
|
||||||
output=True,
|
output=True,
|
||||||
output_device_index=AUDIO_OUTPUT_DEVICE,
|
output_device_index=AUDIO_OUTPUT_DEVICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
stream_rx = p.open(format=pyaudio.paInt16,
|
stream_rx = p.open(format=pyaudio.paInt16,
|
||||||
channels=1,
|
channels=1,
|
||||||
rate=AUDIO_SAMPLE_RATE_RX,
|
rate=AUDIO_SAMPLE_RATE_RX,
|
||||||
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
|
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
|
||||||
input=True,
|
input=True,
|
||||||
input_device_index=AUDIO_INPUT_DEVICE,
|
input_device_index=AUDIO_INPUT_DEVICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# GENERAL PARAMETERS
|
# GENERAL PARAMETERS
|
||||||
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
||||||
|
|
||||||
|
|
||||||
def send_pong(burst,n_total_burst,frame,n_total_frame):
|
def send_pong(burst,n_total_burst,frame,n_total_frame):
|
||||||
|
|
||||||
data_out = bytearray()
|
data_out = bytearray()
|
||||||
|
@ -85,47 +85,47 @@ def send_pong(burst,n_total_burst,frame,n_total_frame):
|
||||||
data_out[1:2] = bytes([n_total_burst])
|
data_out[1:2] = bytes([n_total_burst])
|
||||||
data_out[2:3] = bytes([frame])
|
data_out[2:3] = bytes([frame])
|
||||||
data_out[4:5] = bytes([n_total_frame])
|
data_out[4:5] = bytes([n_total_frame])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
|
||||||
freedv = c_lib.freedv_open(FREEDV_TX_MODE)
|
freedv = c_lib.freedv_open(FREEDV_TX_MODE)
|
||||||
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
|
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
|
||||||
payload_per_frame = bytes_per_frame -2
|
payload_per_frame = bytes_per_frame -2
|
||||||
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
|
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
|
||||||
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2
|
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2
|
||||||
|
|
||||||
mod_out = ctypes.c_short * n_tx_modem_samples
|
mod_out = ctypes.c_short * n_tx_modem_samples
|
||||||
mod_out = mod_out()
|
mod_out = mod_out()
|
||||||
mod_out_preamble = ctypes.c_short * (1760*2) #1760 for mode 10,11,12 #4000 for mode 9
|
mod_out_preamble = ctypes.c_short * (1760*2) #1760 for mode 10,11,12 #4000 for mode 9
|
||||||
mod_out_preamble = mod_out_preamble()
|
mod_out_preamble = mod_out_preamble()
|
||||||
|
|
||||||
buffer = bytearray(payload_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
buffer = bytearray(payload_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
||||||
buffer[:len(data_out)] = data_out # set buffersize to length of data which will be send
|
buffer[:len(data_out)] = data_out # set buffersize to length of data which will be send
|
||||||
|
|
||||||
crc = ctypes.c_ushort(c_lib.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16
|
crc = ctypes.c_ushort(c_lib.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16
|
||||||
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
||||||
buffer += crc # append crc16 to buffer
|
buffer += crc # append crc16 to buffer
|
||||||
|
|
||||||
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble);
|
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble);
|
||||||
txbuffer = bytearray()
|
txbuffer = bytearray()
|
||||||
txbuffer += bytes(mod_out_preamble)
|
txbuffer += bytes(mod_out_preamble)
|
||||||
|
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
c_lib.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and safe it into mod_out pointer
|
c_lib.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and safe it into mod_out pointer
|
||||||
|
|
||||||
txbuffer += bytes(mod_out)
|
txbuffer += bytes(mod_out)
|
||||||
stream_tx.write(bytes(txbuffer))
|
stream_tx.write(bytes(txbuffer))
|
||||||
|
|
||||||
txbuffer = bytearray()
|
txbuffer = bytearray()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# DATA CHANNEL INITIALISATION
|
# DATA CHANNEL INITIALISATION
|
||||||
|
|
||||||
freedv = c_lib.freedv_open(FREEDV_RX_MODE)
|
freedv = c_lib.freedv_open(FREEDV_RX_MODE)
|
||||||
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
|
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8)
|
||||||
n_max_modem_samples = c_lib.freedv_get_n_max_modem_samples(freedv)
|
n_max_modem_samples = c_lib.freedv_get_n_max_modem_samples(freedv)
|
||||||
bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame
|
bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame
|
||||||
bytes_out = bytes_out() #get pointer from bytes_out
|
bytes_out = bytes_out() #get pointer from bytes_out
|
||||||
|
|
||||||
|
@ -136,31 +136,31 @@ while receive == True:
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
||||||
data_in = b''
|
data_in = b''
|
||||||
|
|
||||||
nin = c_lib.freedv_nin(freedv)
|
nin = c_lib.freedv_nin(freedv)
|
||||||
nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE))
|
nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE))
|
||||||
if DEBUGGING_MODE == True:
|
if DEBUGGING_MODE == True:
|
||||||
print("-----------------------------")
|
print("-----------------------------")
|
||||||
print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]")
|
print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]")
|
||||||
|
|
||||||
data_in = stream_rx.read(nin_converted, exception_on_overflow = False)
|
data_in = stream_rx.read(nin_converted, exception_on_overflow = False)
|
||||||
data_in = data_in.rstrip(b'\x00')
|
data_in = data_in.rstrip(b'\x00')
|
||||||
|
|
||||||
c_lib.freedv_rawdatarx.argtype = [ctypes.POINTER(ctypes.c_ubyte), bytes_out, data_in] # check if really neccessary
|
c_lib.freedv_rawdatarx.argtype = [ctypes.POINTER(ctypes.c_ubyte), bytes_out, data_in] # check if really neccessary
|
||||||
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
|
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
|
||||||
|
|
||||||
if DEBUGGING_MODE == True:
|
if DEBUGGING_MODE == True:
|
||||||
print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv)))
|
print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv)))
|
||||||
|
|
||||||
if nbytes == bytes_per_frame:
|
if nbytes == bytes_per_frame:
|
||||||
|
|
||||||
burst = bytes_out[0]
|
burst = bytes_out[0]
|
||||||
n_total_burst = bytes_out[1]
|
n_total_burst = bytes_out[1]
|
||||||
frame = bytes_out[2]
|
frame = bytes_out[2]
|
||||||
n_total_frame = bytes_out[3]
|
n_total_frame = bytes_out[3]
|
||||||
print("RX | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "] >>> SENDING PONG")
|
print("RX | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "] >>> SENDING PONG")
|
||||||
|
|
||||||
TRANSMIT_PONG = threading.Thread(target=send_pong, args=[burst,n_total_burst,frame,n_total_frame], name="SEND PONG")
|
TRANSMIT_PONG = threading.Thread(target=send_pong, args=[burst,n_total_burst,frame,n_total_frame], name="SEND PONG")
|
||||||
TRANSMIT_PONG.start()
|
TRANSMIT_PONG.start()
|
||||||
|
|
||||||
c_lib.freedv_set_sync(freedv,0)
|
c_lib.freedv_set_sync(freedv,0)
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Unit test for FreeDV API resampler functions, from
|
|
||||||
# codec2/unittest/t48_8_short.c - generate a sine wave at 8 KHz,
|
|
||||||
# upsample to 48 kHz, add an interferer, then downsample back to 8 kHz
|
|
||||||
#
|
|
||||||
# You can listen to the output files with:
|
|
||||||
#
|
|
||||||
# aplay -f S16_LE in8.raw
|
|
||||||
# aplay -r 48000 -f S16_LE out48.raw
|
|
||||||
# aplay -f S16_LE out8.raw
|
|
||||||
#
|
|
||||||
# They should sound like clean sine waves
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0,'..')
|
|
||||||
from tnc import codec2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# dig some constants out
|
|
||||||
FDMDV_OS_48 = codec2.api.FDMDV_OS_48
|
|
||||||
FDMDV_OS_TAPS_48K = codec2.api.FDMDV_OS_TAPS_48K
|
|
||||||
FDMDV_OS_TAPS_48_8K = codec2.api.FDMDV_OS_TAPS_48_8K
|
|
||||||
|
|
||||||
N8 = int(180) # processing buffer size at 8 kHz
|
|
||||||
N48 = int(N8*FDMDV_OS_48) # processing buffer size at 48 kHz
|
|
||||||
MEM8 = FDMDV_OS_TAPS_48_8K # 8kHz signal filter memory
|
|
||||||
MEM48 = FDMDV_OS_TAPS_48K # 48kHz signal filter memory
|
|
||||||
FRAMES = int(50) # number of frames to test
|
|
||||||
FS8 = 8000
|
|
||||||
FS48 = 48000
|
|
||||||
AMP = 16000 # sine wave amplitude
|
|
||||||
FTEST8 = 800 # input test frequency at FS=8kHz
|
|
||||||
FINTER48 = 10000 # interferer frequency at FS=48kHz
|
|
||||||
|
|
||||||
# Due to the design of these resamplers, the processing buffer (at 8kHz)
|
|
||||||
# must be an integer multiple of oversampling ratio
|
|
||||||
assert N8 % FDMDV_OS_48 == 0
|
|
||||||
|
|
||||||
# time indexes, we advance every frame
|
|
||||||
t = 0
|
|
||||||
t1 = 0
|
|
||||||
|
|
||||||
# output files to listen to/evaluate result
|
|
||||||
fin8 = open("in8.raw", mode='wb')
|
|
||||||
f48 = open("out48.raw", mode='wb')
|
|
||||||
fout8 = open("out8.raw", mode='wb')
|
|
||||||
|
|
||||||
resampler = codec2.resampler()
|
|
||||||
|
|
||||||
for f in range(FRAMES):
|
|
||||||
|
|
||||||
sine_in8k = (AMP*np.cos(2*np.pi*np.arange(t,t+N8)*FTEST8/FS8)).astype(np.int16)
|
|
||||||
t += N8
|
|
||||||
sine_in8k.tofile(fin8)
|
|
||||||
|
|
||||||
sine_out48k = resampler.resample8_to_48(sine_in8k)
|
|
||||||
sine_out48k.tofile(f48)
|
|
||||||
|
|
||||||
# add interfering sine wave (down sampling filter should remove)
|
|
||||||
sine_in48k = (sine_out48k + (AMP/2)*np.cos(2*np.pi*np.arange(t1,t1+N48)*FINTER48/FS48)).astype(np.int16)
|
|
||||||
t1 += N48
|
|
||||||
|
|
||||||
sine_out8k = resampler.resample48_to_8(sine_in48k)
|
|
||||||
sine_out8k.tofile(fout8)
|
|
||||||
|
|
||||||
fin8.close()
|
|
||||||
f48.close()
|
|
||||||
fout8.close()
|
|
||||||
|
|
||||||
# Automated test evaluation --------------------------------------------
|
|
||||||
|
|
||||||
# The input and output signals will not be time aligned due to the filter
|
|
||||||
# delays, so compare the magnitude spectrum
|
|
||||||
|
|
||||||
in8k = np.fromfile("in8.raw", dtype=np.int16)
|
|
||||||
out8k = np.fromfile("out8.raw", dtype=np.int16)
|
|
||||||
assert len(in8k) == len(out8k)
|
|
||||||
|
|
||||||
n = len(in8k)
|
|
||||||
|
|
||||||
h = np.hanning(len(in8k))
|
|
||||||
S1 = np.abs(np.fft.fft(in8k * h))
|
|
||||||
S2 = np.abs(np.fft.fft(out8k * h))
|
|
||||||
|
|
||||||
error = S1-S2
|
|
||||||
error_energy = np.dot(error,error)
|
|
||||||
ratio = error_energy/np.dot(S1,S1)
|
|
||||||
ratio_dB = 10*np.log10(ratio);
|
|
||||||
print("ratio_dB: %4.2f" % (ratio_dB));
|
|
||||||
threshdB = -40
|
|
||||||
if ratio_dB < threshdB:
|
|
||||||
print("PASS")
|
|
||||||
else:
|
|
||||||
print("FAIL")
|
|
|
@ -7,36 +7,64 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'..')
|
|
||||||
sys.path.insert(0,'../tnc')
|
|
||||||
import data_handler
|
|
||||||
import argparse
|
|
||||||
import codec2
|
import codec2
|
||||||
|
import data_handler
|
||||||
import modem
|
import modem
|
||||||
|
import pytest
|
||||||
parser = argparse.ArgumentParser(description='ARQ TEST')
|
import static
|
||||||
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
|
|
||||||
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}'
|
bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}'
|
||||||
|
|
||||||
# start data handler
|
|
||||||
data_handler.DATA()
|
|
||||||
|
|
||||||
# start modem
|
|
||||||
modem = modem.RF()
|
|
||||||
|
|
||||||
mode = codec2.freedv_get_mode(args.FREEDV_MODE)
|
|
||||||
print(mode)
|
|
||||||
n_frames_per_burst = args.N_FRAMES_PER_BURST
|
|
||||||
|
|
||||||
# enable testmode
|
@pytest.mark.parametrize("freedv_mode", ["datac0", "datac1", "datac3"])
|
||||||
data_handler.TESTMODE = True
|
@pytest.mark.parametrize("n_frames_per_burst", [1, 2, 3])
|
||||||
|
def test_highsnr_arq_short(freedv_mode: str, n_frames_per_burst: int):
|
||||||
|
t_mode = t_repeats = t_repeat_delay = 0
|
||||||
|
t_frames = []
|
||||||
|
|
||||||
# add command to data qeue
|
def t_tx_dummy(mode, repeats, repeat_delay, frames):
|
||||||
data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_FILE', bytes_out, mode, n_frames_per_burst])
|
"""Replacement function for transmit"""
|
||||||
|
print(f"t_tx_dummy: In transmit({mode}, {repeats}, {repeat_delay}, {frames})")
|
||||||
|
nonlocal t_mode, t_repeats, t_repeat_delay, t_frames
|
||||||
|
t_mode = mode
|
||||||
|
t_repeats = repeats
|
||||||
|
t_repeat_delay = repeat_delay
|
||||||
|
t_frames = frames[:]
|
||||||
|
static.TRANSMITTING = False
|
||||||
|
|
||||||
|
# Enable testmode
|
||||||
|
modem.TESTMODE = True
|
||||||
|
# Set some inner variables so the modules don't throw exceptions.
|
||||||
|
modem.RXCHANNEL = "/tmp/rxpipe"
|
||||||
|
modem.TXCHANNEL = "/tmp/txpipe"
|
||||||
|
data_handler.TESTMODE = True
|
||||||
|
static.HAMLIB_RADIOCONTROL = "disabled"
|
||||||
|
|
||||||
|
# start data handler
|
||||||
|
data_handler.DATA()
|
||||||
|
|
||||||
|
# start modem
|
||||||
|
t_modem = modem.RF()
|
||||||
|
|
||||||
|
# Replace transmit routine with our own, an effective No-Op.
|
||||||
|
t_modem.transmit = t_tx_dummy
|
||||||
|
|
||||||
|
mode = codec2.freedv_get_mode_value_by_name(freedv_mode)
|
||||||
|
print(mode)
|
||||||
|
|
||||||
|
# add command to data qeue
|
||||||
|
data_handler.DATA_QUEUE_TRANSMIT.put(
|
||||||
|
["ARQ_FILE", bytes_out, mode, n_frames_per_burst]
|
||||||
|
)
|
||||||
|
|
||||||
|
# This test isn't complete yet, or is obsolete.
|
||||||
|
assert False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
||||||
|
|
|
@ -3,57 +3,86 @@
|
||||||
#
|
#
|
||||||
# tests audio buffer thread safety
|
# tests audio buffer thread safety
|
||||||
|
|
||||||
|
# pylint: disable=global-statement, invalid-name
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'..')
|
|
||||||
from tnc import codec2
|
|
||||||
import threading
|
import threading
|
||||||
import numpy as np
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
import codec2
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
BUFFER_SZ = 1024
|
BUFFER_SZ = 1024
|
||||||
N_MAX = 100 # write a repeating sequence of 0..N_MAX-1
|
N_MAX = 100 # write a repeating sequence of 0..N_MAX-1
|
||||||
WRITE_SZ = 10 # different read and write sized buffers
|
WRITE_SZ = 10 # different read and write sized buffers
|
||||||
READ_SZ = 8
|
READ_SZ = 8
|
||||||
NTESTS = 10000
|
NTESTS = 10000
|
||||||
|
|
||||||
running = True
|
running = True
|
||||||
audio_buffer = codec2.audio_buffer(BUFFER_SZ)
|
audio_buffer = codec2.audio_buffer(BUFFER_SZ)
|
||||||
|
|
||||||
n_write = int(0)
|
n_write = 0
|
||||||
n_read = int(0)
|
|
||||||
|
|
||||||
def writer():
|
|
||||||
|
def t_writer():
|
||||||
|
"""
|
||||||
|
Subprocess to handle writes to the NumPY audio "device."
|
||||||
|
"""
|
||||||
global n_write
|
global n_write
|
||||||
print("writer starting")
|
print("writer starting")
|
||||||
n = int(0)
|
n = 0
|
||||||
buf = np.zeros(WRITE_SZ, dtype=np.int16)
|
buf = np.zeros(WRITE_SZ, dtype=np.int16)
|
||||||
while running:
|
while running:
|
||||||
nfree = audio_buffer.size - audio_buffer.nbuffer
|
nfree = audio_buffer.size - audio_buffer.nbuffer
|
||||||
if nfree >= WRITE_SZ:
|
if nfree >= WRITE_SZ:
|
||||||
for i in range(0,WRITE_SZ):
|
for index in range(WRITE_SZ):
|
||||||
buf[i] = n;
|
buf[index] = n
|
||||||
n += 1
|
n += 1
|
||||||
if n == N_MAX:
|
if n == N_MAX:
|
||||||
n = 0
|
n = 0
|
||||||
n_write += 1
|
n_write += 1
|
||||||
audio_buffer.push(buf)
|
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
|
def test_audiobuffer():
|
||||||
print("n_write: %d n_read: %d errors: %d " % (n_write, n_read, errors))
|
"""
|
||||||
|
Test for the audiobuffer
|
||||||
|
"""
|
||||||
|
global running
|
||||||
|
|
||||||
|
# Start the writer in a new thread.
|
||||||
|
writer_thread = threading.Thread(target=t_writer)
|
||||||
|
writer_thread.start()
|
||||||
|
|
||||||
|
n_out = n_read = errors = 0
|
||||||
|
for _ in range(NTESTS):
|
||||||
|
while audio_buffer.nbuffer < READ_SZ:
|
||||||
|
sleep(0.001)
|
||||||
|
for i in range(READ_SZ):
|
||||||
|
if audio_buffer.buffer[i] != n_out:
|
||||||
|
errors += 1
|
||||||
|
n_out += 1
|
||||||
|
if n_out == N_MAX:
|
||||||
|
n_out = 0
|
||||||
|
n_read += 1
|
||||||
|
audio_buffer.pop(READ_SZ)
|
||||||
|
|
||||||
|
print(f"n_write: {n_write} n_read: {n_read} errors: {errors} ")
|
||||||
|
|
||||||
|
# Indirectly stop the thread
|
||||||
|
running = False
|
||||||
|
sleep(0.1)
|
||||||
|
|
||||||
|
assert not writer_thread.is_alive()
|
||||||
|
assert n_write - n_read < BUFFER_SZ
|
||||||
|
assert errors == 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
||||||
|
|
|
@ -6,41 +6,54 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
@author: DJ2LS
|
@author: DJ2LS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'..')
|
import pyaudio
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
parser = argparse.ArgumentParser(description="FreeDATA audio test")
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
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("--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(
|
||||||
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
"--audiodev",
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
dest="AUDIO_INPUT_DEVICE",
|
||||||
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio device number to use",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
dest="TIMEOUT",
|
||||||
|
default=10,
|
||||||
|
type=int,
|
||||||
|
help="Timeout (seconds) before test ends",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.LIST:
|
if args.LIST:
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
for dev in range(0,p.get_device_count()):
|
for dev in range(p.get_device_count()):
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
class Test():
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.N_BURSTS = args.N_BURSTS
|
self.N_BURSTS = args.N_BURSTS
|
||||||
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
@ -49,65 +62,89 @@ class Test():
|
||||||
self.TIMEOUT = args.TIMEOUT
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
# AUDIO PARAMETERS
|
||||||
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
# v-- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400 * 2
|
||||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
self.AUDIO_SAMPLE_RATE_RX = 48000
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# make sure our resampler will work
|
# make sure our resampler will work
|
||||||
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
assert (
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
|
||||||
|
) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
if self.AUDIO_INPUT_DEVICE != -1:
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
self.p = pyaudio.PyAudio()
|
self.p = pyaudio.PyAudio()
|
||||||
# auto search for loopback devices
|
# auto search for loopback devices
|
||||||
if self.AUDIO_INPUT_DEVICE == -2:
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
loopback_list = []
|
loopback_list = []
|
||||||
for dev in range(0,self.p.get_device_count()):
|
for dev in range(self.p.get_device_count()):
|
||||||
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:
|
||||||
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
# 0 = RX 1 = TX
|
||||||
|
self.AUDIO_INPUT_DEVICE = loopback_list[0]
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
|
||||||
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
|
||||||
|
|
||||||
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=self.AUDIO_SAMPLE_RATE_RX,
|
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
|
||||||
input=True,
|
|
||||||
output=False,
|
|
||||||
input_device_index=self.AUDIO_INPUT_DEVICE,
|
|
||||||
stream_callback=self.callback
|
|
||||||
)
|
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stream_rx = self.p.open(
|
||||||
|
format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("test_callback_multimode_rx: Not written for STDIN usage.")
|
||||||
|
print("Exiting.")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
# open codec2 instance
|
# open codec2 instance
|
||||||
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
self.datac0_freedv = ctypes.cast(
|
||||||
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8)
|
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
|
||||||
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_bytes_per_frame = int(
|
||||||
self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv) / 8
|
||||||
|
)
|
||||||
self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
|
self.datac0_bytes_out = ctypes.create_string_buffer(self.datac0_bytes_per_frame)
|
||||||
self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8)
|
codec2.api.freedv_set_frames_per_burst(
|
||||||
self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame)
|
self.datac0_freedv, self.N_FRAMES_PER_BURST
|
||||||
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.datac0_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)
|
|
||||||
|
|
||||||
|
self.datac1_freedv = ctypes.cast(
|
||||||
|
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), ctypes.c_void_p
|
||||||
|
)
|
||||||
|
self.datac1_bytes_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv) / 8
|
||||||
|
)
|
||||||
|
self.datac1_bytes_out = ctypes.create_string_buffer(self.datac1_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(
|
||||||
|
self.datac1_freedv, self.N_FRAMES_PER_BURST
|
||||||
|
)
|
||||||
|
self.datac1_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
self.datac3_freedv = ctypes.cast(
|
||||||
|
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), ctypes.c_void_p
|
||||||
|
)
|
||||||
|
self.datac3_bytes_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv) / 8
|
||||||
|
)
|
||||||
|
self.datac3_bytes_out = ctypes.create_string_buffer(self.datac3_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(
|
||||||
|
self.datac3_freedv, self.N_FRAMES_PER_BURST
|
||||||
|
)
|
||||||
|
self.datac3_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
# SET COUNTERS
|
# SET COUNTERS
|
||||||
self.rx_total_frames_datac0 = 0
|
self.rx_total_frames_datac0 = 0
|
||||||
|
@ -127,36 +164,38 @@ class Test():
|
||||||
self.timeout = time.time() + self.TIMEOUT
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
self.receive = True
|
self.receive = True
|
||||||
self.resampler = codec2.resampler()
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
self.frx = open("rx48_callback_multimode.raw", mode='wb')
|
self.frx = open("rx48_callback_multimode.raw", mode="wb")
|
||||||
|
|
||||||
|
# initial nin values
|
||||||
# initial nin values
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
|
||||||
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
|
|
||||||
|
self.LOGGER_THREAD = threading.Thread(
|
||||||
self.LOGGER_THREAD = threading.Thread(target=self.print_stats, name="LOGGER_THREAD")
|
target=self.print_stats, name="LOGGER_THREAD"
|
||||||
|
)
|
||||||
self.LOGGER_THREAD.start()
|
self.LOGGER_THREAD.start()
|
||||||
|
|
||||||
|
|
||||||
def callback(self, data_in48k, frame_count, time_info, status):
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
x = np.frombuffer(data_in48k, dtype=np.int16)
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
x.tofile(self.frx)
|
x.tofile(self.frx)
|
||||||
x = self.resampler.resample48_to_8(x)
|
x = self.resampler.resample48_to_8(x)
|
||||||
|
|
||||||
self.datac0_buffer.push(x)
|
self.datac0_buffer.push(x)
|
||||||
self.datac1_buffer.push(x)
|
self.datac1_buffer.push(x)
|
||||||
self.datac3_buffer.push(x)
|
self.datac3_buffer.push(x)
|
||||||
|
|
||||||
|
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
||||||
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
self.datac0_freedv,
|
||||||
|
self.datac0_bytes_out,
|
||||||
|
self.datac0_buffer.buffer.ctypes,
|
||||||
|
)
|
||||||
self.datac0_buffer.pop(self.datac0_nin)
|
self.datac0_buffer.pop(self.datac0_nin)
|
||||||
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
if nbytes == self.datac0_bytes_per_frame:
|
if nbytes == self.datac0_bytes_per_frame:
|
||||||
|
@ -167,10 +206,13 @@ class Test():
|
||||||
self.rx_frames_datac0 = 0
|
self.rx_frames_datac0 = 0
|
||||||
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
|
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
|
||||||
|
|
||||||
|
|
||||||
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
self.datac1_freedv,
|
||||||
|
self.datac1_bytes_out,
|
||||||
|
self.datac1_buffer.buffer.ctypes,
|
||||||
|
)
|
||||||
self.datac1_buffer.pop(self.datac1_nin)
|
self.datac1_buffer.pop(self.datac1_nin)
|
||||||
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
if nbytes == self.datac1_bytes_per_frame:
|
if nbytes == self.datac1_bytes_per_frame:
|
||||||
|
@ -181,10 +223,13 @@ class Test():
|
||||||
self.rx_frames_datac1 = 0
|
self.rx_frames_datac1 = 0
|
||||||
self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1
|
self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1
|
||||||
|
|
||||||
|
|
||||||
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
|
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_buffer.pop(self.datac3_nin)
|
||||||
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
if nbytes == self.datac3_bytes_per_frame:
|
if nbytes == self.datac3_bytes_per_frame:
|
||||||
|
@ -193,39 +238,59 @@ class Test():
|
||||||
|
|
||||||
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
|
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
|
||||||
self.rx_frames_datac3 = 0
|
self.rx_frames_datac3 = 0
|
||||||
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
|
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
|
||||||
|
|
||||||
|
if (
|
||||||
if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS:
|
self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3
|
||||||
|
) == self.N_BURSTS:
|
||||||
self.receive = False
|
self.receive = False
|
||||||
|
|
||||||
return (None, pyaudio.paContinue)
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
def print_stats(self):
|
def print_stats(self):
|
||||||
while self.receive:
|
while self.receive:
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
if self.DEBUGGING_MODE:
|
if self.DEBUGGING_MODE:
|
||||||
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv)
|
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(
|
||||||
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus]
|
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.freedv_get_rx_status(
|
||||||
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus]
|
self.datac1_freedv
|
||||||
|
)
|
||||||
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
|
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[
|
||||||
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus]
|
self.datac1_rxstatus
|
||||||
|
]
|
||||||
|
|
||||||
|
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(
|
||||||
|
self.datac3_freedv
|
||||||
|
)
|
||||||
|
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[
|
||||||
|
self.datac3_rxstatus
|
||||||
|
]
|
||||||
|
|
||||||
|
print(
|
||||||
|
"NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s"
|
||||||
|
% (
|
||||||
|
self.datac0_nin,
|
||||||
|
self.datac0_rxstatus,
|
||||||
|
self.datac1_nin,
|
||||||
|
self.datac1_rxstatus,
|
||||||
|
self.datac3_nin,
|
||||||
|
self.datac3_rxstatus,
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \
|
|
||||||
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus),
|
|
||||||
file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def run_audio(self):
|
def run_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.stream_rx.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.receive and time.time() < self.timeout:
|
while self.receive and time.time() < self.timeout:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
@ -233,18 +298,24 @@ class Test():
|
||||||
if time.time() >= self.timeout and self.stream_rx.is_active():
|
if time.time() >= self.timeout and self.stream_rx.is_active():
|
||||||
print("TIMEOUT REACHED")
|
print("TIMEOUT REACHED")
|
||||||
self.receive = False
|
self.receive = False
|
||||||
|
|
||||||
if self.nread_exceptions:
|
|
||||||
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
|
||||||
self.nread_exceptions, file=sys.stderr)
|
|
||||||
|
|
||||||
print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr)
|
if self.nread_exceptions:
|
||||||
|
print(
|
||||||
|
"nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
|
||||||
|
% self.nread_exceptions,
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
self.frx.close()
|
self.frx.close()
|
||||||
|
|
||||||
# cloese pyaudio instance
|
# cloese pyaudio instance
|
||||||
self.stream_rx.close()
|
self.stream_rx.close()
|
||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
test = Test()
|
test = Test()
|
||||||
test.run_audio()
|
test.run_audio()
|
||||||
|
|
|
@ -6,41 +6,52 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
@author: DJ2LS
|
@author: DJ2LS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'..')
|
import pyaudio
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
parser = argparse.ArgumentParser(description="FreeDATA audio test")
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
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("--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(
|
||||||
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
|
"--audiodev",
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
dest="AUDIO_INPUT_DEVICE",
|
||||||
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio device number to use",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
dest="TIMEOUT",
|
||||||
|
default=10,
|
||||||
|
type=int,
|
||||||
|
help="Timeout (seconds) before test ends",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.LIST:
|
if args.LIST:
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
for dev in range(0,p.get_device_count()):
|
for dev in range(p.get_device_count()):
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
quit()
|
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
class Test():
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.N_BURSTS = args.N_BURSTS
|
self.N_BURSTS = args.N_BURSTS
|
||||||
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
@ -49,63 +60,91 @@ class Test():
|
||||||
self.TIMEOUT = args.TIMEOUT
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
# AUDIO PARAMETERS
|
||||||
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
self.AUDIO_FRAMES_PER_BUFFER = (
|
||||||
|
2400 * 2
|
||||||
|
) # <- consider increasing if you get nread_exceptions > 0
|
||||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
self.AUDIO_SAMPLE_RATE_RX = 48000
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
# make sure our resampler will work
|
# make sure our resampler will work
|
||||||
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
assert (
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
|
||||||
|
) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
if self.AUDIO_INPUT_DEVICE != -1:
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
self.p = pyaudio.PyAudio()
|
self.p = pyaudio.PyAudio()
|
||||||
# auto search for loopback devices
|
# auto search for loopback devices
|
||||||
if self.AUDIO_INPUT_DEVICE == -2:
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
loopback_list = []
|
loopback_list = [
|
||||||
for dev in range(0,self.p.get_device_count()):
|
dev
|
||||||
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
for dev in range(self.p.get_device_count())
|
||||||
loopback_list.append(dev)
|
if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]
|
||||||
|
]
|
||||||
|
|
||||||
if len(loopback_list) >= 2:
|
if len(loopback_list) >= 2:
|
||||||
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
self.AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
|
||||||
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
|
||||||
|
|
||||||
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=self.AUDIO_SAMPLE_RATE_RX,
|
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
|
||||||
input=True,
|
|
||||||
output=False,
|
|
||||||
input_device_index=self.AUDIO_INPUT_DEVICE,
|
|
||||||
stream_callback=self.callback
|
|
||||||
)
|
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
||||||
|
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stream_rx = self.p.open(
|
||||||
|
format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("test_callback_multimode_rx_outside: Not written for STDIN usage.")
|
||||||
|
print("Exiting.")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
# open codec2 instance
|
# open codec2 instance
|
||||||
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
self.datac0_freedv = ctypes.cast(
|
||||||
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8)
|
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
|
||||||
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_bytes_per_frame = int(
|
||||||
self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
|
codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv) / 8
|
||||||
|
)
|
||||||
self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
|
self.datac0_bytes_out = ctypes.create_string_buffer(self.datac0_bytes_per_frame)
|
||||||
self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8)
|
codec2.api.freedv_set_frames_per_burst(
|
||||||
self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame)
|
self.datac0_freedv, self.N_FRAMES_PER_BURST
|
||||||
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.datac0_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)
|
|
||||||
|
|
||||||
|
self.datac1_freedv = ctypes.cast(
|
||||||
|
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), ctypes.c_void_p
|
||||||
|
)
|
||||||
|
self.datac1_bytes_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv) / 8
|
||||||
|
)
|
||||||
|
self.datac1_bytes_out = ctypes.create_string_buffer(self.datac1_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(
|
||||||
|
self.datac1_freedv, self.N_FRAMES_PER_BURST
|
||||||
|
)
|
||||||
|
self.datac1_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
|
self.datac3_freedv = ctypes.cast(
|
||||||
|
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), ctypes.c_void_p
|
||||||
|
)
|
||||||
|
self.datac3_bytes_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv) / 8
|
||||||
|
)
|
||||||
|
self.datac3_bytes_out = ctypes.create_string_buffer(self.datac3_bytes_per_frame)
|
||||||
|
codec2.api.freedv_set_frames_per_burst(
|
||||||
|
self.datac3_freedv, self.N_FRAMES_PER_BURST
|
||||||
|
)
|
||||||
|
self.datac3_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
|
||||||
# SET COUNTERS
|
# SET COUNTERS
|
||||||
self.rx_total_frames_datac0 = 0
|
self.rx_total_frames_datac0 = 0
|
||||||
|
@ -125,62 +164,74 @@ class Test():
|
||||||
self.timeout = time.time() + self.TIMEOUT
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
self.receive = True
|
self.receive = True
|
||||||
self.resampler = codec2.resampler()
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
self.frx = open("rx48_callback_multimode.raw", mode='wb')
|
self.frx = open("rx48_callback_multimode.raw", mode="wb")
|
||||||
|
|
||||||
|
# initial nin values
|
||||||
# initial nin values
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
|
||||||
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
|
|
||||||
|
|
||||||
def callback(self, data_in48k, frame_count, time_info, status):
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
x = np.frombuffer(data_in48k, dtype=np.int16)
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
x.tofile(self.frx)
|
x.tofile(self.frx)
|
||||||
x = self.resampler.resample48_to_8(x)
|
x = self.resampler.resample48_to_8(x)
|
||||||
|
|
||||||
self.datac0_buffer.push(x)
|
self.datac0_buffer.push(x)
|
||||||
self.datac1_buffer.push(x)
|
self.datac1_buffer.push(x)
|
||||||
self.datac3_buffer.push(x)
|
self.datac3_buffer.push(x)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (None, pyaudio.paContinue)
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
def print_stats(self):
|
def print_stats(self):
|
||||||
if self.DEBUGGING_MODE:
|
if self.DEBUGGING_MODE:
|
||||||
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv)
|
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv)
|
||||||
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus]
|
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[
|
||||||
|
self.datac0_rxstatus
|
||||||
|
]
|
||||||
|
|
||||||
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv)
|
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv)
|
||||||
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus]
|
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[
|
||||||
|
self.datac1_rxstatus
|
||||||
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
|
]
|
||||||
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus]
|
|
||||||
|
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
|
||||||
|
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[
|
||||||
|
self.datac3_rxstatus
|
||||||
|
]
|
||||||
|
|
||||||
|
print(
|
||||||
|
"NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s"
|
||||||
|
% (
|
||||||
|
self.datac0_nin,
|
||||||
|
self.datac0_rxstatus,
|
||||||
|
self.datac1_nin,
|
||||||
|
self.datac1_rxstatus,
|
||||||
|
self.datac3_nin,
|
||||||
|
self.datac3_rxstatus,
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \
|
|
||||||
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus),
|
|
||||||
file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def run_audio(self):
|
def run_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.stream_rx.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.receive and time.time() < self.timeout:
|
while self.receive and time.time() < self.timeout:
|
||||||
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
self.datac0_freedv,
|
||||||
|
self.datac0_bytes_out,
|
||||||
|
self.datac0_buffer.buffer.ctypes,
|
||||||
|
)
|
||||||
self.datac0_buffer.pop(self.datac0_nin)
|
self.datac0_buffer.pop(self.datac0_nin)
|
||||||
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
||||||
if nbytes == self.datac0_bytes_per_frame:
|
if nbytes == self.datac0_bytes_per_frame:
|
||||||
|
@ -192,10 +243,13 @@ class Test():
|
||||||
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
|
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
|
||||||
self.print_stats()
|
self.print_stats()
|
||||||
|
|
||||||
|
|
||||||
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
self.datac1_freedv,
|
||||||
|
self.datac1_bytes_out,
|
||||||
|
self.datac1_buffer.buffer.ctypes,
|
||||||
|
)
|
||||||
self.datac1_buffer.pop(self.datac1_nin)
|
self.datac1_buffer.pop(self.datac1_nin)
|
||||||
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
||||||
if nbytes == self.datac1_bytes_per_frame:
|
if nbytes == self.datac1_bytes_per_frame:
|
||||||
|
@ -206,10 +260,14 @@ class Test():
|
||||||
self.rx_frames_datac1 = 0
|
self.rx_frames_datac1 = 0
|
||||||
self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1
|
self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1
|
||||||
self.print_stats()
|
self.print_stats()
|
||||||
|
|
||||||
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
|
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_buffer.pop(self.datac3_nin)
|
||||||
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
||||||
if nbytes == self.datac3_bytes_per_frame:
|
if nbytes == self.datac3_bytes_per_frame:
|
||||||
|
@ -218,27 +276,36 @@ class Test():
|
||||||
|
|
||||||
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
|
if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST:
|
||||||
self.rx_frames_datac3 = 0
|
self.rx_frames_datac3 = 0
|
||||||
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
|
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
|
||||||
self.print_stats()
|
self.print_stats()
|
||||||
|
|
||||||
if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS:
|
if (
|
||||||
self.receive = False
|
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:
|
if time.time() >= self.timeout:
|
||||||
print("TIMEOUT REACHED")
|
print("TIMEOUT REACHED")
|
||||||
|
|
||||||
if self.nread_exceptions:
|
|
||||||
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
|
||||||
self.nread_exceptions, file=sys.stderr)
|
|
||||||
|
|
||||||
print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr)
|
if self.nread_exceptions:
|
||||||
|
print(
|
||||||
|
"nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
|
||||||
|
% self.nread_exceptions,
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
self.frx.close()
|
self.frx.close()
|
||||||
|
|
||||||
# cloese pyaudio instance
|
# cloese pyaudio instance
|
||||||
self.stream_rx.close()
|
self.stream_rx.close()
|
||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
test = Test()
|
test = Test()
|
||||||
test.run_audio()
|
test.run_audio()
|
||||||
|
|
|
@ -6,182 +6,227 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
@author: DJ2LS
|
@author: DJ2LS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
import queue
|
import queue
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'..')
|
import pyaudio
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
parser = argparse.ArgumentParser(description="FreeDATA audio test")
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
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("--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("--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(
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
"--audiodev",
|
||||||
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes")
|
dest="AUDIO_OUTPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio output device number to use",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--testframes",
|
||||||
|
dest="TESTFRAMES",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="generate testframes",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
if args.LIST:
|
if args.LIST:
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
for dev in range(0,p.get_device_count()):
|
for dev in range(p.get_device_count()):
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
class Test():
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
self.dataqueue = queue.Queue()
|
self.dataqueue = queue.Queue()
|
||||||
self.N_BURSTS = args.N_BURSTS
|
self.N_BURSTS = args.N_BURSTS
|
||||||
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
||||||
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
|
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
# AUDIO PARAMETERS
|
||||||
self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0
|
# v-- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400
|
||||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
self.AUDIO_SAMPLE_RATE_TX = 48000
|
self.AUDIO_SAMPLE_RATE_TX = 48000
|
||||||
|
|
||||||
# make sure our resampler will work
|
# make sure our resampler will work
|
||||||
assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
assert (
|
||||||
|
self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE
|
||||||
|
) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
|
||||||
self.transmit = True
|
self.transmit = True
|
||||||
|
|
||||||
self.resampler = codec2.resampler()
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
if self.AUDIO_OUTPUT_DEVICE != -1:
|
if self.AUDIO_OUTPUT_DEVICE != -1:
|
||||||
self.p = pyaudio.PyAudio()
|
self.p = pyaudio.PyAudio()
|
||||||
# auto search for loopback devices
|
# auto search for loopback devices
|
||||||
if self.AUDIO_OUTPUT_DEVICE == -2:
|
if self.AUDIO_OUTPUT_DEVICE == -2:
|
||||||
loopback_list = []
|
loopback_list = [
|
||||||
for dev in range(0,self.p.get_device_count()):
|
dev
|
||||||
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
for dev in range(self.p.get_device_count())
|
||||||
loopback_list.append(dev)
|
if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]
|
||||||
|
]
|
||||||
|
|
||||||
if len(loopback_list) >= 2:
|
if len(loopback_list) >= 2:
|
||||||
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \
|
print(
|
||||||
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr)
|
f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} "
|
||||||
|
f"DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} "
|
||||||
self.stream_tx = self.p.open(format=pyaudio.paInt16,
|
f"AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}",
|
||||||
channels=1,
|
file=sys.stderr,
|
||||||
rate=self.AUDIO_SAMPLE_RATE_TX,
|
)
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
|
||||||
input=False,
|
self.stream_tx = self.p.open(
|
||||||
output=True,
|
format=pyaudio.paInt16,
|
||||||
output_device_index=self.AUDIO_OUTPUT_DEVICE,
|
channels=1,
|
||||||
stream_callback=self.callback
|
rate=self.AUDIO_SAMPLE_RATE_TX,
|
||||||
)
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
|
input=False,
|
||||||
|
output=True,
|
||||||
|
output_device_index=self.AUDIO_OUTPUT_DEVICE,
|
||||||
|
stream_callback=self.callback,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("test_callback_multimode_tx: Not written for STDOUT usage.")
|
||||||
|
print("Exiting.")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
self.ftx = open("tx48_callback.raw", mode='wb')
|
self.ftx = open("tx48_callback.raw", mode="wb")
|
||||||
|
|
||||||
# data binary string
|
# data binary string
|
||||||
if args.TESTFRAMES:
|
if args.TESTFRAMES:
|
||||||
self.data_out = bytearray(14)
|
self.data_out = bytearray(14)
|
||||||
self.data_out[:1] = bytes([255])
|
self.data_out[:1] = bytes([255])
|
||||||
self.data_out[1:2] = bytes([1])
|
self.data_out[1:2] = bytes([1])
|
||||||
self.data_out[2:] = b'HELLO WORLD'
|
self.data_out[2:] = b"HELLO WORLD"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.data_out = b'HELLO WORLD!'
|
self.data_out = b"HELLO WORLD!"
|
||||||
|
|
||||||
|
|
||||||
def callback(self, data_in48k, frame_count, time_info, status):
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
data_out48k = self.dataqueue.get()
|
data_out48k = self.dataqueue.get()
|
||||||
return (data_out48k, pyaudio.paContinue)
|
return (data_out48k, pyaudio.paContinue)
|
||||||
|
|
||||||
def run_audio(self):
|
def run_audio(self):
|
||||||
try:
|
try:
|
||||||
print(f"starting pyaudio callback", file=sys.stderr)
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
self.stream_tx.start_stream()
|
self.stream_tx.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)
|
||||||
|
|
||||||
sheeps = 0
|
sheeps = 0
|
||||||
while self.transmit:
|
while self.transmit:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
sheeps = sheeps + 1
|
sheeps = sheeps + 1
|
||||||
print(f"counting sheeps...{sheeps}")
|
print(f"counting sheeps...{sheeps}")
|
||||||
|
|
||||||
self.ftx.close()
|
self.ftx.close()
|
||||||
|
|
||||||
# close pyaudio instance
|
# close pyaudio instance
|
||||||
self.stream_tx.close()
|
self.stream_tx.close()
|
||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
|
|
||||||
def create_modulation(self):
|
def create_modulation(self):
|
||||||
|
|
||||||
modes = [codec2.api.FREEDV_MODE_DATAC0, codec2.api.FREEDV_MODE_DATAC1, codec2.api.FREEDV_MODE_DATAC3]
|
modes = [
|
||||||
|
codec2.api.FREEDV_MODE_DATAC0,
|
||||||
|
codec2.api.FREEDV_MODE_DATAC1,
|
||||||
|
codec2.api.FREEDV_MODE_DATAC3,
|
||||||
|
]
|
||||||
for m in modes:
|
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)
|
freedv = ctypes.cast(codec2.api.freedv_open(m), ctypes.c_void_p)
|
||||||
mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples)
|
|
||||||
|
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
mod_out = ctypes.create_string_buffer(2 * n_tx_modem_samples)
|
||||||
|
|
||||||
|
n_tx_preamble_modem_samples = (
|
||||||
|
codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
|
||||||
|
)
|
||||||
|
mod_out_preamble = ctypes.create_string_buffer(
|
||||||
|
2 * n_tx_preamble_modem_samples
|
||||||
|
)
|
||||||
|
|
||||||
|
n_tx_postamble_modem_samples = (
|
||||||
|
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||||
|
)
|
||||||
|
mod_out_postamble = ctypes.create_string_buffer(
|
||||||
|
2 * n_tx_postamble_modem_samples
|
||||||
|
)
|
||||||
|
|
||||||
|
bytes_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8
|
||||||
|
)
|
||||||
payload_per_frame = bytes_per_frame - 2
|
payload_per_frame = bytes_per_frame - 2
|
||||||
|
|
||||||
buffer = bytearray(payload_per_frame)
|
buffer = bytearray(payload_per_frame)
|
||||||
# set buffersize to length of data which will be send
|
# set buffersize to length of data which will be send
|
||||||
buffer[:len(self.data_out)] = self.data_out
|
buffer[: len(self.data_out)] = self.data_out
|
||||||
|
|
||||||
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16
|
crc = ctypes.c_ushort(
|
||||||
|
codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)
|
||||||
|
) # generate CRC16
|
||||||
# convert crc to 2 byte hex string
|
# convert crc to 2 byte hex string
|
||||||
crc = crc.value.to_bytes(2, byteorder='big')
|
crc = crc.value.to_bytes(2, byteorder="big")
|
||||||
buffer += crc # append crc16 to buffer
|
buffer += crc # append crc16 to buffer
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
|
||||||
for i in range(1,self.N_BURSTS+1):
|
for i in range(1, self.N_BURSTS + 1):
|
||||||
|
|
||||||
# write preamble to txbuffer
|
# write preamble to txbuffer
|
||||||
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
txbuffer = bytes(mod_out_preamble)
|
txbuffer = bytes(mod_out_preamble)
|
||||||
|
|
||||||
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
||||||
for n in range(1,self.N_FRAMES_PER_BURST+1):
|
for n in range(1, self.N_FRAMES_PER_BURST + 1):
|
||||||
|
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
codec2.api.freedv_rawdatatx(
|
||||||
|
freedv, mod_out, data
|
||||||
|
) # modulate DATA and save it into mod_out pointer
|
||||||
|
|
||||||
txbuffer += bytes(mod_out)
|
txbuffer += bytes(mod_out)
|
||||||
print(f"GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr)
|
print(
|
||||||
|
f"GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
# append postamble to txbuffer
|
# append postamble to txbuffer
|
||||||
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
txbuffer += bytes(mod_out_postamble)
|
txbuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
# append a delay between bursts as audio silence
|
# append a delay between bursts as audio silence
|
||||||
samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS)
|
samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS)
|
||||||
mod_out_silence = create_string_buffer(samples_delay*2)
|
mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
|
||||||
txbuffer += bytes(mod_out_silence)
|
txbuffer += bytes(mod_out_silence)
|
||||||
|
|
||||||
# resample up to 48k (resampler works on np.int16)
|
# resample up to 48k (resampler works on np.int16)
|
||||||
|
@ -189,20 +234,23 @@ class Test():
|
||||||
txbuffer_48k = self.resampler.resample8_to_48(x)
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
||||||
|
|
||||||
# split modulated audio to chunks
|
# split modulated audio to chunks
|
||||||
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
# https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
||||||
txbuffer_48k = bytes(txbuffer_48k)
|
txbuffer_48k = bytes(txbuffer_48k)
|
||||||
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)]
|
chunk = [
|
||||||
|
txbuffer_48k[i : i + self.AUDIO_FRAMES_PER_BUFFER * 2]
|
||||||
|
for i in range(
|
||||||
|
0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER * 2
|
||||||
|
)
|
||||||
|
]
|
||||||
# add modulated chunks to fifo buffer
|
# add modulated chunks to fifo buffer
|
||||||
for c in chunk:
|
for c in chunk:
|
||||||
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
||||||
# to prevent the callback from stucking/crashing
|
# to prevent the callback from stucking/crashing
|
||||||
if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2:
|
if len(c) < self.AUDIO_FRAMES_PER_BUFFER * 2:
|
||||||
c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c))
|
c += bytes(self.AUDIO_FRAMES_PER_BUFFER * 2 - len(c))
|
||||||
self.dataqueue.put(c)
|
self.dataqueue.put(c)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test = Test()
|
test = Test()
|
||||||
test.create_modulation()
|
test.create_modulation()
|
||||||
test.run_audio()
|
test.run_audio()
|
||||||
|
|
|
@ -6,42 +6,56 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
@author: DJ2LS
|
@author: DJ2LS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'..')
|
import pyaudio
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
parser = argparse.ArgumentParser(description="FreeDATA audio test")
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
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("--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(
|
||||||
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int,
|
"--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
|
||||||
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(
|
||||||
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
"--audiodev",
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
dest="AUDIO_INPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio device number to use, use -2 to automatically select a loopback device",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
dest="TIMEOUT",
|
||||||
|
default=10,
|
||||||
|
type=int,
|
||||||
|
help="Timeout (seconds) before test ends",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
if args.LIST:
|
if args.LIST:
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
for dev in range(0,p.get_device_count()):
|
for dev in range(p.get_device_count()):
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
class Test():
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.N_BURSTS = args.N_BURSTS
|
self.N_BURSTS = args.N_BURSTS
|
||||||
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
@ -51,51 +65,61 @@ class Test():
|
||||||
self.TIMEOUT = args.TIMEOUT
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
# AUDIO PARAMETERS
|
||||||
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
self.AUDIO_FRAMES_PER_BUFFER = (
|
||||||
|
2400 * 2
|
||||||
|
) # <- consider increasing if you get nread_exceptions > 0
|
||||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
self.AUDIO_SAMPLE_RATE_RX = 48000
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
# make sure our resampler will work
|
# make sure our resampler will work
|
||||||
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
assert (
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
|
||||||
|
) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
if self.AUDIO_INPUT_DEVICE != -1:
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
self.p = pyaudio.PyAudio()
|
self.p = pyaudio.PyAudio()
|
||||||
# auto search for loopback devices
|
# auto search for loopback devices
|
||||||
if self.AUDIO_INPUT_DEVICE == -2:
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
loopback_list = []
|
loopback_list = []
|
||||||
for dev in range(0,self.p.get_device_count()):
|
for dev in range(self.p.get_device_count()):
|
||||||
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:
|
||||||
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
self.AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
print(
|
||||||
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
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}",
|
||||||
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
file=sys.stderr,
|
||||||
channels=1,
|
)
|
||||||
rate=self.AUDIO_SAMPLE_RATE_RX,
|
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
self.stream_rx = self.p.open(
|
||||||
input=True,
|
format=pyaudio.paInt16,
|
||||||
output=False,
|
channels=1,
|
||||||
input_device_index=self.AUDIO_INPUT_DEVICE,
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
stream_callback=self.callback
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
)
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
|
||||||
|
|
||||||
# open codec2 instance
|
|
||||||
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
|
||||||
|
|
||||||
# get number of bytes per frame for mode
|
# 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_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
|
||||||
self.bytes_out = create_string_buffer(self.bytes_per_frame)
|
)
|
||||||
|
|
||||||
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST)
|
self.bytes_out = ctypes.create_string_buffer(self.bytes_per_frame)
|
||||||
|
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.freedv, self.N_FRAMES_PER_BURST)
|
||||||
|
|
||||||
self.total_n_bytes = 0
|
self.total_n_bytes = 0
|
||||||
self.rx_total_frames = 0
|
self.rx_total_frames = 0
|
||||||
self.rx_frames = 0
|
self.rx_frames = 0
|
||||||
|
@ -104,44 +128,48 @@ class Test():
|
||||||
self.nread_exceptions = 0
|
self.nread_exceptions = 0
|
||||||
self.timeout = time.time() + self.TIMEOUT
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
self.receive = True
|
self.receive = True
|
||||||
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2)
|
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER * 2)
|
||||||
self.resampler = codec2.resampler()
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
self.frx = open("rx48_callback.raw", mode='wb')
|
self.frx = open("rx48_callback.raw", mode="wb")
|
||||||
|
|
||||||
def callback(self, data_in48k, frame_count, time_info, status):
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
x = np.frombuffer(data_in48k, dtype=np.int16)
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
x.tofile(self.frx)
|
x.tofile(self.frx)
|
||||||
x = self.resampler.resample48_to_8(x)
|
x = self.resampler.resample48_to_8(x)
|
||||||
self.audio_buffer.push(x)
|
self.audio_buffer.push(x)
|
||||||
|
|
||||||
|
|
||||||
# when we have enough samples call FreeDV Rx
|
# when we have enough samples call FreeDV Rx
|
||||||
nin = codec2.api.freedv_nin(self.freedv)
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
while self.audio_buffer.nbuffer >= nin:
|
while self.audio_buffer.nbuffer >= nin:
|
||||||
|
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes)
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes
|
||||||
|
)
|
||||||
self.audio_buffer.pop(nin)
|
self.audio_buffer.pop(nin)
|
||||||
|
|
||||||
# call me on every loop!
|
# call me on every loop!
|
||||||
nin = codec2.api.freedv_nin(self.freedv)
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
|
|
||||||
rx_status = codec2.api.freedv_get_rx_status(self.freedv)
|
rx_status = codec2.api.freedv_get_rx_status(self.freedv)
|
||||||
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
||||||
self.rx_errors = self.rx_errors + 1
|
self.rx_errors = self.rx_errors + 1
|
||||||
if self.DEBUGGING_MODE:
|
if self.DEBUGGING_MODE:
|
||||||
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
||||||
print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \
|
print(
|
||||||
(nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr)
|
"nin: %5d rx_status: %4s naudio_buffer: %4d"
|
||||||
|
% (nin, rx_status, self.audio_buffer.nbuffer),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
if nbytes:
|
if nbytes:
|
||||||
self.total_n_bytes = self.total_n_bytes + nbytes
|
self.total_n_bytes = self.total_n_bytes + nbytes
|
||||||
|
|
||||||
if nbytes == self.bytes_per_frame:
|
if nbytes == self.bytes_per_frame:
|
||||||
self.rx_total_frames = self.rx_total_frames + 1
|
self.rx_total_frames = self.rx_total_frames + 1
|
||||||
self.rx_frames = self.rx_frames + 1
|
self.rx_frames = self.rx_frames + 1
|
||||||
|
@ -149,36 +177,41 @@ class Test():
|
||||||
if self.rx_frames == self.N_FRAMES_PER_BURST:
|
if self.rx_frames == self.N_FRAMES_PER_BURST:
|
||||||
self.rx_frames = 0
|
self.rx_frames = 0
|
||||||
self.rx_bursts = self.rx_bursts + 1
|
self.rx_bursts = self.rx_bursts + 1
|
||||||
|
|
||||||
if self.rx_bursts == self.N_BURSTS:
|
if self.rx_bursts == self.N_BURSTS:
|
||||||
self.receive = False
|
self.receive = False
|
||||||
|
|
||||||
return (None, pyaudio.paContinue)
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
def run_audio(self):
|
def run_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.stream_rx.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.receive and time.time() < self.timeout:
|
while self.receive and time.time() < self.timeout:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
if time.time() >= self.timeout:
|
if time.time() >= self.timeout:
|
||||||
print("TIMEOUT REACHED")
|
print("TIMEOUT REACHED")
|
||||||
|
|
||||||
if self.nread_exceptions:
|
if self.nread_exceptions:
|
||||||
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
print(
|
||||||
self.nread_exceptions, file=sys.stderr)
|
"nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
|
||||||
print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr)
|
% 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()
|
self.frx.close()
|
||||||
|
|
||||||
# cloese pyaudio instance
|
# cloese pyaudio instance
|
||||||
self.stream_rx.close()
|
self.stream_rx.close()
|
||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
test = Test()
|
test = Test()
|
||||||
test.run_audio()
|
test.run_audio()
|
||||||
|
|
|
@ -6,42 +6,56 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
@author: DJ2LS
|
@author: DJ2LS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'..')
|
import pyaudio
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
parser = argparse.ArgumentParser(description="FreeDATA audio test")
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
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("--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(
|
||||||
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int,
|
"--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
|
||||||
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(
|
||||||
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
|
"--audiodev",
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
dest="AUDIO_INPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio device number to use, use -2 to automatically select a loopback device",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
dest="TIMEOUT",
|
||||||
|
default=10,
|
||||||
|
type=int,
|
||||||
|
help="Timeout (seconds) before test ends",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
if args.LIST:
|
if args.LIST:
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
for dev in range(0,p.get_device_count()):
|
for dev in range(p.get_device_count()):
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
class Test():
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.N_BURSTS = args.N_BURSTS
|
self.N_BURSTS = args.N_BURSTS
|
||||||
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
@ -51,51 +65,61 @@ class Test():
|
||||||
self.TIMEOUT = args.TIMEOUT
|
self.TIMEOUT = args.TIMEOUT
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
# AUDIO PARAMETERS
|
||||||
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
self.AUDIO_FRAMES_PER_BUFFER = (
|
||||||
|
2400 * 2
|
||||||
|
) # <- consider increasing if you get nread_exceptions > 0
|
||||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
self.AUDIO_SAMPLE_RATE_RX = 48000
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
# make sure our resampler will work
|
# make sure our resampler will work
|
||||||
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
assert (
|
||||||
|
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
|
||||||
|
) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
if self.AUDIO_INPUT_DEVICE != -1:
|
if self.AUDIO_INPUT_DEVICE != -1:
|
||||||
self.p = pyaudio.PyAudio()
|
self.p = pyaudio.PyAudio()
|
||||||
# auto search for loopback devices
|
# auto search for loopback devices
|
||||||
if self.AUDIO_INPUT_DEVICE == -2:
|
if self.AUDIO_INPUT_DEVICE == -2:
|
||||||
loopback_list = []
|
loopback_list = []
|
||||||
for dev in range(0,self.p.get_device_count()):
|
for dev in range(self.p.get_device_count()):
|
||||||
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:
|
||||||
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
self.AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
|
print(
|
||||||
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
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}",
|
||||||
self.stream_rx = self.p.open(format=pyaudio.paInt16,
|
file=sys.stderr,
|
||||||
channels=1,
|
)
|
||||||
rate=self.AUDIO_SAMPLE_RATE_RX,
|
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
self.stream_rx = self.p.open(
|
||||||
input=True,
|
format=pyaudio.paInt16,
|
||||||
output=False,
|
channels=1,
|
||||||
input_device_index=self.AUDIO_INPUT_DEVICE,
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
||||||
stream_callback=self.callback
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
)
|
input=True,
|
||||||
|
output=False,
|
||||||
|
input_device_index=self.AUDIO_INPUT_DEVICE,
|
||||||
|
stream_callback=self.callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
|
||||||
|
|
||||||
# open codec2 instance
|
|
||||||
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
|
||||||
|
|
||||||
# get number of bytes per frame for mode
|
# 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_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
|
||||||
self.bytes_out = create_string_buffer(self.bytes_per_frame * 2)
|
)
|
||||||
|
|
||||||
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST)
|
self.bytes_out = ctypes.create_string_buffer(self.bytes_per_frame * 2)
|
||||||
|
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.freedv, self.N_FRAMES_PER_BURST)
|
||||||
|
|
||||||
self.total_n_bytes = 0
|
self.total_n_bytes = 0
|
||||||
self.rx_total_frames = 0
|
self.rx_total_frames = 0
|
||||||
self.rx_frames = 0
|
self.rx_frames = 0
|
||||||
|
@ -104,55 +128,59 @@ class Test():
|
||||||
self.nread_exceptions = 0
|
self.nread_exceptions = 0
|
||||||
self.timeout = time.time() + self.TIMEOUT
|
self.timeout = time.time() + self.TIMEOUT
|
||||||
self.receive = True
|
self.receive = True
|
||||||
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2)
|
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER * 2)
|
||||||
self.resampler = codec2.resampler()
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
self.frx = open("rx48_callback.raw", mode='wb')
|
self.frx = open("rx48_callback.raw", mode="wb")
|
||||||
|
|
||||||
def callback(self, data_in48k, frame_count, time_info, status):
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
x = np.frombuffer(data_in48k, dtype=np.int16)
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
x.tofile(self.frx)
|
x.tofile(self.frx)
|
||||||
x = self.resampler.resample48_to_8(x)
|
x = self.resampler.resample48_to_8(x)
|
||||||
self.audio_buffer.push(x)
|
self.audio_buffer.push(x)
|
||||||
|
|
||||||
return (None, pyaudio.paContinue)
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
def run_audio(self):
|
def run_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.stream_rx.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.receive and time.time() < self.timeout:
|
while self.receive and time.time() < self.timeout:
|
||||||
#time.sleep(1)
|
# time.sleep(1)
|
||||||
# when we have enough samples call FreeDV Rx
|
# when we have enough samples call FreeDV Rx
|
||||||
nin = codec2.api.freedv_nin(self.freedv)
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
while self.audio_buffer.nbuffer >= nin:
|
while self.audio_buffer.nbuffer >= nin:
|
||||||
|
|
||||||
# demodulate audio
|
# demodulate audio
|
||||||
nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes)
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes
|
||||||
|
)
|
||||||
self.audio_buffer.pop(nin)
|
self.audio_buffer.pop(nin)
|
||||||
|
|
||||||
# call me on every loop!
|
# call me on every loop!
|
||||||
nin = codec2.api.freedv_nin(self.freedv)
|
nin = codec2.api.freedv_nin(self.freedv)
|
||||||
|
|
||||||
rx_status = codec2.api.freedv_get_rx_status(self.freedv)
|
rx_status = codec2.api.freedv_get_rx_status(self.freedv)
|
||||||
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
||||||
self.rx_errors = self.rx_errors + 1
|
self.rx_errors = self.rx_errors + 1
|
||||||
if self.DEBUGGING_MODE:
|
if self.DEBUGGING_MODE:
|
||||||
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
||||||
print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \
|
print(
|
||||||
(nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr)
|
"nin: %5d rx_status: %4s naudio_buffer: %4d"
|
||||||
|
% (nin, rx_status, self.audio_buffer.nbuffer),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
if nbytes:
|
if nbytes:
|
||||||
self.total_n_bytes = self.total_n_bytes + nbytes
|
self.total_n_bytes = self.total_n_bytes + nbytes
|
||||||
|
|
||||||
if nbytes == self.bytes_per_frame:
|
if nbytes == self.bytes_per_frame:
|
||||||
self.rx_total_frames = self.rx_total_frames + 1
|
self.rx_total_frames = self.rx_total_frames + 1
|
||||||
self.rx_frames = self.rx_frames + 1
|
self.rx_frames = self.rx_frames + 1
|
||||||
|
@ -160,22 +188,28 @@ class Test():
|
||||||
if self.rx_frames == self.N_FRAMES_PER_BURST:
|
if self.rx_frames == self.N_FRAMES_PER_BURST:
|
||||||
self.rx_frames = 0
|
self.rx_frames = 0
|
||||||
self.rx_bursts = self.rx_bursts + 1
|
self.rx_bursts = self.rx_bursts + 1
|
||||||
|
|
||||||
if self.rx_bursts == self.N_BURSTS:
|
if self.rx_bursts == self.N_BURSTS:
|
||||||
self.receive = False
|
self.receive = False
|
||||||
if time.time() >= self.timeout:
|
if time.time() >= self.timeout:
|
||||||
print("TIMEOUT REACHED")
|
print("TIMEOUT REACHED")
|
||||||
|
|
||||||
if self.nread_exceptions:
|
if self.nread_exceptions:
|
||||||
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
print(
|
||||||
self.nread_exceptions, file=sys.stderr)
|
"nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
|
||||||
print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr)
|
% 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()
|
self.frx.close()
|
||||||
|
|
||||||
# cloese pyaudio instance
|
# cloese pyaudio instance
|
||||||
self.stream_rx.close()
|
self.stream_rx.close()
|
||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
|
|
||||||
|
|
||||||
test = Test()
|
test = Test()
|
||||||
test.run_audio()
|
test.run_audio()
|
||||||
|
|
|
@ -6,43 +6,58 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
@author: DJ2LS
|
@author: DJ2LS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
import queue
|
import queue
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'..')
|
import pyaudio
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA audio test')
|
parser = argparse.ArgumentParser(description="FreeDATA audio test")
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
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("--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("--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(
|
||||||
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int,
|
"--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
|
||||||
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(
|
||||||
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes")
|
"--audiodev",
|
||||||
|
dest="AUDIO_OUTPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio device number to use, use -2 to automatically select a loopback device",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--testframes",
|
||||||
|
dest="TESTFRAMES",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="generate testframes",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
if args.LIST:
|
if args.LIST:
|
||||||
p = pyaudio.PyAudio()
|
p = pyaudio.PyAudio()
|
||||||
for dev in range(0,p.get_device_count()):
|
for dev in range(p.get_device_count()):
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
class Test():
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
self.dataqueue = queue.Queue()
|
self.dataqueue = queue.Queue()
|
||||||
|
@ -50,180 +65,206 @@ class Test():
|
||||||
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
||||||
self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
||||||
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
|
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
# AUDIO PARAMETERS
|
||||||
self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0
|
# v-- consider increasing if you get nread_exceptions > 0
|
||||||
|
self.AUDIO_FRAMES_PER_BUFFER = 2400
|
||||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
self.AUDIO_SAMPLE_RATE_TX = 48000
|
self.AUDIO_SAMPLE_RATE_TX = 48000
|
||||||
|
|
||||||
# make sure our resampler will work
|
# make sure our resampler will work
|
||||||
assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
assert (
|
||||||
|
self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE
|
||||||
|
) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
|
|
||||||
self.transmit = True
|
self.transmit = True
|
||||||
|
|
||||||
self.resampler = codec2.resampler()
|
self.resampler = codec2.resampler()
|
||||||
|
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
if self.AUDIO_OUTPUT_DEVICE != -1:
|
if self.AUDIO_OUTPUT_DEVICE != -1:
|
||||||
self.p = pyaudio.PyAudio()
|
self.p = pyaudio.PyAudio()
|
||||||
# auto search for loopback devices
|
# auto search for loopback devices
|
||||||
if self.AUDIO_OUTPUT_DEVICE == -2:
|
if self.AUDIO_OUTPUT_DEVICE == -2:
|
||||||
loopback_list = []
|
loopback_list = []
|
||||||
for dev in range(0,self.p.get_device_count()):
|
for dev in range( self.p.get_device_count()):
|
||||||
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:
|
||||||
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
quit()
|
sys.exit()
|
||||||
|
|
||||||
print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \
|
print(
|
||||||
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr)
|
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}",
|
||||||
self.stream_tx = self.p.open(format=pyaudio.paInt16,
|
file=sys.stderr,
|
||||||
channels=1,
|
)
|
||||||
rate=self.AUDIO_SAMPLE_RATE_TX,
|
|
||||||
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
self.stream_tx = self.p.open(
|
||||||
input=False,
|
format=pyaudio.paInt16,
|
||||||
output=True,
|
channels=1,
|
||||||
output_device_index=self.AUDIO_OUTPUT_DEVICE,
|
rate=self.AUDIO_SAMPLE_RATE_TX,
|
||||||
stream_callback=self.callback
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
|
||||||
)
|
input=False,
|
||||||
|
output=True,
|
||||||
|
output_device_index=self.AUDIO_OUTPUT_DEVICE,
|
||||||
|
stream_callback=self.callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
|
||||||
|
|
||||||
# open codec2 instance
|
|
||||||
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
|
||||||
|
|
||||||
# get number of bytes per frame for mode
|
# 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_per_frame = int(
|
||||||
|
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
|
||||||
self.bytes_out = create_string_buffer(self.bytes_per_frame)
|
)
|
||||||
|
|
||||||
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST)
|
self.bytes_out = ctypes.create_string_buffer(self.bytes_per_frame)
|
||||||
|
|
||||||
|
codec2.api.freedv_set_frames_per_burst(self.freedv, self.N_FRAMES_PER_BURST)
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
# aplay -r 48000 -f S16_LE rx48_callback.raw
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
self.ftx = open("tx48_callback.raw", mode='wb')
|
self.ftx = open("tx48_callback.raw", mode="wb")
|
||||||
|
|
||||||
# data binary string
|
# data binary string
|
||||||
if args.TESTFRAMES:
|
if args.TESTFRAMES:
|
||||||
self.data_out = bytearray(14)
|
self.data_out = bytearray(14)
|
||||||
self.data_out[:1] = bytes([255])
|
self.data_out[:1] = bytes([255])
|
||||||
self.data_out[1:2] = bytes([1])
|
self.data_out[1:2] = bytes([1])
|
||||||
self.data_out[2:] = b'HELLO WORLD'
|
self.data_out[2:] = b"HELLO WORLD"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.data_out = b'HELLO WORLD!'
|
self.data_out = b"HELLO WORLD!"
|
||||||
|
|
||||||
|
|
||||||
def callback(self, data_in48k, frame_count, time_info, status):
|
def callback(self, data_in48k, frame_count, time_info, status):
|
||||||
|
|
||||||
data_out48k = self.dataqueue.get()
|
data_out48k = self.dataqueue.get()
|
||||||
return (data_out48k, pyaudio.paContinue)
|
return (data_out48k, pyaudio.paContinue)
|
||||||
|
|
||||||
def run_audio(self):
|
def run_audio(self):
|
||||||
try:
|
try:
|
||||||
print(f"starting pyaudio callback", file=sys.stderr)
|
print(f"starting pyaudio callback", file=sys.stderr)
|
||||||
self.stream_tx.start_stream()
|
self.stream_tx.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)
|
||||||
|
|
||||||
sheeps = 0
|
sheeps = 0
|
||||||
while self.transmit:
|
while self.transmit:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
sheeps = sheeps + 1
|
sheeps = sheeps + 1
|
||||||
print(f"counting sheeps...{sheeps}")
|
print(f"counting sheeps...{sheeps}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.ftx.close()
|
self.ftx.close()
|
||||||
|
|
||||||
# close pyaudio instance
|
# close pyaudio instance
|
||||||
self.stream_tx.close()
|
self.stream_tx.close()
|
||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
|
|
||||||
def create_modulation(self):
|
def create_modulation(self):
|
||||||
|
|
||||||
# open codec2 instance
|
# open codec2 instance
|
||||||
freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
|
||||||
|
|
||||||
# get number of bytes per frame for mode
|
# get number of bytes per frame for mode
|
||||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|
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
|
||||||
|
|
||||||
# init buffer for data
|
# init buffer for data
|
||||||
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||||
mod_out = create_string_buffer(n_tx_modem_samples * 2)
|
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
|
||||||
|
|
||||||
# init buffer for preample
|
# init buffer for preample
|
||||||
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
|
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
|
||||||
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2)
|
freedv
|
||||||
|
)
|
||||||
|
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
|
||||||
|
|
||||||
# init buffer for postamble
|
# init buffer for postamble
|
||||||
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
n_tx_postamble_modem_samples = (
|
||||||
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2)
|
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||||
|
)
|
||||||
|
mod_out_postamble = ctypes.create_string_buffer(n_tx_postamble_modem_samples * 2)
|
||||||
|
|
||||||
# create buffer for data
|
# create buffer for data
|
||||||
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
buffer = bytearray(
|
||||||
buffer[:len(self.data_out)] = self.data_out # set buffersize to length of data which will be send
|
payload_bytes_per_frame
|
||||||
|
) # use this if CRC16 checksum is required ( DATA1-3)
|
||||||
|
buffer[
|
||||||
|
: len(self.data_out)
|
||||||
|
] = self.data_out # set buffersize to length of data which will be send
|
||||||
|
|
||||||
# create crc for data frame - we are using the crc function shipped with codec2 to avoid
|
# create crc for data frame - we are using the crc function shipped with codec2 to avoid
|
||||||
# crc algorithm incompatibilities
|
# crc algorithm incompatibilities
|
||||||
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16
|
crc = ctypes.c_ushort(
|
||||||
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
|
||||||
buffer += crc # append crc16 to buffer
|
) # generate CRC16
|
||||||
|
crc = crc.value.to_bytes(2, byteorder="big") # convert crc to 2 byte hex string
|
||||||
|
buffer += crc # append crc16 to buffer
|
||||||
|
|
||||||
print(f"TOTAL BURSTS: {self.N_BURSTS} TOTAL FRAMES_PER_BURST: {self.N_FRAMES_PER_BURST}", file=sys.stderr)
|
print(
|
||||||
|
f"TOTAL BURSTS: {self.N_BURSTS} TOTAL FRAMES_PER_BURST: {self.N_FRAMES_PER_BURST}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
for i in range(1,self.N_BURSTS+1):
|
for i in range(1, self.N_BURSTS + 1):
|
||||||
|
|
||||||
# write preamble to txbuffer
|
# write preamble to txbuffer
|
||||||
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
txbuffer = bytes(mod_out_preamble)
|
txbuffer = bytes(mod_out_preamble)
|
||||||
|
|
||||||
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
||||||
for n in range(1,self.N_FRAMES_PER_BURST+1):
|
for n in range(1, self.N_FRAMES_PER_BURST + 1):
|
||||||
|
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
codec2.api.freedv_rawdatatx(
|
||||||
|
freedv, mod_out, data
|
||||||
|
) # modulate DATA and save it into mod_out pointer
|
||||||
|
|
||||||
txbuffer += bytes(mod_out)
|
txbuffer += bytes(mod_out)
|
||||||
|
|
||||||
print(f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr)
|
print(
|
||||||
|
f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}",
|
||||||
# append postamble to txbuffer
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
# append postamble to txbuffer
|
||||||
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
txbuffer += bytes(mod_out_postamble)
|
txbuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
# append a delay between bursts as audio silence
|
# append a delay between bursts as audio silence
|
||||||
samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS)
|
samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS)
|
||||||
mod_out_silence = create_string_buffer(samples_delay*2)
|
mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
|
||||||
txbuffer += bytes(mod_out_silence)
|
txbuffer += bytes(mod_out_silence)
|
||||||
print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {self.DELAY_BETWEEN_BURSTS}", file=sys.stderr)
|
print(
|
||||||
|
f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {self.DELAY_BETWEEN_BURSTS}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
# resample up to 48k (resampler works on np.int16)
|
# resample up to 48k (resampler works on np.int16)
|
||||||
x = np.frombuffer(txbuffer, dtype=np.int16)
|
x = np.frombuffer(txbuffer, dtype=np.int16)
|
||||||
txbuffer_48k = self.resampler.resample8_to_48(x)
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
||||||
|
|
||||||
# split modualted audio to chunks
|
# split modualted audio to chunks
|
||||||
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
# https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
||||||
txbuffer_48k = bytes(txbuffer_48k)
|
txbuffer_48k = bytes(txbuffer_48k)
|
||||||
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)]
|
chunk = [
|
||||||
|
txbuffer_48k[i : i + self.AUDIO_FRAMES_PER_BUFFER * 2]
|
||||||
|
for i in range( len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER * 2)
|
||||||
|
]
|
||||||
# add modulated chunks to fifo buffer
|
# add modulated chunks to fifo buffer
|
||||||
for c in chunk:
|
for c in chunk:
|
||||||
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
||||||
# to prevent the callback from stucking/crashing
|
# to prevent the callback from stucking/crashing
|
||||||
if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2:
|
if len(c) < self.AUDIO_FRAMES_PER_BUFFER * 2:
|
||||||
c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c))
|
c += bytes(self.AUDIO_FRAMES_PER_BUFFER * 2 - len(c))
|
||||||
self.dataqueue.put(c)
|
self.dataqueue.put(c)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test = Test()
|
test = Test()
|
||||||
test.create_modulation()
|
test.create_modulation()
|
||||||
test.run_audio()
|
test.run_audio()
|
||||||
|
|
90
test/test_helpers.py
Normal file
90
test/test_helpers.py
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
"""
|
||||||
|
Tests for the FreeDATA TNC state machine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import helpers
|
||||||
|
import pytest
|
||||||
|
import static
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("callsign", ["AA1AA", "DE2DE", "E4AWQ-4"])
|
||||||
|
def test_check_callsign(callsign: str):
|
||||||
|
"""
|
||||||
|
Execute test to demonstrate how to create and verify callsign checksums.
|
||||||
|
"""
|
||||||
|
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
|
|
||||||
|
t_callsign_bytes = helpers.callsign_to_bytes(callsign)
|
||||||
|
t_callsign = helpers.bytes_to_callsign(t_callsign_bytes)
|
||||||
|
t_callsign_crc = helpers.get_crc_24(t_callsign)
|
||||||
|
|
||||||
|
dxcallsign_bytes = helpers.callsign_to_bytes("ZZ9ZZA-0")
|
||||||
|
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
|
||||||
|
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
|
||||||
|
|
||||||
|
assert helpers.check_callsign(t_callsign, t_callsign_crc)[0] is True
|
||||||
|
assert helpers.check_callsign(t_callsign, dxcallsign_crc)[0] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("callsign", ["AA1AA-2", "DE2DE-0", "E4AWQ-4"])
|
||||||
|
def test_callsign_to_bytes(callsign: str):
|
||||||
|
"""
|
||||||
|
Execute test to demonsrate symmetry when converting callsigns to and from byte arrays.
|
||||||
|
"""
|
||||||
|
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
|
|
||||||
|
t_callsign_crc = helpers.get_crc_24(bytes(callsign, "UTF-8"))
|
||||||
|
t_callsign_bytes = helpers.callsign_to_bytes(callsign)
|
||||||
|
t_callsign = helpers.bytes_to_callsign(t_callsign_bytes)
|
||||||
|
|
||||||
|
assert helpers.check_callsign(t_callsign, t_callsign_crc)[0] is True
|
||||||
|
assert helpers.check_callsign(t_callsign, t_callsign_crc)[1] == bytes(
|
||||||
|
callsign, "UTF-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("callsign", ["AA1AA-2", "DE2DE-0", "e4awq-4"])
|
||||||
|
def test_encode_callsign(callsign: str):
|
||||||
|
"""
|
||||||
|
Execute test to demonsrate symmetry when encoding and decoding callsigns.
|
||||||
|
"""
|
||||||
|
callenc = helpers.encode_call(callsign)
|
||||||
|
calldec = helpers.decode_call(callenc)
|
||||||
|
|
||||||
|
assert callsign.upper() != calldec
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("gridsq", ["EM98dc", "DE01GG", "EF42sW"])
|
||||||
|
def test_encode_grid(gridsq: str):
|
||||||
|
"""
|
||||||
|
Execute test to demonsrate symmetry when encoding and decoding grid squares.
|
||||||
|
"""
|
||||||
|
|
||||||
|
gridenc = helpers.encode_grid(gridsq)
|
||||||
|
griddec = helpers.decode_grid(gridenc)
|
||||||
|
|
||||||
|
assert gridsq.upper() == griddec
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("gridsq", ["SM98dc", "DE01GZ", "EV42sY"])
|
||||||
|
@pytest.mark.xfail(reason="Invalid gridsquare provided")
|
||||||
|
def test_invalid_grid(gridsq: str):
|
||||||
|
"""
|
||||||
|
Execute test to demonsrate symmetry when encoding and decoding grid squares.
|
||||||
|
"""
|
||||||
|
|
||||||
|
gridenc = helpers.encode_grid(gridsq)
|
||||||
|
griddec = helpers.decode_grid(gridenc)
|
||||||
|
|
||||||
|
assert gridsq.upper() != griddec
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
123
test/test_highsnr_stdio_C_P_datacx.py
Normal file
123
test/test_highsnr_stdio_C_P_datacx.py
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
"""
|
||||||
|
Tests a high signal-to-noise ratio path with codec2 data formats using codec2 to transmit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=global-statement, invalid-name, unused-import
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
BURSTS = int(os.environ["BURSTS"])
|
||||||
|
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
|
||||||
|
TESTFRAMES = int(os.environ["TESTFRAMES"])
|
||||||
|
except KeyError:
|
||||||
|
BURSTS = 1
|
||||||
|
FRAMESPERBURST = 1
|
||||||
|
TESTFRAMES = 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
|
||||||
|
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
|
||||||
|
@pytest.mark.parametrize("testframes", [TESTFRAMES, 2, 1])
|
||||||
|
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
|
||||||
|
def test_HighSNR_P_P_DATACx(
|
||||||
|
bursts: int, frames_per_burst: int, testframes: int, mode: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test a high signal-to-noise ratio path with DATAC0.
|
||||||
|
|
||||||
|
:param bursts: Number of bursts
|
||||||
|
:type bursts: str
|
||||||
|
:param frames_per_burst: Number of frames transmitted per burst
|
||||||
|
:type frames_per_burst: str
|
||||||
|
:param testframes: Number of test frames to transmit
|
||||||
|
:type testframes: str
|
||||||
|
"""
|
||||||
|
tx_side = "freedv_data_raw_tx"
|
||||||
|
|
||||||
|
# Facilitate running from main directory as well as inside test/
|
||||||
|
rx_side = "test_rx.py"
|
||||||
|
if os.path.exists("test") and os.path.exists(os.path.join("test", rx_side)):
|
||||||
|
rx_side = os.path.join("test", rx_side)
|
||||||
|
os.environ["PYTHONPATH"] += ":."
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
tx_side,
|
||||||
|
mode,
|
||||||
|
"--testframes",
|
||||||
|
f"{testframes}",
|
||||||
|
"--bursts",
|
||||||
|
f"{bursts}",
|
||||||
|
"--framesperburst",
|
||||||
|
f"{frames_per_burst}",
|
||||||
|
"/dev/zero",
|
||||||
|
"-",
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
) as transmit:
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"sox",
|
||||||
|
"-t",
|
||||||
|
".s16",
|
||||||
|
"-r",
|
||||||
|
"8000",
|
||||||
|
"-c",
|
||||||
|
"1",
|
||||||
|
"-",
|
||||||
|
"-t",
|
||||||
|
".s16",
|
||||||
|
"-r",
|
||||||
|
"48000",
|
||||||
|
"-c",
|
||||||
|
"1",
|
||||||
|
"-",
|
||||||
|
],
|
||||||
|
stdin=transmit.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
) as sox_filter:
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"python3",
|
||||||
|
rx_side,
|
||||||
|
"--mode",
|
||||||
|
mode,
|
||||||
|
"--framesperburst",
|
||||||
|
str(frames_per_burst),
|
||||||
|
"--bursts",
|
||||||
|
str(bursts),
|
||||||
|
],
|
||||||
|
stdin=sox_filter.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
) as receive:
|
||||||
|
assert receive.stdout
|
||||||
|
lastline = "".join(
|
||||||
|
[
|
||||||
|
str(line, "UTF-8")
|
||||||
|
for line in receive.stdout.readlines()
|
||||||
|
if "RECEIVED " in str(line, "UTF-8")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert f"RECEIVED BURSTS: {bursts}" in lastline
|
||||||
|
assert f"RECEIVED FRAMES: {frames_per_burst * bursts}" in lastline
|
||||||
|
assert "RX_ERRORS: 0" in lastline
|
||||||
|
print(lastline)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", "-s", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
126
test/test_highsnr_stdio_P_C_datacx.py
Normal file
126
test/test_highsnr_stdio_P_C_datacx.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
"""
|
||||||
|
Tests a high signal-to-noise ratio path with codec2 data formats using codec2 to receive.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=global-statement, invalid-name, unused-import
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
BURSTS = int(os.environ["BURSTS"])
|
||||||
|
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
|
||||||
|
TESTFRAMES = int(os.environ["TESTFRAMES"])
|
||||||
|
except KeyError:
|
||||||
|
BURSTS = 1
|
||||||
|
FRAMESPERBURST = 1
|
||||||
|
TESTFRAMES = 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
|
||||||
|
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
|
||||||
|
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
|
||||||
|
def test_HighSNR_P_P_DATACx(bursts: int, frames_per_burst: int, mode: str):
|
||||||
|
"""
|
||||||
|
Test a high signal-to-noise ratio path with DATAC0.
|
||||||
|
|
||||||
|
:param bursts: Number of bursts
|
||||||
|
:type bursts: str
|
||||||
|
:param frames_per_burst: Number of frames transmitted per burst
|
||||||
|
:type frames_per_burst: str
|
||||||
|
:param testframes: Number of test frames to transmit
|
||||||
|
:type testframes: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Facilitate running from main directory as well as inside test/
|
||||||
|
tx_side = "test_tx.py"
|
||||||
|
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
|
||||||
|
tx_side = os.path.join("test", tx_side)
|
||||||
|
os.environ["PYTHONPATH"] += ":."
|
||||||
|
rx_side = "freedv_data_raw_rx"
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"python3",
|
||||||
|
tx_side,
|
||||||
|
"--mode",
|
||||||
|
mode,
|
||||||
|
"--delay",
|
||||||
|
"500",
|
||||||
|
"--framesperburst",
|
||||||
|
f"{frames_per_burst}",
|
||||||
|
"--bursts",
|
||||||
|
f"{bursts}",
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
) as transmit:
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"sox",
|
||||||
|
"-t",
|
||||||
|
".s16",
|
||||||
|
"-r",
|
||||||
|
"48000",
|
||||||
|
"-c",
|
||||||
|
"1",
|
||||||
|
"-",
|
||||||
|
"-t",
|
||||||
|
".s16",
|
||||||
|
"-r",
|
||||||
|
"8000",
|
||||||
|
"-c",
|
||||||
|
"1",
|
||||||
|
"-",
|
||||||
|
],
|
||||||
|
stdin=transmit.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
) as sox_filter:
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
rx_side,
|
||||||
|
mode,
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
|
"--framesperburst",
|
||||||
|
str(frames_per_burst),
|
||||||
|
],
|
||||||
|
stdin=sox_filter.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
) as receive:
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"hexdump",
|
||||||
|
"-C",
|
||||||
|
],
|
||||||
|
stdin=receive.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
) as hexdump:
|
||||||
|
assert hexdump.stdout
|
||||||
|
lastline = "".join(
|
||||||
|
[
|
||||||
|
str(line, "UTF-8")
|
||||||
|
for line in hexdump.stdout.readlines()
|
||||||
|
if "HELLO" in str(line, "UTF-8")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert "HELLO WORLD!" in lastline
|
||||||
|
print(lastline)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", "-s", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
97
test/test_highsnr_stdio_P_P_datacx.py
Normal file
97
test/test_highsnr_stdio_P_P_datacx.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
"""
|
||||||
|
Tests a high signal-to-noise ratio path with codec2 data formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=global-statement, invalid-name, unused-import
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
BURSTS = int(os.environ["BURSTS"])
|
||||||
|
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
|
||||||
|
TESTFRAMES = int(os.environ["TESTFRAMES"])
|
||||||
|
except KeyError:
|
||||||
|
BURSTS = 1
|
||||||
|
FRAMESPERBURST = 1
|
||||||
|
TESTFRAMES = 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
|
||||||
|
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
|
||||||
|
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
|
||||||
|
def test_HighSNR_P_P_DATACx(bursts: int, frames_per_burst: int, mode: str):
|
||||||
|
"""
|
||||||
|
Test a high signal-to-noise ratio path with Codec2 modes.
|
||||||
|
|
||||||
|
:param bursts: Number of bursts
|
||||||
|
:type bursts: str
|
||||||
|
:param frames_per_burst: Number of frames transmitted per burst
|
||||||
|
:type frames_per_burst: str
|
||||||
|
"""
|
||||||
|
# Facilitate running from main directory as well as inside test/
|
||||||
|
tx_side = "test_tx.py"
|
||||||
|
rx_side = "test_rx.py"
|
||||||
|
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
|
||||||
|
tx_side = os.path.join("test", tx_side)
|
||||||
|
rx_side = os.path.join("test", rx_side)
|
||||||
|
os.environ["PYTHONPATH"] += ":."
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"python3",
|
||||||
|
tx_side,
|
||||||
|
"--mode",
|
||||||
|
mode,
|
||||||
|
"--delay",
|
||||||
|
"500",
|
||||||
|
"--framesperburst",
|
||||||
|
str(frames_per_burst),
|
||||||
|
"--bursts",
|
||||||
|
str(bursts),
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
) as transmit:
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"python3",
|
||||||
|
rx_side,
|
||||||
|
"--mode",
|
||||||
|
mode,
|
||||||
|
"--framesperburst",
|
||||||
|
str(frames_per_burst),
|
||||||
|
"--bursts",
|
||||||
|
str(bursts),
|
||||||
|
"--timeout",
|
||||||
|
"20",
|
||||||
|
],
|
||||||
|
stdin=transmit.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
) as receive:
|
||||||
|
assert receive.stdout
|
||||||
|
lastline = "".join(
|
||||||
|
[
|
||||||
|
str(line, "UTF-8")
|
||||||
|
for line in receive.stdout.readlines()
|
||||||
|
if "RECEIVED " in str(line, "UTF-8")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert f"RECEIVED BURSTS: {bursts}" in lastline
|
||||||
|
assert f"RECEIVED FRAMES: {frames_per_burst * bursts}" in lastline
|
||||||
|
assert "RX_ERRORS: 0" in lastline
|
||||||
|
print(lastline)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", "-s", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
92
test/test_highsnr_stdio_P_P_multi.py
Normal file
92
test/test_highsnr_stdio_P_P_multi.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
"""
|
||||||
|
Tests a high signal-to-noise ratio path with multiple codec2 data formats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=global-statement, invalid-name, unused-import
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
BURSTS = int(os.environ["BURSTS"])
|
||||||
|
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
|
||||||
|
TESTFRAMES = int(os.environ["TESTFRAMES"])
|
||||||
|
except KeyError:
|
||||||
|
BURSTS = 1
|
||||||
|
FRAMESPERBURST = 1
|
||||||
|
TESTFRAMES = 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
|
||||||
|
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
|
||||||
|
def test_HighSNR_P_P_Multi(bursts: int, frames_per_burst: int):
|
||||||
|
"""
|
||||||
|
Test a high signal-to-noise ratio path with DATAC0, DATAC1 and DATAC3.
|
||||||
|
|
||||||
|
:param bursts: Number of bursts
|
||||||
|
:type bursts: int
|
||||||
|
:param frames_per_burst: Number of frames transmitted per burst
|
||||||
|
:type frames_per_burst: int
|
||||||
|
"""
|
||||||
|
# Facilitate running from main directory as well as inside test/
|
||||||
|
tx_side = "test_multimode_tx.py"
|
||||||
|
rx_side = "test_multimode_rx.py"
|
||||||
|
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
|
||||||
|
tx_side = os.path.join("test", tx_side)
|
||||||
|
rx_side = os.path.join("test", rx_side)
|
||||||
|
os.environ["PYTHONPATH"] += ":."
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"python3",
|
||||||
|
tx_side,
|
||||||
|
"--delay",
|
||||||
|
"500",
|
||||||
|
"--framesperburst",
|
||||||
|
str(frames_per_burst),
|
||||||
|
"--bursts",
|
||||||
|
str(bursts),
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
) as transmit:
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
args=[
|
||||||
|
"python3",
|
||||||
|
rx_side,
|
||||||
|
"--framesperburst",
|
||||||
|
str(frames_per_burst),
|
||||||
|
"--bursts",
|
||||||
|
str(bursts),
|
||||||
|
"--timeout",
|
||||||
|
"20",
|
||||||
|
],
|
||||||
|
stdin=transmit.stdout,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
) as receive:
|
||||||
|
assert receive.stdout
|
||||||
|
lastline = "".join(
|
||||||
|
[
|
||||||
|
str(line, "UTF-8")
|
||||||
|
for line in receive.stdout.readlines()
|
||||||
|
if "DATAC" in str(line, "UTF-8")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert f"DATAC0: {bursts}/{frames_per_burst * bursts}" in lastline
|
||||||
|
assert f"DATAC1: {bursts}/{frames_per_burst * bursts}" in lastline
|
||||||
|
assert f"DATAC3: {bursts}/{frames_per_burst * bursts}" in lastline
|
||||||
|
print(lastline)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", "-s", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
160
test/test_modem.py
Normal file
160
test/test_modem.py
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
"""
|
||||||
|
Tests for the FreeDATA modem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import multiprocessing
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
|
sys.path.insert(0, "../tnc")
|
||||||
|
import helpers
|
||||||
|
import modem
|
||||||
|
import static
|
||||||
|
|
||||||
|
|
||||||
|
def print_frame(data: bytearray):
|
||||||
|
"""
|
||||||
|
Pretty-print the provided frame.
|
||||||
|
|
||||||
|
:param data: Frame to be output
|
||||||
|
:type data: bytearray
|
||||||
|
"""
|
||||||
|
print(f"Type : {int(data[0])}")
|
||||||
|
print(f"DXCRC : {bytes(data[1:4])}")
|
||||||
|
print(f"CallCRC: {bytes(data[4:7])}")
|
||||||
|
print(f"Call : {helpers.bytes_to_callsign(data[7:13])}")
|
||||||
|
|
||||||
|
|
||||||
|
def t_create_frame(frame_type: int, mycall: str, dxcall: str) -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate the requested frame.
|
||||||
|
|
||||||
|
:param frame_type: The numerical type of the desired frame.
|
||||||
|
:type frame_type: int
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
|
||||||
|
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
|
||||||
|
mycallsign_crc = helpers.get_crc_24(mycallsign)
|
||||||
|
|
||||||
|
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
|
||||||
|
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
|
||||||
|
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
|
||||||
|
|
||||||
|
frame = bytearray(14)
|
||||||
|
frame[:1] = bytes([frame_type])
|
||||||
|
frame[1:4] = dxcallsign_crc
|
||||||
|
frame[4:7] = mycallsign_crc
|
||||||
|
frame[7:13] = mycallsign_bytes
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate the session_close frame.
|
||||||
|
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
return t_create_frame(223, mycall, dxcall)
|
||||||
|
|
||||||
|
|
||||||
|
def t_create_start_session(mycall: str, dxcall: str) -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate the create_session frame.
|
||||||
|
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
return t_create_frame(221, mycall, dxcall)
|
||||||
|
|
||||||
|
|
||||||
|
def t_tsh_dummy():
|
||||||
|
"""Replacement function for transmit"""
|
||||||
|
print("In t_tsh_dummy")
|
||||||
|
|
||||||
|
|
||||||
|
def t_modem():
|
||||||
|
"""
|
||||||
|
Execute test to validate that receiving a session open frame sets the correct machine
|
||||||
|
state.
|
||||||
|
"""
|
||||||
|
t_mode = t_repeats = t_repeat_delay = 0
|
||||||
|
t_frames = []
|
||||||
|
|
||||||
|
# enable testmode
|
||||||
|
modem.TESTMODE = True
|
||||||
|
modem.RXCHANNEL = "/tmp/hfchannel1"
|
||||||
|
modem.TXCHANNEL = "/tmp/hfchannel2"
|
||||||
|
static.HAMLIB_RADIOCONTROL = "disabled"
|
||||||
|
|
||||||
|
def t_tx_dummy(mode, repeats, repeat_delay, frames):
|
||||||
|
"""Replacement function for transmit"""
|
||||||
|
print(f"t_tx_dummy: In transmit({mode}, {repeats}, {repeat_delay}, {frames})")
|
||||||
|
nonlocal t_mode, t_repeats, t_repeat_delay, t_frames
|
||||||
|
t_mode = mode
|
||||||
|
t_repeats = repeats
|
||||||
|
t_repeat_delay = repeat_delay
|
||||||
|
t_frames = frames[:]
|
||||||
|
static.TRANSMITTING = False
|
||||||
|
|
||||||
|
# Create the modem
|
||||||
|
local_modem = modem.RF()
|
||||||
|
|
||||||
|
# Replace transmit routine with our own, an effective No-Op.
|
||||||
|
local_modem.transmit = t_tx_dummy
|
||||||
|
|
||||||
|
txbuffer = t_create_start_session("AA9AA", "DC2EJ")
|
||||||
|
|
||||||
|
# Start the transmission
|
||||||
|
static.TRANSMITTING = True
|
||||||
|
modem.MODEM_TRANSMIT_QUEUE.put([14, 5, 250, txbuffer])
|
||||||
|
while static.TRANSMITTING:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Check that the contents were transferred correctly.
|
||||||
|
assert t_mode == 14
|
||||||
|
assert t_repeats == 5
|
||||||
|
assert t_repeat_delay == 250
|
||||||
|
assert t_frames == txbuffer
|
||||||
|
|
||||||
|
|
||||||
|
def test_modem_queue():
|
||||||
|
proc = multiprocessing.Process(target=t_modem, args=())
|
||||||
|
# print("Starting threads.")
|
||||||
|
proc.start()
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# print("Terminating threads.")
|
||||||
|
proc.terminate()
|
||||||
|
proc.join()
|
||||||
|
|
||||||
|
# print(f"\n{proc.exitcode=}")
|
||||||
|
assert proc.exitcode == 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", "-s", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
|
@ -1,236 +1,242 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import pyaudio
|
|
||||||
import audioop
|
|
||||||
import time
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes import *
|
import sys
|
||||||
import pathlib
|
import time
|
||||||
sys.path.insert(0,'..')
|
from typing import List
|
||||||
from tnc import codec2
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pyaudio
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
sys.path.insert(0, "..")
|
||||||
parser = argparse.ArgumentParser(description='Simons TEST TNC')
|
from tnc import codec2
|
||||||
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:
|
def test_mm_rx():
|
||||||
p = pyaudio.PyAudio()
|
# AUDIO PARAMETERS
|
||||||
for dev in range(0,p.get_device_count()):
|
AUDIO_FRAMES_PER_BUFFER = 2400 * 2
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
quit()
|
AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
# make sure our resampler will work
|
||||||
|
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
N_BURSTS = args.N_BURSTS
|
# SET COUNTERS
|
||||||
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
rx_bursts_datac = [0, 0, 0]
|
||||||
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
rx_frames_datac = [0, 0, 0]
|
||||||
DEBUGGING_MODE = args.DEBUGGING_MODE
|
rx_total_frames_datac = [0, 0, 0]
|
||||||
TIMEOUT = args.TIMEOUT
|
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
# time meassurement
|
||||||
AUDIO_FRAMES_PER_BUFFER = 2400*2
|
time_end_datac = [0.0, 0.0, 0.0]
|
||||||
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
time_needed_datac = [0.0, 0.0, 0.0]
|
||||||
AUDIO_SAMPLE_RATE_RX = 48000
|
time_start_datac = [0.0, 0.0, 0.0]
|
||||||
# make sure our resampler will work
|
|
||||||
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
|
||||||
|
|
||||||
# SET COUNTERS
|
datac_buffer: List[codec2.audio_buffer] = []
|
||||||
rx_total_frames_datac0 = 0
|
datac_bytes_out: List[ctypes.Array] = []
|
||||||
rx_frames_datac0 = 0
|
datac_bytes_per_frame = []
|
||||||
rx_bursts_datac0 = 0
|
datac_freedv: List[ctypes.c_void_p] = []
|
||||||
|
|
||||||
rx_total_frames_datac1 = 0
|
args = parse_arguments()
|
||||||
rx_frames_datac1 = 0
|
|
||||||
rx_bursts_datac1 = 0
|
|
||||||
|
|
||||||
rx_total_frames_datac3 = 0
|
if args.LIST:
|
||||||
rx_frames_datac3 = 0
|
p_audio = pyaudio.PyAudio()
|
||||||
rx_bursts_datac3 = 0
|
for dev in range(p_audio.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p_audio.get_device_info_by_index(dev)["name"])
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
# time meassurement
|
N_BURSTS = args.N_BURSTS
|
||||||
time_start_datac0 = 0
|
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
time_end_datac0 = 0
|
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
||||||
time_start_datac1 = 0
|
DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
time_end_datac1 = 0
|
MAX_TIME = args.TIMEOUT
|
||||||
time_start_datac3 = 0
|
|
||||||
time_end_datac3 = 0
|
|
||||||
time_needed_datac0 = 0
|
|
||||||
time_needed_datac1 = 0
|
|
||||||
time_needed_datac3 = 0
|
|
||||||
|
|
||||||
# open codec2 instance
|
# open codec2 instances
|
||||||
datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
for idx in range(3):
|
||||||
datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8)
|
datac_freedv.append(
|
||||||
datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame)
|
ctypes.cast(
|
||||||
codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST)
|
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
|
||||||
datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
)
|
||||||
|
)
|
||||||
|
datac_bytes_per_frame.append(
|
||||||
|
int(codec2.api.freedv_get_bits_per_modem_frame(datac_freedv[idx]) / 8)
|
||||||
|
)
|
||||||
|
datac_bytes_out.append(ctypes.create_string_buffer(datac_bytes_per_frame[idx]))
|
||||||
|
codec2.api.freedv_set_frames_per_burst(datac_freedv[idx], N_FRAMES_PER_BURST)
|
||||||
|
datac_buffer.append(codec2.audio_buffer(2 * AUDIO_FRAMES_PER_BUFFER))
|
||||||
|
|
||||||
datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
|
resampler = codec2.resampler()
|
||||||
datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8)
|
|
||||||
datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame)
|
|
||||||
codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST)
|
|
||||||
datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
|
||||||
|
|
||||||
datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p)
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8)
|
if AUDIO_INPUT_DEVICE != -1:
|
||||||
datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame)
|
p_audio = pyaudio.PyAudio()
|
||||||
codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST)
|
# auto search for loopback devices
|
||||||
datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
|
if AUDIO_INPUT_DEVICE == -2:
|
||||||
|
loopback_list = [
|
||||||
|
dev
|
||||||
|
for dev in range(p_audio.get_device_count())
|
||||||
|
if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"]
|
||||||
|
]
|
||||||
|
|
||||||
resampler = codec2.resampler()
|
if len(loopback_list) >= 2:
|
||||||
|
AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
|
||||||
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
print(
|
||||||
if AUDIO_INPUT_DEVICE != -1:
|
f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE} "
|
||||||
p = pyaudio.PyAudio()
|
f"DEVICE: {p_audio.get_device_info_by_index(AUDIO_INPUT_DEVICE)['name']} "
|
||||||
# auto search for loopback devices
|
f"AUDIO SAMPLE RATE: {AUDIO_SAMPLE_RATE_RX}",
|
||||||
if AUDIO_INPUT_DEVICE == -2:
|
file=sys.stderr,
|
||||||
loopback_list = []
|
)
|
||||||
for dev in range(0,p.get_device_count()):
|
stream_rx = p_audio.open(
|
||||||
if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]:
|
format=pyaudio.paInt16,
|
||||||
loopback_list.append(dev)
|
channels=1,
|
||||||
if len(loopback_list) >= 2:
|
rate=AUDIO_SAMPLE_RATE_RX,
|
||||||
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
|
||||||
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
input=True,
|
||||||
|
input_device_index=AUDIO_INPUT_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
|
timeout = time.time() + MAX_TIME
|
||||||
|
print(time.time(), MAX_TIME, timeout)
|
||||||
|
receive = True
|
||||||
|
nread_exceptions = 0
|
||||||
|
|
||||||
|
# initial nin values
|
||||||
|
datac_nin = [0, 0, 0]
|
||||||
|
for idx in range(3):
|
||||||
|
datac_nin[idx] = codec2.api.freedv_nin(datac_freedv[idx])
|
||||||
|
|
||||||
|
def print_stats(time_datac0, time_datac1, time_datac3):
|
||||||
|
if not DEBUGGING_MODE:
|
||||||
|
return
|
||||||
|
|
||||||
|
time_datac = [time_datac0, time_datac1, time_datac3]
|
||||||
|
datac_rxstatus = ["", "", ""]
|
||||||
|
for idx in range(3):
|
||||||
|
datac_rxstatus[idx] = codec2.api.rx_sync_flags_to_text[
|
||||||
|
codec2.api.freedv_get_rx_status(datac_freedv[idx])
|
||||||
|
]
|
||||||
|
|
||||||
|
text_out = ""
|
||||||
|
for idx in range(3):
|
||||||
|
text_out += f"NIN{idx}: {datac_nin[idx]:5d} "
|
||||||
|
text_out += f"RX_STATUS{idx}: {datac_rxstatus[idx]:4s} "
|
||||||
|
text_out += f"TIME: {round(time_datac[idx], 4):.4f} | "
|
||||||
|
text_out = text_out.rstrip(" ").rstrip("|").rstrip(" ")
|
||||||
|
print(text_out, file=sys.stderr)
|
||||||
|
|
||||||
|
while receive and time.time() < timeout:
|
||||||
|
if AUDIO_INPUT_DEVICE != -1:
|
||||||
|
try:
|
||||||
|
data_in48k = stream_rx.read(
|
||||||
|
AUDIO_FRAMES_PER_BUFFER, exception_on_overflow=True
|
||||||
|
)
|
||||||
|
except OSError as err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
if "Input overflowed" in str(err):
|
||||||
|
nread_exceptions += 1
|
||||||
|
if "Stream closed" in str(err):
|
||||||
|
print("Ending....")
|
||||||
|
receive = False
|
||||||
else:
|
else:
|
||||||
quit()
|
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2)
|
||||||
|
|
||||||
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE} DEVICE: {p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['name']} AUDIO SAMPLE RATE: {AUDIO_SAMPLE_RATE_RX}", file=sys.stderr)
|
|
||||||
stream_rx = p.open(format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=AUDIO_SAMPLE_RATE_RX,
|
|
||||||
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
|
|
||||||
input=True,
|
|
||||||
input_device_index=AUDIO_INPUT_DEVICE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
timeout = time.time() + TIMEOUT
|
# insert samples in buffer
|
||||||
print(time.time(),TIMEOUT, timeout)
|
audio_buffer = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
receive = True
|
if len(audio_buffer) != AUDIO_FRAMES_PER_BUFFER:
|
||||||
nread_exceptions = 0
|
print("len(x)", len(audio_buffer))
|
||||||
|
receive = False
|
||||||
|
audio_buffer = resampler.resample48_to_8(audio_buffer)
|
||||||
|
|
||||||
# initial nin values
|
for idx in range(3):
|
||||||
datac0_nin = codec2.api.freedv_nin(datac0_freedv)
|
datac_buffer[idx].push(audio_buffer)
|
||||||
datac1_nin = codec2.api.freedv_nin(datac1_freedv)
|
while datac_buffer[idx].nbuffer >= datac_nin[idx]:
|
||||||
datac3_nin = codec2.api.freedv_nin(datac3_freedv)
|
# demodulate audio
|
||||||
|
time_start_datac[idx] = time.time()
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
datac_freedv[idx],
|
||||||
|
datac_bytes_out[idx],
|
||||||
|
datac_buffer[idx].buffer.ctypes,
|
||||||
|
)
|
||||||
|
time_end_datac[idx] = time.time()
|
||||||
|
datac_buffer[idx].pop(datac_nin[idx])
|
||||||
|
datac_nin[idx] = codec2.api.freedv_nin(datac_freedv[idx])
|
||||||
|
if nbytes == datac_bytes_per_frame[idx]:
|
||||||
|
rx_total_frames_datac[idx] += 1
|
||||||
|
rx_frames_datac[idx] += 1
|
||||||
|
|
||||||
def print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3):
|
if rx_frames_datac[idx] == N_FRAMES_PER_BURST:
|
||||||
if DEBUGGING_MODE:
|
rx_frames_datac[idx] = 0
|
||||||
datac0_rxstatus = codec2.api.freedv_get_rx_status(datac0_freedv)
|
rx_bursts_datac[idx] += 1
|
||||||
datac0_rxstatus = codec2.api.rx_sync_flags_to_text[datac0_rxstatus]
|
time_needed_datac[idx] = time_end_datac[idx] - time_start_datac[idx]
|
||||||
|
print_stats(
|
||||||
|
time_needed_datac[0], time_needed_datac[1], time_needed_datac[2]
|
||||||
|
)
|
||||||
|
|
||||||
datac1_rxstatus = codec2.api.freedv_get_rx_status(datac1_freedv)
|
if (
|
||||||
datac1_rxstatus = codec2.api.rx_sync_flags_to_text[datac1_rxstatus]
|
rx_bursts_datac[0] == N_BURSTS
|
||||||
|
and rx_bursts_datac[1] == N_BURSTS
|
||||||
datac3_rxstatus = codec2.api.freedv_get_rx_status(datac3_freedv)
|
and rx_bursts_datac[2] == N_BURSTS
|
||||||
datac3_rxstatus = codec2.api.rx_sync_flags_to_text[datac3_rxstatus]
|
):
|
||||||
|
receive = False
|
||||||
|
|
||||||
print("NIN0: %5d RX_STATUS0: %4s TIME: %4s | NIN1: %5d RX_STATUS1: %4s TIME: %4s | NIN3: %5d RX_STATUS3: %4s TIME: %4s" % \
|
if nread_exceptions:
|
||||||
(datac0_nin, datac0_rxstatus, round(time_needed_datac0, 4), datac1_nin, datac1_rxstatus, round(time_needed_datac1, 4) ,datac3_nin, datac3_rxstatus, round(time_needed_datac3, 4)),
|
print(
|
||||||
file=sys.stderr)
|
f"nread_exceptions {nread_exceptions:d} - receive audio lost! "
|
||||||
|
"Consider increasing Pyaudio frames_per_buffer...",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
# INFO IF WE REACHED TIMEOUT
|
||||||
|
if time.time() > timeout:
|
||||||
|
print("TIMEOUT REACHED", file=sys.stderr)
|
||||||
|
|
||||||
while receive and time.time() < timeout:
|
print(
|
||||||
if AUDIO_INPUT_DEVICE != -1:
|
f"DATAC0: {rx_bursts_datac[0]}/{rx_total_frames_datac[0]} "
|
||||||
try:
|
f"DATAC1: {rx_bursts_datac[1]}/{rx_total_frames_datac[1]} "
|
||||||
data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True)
|
f"DATAC3: {rx_bursts_datac[2]}/{rx_total_frames_datac[2]}",
|
||||||
except OSError as err:
|
file=sys.stderr,
|
||||||
print(err, file=sys.stderr)
|
)
|
||||||
if str(err).find("Input overflowed") != -1:
|
|
||||||
nread_exceptions += 1
|
|
||||||
if str(err).find("Stream closed") != -1:
|
|
||||||
print("Ending....")
|
|
||||||
receive = False
|
|
||||||
else:
|
|
||||||
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2)
|
|
||||||
|
|
||||||
# insert samples in buffer
|
|
||||||
x = np.frombuffer(data_in48k, dtype=np.int16)
|
|
||||||
if len(x) != AUDIO_FRAMES_PER_BUFFER:
|
|
||||||
print("len(x)",len(x))
|
|
||||||
receive = False
|
|
||||||
x = resampler.resample48_to_8(x)
|
|
||||||
|
|
||||||
datac0_buffer.push(x)
|
|
||||||
datac1_buffer.push(x)
|
|
||||||
datac3_buffer.push(x)
|
|
||||||
print_something = False
|
|
||||||
|
|
||||||
while datac0_buffer.nbuffer >= datac0_nin:
|
|
||||||
# demodulate audio
|
|
||||||
time_start_datac0 = time.time()
|
|
||||||
nbytes = codec2.api.freedv_rawdatarx(datac0_freedv, datac0_bytes_out, datac0_buffer.buffer.ctypes)
|
|
||||||
time_end_datac0 = time.time()
|
|
||||||
datac0_buffer.pop(datac0_nin)
|
|
||||||
datac0_nin = codec2.api.freedv_nin(datac0_freedv)
|
|
||||||
if nbytes == datac0_bytes_per_frame:
|
|
||||||
rx_total_frames_datac0 = rx_total_frames_datac0 + 1
|
|
||||||
rx_frames_datac0 = rx_frames_datac0 + 1
|
|
||||||
|
|
||||||
if rx_frames_datac0 == N_FRAMES_PER_BURST:
|
if AUDIO_INPUT_DEVICE != -1:
|
||||||
rx_frames_datac0 = 0
|
stream_rx.close()
|
||||||
rx_bursts_datac0 = rx_bursts_datac0 + 1
|
p_audio.terminate()
|
||||||
time_needed_datac0 = time_end_datac0 - time_start_datac0
|
|
||||||
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
|
|
||||||
|
|
||||||
while datac1_buffer.nbuffer >= datac1_nin:
|
|
||||||
# demodulate audio
|
|
||||||
time_start_datac1 = time.time()
|
|
||||||
nbytes = codec2.api.freedv_rawdatarx(datac1_freedv, datac1_bytes_out, datac1_buffer.buffer.ctypes)
|
|
||||||
time_end_datac1 = time.time()
|
|
||||||
datac1_buffer.pop(datac1_nin)
|
|
||||||
datac1_nin = codec2.api.freedv_nin(datac1_freedv)
|
|
||||||
if nbytes == datac1_bytes_per_frame:
|
|
||||||
rx_total_frames_datac1 = rx_total_frames_datac1 + 1
|
|
||||||
rx_frames_datac1 = rx_frames_datac1 + 1
|
|
||||||
|
|
||||||
if rx_frames_datac1 == N_FRAMES_PER_BURST:
|
|
||||||
rx_frames_datac1 = 0
|
|
||||||
rx_bursts_datac1 = rx_bursts_datac1 + 1
|
|
||||||
time_needed_datac1 = time_end_datac1 - time_start_datac1
|
|
||||||
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
|
|
||||||
|
|
||||||
while datac3_buffer.nbuffer >= datac3_nin:
|
|
||||||
# demodulate audio
|
|
||||||
time_start_datac3 = time.time()
|
|
||||||
nbytes = codec2.api.freedv_rawdatarx(datac3_freedv, datac3_bytes_out, datac3_buffer.buffer.ctypes)
|
|
||||||
time_end_datac3 = time.time()
|
|
||||||
datac3_buffer.pop(datac3_nin)
|
|
||||||
datac3_nin = codec2.api.freedv_nin(datac3_freedv)
|
|
||||||
if nbytes == datac3_bytes_per_frame:
|
|
||||||
rx_total_frames_datac3 = rx_total_frames_datac3 + 1
|
|
||||||
rx_frames_datac3 = rx_frames_datac3 + 1
|
|
||||||
|
|
||||||
if rx_frames_datac3 == N_FRAMES_PER_BURST:
|
|
||||||
rx_frames_datac3 = 0
|
|
||||||
rx_bursts_datac3 = rx_bursts_datac3 + 1
|
|
||||||
time_needed_datac3 = time_end_datac3 - time_start_datac3
|
|
||||||
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
|
|
||||||
|
|
||||||
if rx_bursts_datac0 == N_BURSTS and rx_bursts_datac1 == N_BURSTS and rx_bursts_datac3 == N_BURSTS:
|
|
||||||
receive = False
|
|
||||||
|
|
||||||
if nread_exceptions:
|
|
||||||
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
|
||||||
nread_exceptions, file=sys.stderr)
|
|
||||||
# INFO IF WE REACHED TIMEOUT
|
|
||||||
if time.time() > timeout:
|
|
||||||
print(f"TIMEOUT REACHED", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description="Simons TEST TNC")
|
||||||
|
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--audiodev",
|
||||||
|
dest="AUDIO_INPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio device number to use",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
dest="TIMEOUT",
|
||||||
|
default=10,
|
||||||
|
type=int,
|
||||||
|
help="Timeout (seconds) before test ends",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
print(f"DATAC0: {rx_bursts_datac0}/{rx_total_frames_datac0} DATAC1: {rx_bursts_datac1}/{rx_total_frames_datac1} DATAC3: {rx_bursts_datac3}/{rx_total_frames_datac3}", file=sys.stderr)
|
if __name__ == "__main__":
|
||||||
|
test_mm_rx()
|
||||||
if AUDIO_INPUT_DEVICE != -1:
|
|
||||||
stream_rx.close()
|
|
||||||
p.terminate()
|
|
||||||
|
|
|
@ -2,150 +2,187 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import pyaudio
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import audioop
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'..')
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pyaudio
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# GET PARAMETER INPUTS
|
|
||||||
parser = argparse.ArgumentParser(description='FreeDATA TEST')
|
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
|
||||||
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
|
||||||
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
|
|
||||||
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, help="audio output device number to use")
|
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.LIST:
|
|
||||||
p = pyaudio.PyAudio()
|
|
||||||
for dev in range(0,p.get_device_count()):
|
|
||||||
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
|
|
||||||
quit()
|
|
||||||
|
|
||||||
N_BURSTS = args.N_BURSTS
|
|
||||||
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
|
||||||
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
|
|
||||||
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
|
||||||
|
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
def test_mm_tx():
|
||||||
AUDIO_FRAMES_PER_BUFFER = 2400
|
# AUDIO PARAMETERS
|
||||||
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
AUDIO_FRAMES_PER_BUFFER = 2400
|
||||||
AUDIO_SAMPLE_RATE_TX = 48000
|
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
|
AUDIO_SAMPLE_RATE_TX = 48000
|
||||||
|
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
|
||||||
|
|
||||||
if AUDIO_OUTPUT_DEVICE != -1:
|
args = parse_arguments()
|
||||||
p = pyaudio.PyAudio()
|
|
||||||
# auto search for loopback devices
|
|
||||||
if AUDIO_OUTPUT_DEVICE == -2:
|
|
||||||
loopback_list = []
|
|
||||||
for dev in range(0,p.get_device_count()):
|
|
||||||
if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]:
|
|
||||||
loopback_list.append(dev)
|
|
||||||
if len(loopback_list) >= 2:
|
|
||||||
AUDIO_OUTPUT_DEVICE = loopback_list[1] #0 = RX 1 = TX
|
|
||||||
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
quit()
|
|
||||||
# pyaudio init
|
|
||||||
stream_tx = p.open(format=pyaudio.paInt16,
|
|
||||||
channels=1,
|
|
||||||
rate=AUDIO_SAMPLE_RATE_TX,
|
|
||||||
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples
|
|
||||||
output=True,
|
|
||||||
output_device_index=AUDIO_OUTPUT_DEVICE,
|
|
||||||
)
|
|
||||||
|
|
||||||
resampler = codec2.resampler()
|
if args.LIST:
|
||||||
modes = [codec2.api.FREEDV_MODE_DATAC0, codec2.api.FREEDV_MODE_DATAC1, codec2.api.FREEDV_MODE_DATAC3]
|
p_audio = pyaudio.PyAudio()
|
||||||
for m in modes:
|
for dev in range(p_audio.get_device_count()):
|
||||||
|
print("audiodev: ", dev, p_audio.get_device_info_by_index(dev)["name"])
|
||||||
freedv = cast(codec2.api.freedv_open(m), c_void_p)
|
sys.exit()
|
||||||
|
|
||||||
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)
|
N_BURSTS = args.N_BURSTS
|
||||||
mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples)
|
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
|
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
|
||||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
||||||
payload_per_frame = bytes_per_frame - 2
|
|
||||||
|
|
||||||
|
resampler = codec2.resampler()
|
||||||
# data binary string
|
|
||||||
data_out = b'HELLO WORLD!'
|
|
||||||
|
|
||||||
buffer = bytearray(payload_per_frame)
|
|
||||||
# set buffersize to length of data which will be send
|
|
||||||
buffer[:len(data_out)] = data_out
|
|
||||||
|
|
||||||
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16
|
# Data binary string
|
||||||
# convert crc to 2 byte hex string
|
data_out = b"HELLO WORLD!"
|
||||||
crc = crc.value.to_bytes(2, byteorder='big')
|
|
||||||
buffer += crc # append crc16 to buffer
|
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
|
||||||
|
|
||||||
for i in range(1,N_BURSTS+1):
|
|
||||||
|
|
||||||
# write preamble to txbuffer
|
modes = [
|
||||||
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
codec2.api.FREEDV_MODE_DATAC0,
|
||||||
txbuffer = bytes(mod_out_preamble)
|
codec2.api.FREEDV_MODE_DATAC1,
|
||||||
|
codec2.api.FREEDV_MODE_DATAC3,
|
||||||
|
]
|
||||||
|
|
||||||
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
if AUDIO_OUTPUT_DEVICE != -1:
|
||||||
for n in range(1,N_FRAMES_PER_BURST+1):
|
p_audio = pyaudio.PyAudio()
|
||||||
|
# Auto search for loopback devices
|
||||||
|
if AUDIO_OUTPUT_DEVICE == -2:
|
||||||
|
loopback_list = [
|
||||||
|
dev
|
||||||
|
for dev in range(p_audio.get_device_count())
|
||||||
|
if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"]
|
||||||
|
]
|
||||||
|
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
if len(loopback_list) >= 2:
|
||||||
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
AUDIO_OUTPUT_DEVICE = loopback_list[1] # 0 = RX 1 = TX
|
||||||
|
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
txbuffer += bytes(mod_out)
|
# pyaudio init
|
||||||
print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{N_FRAMES_PER_BURST}", file=sys.stderr)
|
stream_tx = p_audio.open(
|
||||||
|
format=pyaudio.paInt16,
|
||||||
|
channels=1,
|
||||||
|
rate=AUDIO_SAMPLE_RATE_TX,
|
||||||
|
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples
|
||||||
|
output=True,
|
||||||
|
output_device_index=AUDIO_OUTPUT_DEVICE,
|
||||||
|
)
|
||||||
|
|
||||||
# append postamble to txbuffer
|
for mode in modes:
|
||||||
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
freedv = ctypes.cast(codec2.api.freedv_open(mode), ctypes.c_void_p)
|
||||||
txbuffer += bytes(mod_out_postamble)
|
|
||||||
|
|
||||||
# append a delay between bursts as audio silence
|
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||||
samples_delay = int(MODEM_SAMPLE_RATE*DELAY_BETWEEN_BURSTS)
|
mod_out = ctypes.create_string_buffer(2 * n_tx_modem_samples)
|
||||||
mod_out_silence = create_string_buffer(samples_delay*2)
|
|
||||||
txbuffer += bytes(mod_out_silence)
|
|
||||||
|
|
||||||
# resample up to 48k (resampler works on np.int16)
|
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
|
||||||
x = np.frombuffer(txbuffer, dtype=np.int16)
|
freedv
|
||||||
txbuffer_48k = resampler.resample8_to_48(x)
|
)
|
||||||
|
mod_out_preamble = ctypes.create_string_buffer(2 * n_tx_preamble_modem_samples)
|
||||||
|
|
||||||
# check if we want to use an audio device or stdout
|
n_tx_postamble_modem_samples = (
|
||||||
if AUDIO_OUTPUT_DEVICE != -1:
|
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||||
stream_tx.write(txbuffer_48k.tobytes())
|
)
|
||||||
else:
|
mod_out_postamble = ctypes.create_string_buffer(
|
||||||
# this test needs a lot of time, so we are having a look at times...
|
2 * n_tx_postamble_modem_samples
|
||||||
starttime = time.time()
|
)
|
||||||
|
|
||||||
# print data to terminal for piping the output to other programs
|
|
||||||
sys.stdout.buffer.write(txbuffer_48k)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
# and at least print the needed time to see which time we needed
|
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||||
timeneeded = time.time()-starttime
|
payload_per_frame = bytes_per_frame - 2
|
||||||
#print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
# and at last check if we had an opened pyaudio instance and close it
|
|
||||||
if AUDIO_OUTPUT_DEVICE != -1:
|
|
||||||
time.sleep(stream_tx.get_output_latency())
|
|
||||||
stream_tx.stop_stream()
|
|
||||||
stream_tx.close()
|
|
||||||
p.terminate()
|
|
||||||
|
|
||||||
|
|
||||||
|
buffer = bytearray(payload_per_frame)
|
||||||
|
# Set buffersize to length of data which will be send
|
||||||
|
buffer[: len(data_out)] = data_out
|
||||||
|
|
||||||
|
# Generate CRC16
|
||||||
|
crc = ctypes.c_ushort(
|
||||||
|
codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)
|
||||||
|
)
|
||||||
|
# Convert CRC to 2 byte hex string
|
||||||
|
crc = crc.value.to_bytes(2, byteorder="big")
|
||||||
|
buffer += crc # Append crc16 to buffer
|
||||||
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
|
||||||
|
for brst in range(1, N_BURSTS + 1):
|
||||||
|
# Write preamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
|
txbuffer = bytes(mod_out_preamble)
|
||||||
|
|
||||||
|
# Create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
||||||
|
for frm in range(1, N_FRAMES_PER_BURST + 1):
|
||||||
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
# Modulate DATA and save it into mod_out pointer
|
||||||
|
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
|
||||||
|
|
||||||
|
txbuffer += bytes(mod_out)
|
||||||
|
print(
|
||||||
|
f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{N_FRAMES_PER_BURST}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Append postamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
|
txbuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
|
# Append a delay between bursts as audio silence
|
||||||
|
samples_delay = int(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS)
|
||||||
|
mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
|
||||||
|
txbuffer += bytes(mod_out_silence)
|
||||||
|
|
||||||
|
# Resample up to 48k (resampler works on np.int16)
|
||||||
|
audio_buffer = np.frombuffer(txbuffer, dtype=np.int16)
|
||||||
|
txbuffer_48k = resampler.resample8_to_48(audio_buffer)
|
||||||
|
|
||||||
|
# Check if we want to use an audio device or stdout
|
||||||
|
if AUDIO_OUTPUT_DEVICE != -1:
|
||||||
|
stream_tx.write(txbuffer_48k.tobytes())
|
||||||
|
else:
|
||||||
|
# This test needs a lot of time, so we are having a look at times...
|
||||||
|
starttime = time.time()
|
||||||
|
|
||||||
|
# Print data to terminal for piping the output to other programs
|
||||||
|
sys.stdout.buffer.write(txbuffer_48k)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# and at least print the needed time to see which time we needed
|
||||||
|
timeneeded = time.time() - starttime
|
||||||
|
# print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr)
|
||||||
|
|
||||||
|
# and at last check if we had an opened pyaudio instance and close it
|
||||||
|
if AUDIO_OUTPUT_DEVICE != -1:
|
||||||
|
time.sleep(stream_tx.get_output_latency())
|
||||||
|
stream_tx.stop_stream()
|
||||||
|
stream_tx.close()
|
||||||
|
p_audio.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
# GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description="FreeDATA TEST")
|
||||||
|
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
|
||||||
|
)
|
||||||
|
parser.add_argument("--delay", dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
"--audiodev",
|
||||||
|
dest="AUDIO_OUTPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio output device number to use",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_mm_tx()
|
||||||
|
|
|
@ -3,37 +3,46 @@
|
||||||
#
|
#
|
||||||
# Throw away test program to help understand the care and feeding of PyAudio
|
# Throw away test program to help understand the care and feeding of PyAudio
|
||||||
|
|
||||||
import pyaudio
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pyaudio
|
||||||
|
|
||||||
CHUNK = 1024
|
CHUNK = 1024
|
||||||
FS48 = 48000
|
FS48 = 48000
|
||||||
FTEST = 800
|
FTEST = 800
|
||||||
AMP = 16000
|
AMP = 16000
|
||||||
|
|
||||||
# 1. play sine wave out of default sound device
|
|
||||||
|
|
||||||
p = pyaudio.PyAudio()
|
def test_pa():
|
||||||
stream = p.open(format=pyaudio.paInt16,
|
# 1. play sine wave out of default sound device
|
||||||
channels=1,
|
|
||||||
rate=FS48,
|
|
||||||
frames_per_buffer=CHUNK,
|
|
||||||
output=True
|
|
||||||
)
|
|
||||||
|
|
||||||
f48 = open("out48.raw", mode='wb')
|
p_audio = pyaudio.PyAudio()
|
||||||
t = 0;
|
stream = p_audio.open(
|
||||||
for f in range(50):
|
format=pyaudio.paInt16,
|
||||||
sine_48k = (AMP*np.cos(2*np.pi*np.arange(t,t+CHUNK)*FTEST/FS48)).astype(np.int16)
|
channels=1,
|
||||||
t += CHUNK
|
rate=FS48,
|
||||||
sine_48k.tofile(f48)
|
frames_per_buffer=CHUNK,
|
||||||
stream.write(sine_48k.tobytes())
|
output=True,
|
||||||
sil_48k = np.zeros(CHUNK, dtype=np.int16)
|
)
|
||||||
for f in range(50):
|
|
||||||
sil_48k.tofile(f48)
|
with open("out48.raw", mode="wb") as f48:
|
||||||
stream.write(sil_48k)
|
temp = 0
|
||||||
|
for _ in range(50):
|
||||||
stream.stop_stream()
|
sine_48k = (
|
||||||
stream.close()
|
AMP * np.cos(2 * np.pi * np.arange(temp, temp + CHUNK) * FTEST / FS48)
|
||||||
p.terminate()
|
).astype(np.int16)
|
||||||
f48.close()
|
temp += CHUNK
|
||||||
|
sine_48k.tofile(f48)
|
||||||
|
stream.write(sine_48k.tobytes())
|
||||||
|
sil_48k = np.zeros(CHUNK, dtype=np.int16)
|
||||||
|
|
||||||
|
for _ in range(50):
|
||||||
|
sil_48k.tofile(f48)
|
||||||
|
stream.write(sil_48k)
|
||||||
|
|
||||||
|
stream.stop_stream()
|
||||||
|
stream.close()
|
||||||
|
p_audio.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_pa()
|
||||||
|
|
120
test/test_resample_48_8.py
Normal file
120
test/test_resample_48_8.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Unit test for FreeDV API resampler functions, from
|
||||||
|
# codec2/unittest/t48_8_short.c - generate a sine wave at 8 KHz,
|
||||||
|
# upsample to 48 kHz, add an interferer, then downsample back to 8 kHz
|
||||||
|
#
|
||||||
|
# You can listen to the output files with:
|
||||||
|
#
|
||||||
|
# aplay -f S16_LE in8.raw
|
||||||
|
# aplay -r 48000 -f S16_LE out48.raw
|
||||||
|
# aplay -f S16_LE out8.raw
|
||||||
|
#
|
||||||
|
# They should sound like clean sine waves
|
||||||
|
|
||||||
|
# pylint: disable=global-statement, invalid-name, unused-import
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import codec2
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# dig some constants out
|
||||||
|
FDMDV_OS_48 = codec2.api.FDMDV_OS_48
|
||||||
|
FDMDV_OS_TAPS_48K = codec2.api.FDMDV_OS_TAPS_48K
|
||||||
|
FDMDV_OS_TAPS_48_8K = codec2.api.FDMDV_OS_TAPS_48_8K
|
||||||
|
|
||||||
|
N8 = 180 # processing buffer size at 8 kHz
|
||||||
|
N48 = N8 * FDMDV_OS_48 # processing buffer size at 48 kHz
|
||||||
|
MEM8 = FDMDV_OS_TAPS_48_8K # 8kHz signal filter memory
|
||||||
|
MEM48 = FDMDV_OS_TAPS_48K # 48kHz signal filter memory
|
||||||
|
FRAMES = 50 # number of frames to test
|
||||||
|
FS8 = 8000
|
||||||
|
FS48 = 48000
|
||||||
|
AMP = 16000 # sine wave amplitude
|
||||||
|
FTEST8 = 800 # input test frequency at FS=8kHz
|
||||||
|
FINTER48 = 10000 # interferer frequency at FS=48kHz
|
||||||
|
|
||||||
|
# Due to the design of these resamplers, the processing buffer (at 8kHz)
|
||||||
|
# must be an integer multiple of oversampling ratio
|
||||||
|
assert N8 % FDMDV_OS_48 == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_resampler():
|
||||||
|
"""
|
||||||
|
Test for the codec2 audio resampling routine
|
||||||
|
"""
|
||||||
|
# time indexes, we advance every frame
|
||||||
|
t = 0
|
||||||
|
t1 = 0
|
||||||
|
|
||||||
|
# output files to listen to/evaluate result
|
||||||
|
with open("in8.raw", mode="wb") as fin8:
|
||||||
|
with open("out48.raw", mode="wb") as f48:
|
||||||
|
with open("out8.raw", mode="wb") as fout8:
|
||||||
|
resampler = codec2.resampler()
|
||||||
|
|
||||||
|
# Generate FRAMES of a sine wave
|
||||||
|
for _ in range(FRAMES):
|
||||||
|
# Primary sine wave, which the down-sampling filter should retain.
|
||||||
|
sine_in8k = (
|
||||||
|
AMP * np.cos(2 * np.pi * np.arange(t, t + N8) * FTEST8 / FS8)
|
||||||
|
).astype(np.int16)
|
||||||
|
t += N8
|
||||||
|
sine_in8k.tofile(fin8)
|
||||||
|
|
||||||
|
sine_out48k = resampler.resample8_to_48(sine_in8k)
|
||||||
|
sine_out48k.tofile(f48)
|
||||||
|
|
||||||
|
# Add an interfering sine wave, which the down-sampling filter should (mostly) remove
|
||||||
|
sine_in48k = (
|
||||||
|
sine_out48k
|
||||||
|
+ (AMP / 2)
|
||||||
|
* np.cos(2 * np.pi * np.arange(t1, t1 + N48) * FINTER48 / FS48)
|
||||||
|
).astype(np.int16)
|
||||||
|
t1 += N48
|
||||||
|
|
||||||
|
sine_out8k = resampler.resample48_to_8(sine_in48k)
|
||||||
|
sine_out8k.tofile(fout8)
|
||||||
|
|
||||||
|
# os.unlink("out48.raw")
|
||||||
|
|
||||||
|
# Automated test evaluation --------------------------------------------
|
||||||
|
|
||||||
|
# The input and output signals will not be time aligned due to the filter
|
||||||
|
# delays, so compare the magnitude spectrum
|
||||||
|
|
||||||
|
# Read the raw audio files
|
||||||
|
in8k = np.fromfile("in8.raw", dtype=np.int16)
|
||||||
|
out8k = np.fromfile("out8.raw", dtype=np.int16)
|
||||||
|
assert len(in8k) == len(out8k)
|
||||||
|
# os.unlink("in8.raw")
|
||||||
|
# os.unlink("out8.raw")
|
||||||
|
|
||||||
|
# Apply hanning filter to raw input data samples
|
||||||
|
h = np.hanning(len(in8k))
|
||||||
|
S1 = np.abs(np.fft.fft(in8k * h))
|
||||||
|
S2 = np.abs(np.fft.fft(out8k * h))
|
||||||
|
|
||||||
|
# Calculate the ratio between signal and noise (error energy).
|
||||||
|
error = S1 - S2
|
||||||
|
error_energy = np.dot(error, error)
|
||||||
|
ratio = error_energy / np.dot(S1, S1)
|
||||||
|
ratio_dB = 10 * np.log10(ratio)
|
||||||
|
|
||||||
|
# Establish -40.0 as the noise ratio ceiling
|
||||||
|
threshdB = -40.0
|
||||||
|
print(f"ratio_dB: {ratio_dB:4.2}" % (ratio_dB))
|
||||||
|
assert ratio_dB < threshdB
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("PASS")
|
||||||
|
else:
|
||||||
|
print("FAIL")
|
395
test/test_rx.py
395
test/test_rx.py
|
@ -6,201 +6,242 @@ Created on Wed Dec 23 07:04:24 2020
|
||||||
@author: DJ2LS
|
@author: DJ2LS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import sounddevice as sd
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
sys.path.insert(0,'..')
|
import sounddevice as sd
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
from tnc import codec2
|
from tnc import codec2
|
||||||
|
|
||||||
|
|
||||||
#--------------------------------------------GET PARAMETER INPUTS
|
def test_rx():
|
||||||
parser = argparse.ArgumentParser(description='Simons TEST TNC')
|
args = parse_arguments()
|
||||||
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:
|
||||||
|
|
||||||
if args.LIST:
|
|
||||||
|
|
||||||
devices = sd.query_devices(device=None, kind=None)
|
|
||||||
index = 0
|
|
||||||
for device in devices:
|
|
||||||
print(f"{index} {device['name']}")
|
|
||||||
index += 1
|
|
||||||
sd._terminate()
|
|
||||||
quit()
|
|
||||||
|
|
||||||
N_BURSTS = args.N_BURSTS
|
|
||||||
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
|
||||||
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
|
||||||
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
|
||||||
DEBUGGING_MODE = args.DEBUGGING_MODE
|
|
||||||
TIMEOUT = args.TIMEOUT
|
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
|
||||||
AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0
|
|
||||||
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
|
||||||
AUDIO_SAMPLE_RATE_RX = 48000
|
|
||||||
|
|
||||||
# make sure our resampler will work
|
|
||||||
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
|
||||||
if AUDIO_INPUT_DEVICE != -1:
|
|
||||||
# auto search for loopback devices
|
|
||||||
if AUDIO_INPUT_DEVICE == -2:
|
|
||||||
loopback_list = []
|
|
||||||
|
|
||||||
devices = sd.query_devices(device=None, kind=None)
|
devices = sd.query_devices(device=None, kind=None)
|
||||||
index = 0
|
index = 0
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if 'Loopback: PCM' in device['name']:
|
print(f"{index} {device['name']}")
|
||||||
print(index)
|
|
||||||
loopback_list.append(index)
|
|
||||||
index += 1
|
index += 1
|
||||||
|
sd._terminate()
|
||||||
if len(loopback_list) >= 1:
|
sys.exit()
|
||||||
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX
|
|
||||||
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
print("not enough audio loopback devices ready...")
|
|
||||||
print("you should wait about 30 seconds...")
|
|
||||||
|
|
||||||
sd._terminate()
|
N_BURSTS = args.N_BURSTS
|
||||||
quit()
|
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr)
|
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
|
||||||
|
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
||||||
|
DEBUGGING_MODE = args.DEBUGGING_MODE
|
||||||
|
MAX_TIME = args.TIMEOUT
|
||||||
|
|
||||||
# audio stream init
|
# AUDIO PARAMETERS
|
||||||
stream_rx = sd.RawStream(channels=1, dtype='int16', device=AUDIO_INPUT_DEVICE, samplerate = AUDIO_SAMPLE_RATE_RX, blocksize=4800)
|
# v-- consider increasing if you get nread_exceptions > 0
|
||||||
stream_rx.start()
|
AUDIO_FRAMES_PER_BUFFER = 2400 * 2
|
||||||
|
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
# ----------------------------------------------------------------
|
AUDIO_SAMPLE_RATE_RX = 48000
|
||||||
|
|
||||||
# DATA CHANNEL INITIALISATION
|
|
||||||
|
|
||||||
# open codec2 instance
|
# make sure our resampler will work
|
||||||
freedv = cast(codec2.api.freedv_open(MODE), c_void_p)
|
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
||||||
|
|
||||||
# get number of bytes per frame for mode
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|
|
||||||
payload_bytes_per_frame = bytes_per_frame -2
|
|
||||||
|
|
||||||
n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv)
|
|
||||||
bytes_out = create_string_buffer(bytes_per_frame)
|
|
||||||
|
|
||||||
codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST)
|
|
||||||
|
|
||||||
total_n_bytes = 0
|
|
||||||
rx_total_frames = 0
|
|
||||||
rx_frames = 0
|
|
||||||
rx_bursts = 0
|
|
||||||
rx_errors = 0
|
|
||||||
nread_exceptions = 0
|
|
||||||
timeout = time.time() + TIMEOUT
|
|
||||||
receive = True
|
|
||||||
audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER*2)
|
|
||||||
resampler = codec2.resampler()
|
|
||||||
|
|
||||||
# time meassurement
|
|
||||||
time_start = 0
|
|
||||||
time_end = 0
|
|
||||||
|
|
||||||
# Copy received 48 kHz to a file. Listen to this file with:
|
|
||||||
# aplay -r 48000 -f S16_LE rx48.raw
|
|
||||||
# Corruption of this file is a good way to detect audio card issues
|
|
||||||
frx = open("rx48.raw", mode='wb')
|
|
||||||
|
|
||||||
# initial number of samples we need
|
|
||||||
nin = codec2.api.freedv_nin(freedv)
|
|
||||||
while receive and time.time() < timeout:
|
|
||||||
if AUDIO_INPUT_DEVICE != -1:
|
if AUDIO_INPUT_DEVICE != -1:
|
||||||
try:
|
# auto search for loopback devices
|
||||||
#data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True)
|
if AUDIO_INPUT_DEVICE == -2:
|
||||||
data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER)
|
loopback_list = []
|
||||||
except OSError as err:
|
|
||||||
print(err, file=sys.stderr)
|
|
||||||
#if str(err).find("Input overflowed") != -1:
|
|
||||||
# nread_exceptions += 1
|
|
||||||
#if str(err).find("Stream closed") != -1:
|
|
||||||
# print("Ending...")
|
|
||||||
# receive = False
|
|
||||||
else:
|
|
||||||
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2)
|
|
||||||
|
|
||||||
# insert samples in buffer
|
|
||||||
x = np.frombuffer(data_in48k, dtype=np.int16)
|
|
||||||
#print(x)
|
|
||||||
#x = data_in48k
|
|
||||||
x.tofile(frx)
|
|
||||||
if len(x) != AUDIO_FRAMES_PER_BUFFER:
|
|
||||||
receive = False
|
|
||||||
x = resampler.resample48_to_8(x)
|
|
||||||
audio_buffer.push(x)
|
|
||||||
|
|
||||||
# when we have enough samples call FreeDV Rx
|
|
||||||
while audio_buffer.nbuffer >= nin:
|
|
||||||
# start time measurement
|
|
||||||
time_start = time.time()
|
|
||||||
# demodulate audio
|
|
||||||
nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes)
|
|
||||||
time_end = time.time()
|
|
||||||
|
|
||||||
audio_buffer.pop(nin)
|
|
||||||
|
|
||||||
# call me on every loop!
|
|
||||||
nin = codec2.api.freedv_nin(freedv)
|
|
||||||
|
|
||||||
rx_status = codec2.api.freedv_get_rx_status(freedv)
|
|
||||||
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
|
||||||
rx_errors = rx_errors + 1
|
|
||||||
if DEBUGGING_MODE:
|
|
||||||
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
|
||||||
time_needed = time_end - time_start
|
|
||||||
|
|
||||||
print("nin: %5d rx_status: %4s naudio_buffer: %4d time: %4s" % \
|
devices = sd.query_devices(device=None, kind=None)
|
||||||
(nin,rx_status,audio_buffer.nbuffer, time_needed), file=sys.stderr)
|
|
||||||
|
|
||||||
if nbytes:
|
for index, device in enumerate(devices):
|
||||||
total_n_bytes = total_n_bytes + nbytes
|
if "Loopback: PCM" in device["name"]:
|
||||||
|
print(index)
|
||||||
if nbytes == bytes_per_frame:
|
loopback_list.append(index)
|
||||||
rx_total_frames = rx_total_frames + 1
|
|
||||||
rx_frames = rx_frames + 1
|
|
||||||
|
|
||||||
if rx_frames == N_FRAMES_PER_BURST:
|
if loopback_list:
|
||||||
rx_frames = 0
|
# 0 = RX 1 = TX
|
||||||
rx_bursts = rx_bursts + 1
|
AUDIO_INPUT_DEVICE = loopback_list[0]
|
||||||
|
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
|
||||||
if rx_bursts == N_BURSTS:
|
else:
|
||||||
receive = False
|
print("not enough audio loopback devices ready...")
|
||||||
|
print("you should wait about 30 seconds...")
|
||||||
if time.time() >= timeout:
|
|
||||||
print("TIMEOUT REACHED")
|
sd._terminate()
|
||||||
|
sys.exit()
|
||||||
if nread_exceptions:
|
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr)
|
||||||
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \
|
|
||||||
nread_exceptions, file=sys.stderr)
|
# audio stream init
|
||||||
print(f"RECEIVED BURSTS: {rx_bursts} RECEIVED FRAMES: {rx_total_frames} RX_ERRORS: {rx_errors}", file=sys.stderr)
|
stream_rx = sd.RawStream(
|
||||||
frx.close()
|
channels=1,
|
||||||
|
dtype="int16",
|
||||||
|
device=AUDIO_INPUT_DEVICE,
|
||||||
# and at last check if we had an opened audio instance and close it
|
samplerate=AUDIO_SAMPLE_RATE_RX,
|
||||||
if AUDIO_INPUT_DEVICE != -1:
|
blocksize=4800,
|
||||||
sd._terminate()
|
)
|
||||||
|
stream_rx.start()
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# DATA CHANNEL INITIALISATION
|
||||||
|
|
||||||
|
# open codec2 instance
|
||||||
|
freedv = ctypes.cast(codec2.api.freedv_open(MODE), ctypes.c_void_p)
|
||||||
|
|
||||||
|
# get number of bytes per frame for mode
|
||||||
|
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||||
|
payload_bytes_per_frame = bytes_per_frame - 2
|
||||||
|
|
||||||
|
n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv)
|
||||||
|
bytes_out = ctypes.create_string_buffer(bytes_per_frame)
|
||||||
|
|
||||||
|
codec2.api.freedv_set_frames_per_burst(freedv, N_FRAMES_PER_BURST)
|
||||||
|
|
||||||
|
total_n_bytes = 0
|
||||||
|
rx_total_frames = 0
|
||||||
|
rx_frames = 0
|
||||||
|
rx_bursts = 0
|
||||||
|
rx_errors = 0
|
||||||
|
nread_exceptions = 0
|
||||||
|
timeout = time.time() + MAX_TIME
|
||||||
|
receive = True
|
||||||
|
audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER * 2)
|
||||||
|
resampler = codec2.resampler()
|
||||||
|
|
||||||
|
# time meassurement
|
||||||
|
time_start = 0
|
||||||
|
time_end = 0
|
||||||
|
|
||||||
|
# Copy received 48 kHz to a file. Listen to this file with:
|
||||||
|
# aplay -r 48000 -f S16_LE rx48.raw
|
||||||
|
# Corruption of this file is a good way to detect audio card issues
|
||||||
|
frx = open("rx48.raw", mode="wb")
|
||||||
|
|
||||||
|
# initial number of samples we need
|
||||||
|
nin = codec2.api.freedv_nin(freedv)
|
||||||
|
while receive and time.time() < timeout:
|
||||||
|
if AUDIO_INPUT_DEVICE != -1:
|
||||||
|
try:
|
||||||
|
# data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True)
|
||||||
|
data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER)
|
||||||
|
except OSError as err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
# if str(err).find("Input overflowed") != -1:
|
||||||
|
# nread_exceptions += 1
|
||||||
|
# if str(err).find("Stream closed") != -1:
|
||||||
|
# print("Ending...")
|
||||||
|
# receive = False
|
||||||
|
else:
|
||||||
|
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2)
|
||||||
|
|
||||||
|
# insert samples in buffer
|
||||||
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
||||||
|
# print(x)
|
||||||
|
# x = data_in48k
|
||||||
|
x.tofile(frx)
|
||||||
|
if len(x) != AUDIO_FRAMES_PER_BUFFER:
|
||||||
|
receive = False
|
||||||
|
x = resampler.resample48_to_8(x)
|
||||||
|
audio_buffer.push(x)
|
||||||
|
|
||||||
|
# when we have enough samples call FreeDV Rx
|
||||||
|
while audio_buffer.nbuffer >= nin:
|
||||||
|
# start time measurement
|
||||||
|
time_start = time.time()
|
||||||
|
# demodulate audio
|
||||||
|
nbytes = codec2.api.freedv_rawdatarx(
|
||||||
|
freedv, bytes_out, audio_buffer.buffer.ctypes
|
||||||
|
)
|
||||||
|
time_end = time.time()
|
||||||
|
|
||||||
|
audio_buffer.pop(nin)
|
||||||
|
|
||||||
|
# call me on every loop!
|
||||||
|
nin = codec2.api.freedv_nin(freedv)
|
||||||
|
|
||||||
|
rx_status = codec2.api.freedv_get_rx_status(freedv)
|
||||||
|
if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS:
|
||||||
|
rx_errors = rx_errors + 1
|
||||||
|
if DEBUGGING_MODE:
|
||||||
|
rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
|
||||||
|
time_needed = time_end - time_start
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"nin: {nin:5d} rx_status: {rx_status:4s} "
|
||||||
|
f"naudio_buffer: {audio_buffer.nbuffer:4d} time: {time_needed:4f}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if nbytes:
|
||||||
|
total_n_bytes += nbytes
|
||||||
|
|
||||||
|
if nbytes == bytes_per_frame:
|
||||||
|
rx_total_frames += 1
|
||||||
|
rx_frames += 1
|
||||||
|
|
||||||
|
if rx_frames == N_FRAMES_PER_BURST:
|
||||||
|
rx_frames = 0
|
||||||
|
rx_bursts += 1
|
||||||
|
|
||||||
|
if rx_bursts == N_BURSTS:
|
||||||
|
receive = False
|
||||||
|
|
||||||
|
if time.time() >= timeout:
|
||||||
|
print("TIMEOUT REACHED")
|
||||||
|
|
||||||
|
if nread_exceptions:
|
||||||
|
print(
|
||||||
|
f"nread_exceptions {nread_exceptions:d} - receive audio lost! "
|
||||||
|
"Consider increasing Pyaudio frames_per_buffer...",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"RECEIVED BURSTS: {rx_bursts} "
|
||||||
|
f"RECEIVED FRAMES: {rx_total_frames} "
|
||||||
|
f"RX_ERRORS: {rx_errors}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
frx.close()
|
||||||
|
|
||||||
|
# and at last check if we had an opened audio instance and close it
|
||||||
|
if AUDIO_INPUT_DEVICE != -1:
|
||||||
|
sd._terminate()
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
# --------------------------------------------GET PARAMETER INPUTS
|
||||||
|
parser = argparse.ArgumentParser(description="Simons TEST TNC")
|
||||||
|
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--audiodev",
|
||||||
|
dest="AUDIO_INPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio device number to use, use -2 to automatically select a loopback device",
|
||||||
|
)
|
||||||
|
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
dest="TIMEOUT",
|
||||||
|
default=10,
|
||||||
|
type=int,
|
||||||
|
help="Timeout (seconds) before test ends",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_rx()
|
||||||
|
|
54
test/test_tnc.py
Executable file
54
test/test_tnc.py
Executable file
|
@ -0,0 +1,54 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# pylint: disable=wrong-import-position
|
||||||
|
sys.path.insert(0, "..")
|
||||||
|
sys.path.insert(0, "../tnc")
|
||||||
|
sys.path.insert(0, "test")
|
||||||
|
import test_tnc_IRS as irs
|
||||||
|
import test_tnc_ISS as iss
|
||||||
|
|
||||||
|
|
||||||
|
# These do not update static.INFO.
|
||||||
|
# "CONNECT", "SEND_TEST_FRAME"
|
||||||
|
@pytest.mark.parametrize("command", ["CQ", "PING", "BEACON"])
|
||||||
|
def test_tnc(command):
|
||||||
|
|
||||||
|
iss_proc = multiprocessing.Process(target=iss.t_arq_iss, args=[command])
|
||||||
|
irs_proc = multiprocessing.Process(target=irs.t_arq_irs, args=[command])
|
||||||
|
# print("Starting threads.")
|
||||||
|
iss_proc.start()
|
||||||
|
irs_proc.start()
|
||||||
|
|
||||||
|
time.sleep(12)
|
||||||
|
|
||||||
|
# print("Terminating threads.")
|
||||||
|
irs_proc.terminate()
|
||||||
|
iss_proc.terminate()
|
||||||
|
irs_proc.join()
|
||||||
|
iss_proc.join()
|
||||||
|
|
||||||
|
for idx in range(2):
|
||||||
|
try:
|
||||||
|
os.unlink(f"/tmp/hfchannel{idx+1}")
|
||||||
|
except FileNotFoundError as fnfe:
|
||||||
|
print(f"Unlinking pipe: {fnfe}")
|
||||||
|
|
||||||
|
assert iss_proc.exitcode == 0, f"Transmit side failed test. {iss_proc}"
|
||||||
|
assert irs_proc.exitcode == 0, f"Receive side failed test. {irs_proc}"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-s", "-v", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
81
test/test_tnc_IRS.py
Normal file
81
test/test_tnc_IRS.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
|
sys.path.insert(0, "../tnc")
|
||||||
|
import data_handler
|
||||||
|
import helpers
|
||||||
|
import modem
|
||||||
|
import static
|
||||||
|
|
||||||
|
IRS_original_arq_cleanup = object
|
||||||
|
MESSAGE: str
|
||||||
|
|
||||||
|
|
||||||
|
def irs_arq_cleanup():
|
||||||
|
"""Replacement for modem.arq_cleanup to detect when to exit process."""
|
||||||
|
if "TRANSMISSION;STOPPED" in static.INFO:
|
||||||
|
print(f"{static.INFO=}")
|
||||||
|
time.sleep(2)
|
||||||
|
# sys.exit does not terminate threads.
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
if f"{MESSAGE};RECEIVING" not in static.INFO:
|
||||||
|
print(f"{MESSAGE} was not received.")
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
os._exit(0)
|
||||||
|
IRS_original_arq_cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def t_arq_irs(*args):
|
||||||
|
# pylint: disable=global-statement
|
||||||
|
global IRS_original_arq_cleanup, MESSAGE
|
||||||
|
|
||||||
|
MESSAGE = args[0]
|
||||||
|
|
||||||
|
# enable testmode
|
||||||
|
data_handler.TESTMODE = True
|
||||||
|
modem.TESTMODE = True
|
||||||
|
modem.RXCHANNEL = "/tmp/hfchannel2"
|
||||||
|
modem.TXCHANNEL = "/tmp/hfchannel1"
|
||||||
|
static.HAMLIB_RADIOCONTROL = "disabled"
|
||||||
|
static.RESPOND_TO_CQ = True
|
||||||
|
|
||||||
|
mycallsign = bytes("DN2LS-2", "utf-8")
|
||||||
|
mycallsign = helpers.callsign_to_bytes(mycallsign)
|
||||||
|
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
|
||||||
|
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
|
||||||
|
static.MYGRID = bytes("AA12aa", "utf-8")
|
||||||
|
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
|
||||||
|
# start data handler
|
||||||
|
tnc = data_handler.DATA()
|
||||||
|
|
||||||
|
# Inject a way to exit the TNC infinite loop
|
||||||
|
IRS_original_arq_cleanup = tnc.arq_cleanup
|
||||||
|
tnc.arq_cleanup = irs_arq_cleanup
|
||||||
|
|
||||||
|
# start modem
|
||||||
|
t_modem = modem.RF()
|
||||||
|
|
||||||
|
# Set timeout
|
||||||
|
timeout = time.time() + 10
|
||||||
|
|
||||||
|
while time.time() < timeout:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
assert not "TIMEOUT!"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("This cannot be run as an application.")
|
||||||
|
sys.exit(1)
|
118
test/test_tnc_ISS.py
Normal file
118
test/test_tnc_ISS.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Wed Dec 23 07:04:24 2020
|
||||||
|
|
||||||
|
@author: DJ2LS
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.path.insert(0, "..")
|
||||||
|
sys.path.insert(0, "../tnc")
|
||||||
|
import data_handler
|
||||||
|
import helpers
|
||||||
|
import modem
|
||||||
|
import static
|
||||||
|
|
||||||
|
ISS_original_arq_cleanup = object
|
||||||
|
MESSAGE: str
|
||||||
|
|
||||||
|
|
||||||
|
def iss_arq_cleanup():
|
||||||
|
"""Replacement for modem.arq_cleanup to detect when to exit process."""
|
||||||
|
if "TRANSMISSION;STOPPED" in static.INFO:
|
||||||
|
print(f"{static.INFO=}")
|
||||||
|
time.sleep(1)
|
||||||
|
# sys.exit does not terminate threads.
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
if f"{MESSAGE};SENDING" not in static.INFO:
|
||||||
|
print(f"{MESSAGE} was not sent.")
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
os._exit(0)
|
||||||
|
ISS_original_arq_cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def t_arq_iss(*args):
|
||||||
|
# pylint: disable=global-statement
|
||||||
|
global ISS_original_arq_cleanup, MESSAGE
|
||||||
|
|
||||||
|
MESSAGE = args[0]
|
||||||
|
|
||||||
|
# enable testmode
|
||||||
|
data_handler.TESTMODE = True
|
||||||
|
modem.TESTMODE = True
|
||||||
|
modem.RXCHANNEL = "/tmp/hfchannel1"
|
||||||
|
modem.TXCHANNEL = "/tmp/hfchannel2"
|
||||||
|
static.HAMLIB_RADIOCONTROL = "disabled"
|
||||||
|
|
||||||
|
mycallsign = bytes("DJ2LS-2", "utf-8")
|
||||||
|
mycallsign = helpers.callsign_to_bytes(mycallsign)
|
||||||
|
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
|
||||||
|
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
|
||||||
|
static.MYGRID = bytes("AA12aa", "utf-8")
|
||||||
|
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
|
||||||
|
dxcallsign = b"DN2LS-0"
|
||||||
|
dxcallsign = helpers.callsign_to_bytes(dxcallsign)
|
||||||
|
dxcallsign = helpers.bytes_to_callsign(dxcallsign)
|
||||||
|
static.DXCALLSIGN = dxcallsign
|
||||||
|
static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN)
|
||||||
|
|
||||||
|
bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}'
|
||||||
|
|
||||||
|
# start data handler
|
||||||
|
tnc = data_handler.DATA()
|
||||||
|
|
||||||
|
# Inject a way to exit the TNC infinite loop
|
||||||
|
ISS_original_arq_cleanup = tnc.arq_cleanup
|
||||||
|
tnc.arq_cleanup = iss_arq_cleanup
|
||||||
|
|
||||||
|
# start modem
|
||||||
|
t_modem = modem.RF()
|
||||||
|
|
||||||
|
# mode = codec2.freedv_get_mode_value_by_name(FREEDV_MODE)
|
||||||
|
# n_frames_per_burst = N_FRAMES_PER_BURST
|
||||||
|
|
||||||
|
# add command to data qeue
|
||||||
|
"""
|
||||||
|
elif data[0] == 'ARQ_RAW':
|
||||||
|
# [0] ARQ_RAW
|
||||||
|
# [1] DATA_OUT bytes
|
||||||
|
# [2] MODE int
|
||||||
|
# [3] N_FRAMES_PER_BURST int
|
||||||
|
# [4] self.transmission_uuid str
|
||||||
|
# [5] mycallsign with ssid
|
||||||
|
"""
|
||||||
|
# data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_RAW', bytes_out, 255, n_frames_per_burst, '123', b'DJ2LS-0'])
|
||||||
|
|
||||||
|
# for _ in range(4):
|
||||||
|
if MESSAGE in ["BEACON"]:
|
||||||
|
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE, 5, True])
|
||||||
|
elif MESSAGE in ["PING", "CONNECT"]:
|
||||||
|
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE, dxcallsign])
|
||||||
|
else:
|
||||||
|
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE])
|
||||||
|
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
# for i in range(4):
|
||||||
|
# data_handler.DATA_QUEUE_TRANSMIT.put(['PING', b'DN2LS-2'])
|
||||||
|
|
||||||
|
data_handler.DATA_QUEUE_TRANSMIT.put(["STOP"])
|
||||||
|
|
||||||
|
# Set timeout
|
||||||
|
timeout = time.time() + 10
|
||||||
|
|
||||||
|
while time.time() < timeout:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
assert not "TIMEOUT!"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("This cannot be run as an application.")
|
||||||
|
sys.exit(-1)
|
225
test/test_tnc_states.py
Normal file
225
test/test_tnc_states.py
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
"""
|
||||||
|
Tests for the FreeDATA TNC state machine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# pylint: disable=wrong-import-position
|
||||||
|
sys.path.insert(0, "..")
|
||||||
|
sys.path.insert(0, "../tnc")
|
||||||
|
import data_handler
|
||||||
|
import helpers
|
||||||
|
import static
|
||||||
|
|
||||||
|
|
||||||
|
def print_frame(data: bytearray):
|
||||||
|
"""
|
||||||
|
Pretty-print the provided frame.
|
||||||
|
|
||||||
|
:param data: Frame to be output
|
||||||
|
:type data: bytearray
|
||||||
|
"""
|
||||||
|
print(f"Type : {int(data[0])}")
|
||||||
|
print(f"DXCRC : {bytes(data[1:4])}")
|
||||||
|
print(f"CallCRC: {bytes(data[4:7])}")
|
||||||
|
print(f"Call : {helpers.bytes_to_callsign(data[7:13])}")
|
||||||
|
|
||||||
|
|
||||||
|
def t_create_frame(frame_type: int, mycall: str, dxcall: str) -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate the requested frame.
|
||||||
|
|
||||||
|
:param frame_type: The numerical type of the desired frame.
|
||||||
|
:type frame_type: int
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
|
||||||
|
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
|
||||||
|
mycallsign_crc = helpers.get_crc_24(mycallsign)
|
||||||
|
|
||||||
|
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
|
||||||
|
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
|
||||||
|
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
|
||||||
|
|
||||||
|
frame = bytearray(14)
|
||||||
|
frame[:1] = bytes([frame_type])
|
||||||
|
frame[1:4] = dxcallsign_crc
|
||||||
|
frame[4:7] = mycallsign_crc
|
||||||
|
frame[7:13] = mycallsign_bytes
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate the session_close frame.
|
||||||
|
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
return t_create_frame(223, mycall, dxcall)
|
||||||
|
|
||||||
|
|
||||||
|
def t_create_start_session(mycall: str, dxcall: str) -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate the create_session frame.
|
||||||
|
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
return t_create_frame(221, mycall, dxcall)
|
||||||
|
|
||||||
|
|
||||||
|
def t_tsh_dummy():
|
||||||
|
"""Replacement function for transmit_session_heartbeat"""
|
||||||
|
print("In transmit_session_heartbeat")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mycall", ["AA1AA-2", "DE2DE-0", "M4AWQ-4"])
|
||||||
|
@pytest.mark.parametrize("dxcall", ["AA9AA-1", "DE2ED-0", "F6QWE-3"])
|
||||||
|
def test_valid_disconnect(mycall: str, dxcall: str):
|
||||||
|
"""
|
||||||
|
Execute test to validate that receiving a session open frame sets the correct machine
|
||||||
|
state.
|
||||||
|
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
# Set the SSIDs we'll use for this test.
|
||||||
|
static.SSID_LIST = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
|
# Setup the static parameters for the connection.
|
||||||
|
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
|
||||||
|
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
|
||||||
|
static.MYCALLSIGN = mycallsign
|
||||||
|
static.MYCALLSIGN_CRC = helpers.get_crc_24(mycallsign)
|
||||||
|
|
||||||
|
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
|
||||||
|
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
|
||||||
|
static.DXCALLSIGN = dxcallsign
|
||||||
|
static.DXCALLSIGN_CRC = helpers.get_crc_24(dxcallsign)
|
||||||
|
|
||||||
|
# Create the TNC
|
||||||
|
tnc = data_handler.DATA()
|
||||||
|
|
||||||
|
# Replace the heartbeat transmit routine with our own, a No-Op.
|
||||||
|
tnc.transmit_session_heartbeat = t_tsh_dummy
|
||||||
|
|
||||||
|
# Create packet to be 'received' by this station.
|
||||||
|
create_frame = t_create_start_session(mycall=dxcall, dxcall=mycall)
|
||||||
|
print_frame(create_frame)
|
||||||
|
tnc.received_session_opener(create_frame)
|
||||||
|
|
||||||
|
assert static.ARQ_SESSION is True
|
||||||
|
assert static.TNC_STATE == "BUSY"
|
||||||
|
assert static.ARQ_SESSION_STATE == "connecting"
|
||||||
|
|
||||||
|
# Create packet to be 'received' by this station.
|
||||||
|
close_frame = t_create_session_close(mycall=dxcall, dxcall=mycall)
|
||||||
|
print_frame(close_frame)
|
||||||
|
tnc.received_session_close(close_frame)
|
||||||
|
|
||||||
|
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == mycallsign_bytes
|
||||||
|
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == dxcallsign_bytes
|
||||||
|
|
||||||
|
assert static.ARQ_SESSION is False
|
||||||
|
assert static.TNC_STATE == "IDLE"
|
||||||
|
assert static.ARQ_SESSION_STATE == "disconnected"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mycall", ["AA1AA-2", "DE2DE-0", "E4AWQ-4"])
|
||||||
|
@pytest.mark.parametrize("dxcall", ["AA9AA-1", "DE2ED-0", "F6QWE-3"])
|
||||||
|
def test_foreign_disconnect(mycall: str, dxcall: str):
|
||||||
|
"""
|
||||||
|
Execute test to validate that receiving a session open frame sets the correct machine
|
||||||
|
state.
|
||||||
|
|
||||||
|
:param mycall: Callsign of the near station
|
||||||
|
:type mycall: str
|
||||||
|
:param dxcall: Callsign of the far station
|
||||||
|
:type dxcall: str
|
||||||
|
:return: Bytearray of the requested frame
|
||||||
|
:rtype: bytearray
|
||||||
|
"""
|
||||||
|
# Setup the static parameters for the connection.
|
||||||
|
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
|
||||||
|
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
|
||||||
|
static.MYCALLSIGN = mycallsign
|
||||||
|
static.MYCALLSIGN_CRC = helpers.get_crc_24(mycallsign)
|
||||||
|
|
||||||
|
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
|
||||||
|
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
|
||||||
|
static.DXCALLSIGN = dxcallsign
|
||||||
|
static.DXCALLSIGN_CRC = helpers.get_crc_24(dxcallsign)
|
||||||
|
|
||||||
|
# Create the TNC
|
||||||
|
tnc = data_handler.DATA()
|
||||||
|
|
||||||
|
# Replace the heartbeat transmit routine with a No-Op.
|
||||||
|
tnc.transmit_session_heartbeat = t_tsh_dummy
|
||||||
|
|
||||||
|
# Create frame to be 'received' by this station.
|
||||||
|
create_frame = t_create_start_session(mycall=dxcall, dxcall=mycall)
|
||||||
|
print_frame(create_frame)
|
||||||
|
tnc.received_session_opener(create_frame)
|
||||||
|
|
||||||
|
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == mycallsign_bytes
|
||||||
|
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == dxcallsign_bytes
|
||||||
|
|
||||||
|
assert static.ARQ_SESSION is True
|
||||||
|
assert static.TNC_STATE == "BUSY"
|
||||||
|
assert static.ARQ_SESSION_STATE == "connecting"
|
||||||
|
|
||||||
|
# Set up a frame from a non-associated station.
|
||||||
|
foreigncall_bytes = helpers.callsign_to_bytes("ZZ0ZZ-0")
|
||||||
|
foreigncall = helpers.bytes_to_callsign(foreigncall_bytes)
|
||||||
|
|
||||||
|
close_frame = t_create_session_close("ZZ0ZZ-0", "ZZ0ZZ-0")
|
||||||
|
print_frame(close_frame)
|
||||||
|
assert (
|
||||||
|
helpers.check_callsign(static.DXCALLSIGN, bytes(close_frame[4:7]))[0] is False
|
||||||
|
)
|
||||||
|
|
||||||
|
assert helpers.check_callsign(foreigncall, bytes(close_frame[4:7]))[0] is True
|
||||||
|
|
||||||
|
# Send the non-associated session close frame to the TNC
|
||||||
|
tnc.received_session_close(close_frame)
|
||||||
|
|
||||||
|
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == helpers.callsign_to_bytes(
|
||||||
|
mycall
|
||||||
|
)
|
||||||
|
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == helpers.callsign_to_bytes(
|
||||||
|
dxcall
|
||||||
|
)
|
||||||
|
|
||||||
|
assert static.ARQ_SESSION is True
|
||||||
|
assert static.TNC_STATE == "BUSY"
|
||||||
|
assert static.ARQ_SESSION_STATE == "connecting"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Run pytest with the current script as the filename.
|
||||||
|
ecode = pytest.main(["-v", sys.argv[0]])
|
||||||
|
if ecode == 0:
|
||||||
|
print("errors: 0")
|
||||||
|
else:
|
||||||
|
print(ecode)
|
347
test/test_tx.py
347
test/test_tx.py
|
@ -2,174 +2,217 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
from ctypes import *
|
|
||||||
import pathlib
|
|
||||||
import sounddevice as sd
|
|
||||||
import time
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import ctypes
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0,'..')
|
|
||||||
from tnc import codec2
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import sounddevice as sd
|
||||||
|
|
||||||
# GET PARAMETER INPUTS
|
sys.path.insert(0, "..")
|
||||||
parser = argparse.ArgumentParser(description='Simons TEST TNC')
|
from tnc import codec2
|
||||||
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
|
|
||||||
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
|
|
||||||
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int,
|
|
||||||
help="delay between bursts in ms")
|
|
||||||
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
|
|
||||||
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int,
|
|
||||||
help="audio output device number to use, use -2 to automatically select a loopback device")
|
|
||||||
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
|
|
||||||
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="list audio devices by number and exit")
|
|
||||||
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
def test_tx():
|
||||||
|
args = parse_arguments()
|
||||||
|
|
||||||
if args.LIST:
|
if args.LIST:
|
||||||
|
|
||||||
devices = sd.query_devices(device=None, kind=None)
|
|
||||||
index = 0
|
|
||||||
for device in devices:
|
|
||||||
print(f"{index} {device['name']}")
|
|
||||||
index += 1
|
|
||||||
sd._terminate()
|
|
||||||
quit()
|
|
||||||
|
|
||||||
N_BURSTS = args.N_BURSTS
|
|
||||||
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
|
||||||
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
|
|
||||||
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
|
||||||
|
|
||||||
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
|
||||||
|
|
||||||
# AUDIO PARAMETERS
|
|
||||||
AUDIO_FRAMES_PER_BUFFER = 2400
|
|
||||||
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
|
||||||
AUDIO_SAMPLE_RATE_TX = 48000
|
|
||||||
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
|
|
||||||
|
|
||||||
# check if we want to use an audio device then do an pyaudio init
|
|
||||||
if AUDIO_OUTPUT_DEVICE != -1:
|
|
||||||
# auto search for loopback devices
|
|
||||||
if AUDIO_OUTPUT_DEVICE == -2:
|
|
||||||
loopback_list = []
|
|
||||||
|
|
||||||
devices = sd.query_devices(device=None, kind=None)
|
devices = sd.query_devices(device=None, kind=None)
|
||||||
index = 0
|
|
||||||
|
|
||||||
for device in devices:
|
|
||||||
if 'Loopback: PCM' in device['name']:
|
|
||||||
print(index)
|
|
||||||
loopback_list.append(index)
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
if len(loopback_list) >= 1:
|
|
||||||
AUDIO_OUTPUT_DEVICE = loopback_list[len(loopback_list)-1] #0 = RX 1 = TX
|
|
||||||
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
print("not enough audio loopback devices ready...")
|
|
||||||
print("you should wait about 30 seconds...")
|
|
||||||
sd._terminate()
|
|
||||||
quit()
|
|
||||||
print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr)
|
|
||||||
|
|
||||||
# audio stream init
|
for index, device in enumerate(devices):
|
||||||
stream_tx = sd.RawStream(channels=1, dtype='int16', device=(0, AUDIO_OUTPUT_DEVICE), samplerate = AUDIO_SAMPLE_RATE_TX, blocksize=4800)
|
print(f"{index} {device['name']}")
|
||||||
resampler = codec2.resampler()
|
sd._terminate()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
# data binary string
|
N_BURSTS = args.N_BURSTS
|
||||||
if args.TESTFRAMES:
|
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
|
||||||
data_out = bytearray(14)
|
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
|
||||||
data_out[:1] = bytes([255])
|
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
|
||||||
data_out[1:2] = bytes([1])
|
|
||||||
data_out[2:] = b'HELLO WORLD'
|
|
||||||
|
|
||||||
else:
|
|
||||||
data_out = b'HELLO WORLD!'
|
|
||||||
|
|
||||||
|
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# AUDIO PARAMETERS
|
||||||
|
AUDIO_FRAMES_PER_BUFFER = 2400
|
||||||
|
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||||
|
AUDIO_SAMPLE_RATE_TX = 48000
|
||||||
|
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
|
||||||
|
|
||||||
|
# check if we want to use an audio device then do an pyaudio init
|
||||||
|
|
||||||
# open codec2 instance
|
|
||||||
freedv = cast(codec2.api.freedv_open(MODE), c_void_p)
|
|
||||||
|
|
||||||
# get number of bytes per frame for mode
|
|
||||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|
|
||||||
payload_bytes_per_frame = bytes_per_frame -2
|
|
||||||
|
|
||||||
# init buffer for data
|
|
||||||
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
|
||||||
mod_out = create_string_buffer(n_tx_modem_samples * 2)
|
|
||||||
|
|
||||||
# init buffer for preample
|
|
||||||
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
|
|
||||||
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2)
|
|
||||||
|
|
||||||
# init buffer for postamble
|
|
||||||
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
|
||||||
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2)
|
|
||||||
|
|
||||||
|
|
||||||
# create buffer for data
|
|
||||||
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
|
||||||
buffer[:len(data_out)] = data_out # set buffersize to length of data which will be send
|
|
||||||
|
|
||||||
# create crc for data frame - we are using the crc function shipped with codec2 to avoid
|
|
||||||
# crc algorithm incompatibilities
|
|
||||||
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16
|
|
||||||
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
|
||||||
buffer += crc # append crc16 to buffer
|
|
||||||
|
|
||||||
print(f"TOTAL BURSTS: {N_BURSTS} TOTAL FRAMES_PER_BURST: {N_FRAMES_PER_BURST}", file=sys.stderr)
|
|
||||||
|
|
||||||
for i in range(1,N_BURSTS+1):
|
|
||||||
|
|
||||||
# write preamble to txbuffer
|
|
||||||
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
|
||||||
txbuffer = bytes(mod_out_preamble)
|
|
||||||
|
|
||||||
# create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
|
||||||
for n in range(1,N_FRAMES_PER_BURST+1):
|
|
||||||
|
|
||||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
|
||||||
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
|
||||||
|
|
||||||
txbuffer += bytes(mod_out)
|
|
||||||
|
|
||||||
print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{N_FRAMES_PER_BURST}", file=sys.stderr)
|
|
||||||
|
|
||||||
# append postamble to txbuffer
|
|
||||||
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
|
||||||
txbuffer += bytes(mod_out_postamble)
|
|
||||||
|
|
||||||
# append a delay between bursts as audio silence
|
|
||||||
samples_delay = int(MODEM_SAMPLE_RATE*DELAY_BETWEEN_BURSTS)
|
|
||||||
mod_out_silence = create_string_buffer(samples_delay*2)
|
|
||||||
txbuffer += bytes(mod_out_silence)
|
|
||||||
#print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {DELAY_BETWEEN_BURSTS}", file=sys.stderr)
|
|
||||||
|
|
||||||
# resample up to 48k (resampler works on np.int16)
|
|
||||||
x = np.frombuffer(txbuffer, dtype=np.int16)
|
|
||||||
txbuffer_48k = resampler.resample8_to_48(x)
|
|
||||||
|
|
||||||
# check if we want to use an audio device or stdout
|
|
||||||
if AUDIO_OUTPUT_DEVICE != -1:
|
if AUDIO_OUTPUT_DEVICE != -1:
|
||||||
stream_tx.start()
|
# auto search for loopback devices
|
||||||
stream_tx.write(txbuffer_48k)
|
if AUDIO_OUTPUT_DEVICE == -2:
|
||||||
|
loopback_list = []
|
||||||
|
|
||||||
|
devices = sd.query_devices(device=None, kind=None)
|
||||||
|
|
||||||
|
for index, device in enumerate(devices):
|
||||||
|
if "Loopback: PCM" in device["name"]:
|
||||||
|
print(index)
|
||||||
|
loopback_list.append(index)
|
||||||
|
|
||||||
|
if loopback_list:
|
||||||
|
# 0 = RX 1 = TX
|
||||||
|
AUDIO_OUTPUT_DEVICE = loopback_list[-1]
|
||||||
|
print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print("not enough audio loopback devices ready...")
|
||||||
|
print("you should wait about 30 seconds...")
|
||||||
|
sd._terminate()
|
||||||
|
sys.exit()
|
||||||
|
print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr)
|
||||||
|
|
||||||
|
# audio stream init
|
||||||
|
stream_tx = sd.RawStream(
|
||||||
|
channels=1,
|
||||||
|
dtype="int16",
|
||||||
|
device=(0, AUDIO_OUTPUT_DEVICE),
|
||||||
|
samplerate=AUDIO_SAMPLE_RATE_TX,
|
||||||
|
blocksize=4800,
|
||||||
|
)
|
||||||
|
|
||||||
|
resampler = codec2.resampler()
|
||||||
|
|
||||||
|
# data binary string
|
||||||
|
if args.TESTFRAMES:
|
||||||
|
data_out = bytearray(14)
|
||||||
|
data_out[:1] = bytes([255])
|
||||||
|
data_out[1:2] = bytes([1])
|
||||||
|
data_out[2:] = b"HELLO WORLD"
|
||||||
else:
|
else:
|
||||||
# print data to terminal for piping the output to other programs
|
data_out = b"HELLO WORLD!"
|
||||||
sys.stdout.buffer.write(txbuffer_48k)
|
|
||||||
sys.stdout.flush()
|
# ----------------------------------------------------------------
|
||||||
|
|
||||||
|
# Open codec2 instance
|
||||||
|
freedv = ctypes.cast(codec2.api.freedv_open(MODE), ctypes.c_void_p)
|
||||||
|
|
||||||
|
# Get number of bytes per frame for mode
|
||||||
|
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||||
|
payload_bytes_per_frame = bytes_per_frame - 2
|
||||||
|
|
||||||
|
# Init buffer for data
|
||||||
|
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||||
|
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
|
||||||
|
|
||||||
|
# Init buffer for preample
|
||||||
|
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
|
||||||
|
freedv
|
||||||
|
)
|
||||||
|
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
|
||||||
|
|
||||||
|
# Init buffer for postamble
|
||||||
|
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(
|
||||||
|
freedv
|
||||||
|
)
|
||||||
|
mod_out_postamble = ctypes.create_string_buffer(n_tx_postamble_modem_samples * 2)
|
||||||
|
|
||||||
|
# Create buffer for data
|
||||||
|
# Use this if CRC16 checksum is required (DATA1-3)
|
||||||
|
buffer = bytearray(payload_bytes_per_frame)
|
||||||
|
# set buffersize to length of data which will be send
|
||||||
|
buffer[: len(data_out)] = data_out
|
||||||
|
|
||||||
|
# Create CRC for data frame - we are using the CRC function shipped with codec2 to avoid
|
||||||
|
# CRC algorithm incompatibilities
|
||||||
|
# generate CRC16
|
||||||
|
crc = ctypes.c_ushort(
|
||||||
|
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
|
||||||
|
)
|
||||||
|
crc = crc.value.to_bytes(2, byteorder="big") # convert crc to 2 byte hex string
|
||||||
|
buffer += crc # append crc16 to buffer
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"TOTAL BURSTS: {N_BURSTS} TOTAL FRAMES_PER_BURST: {N_FRAMES_PER_BURST}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
for brst in range(1, N_BURSTS + 1):
|
||||||
|
# Write preamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||||
|
txbuffer = bytes(mod_out_preamble)
|
||||||
|
|
||||||
|
# Create modulaton for N = FRAMESPERBURST and append it to txbuffer
|
||||||
|
for frm in range(1, N_FRAMES_PER_BURST + 1):
|
||||||
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||||
|
# Modulate DATA and save it into mod_out pointer
|
||||||
|
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
|
||||||
|
|
||||||
|
txbuffer += bytes(mod_out)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{N_FRAMES_PER_BURST}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Append postamble to txbuffer
|
||||||
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||||
|
txbuffer += bytes(mod_out_postamble)
|
||||||
|
|
||||||
|
# Append a delay between bursts as audio silence
|
||||||
|
samples_delay = int(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS)
|
||||||
|
mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
|
||||||
|
txbuffer += bytes(mod_out_silence)
|
||||||
|
# print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {DELAY_BETWEEN_BURSTS}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Resample up to 48k (resampler works on np.int16)
|
||||||
|
np_buffer = np.frombuffer(txbuffer, dtype=np.int16)
|
||||||
|
txbuffer_48k = resampler.resample8_to_48(np_buffer)
|
||||||
|
|
||||||
|
# Check if we want to use an audio device or stdout
|
||||||
|
if AUDIO_OUTPUT_DEVICE != -1:
|
||||||
|
stream_tx.start()
|
||||||
|
stream_tx.write(txbuffer_48k)
|
||||||
|
else:
|
||||||
|
# Print data to terminal for piping the output to other programs
|
||||||
|
sys.stdout.buffer.write(txbuffer_48k)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# and at last check if we had an opened audio instance and close it
|
||||||
|
if AUDIO_OUTPUT_DEVICE != -1:
|
||||||
|
sd._terminate()
|
||||||
|
|
||||||
|
|
||||||
# and at last check if we had an opened audio instance and close it
|
def parse_arguments():
|
||||||
if AUDIO_OUTPUT_DEVICE != -1:
|
# GET PARAMETER INPUTS
|
||||||
sd._terminate()
|
parser = argparse.ArgumentParser(description="Simons TEST TNC")
|
||||||
|
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
|
||||||
|
parser.add_argument(
|
||||||
|
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--delay",
|
||||||
|
dest="DELAY_BETWEEN_BURSTS",
|
||||||
|
default=500,
|
||||||
|
type=int,
|
||||||
|
help="delay between bursts in ms",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--audiodev",
|
||||||
|
dest="AUDIO_OUTPUT_DEVICE",
|
||||||
|
default=-1,
|
||||||
|
type=int,
|
||||||
|
help="audio output device number to use, use -2 to automatically select a loopback device",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="LIST",
|
||||||
|
action="store_true",
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--testframes",
|
||||||
|
dest="TESTFRAMES",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="list audio devices by number and exit",
|
||||||
|
)
|
||||||
|
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_tx()
|
||||||
|
|
|
@ -191,7 +191,7 @@ class DAEMON:
|
||||||
options.append('--radiocontrol')
|
options.append('--radiocontrol')
|
||||||
options.append(data[13])
|
options.append(data[13])
|
||||||
|
|
||||||
if data[13] != 'rigctld':
|
if data[13] == 'rigctld':
|
||||||
options.append('--rigctld_ip')
|
options.append('--rigctld_ip')
|
||||||
options.append(data[14])
|
options.append(data[14])
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -647,8 +647,8 @@ class RF:
|
||||||
|
|
||||||
snr = round(modem_stats_snr, 1)
|
snr = round(modem_stats_snr, 1)
|
||||||
structlog.get_logger("structlog").info("[MDM] calculate_snr: ", snr=snr)
|
structlog.get_logger("structlog").info("[MDM] calculate_snr: ", snr=snr)
|
||||||
# print(snr)
|
# static.SNR = np.clip(snr, 0, 255) # limit to max value of 255
|
||||||
static.SNR = np.clip(snr, 0, 255) # limit to max value of 255
|
static.SNR = np.clip(snr, -128, 128) # limit to max value of -128/128 as a possible fix of #188
|
||||||
return static.SNR
|
return static.SNR
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
structlog.get_logger("structlog").error(f"[MDM] calculate_snr: Exception: {e}")
|
structlog.get_logger("structlog").error(f"[MDM] calculate_snr: Exception: {e}")
|
||||||
|
|
|
@ -51,7 +51,7 @@ class radio():
|
||||||
"""
|
"""
|
||||||
self.hostname = rigctld_ip
|
self.hostname = rigctld_ip
|
||||||
self.port = int(rigctld_port)
|
self.port = int(rigctld_port)
|
||||||
|
|
||||||
if self.connect():
|
if self.connect():
|
||||||
logging.debug("Rigctl intialized")
|
logging.debug("Rigctl intialized")
|
||||||
return True
|
return True
|
||||||
|
|
Loading…
Reference in a new issue