From b31b2a2446cf2516a39672bc99902352170dffca Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:40:38 +0100 Subject: [PATCH 1/3] first run porting hardcodec modes to custom modes --- freedata_server/codec2.py | 96 ++++++++++++++----- freedata_server/modulator.py | 6 ++ .../over_the_air_mode_test.py | 13 +-- 3 files changed, 86 insertions(+), 29 deletions(-) diff --git a/freedata_server/codec2.py b/freedata_server/codec2.py index 01126240c..8562d7752 100644 --- a/freedata_server/codec2.py +++ b/freedata_server/codec2.py @@ -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 @@ -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] @@ -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( @@ -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 @@ -550,31 +598,30 @@ def create_tx_uw(nuwbits, uw_sequence): 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.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 @@ -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 diff --git a/freedata_server/modulator.py b/freedata_server/modulator.py index 687a96a5f..3d47fce3a 100644 --- a/freedata_server/modulator.py +++ b/freedata_server/modulator.py @@ -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) @@ -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, diff --git a/tools/custom_mode_tests/over_the_air_mode_test.py b/tools/custom_mode_tests/over_the_air_mode_test.py index 06c93a95d..dba490feb 100644 --- a/tools/custom_mode_tests/over_the_air_mode_test.py +++ b/tools/custom_mode_tests/over_the_air_mode_test.py @@ -52,18 +52,19 @@ def write_to_file(self, txbuffer, filename): # Usage example if __name__ == "__main__": - MODE = FREEDV_MODE.data_ofdm_2438 + MODE = FREEDV_MODE.datac13 + RX_MODE = FREEDV_MODE.data_ofdm_200 + 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 From aa568d95fbaf2ebba6586fafc4742d30757eea91 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:33:36 +0100 Subject: [PATCH 2/3] adjusted mode tests --- tools/custom_mode_tests/over_the_air_mode_test.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/custom_mode_tests/over_the_air_mode_test.py b/tools/custom_mode_tests/over_the_air_mode_test.py index dba490feb..833e022d5 100644 --- a/tools/custom_mode_tests/over_the_air_mode_test.py +++ b/tools/custom_mode_tests/over_the_air_mode_test.py @@ -52,8 +52,14 @@ def write_to_file(self, txbuffer, filename): # Usage example if __name__ == "__main__": - MODE = FREEDV_MODE.datac13 - RX_MODE = FREEDV_MODE.data_ofdm_200 + + # 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 From 6563c684954a4d050d3a8ae8e396ec7347cdec7a Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Thu, 30 Jan 2025 18:41:42 +0100 Subject: [PATCH 3/3] adjusted mode tests --- freedata_server/codec2.py | 6 +- tools/custom_mode_tests/run_mode_tests.py | 176 ++++++++++++++++++++++ 2 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 tools/custom_mode_tests/run_mode_tests.py diff --git a/freedata_server/codec2.py b/freedata_server/codec2.py index 8562d7752..95047726a 100644 --- a/freedata_server/codec2.py +++ b/freedata_server/codec2.py @@ -593,11 +593,11 @@ 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.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 diff --git a/tools/custom_mode_tests/run_mode_tests.py b/tools/custom_mode_tests/run_mode_tests.py new file mode 100644 index 000000000..bd5dc8a99 --- /dev/null +++ b/tools/custom_mode_tests/run_mode_tests.py @@ -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)