Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting codec2 modes to custom modes #871

Merged
merged 3 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 76 additions & 26 deletions freedata_server/codec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ class FREEDV_MODE(Enum):
datac4 = 18
datac13 = 19
datac14 = 20
data_ofdm_200 = 21200
data_ofdm_250 = 21250
data_ofdm_500 = 21500
data_ofdm_1700 = 211700
data_ofdm_2438 = 2124381
#data_qam_2438 = 2124382
#qam16c2 = 22
Expand All @@ -51,7 +54,10 @@ class FREEDV_MODE_USED_SLOTS(Enum):
datac4 = [False, False, True, False, False]
datac13 = [False, False, True, False, False]
datac14 = [False, False, True, False, False]
data_ofdm_200 = [False, False, True, False, False]
data_ofdm_250 = [False, False, True, False, False]
data_ofdm_500 = [False, False, True, False, False]
data_ofdm_1700 = [False, True, True, True, False]
data_ofdm_2438 = [True, True, True, True, True]
data_qam_2438 = [True, True, True, True, True]
qam16c2 = [True, True, True, True, True]
Expand Down Expand Up @@ -375,10 +381,9 @@ def resample8_to_48(self, in8):

return out48


def open_instance(mode: int) -> ctypes.c_void_p:
data_custom = 21
if mode in [FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_2438.value]:
if mode in [FREEDV_MODE.data_ofdm_200.value, FREEDV_MODE.data_ofdm_250.value, FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_1700.value, FREEDV_MODE.data_ofdm_2438.value]:
#if mode in [FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_2438.value, FREEDV_MODE.data_qam_2438]:
custom_params = ofdm_configurations[mode]
return ctypes.cast(
Expand Down Expand Up @@ -535,6 +540,49 @@ def create_tx_uw(nuwbits, uw_sequence):

# ---------------- OFDM 500 Hz Bandwidth ---------------#

# DATAC13 # OFDM 200
data_ofdm_200_config = create_default_ofdm_config()
data_ofdm_200_config.config.contents.ns = 5
data_ofdm_200_config.config.contents.np = 18
data_ofdm_200_config.config.contents.tcp = 0.006
data_ofdm_200_config.config.contents.ts = 0.016
data_ofdm_200_config.config.contents.rs = 1.0 / data_ofdm_200_config.config.contents.ts
data_ofdm_200_config.config.contents.nc = 3
data_ofdm_200_config.config.contents.timing_mx_thresh = 0.45
data_ofdm_200_config.config.contents.bad_uw_errors = 18
data_ofdm_200_config.config.contents.codename = "H_256_512_4".encode('utf-8')
data_ofdm_200_config.config.contents.amp_scale = 2.5*300E3
data_ofdm_200_config.config.contents.nuwbits = 48
data_ofdm_200_config.config.contents.tx_uw = create_tx_uw(data_ofdm_200_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0])
data_ofdm_200_config.config.contents.clip_gain1 = 1.2
data_ofdm_200_config.config.contents.clip_gain2 = 1.0
data_ofdm_200_config.config.contents.tx_bpf_en = False
data_ofdm_200_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 400, 101)
data_ofdm_200_config.config.contents.tx_bpf_proto_n = 101 # TODO sizeof(filtP200S400) / sizeof(float);


# DATAC4 # OFDM 250
data_ofdm_250_config = create_default_ofdm_config()
data_ofdm_250_config.config.contents.ns = 5
data_ofdm_250_config.config.contents.np = 47
data_ofdm_250_config.config.contents.tcp = 0.006
data_ofdm_250_config.config.contents.ts = 0.016
data_ofdm_250_config.config.contents.rs = 1.0 / data_ofdm_250_config.config.contents.ts
data_ofdm_250_config.config.contents.nc = 4
data_ofdm_250_config.config.contents.timing_mx_thresh = 0.5
data_ofdm_250_config.config.contents.bad_uw_errors = 12
data_ofdm_250_config.config.contents.codename = "H_1024_2048_4f".encode('utf-8')
data_ofdm_250_config.config.contents.amp_scale = 2*300E3
data_ofdm_250_config.config.contents.nuwbits = 32
data_ofdm_250_config.config.contents.tx_uw = create_tx_uw(data_ofdm_250_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0])
data_ofdm_250_config.config.contents.clip_gain1 = 1.2
data_ofdm_250_config.config.contents.clip_gain2 = 1.0
data_ofdm_250_config.config.contents.tx_bpf_en = True
data_ofdm_250_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 400, 101)
data_ofdm_250_config.config.contents.tx_bpf_proto_n = 101 # TODO sizeof(filtP200S400) / sizeof(float);


# OFDM 500
data_ofdm_500_config = create_default_ofdm_config()
data_ofdm_500_config.config.contents.ns = 5
data_ofdm_500_config.config.contents.np = 32
Expand All @@ -545,36 +593,35 @@ def create_tx_uw(nuwbits, uw_sequence):
data_ofdm_500_config.config.contents.timing_mx_thresh = 0.1
data_ofdm_500_config.config.contents.bad_uw_errors = 18
data_ofdm_500_config.config.contents.codename = "H_1024_2048_4f".encode('utf-8')
data_ofdm_500_config.config.contents.amp_scale = 290E3
data_ofdm_500_config.config.contents.amp_scale = 300E3 # 290E3
data_ofdm_500_config.config.contents.nuwbits = 56
data_ofdm_500_config.config.contents.tx_uw = create_tx_uw(data_ofdm_500_config.config.contents.nuwbits, [0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1])
data_ofdm_500_config.config.contents.clip_gain1 = 2.8
data_ofdm_500_config.config.contents.clip_gain2 = 0.9
data_ofdm_500_config.config.contents.tx_bpf_en = False
data_ofdm_500_config.config.contents.clip_gain1 = 2.5 # 2.8
data_ofdm_500_config.config.contents.clip_gain2 = 1.0 #0.9
data_ofdm_500_config.config.contents.tx_bpf_en = True
data_ofdm_500_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 600, 100)
data_ofdm_500_config.config.contents.tx_bpf_proto_n = 100


"""
# DATAC1
data_ofdm_500_config = create_default_ofdm_config()
data_ofdm_500_config.config.contents.ns = 5
data_ofdm_500_config.config.contents.np = 38
data_ofdm_500_config.config.contents.tcp = 0.006
data_ofdm_500_config.config.contents.ts = 0.016
data_ofdm_500_config.config.contents.nc = 27
data_ofdm_500_config.config.contents.nuwbits = 16
data_ofdm_500_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_500_config.config.contents.bad_uw_errors = 6
data_ofdm_500_config.config.contents.codename = b"H_4096_8192_3d"
data_ofdm_500_config.config.contents.clip_gain1 = 2.7
data_ofdm_500_config.config.contents.clip_gain2 = 0.8
data_ofdm_500_config.config.contents.amp_scale = 145E3
data_ofdm_500_config.config.contents.tx_bpf_en = False
#data_ofdm_500_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 2000, 101)
data_ofdm_500_config.config.contents.tx_bpf_proto_n = 101
data_ofdm_500_config.config.contents.tx_uw = create_tx_uw(data_ofdm_500_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0])
"""
# DATAC1 # OFDM1700
data_ofdm_1700_config = create_default_ofdm_config()
data_ofdm_1700_config.config.contents.ns = 5
data_ofdm_1700_config.config.contents.np = 38
data_ofdm_1700_config.config.contents.tcp = 0.006
data_ofdm_1700_config.config.contents.ts = 0.016
data_ofdm_1700_config.config.contents.nc = 27
data_ofdm_1700_config.config.contents.nuwbits = 16
data_ofdm_1700_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_1700_config.config.contents.bad_uw_errors = 6
data_ofdm_1700_config.config.contents.codename = b"H_4096_8192_3d"
data_ofdm_1700_config.config.contents.clip_gain1 = 2.7
data_ofdm_1700_config.config.contents.clip_gain2 = 0.8
data_ofdm_1700_config.config.contents.amp_scale = 145E3
data_ofdm_1700_config.config.contents.tx_bpf_en = False
data_ofdm_1700_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 2000, 100)
data_ofdm_1700_config.config.contents.tx_bpf_proto_n = 100
data_ofdm_1700_config.config.contents.tx_uw = create_tx_uw(data_ofdm_1700_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0])


"""
# DATAC3
Expand Down Expand Up @@ -639,7 +686,10 @@ def create_tx_uw(nuwbits, uw_sequence):
"""

ofdm_configurations = {
FREEDV_MODE.data_ofdm_200.value: data_ofdm_200_config,
FREEDV_MODE.data_ofdm_250.value: data_ofdm_250_config,
FREEDV_MODE.data_ofdm_500.value: data_ofdm_500_config,
FREEDV_MODE.data_ofdm_1700.value: data_ofdm_1700_config,
FREEDV_MODE.data_ofdm_2438.value: data_ofdm_2438_config,
#FREEDV_MODE.data_qam_2438.value: data_qam_2438_config

Expand Down
6 changes: 6 additions & 0 deletions freedata_server/modulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ def init_codec2(self):
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
self.freedv_datac14_tx = codec2.open_instance(codec2.FREEDV_MODE.datac14.value)
self.data_ofdm_200_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_200.value)
self.data_ofdm_250_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_250.value)
self.data_ofdm_500_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_500.value)
self.data_ofdm_1700_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_1700.value)
self.data_ofdm_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_2438.value)
#self.freedv_qam16c2_tx = codec2.open_instance(codec2.FREEDV_MODE.qam16c2.value)
#self.data_qam_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_qam_2438.value)
Expand Down Expand Up @@ -119,7 +122,10 @@ def create_burst(
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac14: self.freedv_datac14_tx,
codec2.FREEDV_MODE.data_ofdm_200: self.data_ofdm_200_tx,
codec2.FREEDV_MODE.data_ofdm_250: self.data_ofdm_250_tx,
codec2.FREEDV_MODE.data_ofdm_500: self.data_ofdm_500_tx,
codec2.FREEDV_MODE.data_ofdm_1700: self.data_ofdm_1700_tx,
codec2.FREEDV_MODE.data_ofdm_2438: self.data_ofdm_2438_tx,
#codec2.FREEDV_MODE.qam16c2: self.freedv_qam16c2_tx,
#codec2.FREEDV_MODE.data_qam_2438: self.freedv_data_qam_2438_tx,
Expand Down
19 changes: 13 additions & 6 deletions tools/custom_mode_tests/over_the_air_mode_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,25 @@ def write_to_file(self, txbuffer, filename):

# Usage example
if __name__ == "__main__":
MODE = FREEDV_MODE.data_ofdm_2438
FRAMES = 1

freedv_instance = FreeDV(MODE, 'config.ini')
# geht
MODE = FREEDV_MODE.data_ofdm_250
RX_MODE = FREEDV_MODE.datac4

# fail
#MODE = FREEDV_MODE.datac4
#RX_MODE = FREEDV_MODE.data_ofdm_250

FRAMES = 1

freedv_instance = FreeDV(MODE, 'config.ini')
freedv_rx_instance = FreeDV(RX_MODE, 'config.ini')

message = b'A'
txbuffer = freedv_instance.modulator.create_burst(MODE, 1, 100, message)
message = b'ABC'
txbuffer = freedv_instance.modulator.create_burst(MODE, FRAMES, 100, message)
freedv_instance.write_to_file(txbuffer, 'ota_audio.raw')
txbuffer = np.frombuffer(txbuffer, dtype=np.int16)
freedv_instance.demodulate(txbuffer)
freedv_rx_instance.demodulate(txbuffer)


# ./src/freedv_data_raw_rx --framesperburst 2 --testframes DATAC0 - /dev/null --vv
Expand Down
176 changes: 176 additions & 0 deletions tools/custom_mode_tests/run_mode_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""
AI-Generated FreeDATA Mode Testing Script by DJ2LS using ChatGPT

This script tests different FreeDV modes for their ability to modulate and demodulate data.
It evaluates the following metrics:
- Average audio volume in dB
- Max possible audio volume in dB
- Peak-to-Average Power Ratio (PAPR)
- Frequency spectrum analysis using FFT

The script runs predefined mode pairs in both transmission and reception directions,
and visualizes the results in separate plots.
"""


import sys

sys.path.append('freedata_server')

import ctypes
import threading
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from collections import defaultdict
from scipy.fftpack import fft
from codec2 import open_instance, api, audio_buffer, FREEDV_MODE, resampler
import modulator
import config
import helpers


class FreeDV:
def __init__(self, mode, config_file):
self.mode = mode
self.config = config.CONFIG(config_file)
self.modulator = modulator.Modulator(self.config.read())
self.freedv = open_instance(self.mode.value)

def demodulate(self, txbuffer):
c2instance = open_instance(self.mode.value)
bytes_per_frame = int(api.freedv_get_bits_per_modem_frame(c2instance) / 8)
bytes_out = ctypes.create_string_buffer(bytes_per_frame)
api.freedv_set_frames_per_burst(c2instance, 1)
audiobuffer = audio_buffer(len(txbuffer))
nin = api.freedv_nin(c2instance)
audiobuffer.push(txbuffer)
threading.Event().wait(0.01)

while audiobuffer.nbuffer >= nin:
nbytes = api.freedv_rawdatarx(self.freedv, bytes_out, audiobuffer.buffer.ctypes)
rx_status = api.freedv_get_rx_status(self.freedv)
nin = api.freedv_nin(self.freedv)
audiobuffer.pop(nin)
if nbytes == bytes_per_frame:
api.freedv_set_sync(self.freedv, 0)
return True # Passed

return False # Failed

def compute_audio_metrics(self, txbuffer):
"""Compute Average Volume in dB, Max Possible Volume, PAPR, and FFT for a given signal."""
# Ensure correct dtype and normalize to float range [-1, 1]
txbuffer = txbuffer.astype(np.float32) / 32768.0

avg_volume = np.mean(np.abs(txbuffer))
avg_volume_db = 20 * np.log10(avg_volume) if avg_volume > 0 else -np.inf
max_possible_volume_db = 20 * np.log10(1.0) # Max possible volume when signal is fully utilized
max_val = np.max(np.abs(txbuffer))

# Prevent division by zero and ensure reasonable values
if avg_volume == 0 or max_val == 0:
papr = 0
else:
papr = 10 * np.log10((max_val ** 2) / (avg_volume ** 2))

# Compute FFT
fft_values = np.abs(fft(txbuffer))[:len(txbuffer) // 2]
freqs = np.fft.fftfreq(len(txbuffer), d=1 / 8000)[:len(txbuffer) // 2] # Assuming 8 kHz sample rate

return avg_volume_db, max_possible_volume_db, papr, freqs, fft_values

def write_to_file(self, txbuffer, filename):
with open(filename, 'wb') as f:
f.write(txbuffer)


def plot_audio_metrics(avg_volume_per_mode, avg_max_volume_per_mode, avg_papr_per_mode):
"""Plot audio metrics in a separate window."""
plt.figure(figsize=(10, 5))
modes = list(avg_volume_per_mode.keys())
volume_values = list(avg_volume_per_mode.values())
max_volume_values = list(avg_max_volume_per_mode.values())
papr_values = list(avg_papr_per_mode.values())

plt.plot(modes, volume_values, marker='o', linestyle='-', label='Average Volume (dB)')
plt.plot(modes, max_volume_values, marker='x', linestyle='--', label='Max Possible Volume (dB)', color='blue')
plt.plot(modes, papr_values, marker='s', linestyle='-', label='Average PAPR (dB)', color='red')
plt.ylabel('Volume (dB) / PAPR (dB)')
plt.xlabel('Modes')
plt.title('Audio Metrics per Mode')
plt.legend()
plt.xticks(rotation=45, ha='right')
plt.pause(0.1)


def plot_fft_per_mode(fft_data):
"""Plot FFTs in a separate window."""
for mode, (freqs, fft_values) in fft_data.items():
plt.figure(figsize=(8, 4))
plt.plot(freqs, fft_values, label=f'FFT {mode}')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude')
plt.title(f'FFT of {mode}')
plt.legend()
plt.pause(0.1)


def plot_results_summary(results):
"""Plot pass/fail results for each mode pair."""
mode_pairs = [f"{tx} -> {rx}" for tx, rx, _, _, _, _ in results]
pass_fail = [1 if result[2] else -1 for result in results] # Convert True/False to 1/0
colors = ['green' if r == 1 else 'red' for r in pass_fail]

plt.figure(figsize=(10, 5))
plt.bar(mode_pairs, pass_fail, color=colors)
plt.ylabel('Pass (1) / Fail (0)')
plt.xlabel('Mode Pairs')
plt.title('Mode Constellation Pass/Fail Summary')
plt.xticks(rotation=45, ha='right')
plt.ylim(-1, 1) # Ensure bars are properly visible
plt.show()

def test_freedv_mode_pairs(mode_pairs, config_file='config.ini'):
results = []
fft_data = {}
volume_per_mode = {}
max_volume_per_mode = {}
papr_per_mode = {}

for tx_mode, rx_mode in mode_pairs:
for test_tx, test_rx in [(tx_mode, rx_mode), (rx_mode, tx_mode)]:
freedv_tx = FreeDV(test_tx, config_file)
freedv_rx = FreeDV(test_rx, config_file)

message = b'ABC'
txbuffer = freedv_tx.modulator.create_burst(test_tx, 1, 100, message)
txbuffer = np.frombuffer(txbuffer, dtype=np.int16)

result = freedv_rx.demodulate(txbuffer)
avg_volume_db, max_possible_volume_db, papr, freqs, fft_values = freedv_tx.compute_audio_metrics(txbuffer)
results.append((test_tx.name, test_rx.name, result, avg_volume_db, max_possible_volume_db, papr))
volume_per_mode[test_tx.name] = avg_volume_db
max_volume_per_mode[test_tx.name] = max_possible_volume_db
papr_per_mode[test_tx.name] = papr
fft_data[test_tx.name] = (freqs, fft_values)

return results, volume_per_mode, max_volume_per_mode, papr_per_mode, fft_data


if __name__ == "__main__":
test_mode_pairs = [
(FREEDV_MODE.datac13, FREEDV_MODE.data_ofdm_200),
(FREEDV_MODE.datac14, FREEDV_MODE.datac14),
(FREEDV_MODE.datac4, FREEDV_MODE.data_ofdm_250),
(FREEDV_MODE.data_ofdm_500, FREEDV_MODE.data_ofdm_500),
(FREEDV_MODE.datac0, FREEDV_MODE.datac0),
(FREEDV_MODE.datac3, FREEDV_MODE.datac3),
(FREEDV_MODE.datac1, FREEDV_MODE.data_ofdm_1700),
(FREEDV_MODE.data_ofdm_2438, FREEDV_MODE.data_ofdm_2438),
]
results, avg_volume_per_mode, avg_max_volume_per_mode, avg_papr_per_mode, fft_data = test_freedv_mode_pairs(
test_mode_pairs)
plot_audio_metrics(avg_volume_per_mode, avg_max_volume_per_mode, avg_papr_per_mode)
plot_fft_per_mode(fft_data)
plot_results_summary(results)