From a2cfae3a22605714a1047dff1392752fae5e89c2 Mon Sep 17 00:00:00 2001 From: zach Date: Sat, 27 Dec 2025 15:32:32 -0700 Subject: [PATCH] updated ui added new features --- android/app/build.gradle.kts | 14 + android/app/src/main/AndroidManifest.xml | 11 + android/app/src/main/cpp/CMakeLists.txt | 33 + android/app/src/main/cpp/codec2_sources.cmake | 65 + ios/Podfile | 38 + ios/Runner/Info.plist | 6 + lib/connector/meshcore_connector.dart | 691 +- lib/main.dart | 12 + lib/models/app_settings.dart | 31 + lib/models/channel_message.dart | 42 +- lib/models/contact.dart | 28 +- lib/models/message.dart | 18 + lib/screens/app_settings_screen.dart | 44 + lib/screens/channel_chat_screen.dart | 7 +- lib/screens/channel_message_path_screen.dart | 312 +- lib/screens/channels_screen.dart | 172 +- lib/screens/chat_screen.dart | 400 +- lib/screens/contacts_screen.dart | 602 +- lib/screens/device_screen.dart | 363 +- lib/screens/map_cache_screen.dart | 390 + lib/screens/map_screen.dart | 91 +- lib/screens/scanner_screen.dart | 4 +- lib/services/app_settings_service.dart | 19 + lib/services/background_service.dart | 82 + lib/services/codec2_ffi.dart | 152 + lib/services/map_tile_cache_service.dart | 241 + lib/services/message_retry_service.dart | 1 + lib/services/notification_service.dart | 49 + lib/services/voice_message_service.dart | 220 + lib/storage/channel_message_store.dart | 4 + lib/storage/contact_store.dart | 6 +- lib/storage/message_store.dart | 8 + lib/utils/contact_search.dart | 26 + lib/utils/route_transitions.dart | 26 + lib/widgets/quick_switch_bar.dart | 83 + lib/widgets/voice_message.dart | 134 + linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 8 + pubspec.lock | 292 +- pubspec.yaml | 9 + third_party/codec2/.clang-format | 168 + .../codec2/.github/workflows/cmake-sm1000.yml | 43 + .../codec2/.github/workflows/cmake.yml | 58 + third_party/codec2/.gitignore | 8 + third_party/codec2/CMakeLists.txt | 1434 ++ third_party/codec2/COPYING | 502 + third_party/codec2/README.md | 293 + third_party/codec2/README_cohpsk.md | 43 + third_party/codec2/README_data.md | 378 + third_party/codec2/README_fdmdv.md | 106 + third_party/codec2/README_freedv.md | 217 + third_party/codec2/README_fsk.md | 175 + third_party/codec2/README_ofdm.md | 252 + .../codec2/cmake/GetDependencies.cmake.in | 24 + third_party/codec2/cmake/config.h.in | 23 + third_party/codec2/cmake/version.h.in | 37 + third_party/codec2/codec2.pc.in | 10 + third_party/codec2/codec2.podspec | 18 + third_party/codec2/demo/CMakeLists.txt | 17 + third_party/codec2/demo/c2demo.c | 77 + third_party/codec2/demo/freedv_700d_rx.c | 55 + third_party/codec2/demo/freedv_700d_rx.py | 49 + third_party/codec2/demo/freedv_700d_tx.c | 68 + third_party/codec2/demo/freedv_datac0c1_rx.c | 130 + third_party/codec2/demo/freedv_datac0c1_tx.c | 109 + third_party/codec2/demo/freedv_datac1_rx.c | 63 + third_party/codec2/demo/freedv_datac1_tx.c | 98 + third_party/codec2/doc/Makefile | 35 + third_party/codec2/doc/c_tx_comp.png | Bin 0 -> 39874 bytes third_party/codec2/doc/c_tx_comp_thruput.png | Bin 0 -> 34357 bytes third_party/codec2/doc/codec2.pdf | Bin 0 -> 316470 bytes third_party/codec2/doc/codec2.tex | 1084 + third_party/codec2/doc/codec2_refs.bib | 84 + .../codec2/doc/fsk_modem_ber_8000_100.png | Bin 0 -> 47376 bytes third_party/codec2/doc/lockdown_3s.wav | Bin 0 -> 48044 bytes .../codec2/doc/modem_codec_frame_design.ods | Bin 0 -> 44489 bytes third_party/codec2/doc/pre_post_amble_mpp.png | Bin 0 -> 4313 bytes third_party/codec2/doc/ratek_mel_fhz.png | Bin 0 -> 11685 bytes third_party/codec2/doc/snrest_snr_ctx.png | Bin 0 -> 26891 bytes third_party/codec2/doc/snrest_snr_ctxc.png | Bin 0 -> 31398 bytes .../codec2/doc/test_datac1_006_scatter.png | Bin 0 -> 40414 bytes .../doc/test_datac1_006_spectrogram.png | Bin 0 -> 867507 bytes third_party/codec2/doc/warp_fhz_k.png | Bin 0 -> 9209 bytes third_party/codec2/doc/wenet_image.jpg | Bin 0 -> 380212 bytes third_party/codec2/doc/wenet_spectrum_3d.png | Bin 0 -> 59671 bytes third_party/codec2/include/codec2/version.h | 9 + third_party/codec2/include/config.h | 19 + .../codec2/octave/H2064_516_sparse.mat | 7231 ++++++ third_party/codec2/octave/HRA_112_112.txt | 119 + third_party/codec2/octave/HRA_112_56.txt | 63 + third_party/codec2/octave/HRA_504_396.txt | 108 + third_party/codec2/octave/HRA_56_28.txt | 35 + third_party/codec2/octave/HRA_56_56.txt | 63 + third_party/codec2/octave/HRAa_1536_512.mat | Bin 0 -> 19482 bytes third_party/codec2/octave/H_1024_2048_4f.mat | Bin 0 -> 26470 bytes third_party/codec2/octave/H_128_256_5.mat | Bin 0 -> 1528 bytes third_party/codec2/octave/H_256_512_4.mat | 263 + third_party/codec2/octave/H_256_768_22.txt | 519 + third_party/codec2/octave/H_4096_8192_3d.mat | Bin 0 -> 203047 bytes third_party/codec2/octave/Mat2Hrows.m | 28 + third_party/codec2/octave/autotest.m | 119 + third_party/codec2/octave/ch_fading.m | 27 + third_party/codec2/octave/channel_lib.m | 72 + third_party/codec2/octave/cohpsk_demod_plot.m | 69 + third_party/codec2/octave/cohpsk_dev.m | 439 + third_party/codec2/octave/cohpsk_lib.m | 509 + third_party/codec2/octave/crc16.m | 55 + third_party/codec2/octave/diff_fft_mag.m | 27 + third_party/codec2/octave/doppler_spread.m | 38 + third_party/codec2/octave/esno_est.m | 147 + third_party/codec2/octave/fdmdv.m | 971 + third_party/codec2/octave/fdmdv_common.m | 221 + third_party/codec2/octave/fdmdv_demod.m | 365 + third_party/codec2/octave/fdmdv_demod_c.m | 134 + third_party/codec2/octave/fdmdv_demod_coh.m | 253 + third_party/codec2/octave/fdmdv_mod.m | 34 + third_party/codec2/octave/fdmdv_ut.m | 362 + third_party/codec2/octave/fsk_demod_file.m | 139 + third_party/codec2/octave/fsk_horus.m | 876 + third_party/codec2/octave/fsk_lib.m | 469 + third_party/codec2/octave/fsk_lib_demo.m | 101 + third_party/codec2/octave/fsk_lib_ldpc.m | 45 + third_party/codec2/octave/fsk_lib_ldpc_demo.m | 172 + third_party/codec2/octave/gen_rn_coeffs.m | 40 + third_party/codec2/octave/gp_interleaver.m | 59 + third_party/codec2/octave/h0p25d.mat | Bin 0 -> 20585 bytes .../codec2/octave/horus_high_speed.bin | Bin 0 -> 2760 bytes .../codec2/octave/horus_payload_rtty.txt | 1 + .../codec2/octave/horus_tx_bits_binary.txt | 1 + third_party/codec2/octave/ldpc.m | 194 + third_party/codec2/octave/ldpc_fsk_lib.m | 269 + third_party/codec2/octave/ldpcut.m | 288 + third_party/codec2/octave/linreg.m | 35 + third_party/codec2/octave/load_raw.m | 8 + third_party/codec2/octave/mag_to_phase.m | 62 + third_party/codec2/octave/melvq.m | 165 + third_party/codec2/octave/newamp_700c.m | 358 + third_party/codec2/octave/ofdm_acquisition.m | 249 + third_party/codec2/octave/ofdm_demod_c.m | 43 + third_party/codec2/octave/ofdm_helper.m | 273 + third_party/codec2/octave/ofdm_ldpc_rx.m | 279 + third_party/codec2/octave/ofdm_ldpc_tx.m | 144 + third_party/codec2/octave/ofdm_lib.m | 1284 + third_party/codec2/octave/ofdm_load_const.m | 57 + third_party/codec2/octave/ofdm_mode.m | 269 + third_party/codec2/octave/ofdm_rx.m | 253 + third_party/codec2/octave/ofdm_state.m | 271 + third_party/codec2/octave/ofdm_time_sync.m | 22 + third_party/codec2/octave/ofdm_tx.m | 95 + third_party/codec2/octave/plamp.m | 178 + .../codec2/octave/plot_fsk_demod_stats.py | 95 + third_party/codec2/octave/plot_specgram.m | 22 + third_party/codec2/octave/qam16.m | 35 + third_party/codec2/octave/qpsk.m | 140 + .../codec2/octave/sample_clock_offset.m | 21 + third_party/codec2/octave/snr_curves_plot.m | 267 + third_party/codec2/octave/spec.m | 86 + third_party/codec2/octave/tcohpsk.m | 745 + third_party/codec2/octave/tfdmdv.m | 307 + third_party/codec2/octave/tfmfsk.m | 497 + third_party/codec2/octave/tfsk.m | 611 + third_party/codec2/octave/tnewamp1.m | 256 + third_party/codec2/octave/tofdm.m | 272 + third_party/codec2/octave/tofdm_acq.m | 81 + third_party/codec2/octave/train_120_1.txt | 512 + third_party/codec2/octave/train_120_2.txt | 512 + third_party/codec2/raw/hts1.raw | Bin 0 -> 96000 bytes third_party/codec2/raw/hts1a.raw | Bin 0 -> 48000 bytes third_party/codec2/raw/hts2a.raw | Bin 0 -> 48000 bytes third_party/codec2/raw/kristoff.raw | Bin 0 -> 80000 bytes third_party/codec2/raw/testframes_700d.raw | Bin 0 -> 12800 bytes third_party/codec2/raw/ve9qrp.raw | Bin 0 -> 1799168 bytes third_party/codec2/raw/ve9qrp_10s.raw | Bin 0 -> 160000 bytes third_party/codec2/src/CMakeLists.txt | 387 + .../codec2/src/H2064_516_sparse_test.h | 1918 ++ third_party/codec2/src/HRA_112_112.c | 145 + third_party/codec2/src/HRA_112_112.h | 20 + third_party/codec2/src/HRA_112_112_test.h | 153 + third_party/codec2/src/HRA_56_56.c | 76 + third_party/codec2/src/HRA_56_56.h | 20 + third_party/codec2/src/HRAa_1536_512.c | 1702 ++ third_party/codec2/src/HRAa_1536_512.h | 20 + third_party/codec2/src/HRAb_396_504.c | 476 + third_party/codec2/src/HRAb_396_504.h | 20 + third_party/codec2/src/H_1024_2048_4f.c | 5645 +++++ third_party/codec2/src/H_1024_2048_4f.h | 18 + third_party/codec2/src/H_128_256_5.c | 198 + third_party/codec2/src/H_128_256_5.h | 20 + third_party/codec2/src/H_16200_9720.c | 14593 +++++++++++ third_party/codec2/src/H_16200_9720.h | 18 + third_party/codec2/src/H_2064_516_sparse.c | 1920 ++ third_party/codec2/src/H_2064_516_sparse.h | 22 + third_party/codec2/src/H_212_158.c | 132 + third_party/codec2/src/H_212_158.h | 18 + third_party/codec2/src/H_256_512_4.c | 345 + third_party/codec2/src/H_256_512_4.h | 20 + third_party/codec2/src/H_256_768_22.c | 440 + third_party/codec2/src/H_256_768_22.h | 20 + third_party/codec2/src/H_4096_8192_3d.c | 10584 ++++++++ third_party/codec2/src/H_4096_8192_3d.h | 20 + third_party/codec2/src/_kiss_fft_guts.h | 194 + third_party/codec2/src/bpf.h | 18 + third_party/codec2/src/bpfb.h | 18 + third_party/codec2/src/c2dec.c | 420 + third_party/codec2/src/c2enc.c | 207 + third_party/codec2/src/c2file.h | 19 + third_party/codec2/src/c2sim.c | 1187 + third_party/codec2/src/ch.c | 531 + third_party/codec2/src/codebook.c | 285 + third_party/codec2/src/codebook/codes_450.txt | 501 + third_party/codec2/src/codebook/dlsp1.txt | 35 + third_party/codec2/src/codebook/dlsp10.txt | 35 + third_party/codec2/src/codebook/dlsp2.txt | 35 + third_party/codec2/src/codebook/dlsp3.txt | 35 + third_party/codec2/src/codebook/dlsp4.txt | 35 + third_party/codec2/src/codebook/dlsp5.txt | 35 + third_party/codec2/src/codebook/dlsp6.txt | 35 + third_party/codec2/src/codebook/dlsp7.txt | 35 + third_party/codec2/src/codebook/dlsp8.txt | 35 + third_party/codec2/src/codebook/dlsp9.txt | 35 + third_party/codec2/src/codebook/gecb.txt | 257 + third_party/codec2/src/codebook/lsp1.txt | 17 + third_party/codec2/src/codebook/lsp10.txt | 6 + third_party/codec2/src/codebook/lsp2.txt | 17 + third_party/codec2/src/codebook/lsp3.txt | 17 + third_party/codec2/src/codebook/lsp4.txt | 17 + third_party/codec2/src/codebook/lsp5.txt | 19 + third_party/codec2/src/codebook/lsp6.txt | 19 + third_party/codec2/src/codebook/lsp7.txt | 19 + third_party/codec2/src/codebook/lsp8.txt | 11 + third_party/codec2/src/codebook/lsp8910.txt | 65 + third_party/codec2/src/codebook/lsp9.txt | 11 + third_party/codec2/src/codebook/lspjmv1.txt | 513 + third_party/codec2/src/codebook/lspjmv2.txt | 513 + third_party/codec2/src/codebook/lspjmv3.txt | 513 + third_party/codec2/src/codebook/lspvqexp1.txt | 2049 ++ third_party/codec2/src/codebook/lspvqexp2.txt | 2049 ++ third_party/codec2/src/codebook/lspvqexp3.txt | 2049 ++ .../codec2/src/codebook/newamp1_energy_q.txt | 17 + .../codec2/src/codebook/newamp2_energy_q.txt | 9 + .../codec2/src/codebook/train_120_1.txt | 513 + .../codec2/src/codebook/train_120_2.txt | 513 + third_party/codec2/src/codebookd.c | 473 + third_party/codec2/src/codebookge.c | 283 + third_party/codec2/src/codebookjmv.c | 1591 ++ third_party/codec2/src/codebooknewamp1.c | 1065 + .../codec2/src/codebooknewamp1_energy.c | 43 + third_party/codec2/src/codebooknewamp2.c | 527 + .../codec2/src/codebooknewamp2_energy.c | 35 + third_party/codec2/src/codec2.c | 1921 ++ third_party/codec2/src/codec2.h | 120 + third_party/codec2/src/codec2_cohpsk.h | 73 + third_party/codec2/src/codec2_fdmdv.h | 138 + third_party/codec2/src/codec2_fft.c | 150 + third_party/codec2/src/codec2_fft.h | 99 + third_party/codec2/src/codec2_fifo.c | 142 + third_party/codec2/src/codec2_fifo.h | 57 + third_party/codec2/src/codec2_fm.h | 52 + third_party/codec2/src/codec2_internal.h | 103 + third_party/codec2/src/codec2_math.h | 83 + third_party/codec2/src/codec2_math_arm.c | 73 + third_party/codec2/src/codec2_ofdm.h | 109 + third_party/codec2/src/cohpsk.c | 1438 ++ third_party/codec2/src/cohpsk_defs.h | 11 + third_party/codec2/src/cohpsk_demod.c | 269 + third_party/codec2/src/cohpsk_get_test_bits.c | 84 + third_party/codec2/src/cohpsk_internal.h | 136 + third_party/codec2/src/cohpsk_mod.c | 123 + third_party/codec2/src/cohpsk_put_test_bits.c | 112 + third_party/codec2/src/comp.h | 38 + third_party/codec2/src/comp_prim.h | 138 + third_party/codec2/src/debug_alloc.h | 71 + third_party/codec2/src/defines.h | 122 + third_party/codec2/src/deframer.c | 182 + third_party/codec2/src/dump.c | 616 + third_party/codec2/src/dump.h | 82 + third_party/codec2/src/fdmdv.c | 2047 ++ third_party/codec2/src/fdmdv_demod.c | 257 + third_party/codec2/src/fdmdv_get_test_bits.c | 124 + third_party/codec2/src/fdmdv_internal.h | 213 + third_party/codec2/src/fdmdv_mod.c | 163 + third_party/codec2/src/fdmdv_put_test_bits.c | 167 + third_party/codec2/src/filter.c | 288 + third_party/codec2/src/filter.h | 50 + third_party/codec2/src/filter_coef.h | 478 + third_party/codec2/src/fm.c | 282 + third_party/codec2/src/fm_fir_coeff.h | 87 + third_party/codec2/src/fmfsk.c | 368 + third_party/codec2/src/fmfsk.h | 112 + third_party/codec2/src/framer.c | 99 + third_party/codec2/src/freedv_1600.c | 272 + third_party/codec2/src/freedv_2020.c | 363 + third_party/codec2/src/freedv_700.c | 619 + third_party/codec2/src/freedv_api.c | 1632 ++ third_party/codec2/src/freedv_api.h | 351 + third_party/codec2/src/freedv_api_internal.h | 267 + third_party/codec2/src/freedv_data_channel.c | 303 + third_party/codec2/src/freedv_data_channel.h | 80 + third_party/codec2/src/freedv_data_raw_rx.c | 377 + third_party/codec2/src/freedv_data_raw_tx.c | 540 + third_party/codec2/src/freedv_data_rx.c | 225 + third_party/codec2/src/freedv_data_tx.c | 273 + third_party/codec2/src/freedv_fsk.c | 710 + third_party/codec2/src/freedv_mixed_rx.c | 226 + third_party/codec2/src/freedv_mixed_tx.c | 370 + third_party/codec2/src/freedv_rx.c | 343 + third_party/codec2/src/freedv_tx.c | 218 + third_party/codec2/src/freedv_vhf_framing.c | 871 + third_party/codec2/src/freedv_vhf_framing.h | 105 + third_party/codec2/src/fsk.c | 1060 + third_party/codec2/src/fsk.h | 228 + third_party/codec2/src/fsk_demod.c | 495 + third_party/codec2/src/fsk_get_test_bits.c | 98 + third_party/codec2/src/fsk_mod.c | 145 + third_party/codec2/src/fsk_put_test_bits.c | 177 + third_party/codec2/src/generate_codebook.c | 174 + third_party/codec2/src/golay23.c | 315 + third_party/codec2/src/golay23.h | 45 + third_party/codec2/src/golaydectable.h | 296 + third_party/codec2/src/golayenctable.h | 589 + third_party/codec2/src/gp_interleaver.c | 134 + third_party/codec2/src/gp_interleaver.h | 43 + third_party/codec2/src/hanning.h | 131 + third_party/codec2/src/ht_coeff.h | 92 + third_party/codec2/src/interldpc.c | 340 + third_party/codec2/src/interldpc.h | 61 + third_party/codec2/src/interp.c | 308 + third_party/codec2/src/interp.h | 47 + third_party/codec2/src/kiss_fft.c | 426 + third_party/codec2/src/kiss_fft.h | 126 + third_party/codec2/src/kiss_fftr.c | 168 + third_party/codec2/src/kiss_fftr.h | 47 + third_party/codec2/src/ldpc_codes.c | 135 + third_party/codec2/src/ldpc_codes.h | 27 + third_party/codec2/src/ldpc_dec.c | 268 + third_party/codec2/src/ldpc_dec_test.c | 374 + third_party/codec2/src/ldpc_enc.c | 156 + third_party/codec2/src/ldpc_enc_test.c | 165 + third_party/codec2/src/ldpc_noise.c | 81 + third_party/codec2/src/linreg.c | 101 + third_party/codec2/src/linreg.h | 35 + third_party/codec2/src/lpc.c | 277 + third_party/codec2/src/lpc.h | 43 + third_party/codec2/src/lpcnet_freq.c | 96 + third_party/codec2/src/lpcnet_freq.h | 44 + third_party/codec2/src/lsp.c | 313 + third_party/codec2/src/lsp.h | 37 + third_party/codec2/src/machdep.h | 52 + third_party/codec2/src/mbest.c | 191 + third_party/codec2/src/mbest.h | 56 + third_party/codec2/src/modem_probe.c | 236 + third_party/codec2/src/modem_probe.h | 129 + third_party/codec2/src/modem_stats.c | 126 + third_party/codec2/src/modem_stats.h | 90 + third_party/codec2/src/mpdecode_core.c | 751 + third_party/codec2/src/mpdecode_core.h | 61 + third_party/codec2/src/newamp1.c | 656 + third_party/codec2/src/newamp1.h | 86 + third_party/codec2/src/nlp.c | 470 + third_party/codec2/src/nlp.h | 39 + third_party/codec2/src/noise_samples.h | 20005 ++++++++++++++++ third_party/codec2/src/octave.c | 141 + third_party/codec2/src/octave.h | 42 + third_party/codec2/src/ofdm.c | 2696 +++ third_party/codec2/src/ofdm_demod.c | 751 + third_party/codec2/src/ofdm_get_test_bits.c | 157 + third_party/codec2/src/ofdm_internal.h | 295 + third_party/codec2/src/ofdm_mod.c | 441 + third_party/codec2/src/ofdm_mode.c | 280 + third_party/codec2/src/ofdm_put_test_bits.c | 124 + third_party/codec2/src/optparse.h | 361 + third_party/codec2/src/os.h | 35 + third_party/codec2/src/pack.c | 130 + third_party/codec2/src/phase.c | 275 + third_party/codec2/src/phase.h | 41 + third_party/codec2/src/phi0.c | 290 + third_party/codec2/src/phi0.h | 7 + third_party/codec2/src/pilot_coeff.h | 15 + third_party/codec2/src/pilots_coh.h | 5 + third_party/codec2/src/postfilter.c | 138 + third_party/codec2/src/postfilter.h | 35 + third_party/codec2/src/quantise.c | 1131 + third_party/codec2/src/quantise.h | 103 + third_party/codec2/src/reliable_text.c | 480 + third_party/codec2/src/reliable_text.h | 67 + third_party/codec2/src/rn.h | 195 + third_party/codec2/src/rn_coh.h | 366 + third_party/codec2/src/rxdec_coeff.h | 10 + third_party/codec2/src/sd.c | 83 + third_party/codec2/src/sd.h | 36 + third_party/codec2/src/sine.c | 657 + third_party/codec2/src/sine.h | 52 + third_party/codec2/src/ssbfilt_coeff.h | 23 + third_party/codec2/src/test_bits.h | 10 + third_party/codec2/src/test_bits_coh.h | 26 + third_party/codec2/src/test_bits_ofdm.h | 31 + third_party/codec2/src/tollr.c | 19 + third_party/codec2/src/varicode.c | 520 + third_party/codec2/src/varicode.h | 55 + third_party/codec2/src/varicode_table.h | 308 + third_party/codec2/src/vhf_deframe_c2.c | 113 + third_party/codec2/src/vhf_frame_c2.c | 107 + third_party/codec2/src/wval.h | 117 + third_party/codec2/stm32/CMakeLists.txt | 311 + third_party/codec2/stm32/README.md | 101 + .../codec2/stm32/cmake/STM32_Lib.cmake | 348 + .../codec2/stm32/cmake/STM32_Toolchain.cmake | 15 + .../codec2/stm32/cmake/arm_header.cmake | 64 + .../codec2/stm32/cmake/gencodebooks.cmake | 173 + .../codec2/stm32/doc/3dot5mm_cable_config.png | Bin 0 -> 202217 bytes third_party/codec2/stm32/doc/sm1000_cn12.png | Bin 0 -> 86387 bytes .../codec2/stm32/doc/sm1000_cn12_rev2.odg | Bin 0 -> 12778 bytes .../codec2/stm32/doc/sm1000_cn12_rev2.png | Bin 0 -> 48574 bytes .../codec2/stm32/doc/sm1000_cn4_cn12.jpg | Bin 0 -> 57929 bytes .../codec2/stm32/doc/sm1000_enc_sm.jpg | Bin 0 -> 277646 bytes third_party/codec2/stm32/doc/sm1000_manual.md | 184 + third_party/codec2/stm32/inc/debugblinky.h | 35 + third_party/codec2/stm32/inc/memtools.h | 13 + third_party/codec2/stm32/inc/menu.h | 92 + third_party/codec2/stm32/inc/morse.h | 65 + third_party/codec2/stm32/inc/sfx.h | 63 + .../codec2/stm32/inc/sm1000_leds_switches.h | 86 + third_party/codec2/stm32/inc/sounds.h | 38 + third_party/codec2/stm32/inc/stm32f4_adc.h | 46 + third_party/codec2/stm32/inc/stm32f4_dac.h | 46 + third_party/codec2/stm32/inc/stm32f4_usart.h | 35 + .../codec2/stm32/inc/stm32f4_usb_vcp.h | 24 + third_party/codec2/stm32/inc/stm32f4_vrom.h | 70 + third_party/codec2/stm32/inc/stm32f4xx_conf.h | 94 + third_party/codec2/stm32/inc/tone.h | 84 + third_party/codec2/stm32/inc/tot.h | 115 + third_party/codec2/stm32/src/adc_rec_usb.c | 85 + third_party/codec2/stm32/src/dac_ut.c | 57 + third_party/codec2/stm32/src/debugblinky.c | 57 + third_party/codec2/stm32/src/memtools.c | 67 + third_party/codec2/stm32/src/menu.c | 98 + third_party/codec2/stm32/src/morse.c | 175 + third_party/codec2/stm32/src/sfx.c | 67 + .../codec2/stm32/src/sm1000_leds_switches.c | 229 + .../stm32/src/sm1000_leds_switches_ut.c | 41 + third_party/codec2/stm32/src/sm1000_main.c | 1476 ++ third_party/codec2/stm32/src/sounds.c | 62 + .../codec2/stm32/src/startup_stm32f4xx.s | 526 + third_party/codec2/stm32/src/stm32f4_adc.c | 286 + third_party/codec2/stm32/src/stm32f4_dac.c | 427 + .../codec2/stm32/src/stm32f4_machdep.c | 92 + third_party/codec2/stm32/src/stm32f4_usart.c | 71 + .../codec2/stm32/src/stm32f4_usb_vcp.c | 90 + third_party/codec2/stm32/src/stm32f4_vrom.c | 724 + .../codec2/stm32/src/system_stm32f4xx.c | 585 + third_party/codec2/stm32/src/tone.c | 151 + third_party/codec2/stm32/src/tot.c | 90 + third_party/codec2/stm32/src/usart_ut.c | 30 + third_party/codec2/stm32/src/usb_vcp_ut.c | 96 + third_party/codec2/stm32/src/usb_vsp_ut.c | 192 + third_party/codec2/stm32/stlink/elfsym.c | 145 + third_party/codec2/stm32/stlink/elfsym.h | 14 + third_party/codec2/stm32/stlink/stlink.patch | 428 + third_party/codec2/stm32/stm32_flash.ld | 151 + third_party/codec2/stm32/stm32_ram.ld | 116 + .../codec2/stm32/unittest/README_unittest.md | 259 + .../unittest/lib/octave/ofdm_demod_check.m | 62 + .../stm32/unittest/lib/python/sum_profiles.py | 51 + .../codec2/stm32/unittest/lib/ut_travis.enc | Bin 0 -> 400 bytes .../stm32/unittest/scripts/check_ram_limit | 25 + .../stm32/unittest/scripts/kill_run_stm32_tst | 15 + .../unittest/scripts/plot_ofdm_demod_syms | 54 + .../unittest/scripts/run_all_codec2_tests | 27 + .../stm32/unittest/scripts/run_all_ldpc_tests | 26 + .../stm32/unittest/scripts/run_all_ofdm_tests | 32 + .../unittest/scripts/run_all_stm32_tests | 25 + .../stm32/unittest/scripts/run_stm32_prog | 91 + .../stm32/unittest/scripts/run_stm32_tst | 53 + .../unittest/scripts/run_tests_common.sh | 79 + .../codec2/stm32/unittest/scripts/setup.sh | 19 + .../stm32/unittest/scripts/stm_stderr.txt | 2 + .../stm32/unittest/scripts/stm_stdout.txt | 1 + .../stm32/unittest/scripts/sum_profiles | 45 + .../unittest/scripts/tst_api_demod_check | 214 + .../unittest/scripts/tst_api_demod_setup | 151 + .../stm32/unittest/scripts/tst_api_mod_check | 115 + .../stm32/unittest/scripts/tst_api_mod_setup | 86 + .../unittest/scripts/tst_codec2_dec_check | 43 + .../unittest/scripts/tst_codec2_dec_setup | 54 + .../unittest/scripts/tst_codec2_enc_check | 59 + .../unittest/scripts/tst_codec2_enc_setup | 54 + .../stm32/unittest/scripts/tst_ldpc_dec_check | 71 + .../stm32/unittest/scripts/tst_ldpc_dec_setup | 50 + .../stm32/unittest/scripts/tst_ldpc_enc_check | 68 + .../stm32/unittest/scripts/tst_ldpc_enc_setup | 39 + .../unittest/scripts/tst_ofdm_demod_check | 503 + .../unittest/scripts/tst_ofdm_demod_setup | 100 + .../stm32/unittest/scripts/tst_ofdm_mod_check | 65 + .../stm32/unittest/scripts/tst_ofdm_mod_setup | 42 + .../codec2/stm32/unittest/src/CMakeLists.txt | 161 + .../codec2/stm32/unittest/src/Makefile | 745 + third_party/codec2/stm32/unittest/src/init.c | 10 + .../codec2/stm32/unittest/src/semihosting.c | 19 + .../codec2/stm32/unittest/src/semihosting.h | 7 + .../stm32/unittest/src/startup_stm32f4xx.s | 529 + .../codec2/stm32/unittest/src/tst_api_demod.c | 235 + .../unittest/src/tst_api_demod_700d_profile.c | 136 + .../codec2/stm32/unittest/src/tst_api_mod.c | 274 + .../unittest/src/tst_api_mod_700d_profile.c | 167 + .../codec2/stm32/unittest/src/tst_api_tx.c | 94 + .../stm32/unittest/src/tst_codec2_dec.c | 164 + .../stm32/unittest/src/tst_codec2_enc.c | 172 + .../stm32/unittest/src/tst_codec2_fft_init.c | 104 + .../codec2/stm32/unittest/src/tst_ldpc_dec.c | 204 + .../codec2/stm32/unittest/src/tst_ldpc_enc.c | 114 + .../stm32/unittest/src/tst_ofdm_demod.c | 438 + .../codec2/stm32/unittest/src/tst_ofdm_mod.c | 248 + .../codec2/stm32/unittest/src/tst_semihost.c | 91 + third_party/codec2/stm32/usb_conf/usb_bsp.c | 337 + third_party/codec2/stm32/usb_conf/usb_bsp.h | 97 + third_party/codec2/stm32/usb_conf/usb_conf.h | 287 + third_party/codec2/stm32/usb_conf/usbd_conf.h | 97 + third_party/codec2/stm32/usb_conf/usbd_desc.c | 324 + third_party/codec2/stm32/usb_conf/usbd_desc.h | 114 + third_party/codec2/stm32/usb_conf/usbd_usr.c | 126 + .../codec2/stm32/usb_lib/cdc/usbd_cdc_core.c | 811 + .../codec2/stm32/usb_lib/cdc/usbd_cdc_core.h | 137 + .../codec2/stm32/usb_lib/cdc/usbd_cdc_vcp.c | 280 + .../codec2/stm32/usb_lib/cdc/usbd_cdc_vcp.h | 68 + .../codec2/stm32/usb_lib/core/usbd_core.c | 476 + .../codec2/stm32/usb_lib/core/usbd_core.h | 114 + .../codec2/stm32/usb_lib/core/usbd_def.h | 149 + .../codec2/stm32/usb_lib/core/usbd_ioreq.c | 237 + .../codec2/stm32/usb_lib/core/usbd_ioreq.h | 115 + .../codec2/stm32/usb_lib/core/usbd_req.c | 868 + .../codec2/stm32/usb_lib/core/usbd_req.h | 102 + .../codec2/stm32/usb_lib/core/usbd_usr.h | 135 + .../codec2/stm32/usb_lib/otg/usb_core.c | 2187 ++ .../codec2/stm32/usb_lib/otg/usb_core.h | 408 + .../codec2/stm32/usb_lib/otg/usb_dcd.c | 475 + .../codec2/stm32/usb_lib/otg/usb_dcd.h | 158 + .../codec2/stm32/usb_lib/otg/usb_dcd_int.c | 889 + .../codec2/stm32/usb_lib/otg/usb_dcd_int.h | 121 + .../codec2/stm32/usb_lib/otg/usb_defines.h | 244 + .../codec2/stm32/usb_lib/otg/usb_regs.h | 1206 + third_party/codec2/unittest/CMakeLists.txt | 123 + third_party/codec2/unittest/check_comp.sh | 29 + third_party/codec2/unittest/check_peak.sh | 58 + .../codec2/unittest/check_real_comp.sh | 15 + third_party/codec2/unittest/compare_floats.c | 87 + third_party/codec2/unittest/compare_ints.c | 160 + third_party/codec2/unittest/fading_files.sh | 14 + .../codec2/unittest/freedv_700d_comprx.c | 140 + .../codec2/unittest/freedv_700d_comptx.c | 44 + third_party/codec2/unittest/hts1a.h | 890 + third_party/codec2/unittest/mksine.c | 54 + third_party/codec2/unittest/ofdm_check | 68 + third_party/codec2/unittest/ofdm_fade.sh | 12 + .../codec2/unittest/ofdm_phase_est_bw.sh | 24 + third_party/codec2/unittest/ofdm_time_sync.sh | 30 + .../codec2/unittest/raw_data_curves/Makefile | 149 + .../unittest/raw_data_curves/snr_curves.sh | 191 + .../codec2/unittest/reliable_text_fade.sh | 27 + third_party/codec2/unittest/sum_debug_alloc | 79 + third_party/codec2/unittest/t16_8.c | 99 + third_party/codec2/unittest/t16_8_short.c | 90 + third_party/codec2/unittest/t48_8.c | 104 + third_party/codec2/unittest/t48_8_short.c | 81 + third_party/codec2/unittest/tcohpsk.c | 324 + third_party/codec2/unittest/tesno_est.c | 32 + third_party/codec2/unittest/test_700c_eq.sh | 12 + third_party/codec2/unittest/tfdmdv.c | 319 + third_party/codec2/unittest/tfifo.c | 103 + third_party/codec2/unittest/tfmfsk.c | 200 + .../codec2/unittest/tfreedv_2400A_rawdata.c | 113 + .../codec2/unittest/tfreedv_2400B_rawdata.c | 113 + .../codec2/unittest/tfreedv_800XA_rawdata.c | 147 + .../codec2/unittest/tfreedv_data_channel.c | 282 + third_party/codec2/unittest/tfsk.c | 234 + third_party/codec2/unittest/tfsk_llr.c | 60 + third_party/codec2/unittest/thash.c | 18 + third_party/codec2/unittest/tnewamp1.c | 295 + third_party/codec2/unittest/tofdm.c | 626 + third_party/codec2/unittest/tofdm_acq.c | 94 + third_party/codec2/unittest/tqam16.c | 35 + third_party/codec2/unittest/tquisk_filter.c | 46 + third_party/codec2/unittest/tvq_mbest.c | 32 + third_party/codec2/unittest/vq_mbest.c | 300 + third_party/codec2/wav/david4.wav | Bin 0 -> 480044 bytes third_party/codec2/wav/vk2tpm_004.wav | Bin 0 -> 560044 bytes third_party/codec2/wav/wia_16kHz.wav | Bin 0 -> 32044 bytes .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 589 files changed, 181780 insertions(+), 569 deletions(-) create mode 100644 android/app/src/main/cpp/CMakeLists.txt create mode 100644 android/app/src/main/cpp/codec2_sources.cmake create mode 100644 ios/Podfile create mode 100644 lib/screens/map_cache_screen.dart create mode 100644 lib/services/background_service.dart create mode 100644 lib/services/codec2_ffi.dart create mode 100644 lib/services/map_tile_cache_service.dart create mode 100644 lib/services/voice_message_service.dart create mode 100644 lib/utils/contact_search.dart create mode 100644 lib/utils/route_transitions.dart create mode 100644 lib/widgets/quick_switch_bar.dart create mode 100644 lib/widgets/voice_message.dart create mode 100644 third_party/codec2/.clang-format create mode 100644 third_party/codec2/.github/workflows/cmake-sm1000.yml create mode 100644 third_party/codec2/.github/workflows/cmake.yml create mode 100644 third_party/codec2/.gitignore create mode 100644 third_party/codec2/CMakeLists.txt create mode 100644 third_party/codec2/COPYING create mode 100644 third_party/codec2/README.md create mode 100644 third_party/codec2/README_cohpsk.md create mode 100644 third_party/codec2/README_data.md create mode 100644 third_party/codec2/README_fdmdv.md create mode 100644 third_party/codec2/README_freedv.md create mode 100644 third_party/codec2/README_fsk.md create mode 100644 third_party/codec2/README_ofdm.md create mode 100644 third_party/codec2/cmake/GetDependencies.cmake.in create mode 100644 third_party/codec2/cmake/config.h.in create mode 100644 third_party/codec2/cmake/version.h.in create mode 100644 third_party/codec2/codec2.pc.in create mode 100644 third_party/codec2/codec2.podspec create mode 100644 third_party/codec2/demo/CMakeLists.txt create mode 100644 third_party/codec2/demo/c2demo.c create mode 100644 third_party/codec2/demo/freedv_700d_rx.c create mode 100755 third_party/codec2/demo/freedv_700d_rx.py create mode 100644 third_party/codec2/demo/freedv_700d_tx.c create mode 100644 third_party/codec2/demo/freedv_datac0c1_rx.c create mode 100644 third_party/codec2/demo/freedv_datac0c1_tx.c create mode 100644 third_party/codec2/demo/freedv_datac1_rx.c create mode 100644 third_party/codec2/demo/freedv_datac1_tx.c create mode 100644 third_party/codec2/doc/Makefile create mode 100644 third_party/codec2/doc/c_tx_comp.png create mode 100644 third_party/codec2/doc/c_tx_comp_thruput.png create mode 100644 third_party/codec2/doc/codec2.pdf create mode 100644 third_party/codec2/doc/codec2.tex create mode 100644 third_party/codec2/doc/codec2_refs.bib create mode 100644 third_party/codec2/doc/fsk_modem_ber_8000_100.png create mode 100644 third_party/codec2/doc/lockdown_3s.wav create mode 100644 third_party/codec2/doc/modem_codec_frame_design.ods create mode 100644 third_party/codec2/doc/pre_post_amble_mpp.png create mode 100644 third_party/codec2/doc/ratek_mel_fhz.png create mode 100644 third_party/codec2/doc/snrest_snr_ctx.png create mode 100644 third_party/codec2/doc/snrest_snr_ctxc.png create mode 100644 third_party/codec2/doc/test_datac1_006_scatter.png create mode 100644 third_party/codec2/doc/test_datac1_006_spectrogram.png create mode 100644 third_party/codec2/doc/warp_fhz_k.png create mode 100644 third_party/codec2/doc/wenet_image.jpg create mode 100644 third_party/codec2/doc/wenet_spectrum_3d.png create mode 100644 third_party/codec2/include/codec2/version.h create mode 100644 third_party/codec2/include/config.h create mode 100644 third_party/codec2/octave/H2064_516_sparse.mat create mode 100644 third_party/codec2/octave/HRA_112_112.txt create mode 100644 third_party/codec2/octave/HRA_112_56.txt create mode 100644 third_party/codec2/octave/HRA_504_396.txt create mode 100644 third_party/codec2/octave/HRA_56_28.txt create mode 100644 third_party/codec2/octave/HRA_56_56.txt create mode 100644 third_party/codec2/octave/HRAa_1536_512.mat create mode 100644 third_party/codec2/octave/H_1024_2048_4f.mat create mode 100644 third_party/codec2/octave/H_128_256_5.mat create mode 100644 third_party/codec2/octave/H_256_512_4.mat create mode 100644 third_party/codec2/octave/H_256_768_22.txt create mode 100644 third_party/codec2/octave/H_4096_8192_3d.mat create mode 100644 third_party/codec2/octave/Mat2Hrows.m create mode 100644 third_party/codec2/octave/autotest.m create mode 100644 third_party/codec2/octave/ch_fading.m create mode 100644 third_party/codec2/octave/channel_lib.m create mode 100644 third_party/codec2/octave/cohpsk_demod_plot.m create mode 100644 third_party/codec2/octave/cohpsk_dev.m create mode 100644 third_party/codec2/octave/cohpsk_lib.m create mode 100644 third_party/codec2/octave/crc16.m create mode 100644 third_party/codec2/octave/diff_fft_mag.m create mode 100644 third_party/codec2/octave/doppler_spread.m create mode 100644 third_party/codec2/octave/esno_est.m create mode 100644 third_party/codec2/octave/fdmdv.m create mode 100644 third_party/codec2/octave/fdmdv_common.m create mode 100644 third_party/codec2/octave/fdmdv_demod.m create mode 100644 third_party/codec2/octave/fdmdv_demod_c.m create mode 100644 third_party/codec2/octave/fdmdv_demod_coh.m create mode 100644 third_party/codec2/octave/fdmdv_mod.m create mode 100644 third_party/codec2/octave/fdmdv_ut.m create mode 100644 third_party/codec2/octave/fsk_demod_file.m create mode 100644 third_party/codec2/octave/fsk_horus.m create mode 100644 third_party/codec2/octave/fsk_lib.m create mode 100644 third_party/codec2/octave/fsk_lib_demo.m create mode 100644 third_party/codec2/octave/fsk_lib_ldpc.m create mode 100644 third_party/codec2/octave/fsk_lib_ldpc_demo.m create mode 100644 third_party/codec2/octave/gen_rn_coeffs.m create mode 100644 third_party/codec2/octave/gp_interleaver.m create mode 100644 third_party/codec2/octave/h0p25d.mat create mode 100644 third_party/codec2/octave/horus_high_speed.bin create mode 100644 third_party/codec2/octave/horus_payload_rtty.txt create mode 100644 third_party/codec2/octave/horus_tx_bits_binary.txt create mode 100644 third_party/codec2/octave/ldpc.m create mode 100644 third_party/codec2/octave/ldpc_fsk_lib.m create mode 100644 third_party/codec2/octave/ldpcut.m create mode 100644 third_party/codec2/octave/linreg.m create mode 100644 third_party/codec2/octave/load_raw.m create mode 100644 third_party/codec2/octave/mag_to_phase.m create mode 100644 third_party/codec2/octave/melvq.m create mode 100644 third_party/codec2/octave/newamp_700c.m create mode 100644 third_party/codec2/octave/ofdm_acquisition.m create mode 100644 third_party/codec2/octave/ofdm_demod_c.m create mode 100644 third_party/codec2/octave/ofdm_helper.m create mode 100644 third_party/codec2/octave/ofdm_ldpc_rx.m create mode 100644 third_party/codec2/octave/ofdm_ldpc_tx.m create mode 100644 third_party/codec2/octave/ofdm_lib.m create mode 100644 third_party/codec2/octave/ofdm_load_const.m create mode 100644 third_party/codec2/octave/ofdm_mode.m create mode 100644 third_party/codec2/octave/ofdm_rx.m create mode 100644 third_party/codec2/octave/ofdm_state.m create mode 100644 third_party/codec2/octave/ofdm_time_sync.m create mode 100644 third_party/codec2/octave/ofdm_tx.m create mode 100644 third_party/codec2/octave/plamp.m create mode 100644 third_party/codec2/octave/plot_fsk_demod_stats.py create mode 100644 third_party/codec2/octave/plot_specgram.m create mode 100644 third_party/codec2/octave/qam16.m create mode 100644 third_party/codec2/octave/qpsk.m create mode 100644 third_party/codec2/octave/sample_clock_offset.m create mode 100644 third_party/codec2/octave/snr_curves_plot.m create mode 100644 third_party/codec2/octave/spec.m create mode 100644 third_party/codec2/octave/tcohpsk.m create mode 100644 third_party/codec2/octave/tfdmdv.m create mode 100644 third_party/codec2/octave/tfmfsk.m create mode 100644 third_party/codec2/octave/tfsk.m create mode 100644 third_party/codec2/octave/tnewamp1.m create mode 100644 third_party/codec2/octave/tofdm.m create mode 100644 third_party/codec2/octave/tofdm_acq.m create mode 100644 third_party/codec2/octave/train_120_1.txt create mode 100644 third_party/codec2/octave/train_120_2.txt create mode 100644 third_party/codec2/raw/hts1.raw create mode 100644 third_party/codec2/raw/hts1a.raw create mode 100644 third_party/codec2/raw/hts2a.raw create mode 100644 third_party/codec2/raw/kristoff.raw create mode 100644 third_party/codec2/raw/testframes_700d.raw create mode 100644 third_party/codec2/raw/ve9qrp.raw create mode 100644 third_party/codec2/raw/ve9qrp_10s.raw create mode 100644 third_party/codec2/src/CMakeLists.txt create mode 100644 third_party/codec2/src/H2064_516_sparse_test.h create mode 100644 third_party/codec2/src/HRA_112_112.c create mode 100644 third_party/codec2/src/HRA_112_112.h create mode 100644 third_party/codec2/src/HRA_112_112_test.h create mode 100644 third_party/codec2/src/HRA_56_56.c create mode 100644 third_party/codec2/src/HRA_56_56.h create mode 100644 third_party/codec2/src/HRAa_1536_512.c create mode 100644 third_party/codec2/src/HRAa_1536_512.h create mode 100644 third_party/codec2/src/HRAb_396_504.c create mode 100644 third_party/codec2/src/HRAb_396_504.h create mode 100644 third_party/codec2/src/H_1024_2048_4f.c create mode 100644 third_party/codec2/src/H_1024_2048_4f.h create mode 100644 third_party/codec2/src/H_128_256_5.c create mode 100644 third_party/codec2/src/H_128_256_5.h create mode 100644 third_party/codec2/src/H_16200_9720.c create mode 100644 third_party/codec2/src/H_16200_9720.h create mode 100644 third_party/codec2/src/H_2064_516_sparse.c create mode 100644 third_party/codec2/src/H_2064_516_sparse.h create mode 100644 third_party/codec2/src/H_212_158.c create mode 100644 third_party/codec2/src/H_212_158.h create mode 100644 third_party/codec2/src/H_256_512_4.c create mode 100644 third_party/codec2/src/H_256_512_4.h create mode 100644 third_party/codec2/src/H_256_768_22.c create mode 100644 third_party/codec2/src/H_256_768_22.h create mode 100644 third_party/codec2/src/H_4096_8192_3d.c create mode 100644 third_party/codec2/src/H_4096_8192_3d.h create mode 100644 third_party/codec2/src/_kiss_fft_guts.h create mode 100644 third_party/codec2/src/bpf.h create mode 100644 third_party/codec2/src/bpfb.h create mode 100644 third_party/codec2/src/c2dec.c create mode 100644 third_party/codec2/src/c2enc.c create mode 100644 third_party/codec2/src/c2file.h create mode 100644 third_party/codec2/src/c2sim.c create mode 100644 third_party/codec2/src/ch.c create mode 100644 third_party/codec2/src/codebook.c create mode 100644 third_party/codec2/src/codebook/codes_450.txt create mode 100644 third_party/codec2/src/codebook/dlsp1.txt create mode 100644 third_party/codec2/src/codebook/dlsp10.txt create mode 100644 third_party/codec2/src/codebook/dlsp2.txt create mode 100644 third_party/codec2/src/codebook/dlsp3.txt create mode 100644 third_party/codec2/src/codebook/dlsp4.txt create mode 100644 third_party/codec2/src/codebook/dlsp5.txt create mode 100644 third_party/codec2/src/codebook/dlsp6.txt create mode 100644 third_party/codec2/src/codebook/dlsp7.txt create mode 100644 third_party/codec2/src/codebook/dlsp8.txt create mode 100644 third_party/codec2/src/codebook/dlsp9.txt create mode 100644 third_party/codec2/src/codebook/gecb.txt create mode 100644 third_party/codec2/src/codebook/lsp1.txt create mode 100644 third_party/codec2/src/codebook/lsp10.txt create mode 100644 third_party/codec2/src/codebook/lsp2.txt create mode 100644 third_party/codec2/src/codebook/lsp3.txt create mode 100644 third_party/codec2/src/codebook/lsp4.txt create mode 100644 third_party/codec2/src/codebook/lsp5.txt create mode 100644 third_party/codec2/src/codebook/lsp6.txt create mode 100644 third_party/codec2/src/codebook/lsp7.txt create mode 100644 third_party/codec2/src/codebook/lsp8.txt create mode 100644 third_party/codec2/src/codebook/lsp8910.txt create mode 100644 third_party/codec2/src/codebook/lsp9.txt create mode 100644 third_party/codec2/src/codebook/lspjmv1.txt create mode 100644 third_party/codec2/src/codebook/lspjmv2.txt create mode 100644 third_party/codec2/src/codebook/lspjmv3.txt create mode 100644 third_party/codec2/src/codebook/lspvqexp1.txt create mode 100644 third_party/codec2/src/codebook/lspvqexp2.txt create mode 100644 third_party/codec2/src/codebook/lspvqexp3.txt create mode 100644 third_party/codec2/src/codebook/newamp1_energy_q.txt create mode 100644 third_party/codec2/src/codebook/newamp2_energy_q.txt create mode 100644 third_party/codec2/src/codebook/train_120_1.txt create mode 100644 third_party/codec2/src/codebook/train_120_2.txt create mode 100644 third_party/codec2/src/codebookd.c create mode 100644 third_party/codec2/src/codebookge.c create mode 100644 third_party/codec2/src/codebookjmv.c create mode 100644 third_party/codec2/src/codebooknewamp1.c create mode 100644 third_party/codec2/src/codebooknewamp1_energy.c create mode 100644 third_party/codec2/src/codebooknewamp2.c create mode 100644 third_party/codec2/src/codebooknewamp2_energy.c create mode 100644 third_party/codec2/src/codec2.c create mode 100644 third_party/codec2/src/codec2.h create mode 100644 third_party/codec2/src/codec2_cohpsk.h create mode 100644 third_party/codec2/src/codec2_fdmdv.h create mode 100644 third_party/codec2/src/codec2_fft.c create mode 100644 third_party/codec2/src/codec2_fft.h create mode 100644 third_party/codec2/src/codec2_fifo.c create mode 100644 third_party/codec2/src/codec2_fifo.h create mode 100644 third_party/codec2/src/codec2_fm.h create mode 100644 third_party/codec2/src/codec2_internal.h create mode 100644 third_party/codec2/src/codec2_math.h create mode 100644 third_party/codec2/src/codec2_math_arm.c create mode 100644 third_party/codec2/src/codec2_ofdm.h create mode 100644 third_party/codec2/src/cohpsk.c create mode 100644 third_party/codec2/src/cohpsk_defs.h create mode 100644 third_party/codec2/src/cohpsk_demod.c create mode 100644 third_party/codec2/src/cohpsk_get_test_bits.c create mode 100644 third_party/codec2/src/cohpsk_internal.h create mode 100644 third_party/codec2/src/cohpsk_mod.c create mode 100644 third_party/codec2/src/cohpsk_put_test_bits.c create mode 100644 third_party/codec2/src/comp.h create mode 100644 third_party/codec2/src/comp_prim.h create mode 100644 third_party/codec2/src/debug_alloc.h create mode 100644 third_party/codec2/src/defines.h create mode 100644 third_party/codec2/src/deframer.c create mode 100644 third_party/codec2/src/dump.c create mode 100644 third_party/codec2/src/dump.h create mode 100644 third_party/codec2/src/fdmdv.c create mode 100644 third_party/codec2/src/fdmdv_demod.c create mode 100644 third_party/codec2/src/fdmdv_get_test_bits.c create mode 100644 third_party/codec2/src/fdmdv_internal.h create mode 100644 third_party/codec2/src/fdmdv_mod.c create mode 100644 third_party/codec2/src/fdmdv_put_test_bits.c create mode 100644 third_party/codec2/src/filter.c create mode 100644 third_party/codec2/src/filter.h create mode 100644 third_party/codec2/src/filter_coef.h create mode 100644 third_party/codec2/src/fm.c create mode 100644 third_party/codec2/src/fm_fir_coeff.h create mode 100644 third_party/codec2/src/fmfsk.c create mode 100644 third_party/codec2/src/fmfsk.h create mode 100644 third_party/codec2/src/framer.c create mode 100644 third_party/codec2/src/freedv_1600.c create mode 100644 third_party/codec2/src/freedv_2020.c create mode 100644 third_party/codec2/src/freedv_700.c create mode 100644 third_party/codec2/src/freedv_api.c create mode 100644 third_party/codec2/src/freedv_api.h create mode 100644 third_party/codec2/src/freedv_api_internal.h create mode 100644 third_party/codec2/src/freedv_data_channel.c create mode 100644 third_party/codec2/src/freedv_data_channel.h create mode 100644 third_party/codec2/src/freedv_data_raw_rx.c create mode 100644 third_party/codec2/src/freedv_data_raw_tx.c create mode 100644 third_party/codec2/src/freedv_data_rx.c create mode 100644 third_party/codec2/src/freedv_data_tx.c create mode 100644 third_party/codec2/src/freedv_fsk.c create mode 100644 third_party/codec2/src/freedv_mixed_rx.c create mode 100644 third_party/codec2/src/freedv_mixed_tx.c create mode 100644 third_party/codec2/src/freedv_rx.c create mode 100644 third_party/codec2/src/freedv_tx.c create mode 100644 third_party/codec2/src/freedv_vhf_framing.c create mode 100644 third_party/codec2/src/freedv_vhf_framing.h create mode 100644 third_party/codec2/src/fsk.c create mode 100644 third_party/codec2/src/fsk.h create mode 100644 third_party/codec2/src/fsk_demod.c create mode 100644 third_party/codec2/src/fsk_get_test_bits.c create mode 100644 third_party/codec2/src/fsk_mod.c create mode 100644 third_party/codec2/src/fsk_put_test_bits.c create mode 100644 third_party/codec2/src/generate_codebook.c create mode 100644 third_party/codec2/src/golay23.c create mode 100644 third_party/codec2/src/golay23.h create mode 100644 third_party/codec2/src/golaydectable.h create mode 100644 third_party/codec2/src/golayenctable.h create mode 100644 third_party/codec2/src/gp_interleaver.c create mode 100644 third_party/codec2/src/gp_interleaver.h create mode 100644 third_party/codec2/src/hanning.h create mode 100644 third_party/codec2/src/ht_coeff.h create mode 100644 third_party/codec2/src/interldpc.c create mode 100644 third_party/codec2/src/interldpc.h create mode 100644 third_party/codec2/src/interp.c create mode 100644 third_party/codec2/src/interp.h create mode 100644 third_party/codec2/src/kiss_fft.c create mode 100644 third_party/codec2/src/kiss_fft.h create mode 100644 third_party/codec2/src/kiss_fftr.c create mode 100644 third_party/codec2/src/kiss_fftr.h create mode 100644 third_party/codec2/src/ldpc_codes.c create mode 100644 third_party/codec2/src/ldpc_codes.h create mode 100644 third_party/codec2/src/ldpc_dec.c create mode 100644 third_party/codec2/src/ldpc_dec_test.c create mode 100644 third_party/codec2/src/ldpc_enc.c create mode 100644 third_party/codec2/src/ldpc_enc_test.c create mode 100644 third_party/codec2/src/ldpc_noise.c create mode 100644 third_party/codec2/src/linreg.c create mode 100644 third_party/codec2/src/linreg.h create mode 100644 third_party/codec2/src/lpc.c create mode 100644 third_party/codec2/src/lpc.h create mode 100644 third_party/codec2/src/lpcnet_freq.c create mode 100644 third_party/codec2/src/lpcnet_freq.h create mode 100644 third_party/codec2/src/lsp.c create mode 100644 third_party/codec2/src/lsp.h create mode 100644 third_party/codec2/src/machdep.h create mode 100644 third_party/codec2/src/mbest.c create mode 100644 third_party/codec2/src/mbest.h create mode 100644 third_party/codec2/src/modem_probe.c create mode 100644 third_party/codec2/src/modem_probe.h create mode 100644 third_party/codec2/src/modem_stats.c create mode 100644 third_party/codec2/src/modem_stats.h create mode 100644 third_party/codec2/src/mpdecode_core.c create mode 100644 third_party/codec2/src/mpdecode_core.h create mode 100644 third_party/codec2/src/newamp1.c create mode 100644 third_party/codec2/src/newamp1.h create mode 100644 third_party/codec2/src/nlp.c create mode 100644 third_party/codec2/src/nlp.h create mode 100644 third_party/codec2/src/noise_samples.h create mode 100644 third_party/codec2/src/octave.c create mode 100644 third_party/codec2/src/octave.h create mode 100644 third_party/codec2/src/ofdm.c create mode 100644 third_party/codec2/src/ofdm_demod.c create mode 100644 third_party/codec2/src/ofdm_get_test_bits.c create mode 100644 third_party/codec2/src/ofdm_internal.h create mode 100644 third_party/codec2/src/ofdm_mod.c create mode 100644 third_party/codec2/src/ofdm_mode.c create mode 100644 third_party/codec2/src/ofdm_put_test_bits.c create mode 100644 third_party/codec2/src/optparse.h create mode 100644 third_party/codec2/src/os.h create mode 100644 third_party/codec2/src/pack.c create mode 100644 third_party/codec2/src/phase.c create mode 100644 third_party/codec2/src/phase.h create mode 100644 third_party/codec2/src/phi0.c create mode 100644 third_party/codec2/src/phi0.h create mode 100644 third_party/codec2/src/pilot_coeff.h create mode 100644 third_party/codec2/src/pilots_coh.h create mode 100644 third_party/codec2/src/postfilter.c create mode 100644 third_party/codec2/src/postfilter.h create mode 100644 third_party/codec2/src/quantise.c create mode 100644 third_party/codec2/src/quantise.h create mode 100644 third_party/codec2/src/reliable_text.c create mode 100644 third_party/codec2/src/reliable_text.h create mode 100644 third_party/codec2/src/rn.h create mode 100644 third_party/codec2/src/rn_coh.h create mode 100644 third_party/codec2/src/rxdec_coeff.h create mode 100644 third_party/codec2/src/sd.c create mode 100644 third_party/codec2/src/sd.h create mode 100644 third_party/codec2/src/sine.c create mode 100644 third_party/codec2/src/sine.h create mode 100644 third_party/codec2/src/ssbfilt_coeff.h create mode 100644 third_party/codec2/src/test_bits.h create mode 100644 third_party/codec2/src/test_bits_coh.h create mode 100644 third_party/codec2/src/test_bits_ofdm.h create mode 100644 third_party/codec2/src/tollr.c create mode 100644 third_party/codec2/src/varicode.c create mode 100644 third_party/codec2/src/varicode.h create mode 100644 third_party/codec2/src/varicode_table.h create mode 100644 third_party/codec2/src/vhf_deframe_c2.c create mode 100644 third_party/codec2/src/vhf_frame_c2.c create mode 100644 third_party/codec2/src/wval.h create mode 100644 third_party/codec2/stm32/CMakeLists.txt create mode 100644 third_party/codec2/stm32/README.md create mode 100644 third_party/codec2/stm32/cmake/STM32_Lib.cmake create mode 100644 third_party/codec2/stm32/cmake/STM32_Toolchain.cmake create mode 100644 third_party/codec2/stm32/cmake/arm_header.cmake create mode 100644 third_party/codec2/stm32/cmake/gencodebooks.cmake create mode 100644 third_party/codec2/stm32/doc/3dot5mm_cable_config.png create mode 100644 third_party/codec2/stm32/doc/sm1000_cn12.png create mode 100644 third_party/codec2/stm32/doc/sm1000_cn12_rev2.odg create mode 100644 third_party/codec2/stm32/doc/sm1000_cn12_rev2.png create mode 100644 third_party/codec2/stm32/doc/sm1000_cn4_cn12.jpg create mode 100644 third_party/codec2/stm32/doc/sm1000_enc_sm.jpg create mode 100644 third_party/codec2/stm32/doc/sm1000_manual.md create mode 100644 third_party/codec2/stm32/inc/debugblinky.h create mode 100644 third_party/codec2/stm32/inc/memtools.h create mode 100644 third_party/codec2/stm32/inc/menu.h create mode 100644 third_party/codec2/stm32/inc/morse.h create mode 100644 third_party/codec2/stm32/inc/sfx.h create mode 100644 third_party/codec2/stm32/inc/sm1000_leds_switches.h create mode 100644 third_party/codec2/stm32/inc/sounds.h create mode 100644 third_party/codec2/stm32/inc/stm32f4_adc.h create mode 100644 third_party/codec2/stm32/inc/stm32f4_dac.h create mode 100644 third_party/codec2/stm32/inc/stm32f4_usart.h create mode 100644 third_party/codec2/stm32/inc/stm32f4_usb_vcp.h create mode 100644 third_party/codec2/stm32/inc/stm32f4_vrom.h create mode 100644 third_party/codec2/stm32/inc/stm32f4xx_conf.h create mode 100644 third_party/codec2/stm32/inc/tone.h create mode 100644 third_party/codec2/stm32/inc/tot.h create mode 100644 third_party/codec2/stm32/src/adc_rec_usb.c create mode 100644 third_party/codec2/stm32/src/dac_ut.c create mode 100644 third_party/codec2/stm32/src/debugblinky.c create mode 100644 third_party/codec2/stm32/src/memtools.c create mode 100644 third_party/codec2/stm32/src/menu.c create mode 100644 third_party/codec2/stm32/src/morse.c create mode 100644 third_party/codec2/stm32/src/sfx.c create mode 100644 third_party/codec2/stm32/src/sm1000_leds_switches.c create mode 100644 third_party/codec2/stm32/src/sm1000_leds_switches_ut.c create mode 100644 third_party/codec2/stm32/src/sm1000_main.c create mode 100644 third_party/codec2/stm32/src/sounds.c create mode 100644 third_party/codec2/stm32/src/startup_stm32f4xx.s create mode 100644 third_party/codec2/stm32/src/stm32f4_adc.c create mode 100644 third_party/codec2/stm32/src/stm32f4_dac.c create mode 100644 third_party/codec2/stm32/src/stm32f4_machdep.c create mode 100644 third_party/codec2/stm32/src/stm32f4_usart.c create mode 100644 third_party/codec2/stm32/src/stm32f4_usb_vcp.c create mode 100644 third_party/codec2/stm32/src/stm32f4_vrom.c create mode 100644 third_party/codec2/stm32/src/system_stm32f4xx.c create mode 100644 third_party/codec2/stm32/src/tone.c create mode 100644 third_party/codec2/stm32/src/tot.c create mode 100644 third_party/codec2/stm32/src/usart_ut.c create mode 100644 third_party/codec2/stm32/src/usb_vcp_ut.c create mode 100644 third_party/codec2/stm32/src/usb_vsp_ut.c create mode 100644 third_party/codec2/stm32/stlink/elfsym.c create mode 100644 third_party/codec2/stm32/stlink/elfsym.h create mode 100644 third_party/codec2/stm32/stlink/stlink.patch create mode 100644 third_party/codec2/stm32/stm32_flash.ld create mode 100644 third_party/codec2/stm32/stm32_ram.ld create mode 100644 third_party/codec2/stm32/unittest/README_unittest.md create mode 100644 third_party/codec2/stm32/unittest/lib/octave/ofdm_demod_check.m create mode 100755 third_party/codec2/stm32/unittest/lib/python/sum_profiles.py create mode 100644 third_party/codec2/stm32/unittest/lib/ut_travis.enc create mode 100755 third_party/codec2/stm32/unittest/scripts/check_ram_limit create mode 100755 third_party/codec2/stm32/unittest/scripts/kill_run_stm32_tst create mode 100755 third_party/codec2/stm32/unittest/scripts/plot_ofdm_demod_syms create mode 100755 third_party/codec2/stm32/unittest/scripts/run_all_codec2_tests create mode 100755 third_party/codec2/stm32/unittest/scripts/run_all_ldpc_tests create mode 100755 third_party/codec2/stm32/unittest/scripts/run_all_ofdm_tests create mode 100755 third_party/codec2/stm32/unittest/scripts/run_all_stm32_tests create mode 100755 third_party/codec2/stm32/unittest/scripts/run_stm32_prog create mode 100755 third_party/codec2/stm32/unittest/scripts/run_stm32_tst create mode 100644 third_party/codec2/stm32/unittest/scripts/run_tests_common.sh create mode 100644 third_party/codec2/stm32/unittest/scripts/setup.sh create mode 100644 third_party/codec2/stm32/unittest/scripts/stm_stderr.txt create mode 100644 third_party/codec2/stm32/unittest/scripts/stm_stdout.txt create mode 100644 third_party/codec2/stm32/unittest/scripts/sum_profiles create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_api_demod_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_api_demod_setup create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_api_mod_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_api_mod_setup create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_codec2_dec_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_codec2_dec_setup create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_codec2_enc_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_codec2_enc_setup create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ldpc_dec_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ldpc_dec_setup create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ldpc_enc_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ldpc_enc_setup create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ofdm_demod_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ofdm_demod_setup create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ofdm_mod_check create mode 100755 third_party/codec2/stm32/unittest/scripts/tst_ofdm_mod_setup create mode 100644 third_party/codec2/stm32/unittest/src/CMakeLists.txt create mode 100644 third_party/codec2/stm32/unittest/src/Makefile create mode 100644 third_party/codec2/stm32/unittest/src/init.c create mode 100644 third_party/codec2/stm32/unittest/src/semihosting.c create mode 100644 third_party/codec2/stm32/unittest/src/semihosting.h create mode 100644 third_party/codec2/stm32/unittest/src/startup_stm32f4xx.s create mode 100644 third_party/codec2/stm32/unittest/src/tst_api_demod.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_api_demod_700d_profile.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_api_mod.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_api_mod_700d_profile.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_api_tx.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_codec2_dec.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_codec2_enc.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_codec2_fft_init.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_ldpc_dec.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_ldpc_enc.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_ofdm_demod.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_ofdm_mod.c create mode 100644 third_party/codec2/stm32/unittest/src/tst_semihost.c create mode 100644 third_party/codec2/stm32/usb_conf/usb_bsp.c create mode 100644 third_party/codec2/stm32/usb_conf/usb_bsp.h create mode 100644 third_party/codec2/stm32/usb_conf/usb_conf.h create mode 100644 third_party/codec2/stm32/usb_conf/usbd_conf.h create mode 100644 third_party/codec2/stm32/usb_conf/usbd_desc.c create mode 100644 third_party/codec2/stm32/usb_conf/usbd_desc.h create mode 100644 third_party/codec2/stm32/usb_conf/usbd_usr.c create mode 100644 third_party/codec2/stm32/usb_lib/cdc/usbd_cdc_core.c create mode 100644 third_party/codec2/stm32/usb_lib/cdc/usbd_cdc_core.h create mode 100644 third_party/codec2/stm32/usb_lib/cdc/usbd_cdc_vcp.c create mode 100644 third_party/codec2/stm32/usb_lib/cdc/usbd_cdc_vcp.h create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_core.c create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_core.h create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_def.h create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_ioreq.c create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_ioreq.h create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_req.c create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_req.h create mode 100644 third_party/codec2/stm32/usb_lib/core/usbd_usr.h create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_core.c create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_core.h create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_dcd.c create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_dcd.h create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_dcd_int.c create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_dcd_int.h create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_defines.h create mode 100644 third_party/codec2/stm32/usb_lib/otg/usb_regs.h create mode 100644 third_party/codec2/unittest/CMakeLists.txt create mode 100755 third_party/codec2/unittest/check_comp.sh create mode 100755 third_party/codec2/unittest/check_peak.sh create mode 100755 third_party/codec2/unittest/check_real_comp.sh create mode 100644 third_party/codec2/unittest/compare_floats.c create mode 100644 third_party/codec2/unittest/compare_ints.c create mode 100755 third_party/codec2/unittest/fading_files.sh create mode 100644 third_party/codec2/unittest/freedv_700d_comprx.c create mode 100644 third_party/codec2/unittest/freedv_700d_comptx.c create mode 100644 third_party/codec2/unittest/hts1a.h create mode 100644 third_party/codec2/unittest/mksine.c create mode 100755 third_party/codec2/unittest/ofdm_check create mode 100755 third_party/codec2/unittest/ofdm_fade.sh create mode 100755 third_party/codec2/unittest/ofdm_phase_est_bw.sh create mode 100755 third_party/codec2/unittest/ofdm_time_sync.sh create mode 100644 third_party/codec2/unittest/raw_data_curves/Makefile create mode 100755 third_party/codec2/unittest/raw_data_curves/snr_curves.sh create mode 100755 third_party/codec2/unittest/reliable_text_fade.sh create mode 100755 third_party/codec2/unittest/sum_debug_alloc create mode 100644 third_party/codec2/unittest/t16_8.c create mode 100644 third_party/codec2/unittest/t16_8_short.c create mode 100644 third_party/codec2/unittest/t48_8.c create mode 100644 third_party/codec2/unittest/t48_8_short.c create mode 100644 third_party/codec2/unittest/tcohpsk.c create mode 100644 third_party/codec2/unittest/tesno_est.c create mode 100755 third_party/codec2/unittest/test_700c_eq.sh create mode 100644 third_party/codec2/unittest/tfdmdv.c create mode 100644 third_party/codec2/unittest/tfifo.c create mode 100644 third_party/codec2/unittest/tfmfsk.c create mode 100644 third_party/codec2/unittest/tfreedv_2400A_rawdata.c create mode 100644 third_party/codec2/unittest/tfreedv_2400B_rawdata.c create mode 100644 third_party/codec2/unittest/tfreedv_800XA_rawdata.c create mode 100644 third_party/codec2/unittest/tfreedv_data_channel.c create mode 100644 third_party/codec2/unittest/tfsk.c create mode 100644 third_party/codec2/unittest/tfsk_llr.c create mode 100644 third_party/codec2/unittest/thash.c create mode 100644 third_party/codec2/unittest/tnewamp1.c create mode 100644 third_party/codec2/unittest/tofdm.c create mode 100644 third_party/codec2/unittest/tofdm_acq.c create mode 100644 third_party/codec2/unittest/tqam16.c create mode 100644 third_party/codec2/unittest/tquisk_filter.c create mode 100644 third_party/codec2/unittest/tvq_mbest.c create mode 100644 third_party/codec2/unittest/vq_mbest.c create mode 100644 third_party/codec2/wav/david4.wav create mode 100644 third_party/codec2/wav/vk2tpm_004.wav create mode 100644 third_party/codec2/wav/wia_16kHz.wav diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d9723d2..06f674a 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -29,6 +29,14 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + externalNativeBuild { + cmake { + arguments += listOf("-DANDROID_STL=c++_shared") + } + } + ndk { + abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64") + } } buildTypes { @@ -38,6 +46,12 @@ android { signingConfig = signingConfigs.getByName("debug") } } + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + } + } } flutter { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1f6da2d..646400d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,11 +12,22 @@ + + + + + + + :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('Flutter', 'Generated.xcconfig'), __dir__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first." + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT=(.*)/) + return matches[1].strip if matches + end + raise 'FLUTTER_ROOT not found in Generated.xcconfig. Try deleting Flutter/Generated.xcconfig, then run flutter pub get.' +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + pod 'codec2', :path => '../third_party/codec2' + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 2b505d0..9a2f752 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,9 +45,15 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + bluetooth-central + NSBluetoothAlwaysUsageDescription This app uses Bluetooth to communicate with MeshCore devices. NSBluetoothPeripheralUsageDescription This app uses Bluetooth to communicate with MeshCore devices. + NSMicrophoneUsageDescription + This app needs microphone access to record voice messages. diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 60880df..c22477a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:uuid/uuid.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; @@ -17,7 +19,9 @@ import '../services/ble_debug_log_service.dart'; import '../services/message_retry_service.dart'; import '../services/path_history_service.dart'; import '../services/app_settings_service.dart'; +import '../services/background_service.dart'; import '../services/notification_service.dart'; +import '../services/voice_message_service.dart'; import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; @@ -48,6 +52,10 @@ class MeshCoreConnector extends ChangeNotifier { BluetoothCharacteristic? _txCharacteristic; String? _deviceDisplayName; String? _deviceId; + BluetoothDevice? _lastDevice; + String? _lastDeviceId; + String? _lastDeviceDisplayName; + bool _manualDisconnect = false; final List _scanResults = []; final List _contacts = []; @@ -60,6 +68,8 @@ class MeshCoreConnector extends ChangeNotifier { StreamSubscription? _connectionSubscription; StreamSubscription>? _notifySubscription; Timer? _selfInfoRetryTimer; + Timer? _reconnectTimer; + int _reconnectAttempts = 0; final StreamController _receivedFramesController = StreamController.broadcast(); @@ -93,6 +103,7 @@ class MeshCoreConnector extends ChangeNotifier { MessageRetryService? _retryService; PathHistoryService? _pathHistoryService; AppSettingsService? _appSettingsService; + BackgroundService? _backgroundService; final NotificationService _notificationService = NotificationService(); BleDebugLogService? _bleDebugLogService; final ChannelMessageStore _channelMessageStore = ChannelMessageStore(); @@ -102,6 +113,9 @@ class MeshCoreConnector extends ChangeNotifier { final ContactSettingsStore _contactSettingsStore = ContactSettingsStore(); final ContactStore _contactStore = ContactStore(); final UnreadStore _unreadStore = UnreadStore(); + final VoiceMessageService _voiceMessageService = VoiceMessageService.instance; + final Map _voiceAssemblies = {}; + _VoiceSendSession? _voiceSendSession; final Map _channelSmazEnabled = {}; final Map _contactSmazEnabled = {}; final Set _knownContactKeys = {}; @@ -110,12 +124,23 @@ class MeshCoreConnector extends ChangeNotifier { String? _activeContactKey; int? _activeChannelIndex; List _channelOrder = []; + int _lastVoiceTimestampSeconds = 0; // Getters MeshCoreConnectionState get state => _state; BluetoothDevice? get device => _device; String? get deviceId => _deviceId; String get deviceIdLabel => _deviceId ?? 'Unknown'; + bool get isVoiceSending => _voiceSendSession != null; + + void cancelVoiceSend() { + final session = _voiceSendSession; + if (session == null) return; + session.cancel(); + _voiceSendSession = null; + _updateVoiceMessageStatus(session.messageId, MessageStatus.failed); + notifyListeners(); + } String get deviceDisplayName { if (_selfName != null && _selfName!.isNotEmpty) { return _selfName!; @@ -130,7 +155,15 @@ class MeshCoreConnector extends ChangeNotifier { return 'Unknown Device'; } List get scanResults => List.unmodifiable(_scanResults); - List get contacts => List.unmodifiable(_contacts); + List get contacts { + final selfKey = _selfPublicKey; + if (selfKey == null) { + return List.unmodifiable(_contacts); + } + return List.unmodifiable( + _contacts.where((contact) => !listEquals(contact.publicKey, selfKey)), + ); + } List get channels => List.unmodifiable(_channels); bool get isConnected => _state == MeshCoreConnectionState.connected; bool get isLoadingContacts => _isLoadingContacts; @@ -194,6 +227,12 @@ class MeshCoreConnector extends ChangeNotifier { if (messages == null) return; final removed = messages.remove(message); if (!removed) return; + if (message.isVoice && message.voicePath != null) { + final file = File(message.voicePath!); + if (await file.exists()) { + await file.delete(); + } + } await _messageStore.saveMessages(contactKeyHex, messages); notifyListeners(); } @@ -358,11 +397,13 @@ class MeshCoreConnector extends ChangeNotifier { required PathHistoryService pathHistoryService, AppSettingsService? appSettingsService, BleDebugLogService? bleDebugLogService, + BackgroundService? backgroundService, }) { _retryService = retryService; _pathHistoryService = pathHistoryService; _appSettingsService = appSettingsService; _bleDebugLogService = bleDebugLogService; + _backgroundService = backgroundService; // Initialize notification service _notificationService.initialize(); @@ -461,6 +502,7 @@ class MeshCoreConnector extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: contact.lastSeen, + lastMessageAt: contact.lastMessageAt, ); } @@ -515,6 +557,12 @@ class MeshCoreConnector extends ChangeNotifier { } else if (device.platformName.isNotEmpty) { _deviceDisplayName = device.platformName; } + _lastDevice = device; + _lastDeviceId = _deviceId; + _lastDeviceDisplayName = _deviceDisplayName; + _manualDisconnect = false; + _cancelReconnectTimer(); + unawaited(_backgroundService?.start()); notifyListeners(); try { @@ -565,6 +613,9 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } + // Give the device a moment to be ready for descriptor writes + await Future.delayed(const Duration(milliseconds: 300)); + await _txCharacteristic!.setNotifyValue(true); _notifySubscription = _txCharacteristic!.onValueReceived.listen(_handleFrame); @@ -583,7 +634,7 @@ class MeshCoreConnector extends ChangeNotifier { await syncTime(); } catch (e) { debugPrint("Connection error: $e"); - await disconnect(); + await disconnect(manual: false); rethrow; } } @@ -619,9 +670,58 @@ class MeshCoreConnector extends ChangeNotifier { return result; } - Future disconnect() async { + bool get _shouldAutoReconnect => + !_manualDisconnect && _lastDeviceId != null; + + void _cancelReconnectTimer() { + _reconnectTimer?.cancel(); + _reconnectTimer = null; + _reconnectAttempts = 0; + } + + int _nextReconnectDelayMs() { + final attempt = _reconnectAttempts < 6 ? _reconnectAttempts : 6; + _reconnectAttempts += 1; + final delayMs = 1000 * (1 << attempt); + return delayMs > 30000 ? 30000 : delayMs; + } + + void _scheduleReconnect() { + if (!_shouldAutoReconnect) return; + if (_reconnectTimer?.isActive == true) return; + + final delayMs = _nextReconnectDelayMs(); + _reconnectTimer = Timer(Duration(milliseconds: delayMs), () async { + if (!_shouldAutoReconnect) return; + if (_state == MeshCoreConnectionState.connecting || + _state == MeshCoreConnectionState.connected) { + return; + } + + final device = _lastDevice ?? + (_lastDeviceId == null + ? null + : BluetoothDevice.fromId(_lastDeviceId!)); + if (device == null) return; + + try { + await connect(device, displayName: _lastDeviceDisplayName); + } catch (_) { + _scheduleReconnect(); + } + }); + } + + Future disconnect({bool manual = true}) async { if (_state == MeshCoreConnectionState.disconnecting) return; + if (manual) { + _manualDisconnect = true; + _cancelReconnectTimer(); + unawaited(_backgroundService?.stop()); + } else { + _manualDisconnect = false; + } _setState(MeshCoreConnectionState.disconnecting); await _notifySubscription?.cancel(); @@ -633,7 +733,8 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; try { - await _device?.disconnect(); + // Skip queued BLE operations so disconnect doesn't get stuck behind them. + await _device?.disconnect(queue: false); } catch (e) { debugPrint("Disconnect error: $e"); } @@ -663,6 +764,9 @@ class MeshCoreConnector extends ChangeNotifier { _didInitialQueueSync = false; _setState(MeshCoreConnectionState.disconnected); + if (!manual) { + _scheduleReconnect(); + } } Future sendFrame(Uint8List data) async { @@ -762,6 +866,10 @@ class MeshCoreConnector extends ChangeNotifier { int? customPathLen, }) async { if (!isConnected || text.isEmpty) return; + if (_voiceSendSession != null) { + debugPrint('Voice send in progress, skipping text send.'); + return; + } // If custom path is provided, temporarily update the contact's path if (customPath != null && customPathLen != null && customPathLen >= 0) { @@ -825,6 +933,142 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future sendVoiceMessage({ + required Contact contact, + required Uint8List codec2Bytes, + required String voicePath, + required int durationMs, + int? timestampSeconds, + }) async { + if (!isConnected || codec2Bytes.isEmpty) return; + if (_voiceSendSession != null) return; + + final voiceTimestampSeconds = timestampSeconds ?? _nextVoiceTimestampSeconds(); + final chunks = _voiceMessageService.buildVoiceChunks(codec2Bytes); + if (chunks.isEmpty) return; + + final messageId = const Uuid().v4(); + final message = Message( + senderKey: contact.publicKey, + text: 'Voice message', + timestamp: DateTime.fromMillisecondsSinceEpoch(voiceTimestampSeconds * 1000), + isOutgoing: true, + isCli: false, + status: MessageStatus.pending, + messageId: messageId, + forceFlood: false, + isVoice: true, + voicePath: voicePath, + voiceDurationMs: durationMs, + voiceCodec: VoiceMessageService.codecName, + ); + + _addMessage(contact.publicKeyHex, message); + notifyListeners(); + + final session = _VoiceSendSession( + contact: contact, + messageId: messageId, + chunks: chunks, + timestampSeconds: voiceTimestampSeconds, + ); + _voiceSendSession = session; + notifyListeners(); + + unawaited(_sendVoiceChunks(session)); + } + + int reserveVoiceTimestampSeconds() { + return _nextVoiceTimestampSeconds(); + } + + int _nextVoiceTimestampSeconds() { + final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; + if (nowSeconds <= _lastVoiceTimestampSeconds) { + _lastVoiceTimestampSeconds += 1; + } else { + _lastVoiceTimestampSeconds = nowSeconds; + } + return _lastVoiceTimestampSeconds; + } + + Future _sendVoiceChunks(_VoiceSendSession session) async { + for (var i = 0; i < session.chunks.length; i++) { + if (session.isCancelled) return; + final ok = await _sendVoiceChunk(session, i); + if (!ok) { + if (session.isCancelled) return; + _updateVoiceMessageStatus(session.messageId, MessageStatus.failed); + _voiceSendSession = null; + notifyListeners(); + return; + } + } + if (session.isCancelled) return; + _updateVoiceMessageStatus(session.messageId, MessageStatus.delivered); + _voiceSendSession = null; + notifyListeners(); + } + + Future _sendVoiceChunk(_VoiceSendSession session, int index) async { + if (session.isCancelled) return false; + session.beginChunk(index); + await sendFrame( + buildSendTextMsgFrame( + session.contact.publicKey, + session.chunks[index], + forceFlood: false, + attempt: 0, + timestampSeconds: session.timestampSeconds, + ), + ); + + try { + await session.sentCompleter!.future.timeout(const Duration(seconds: 10)); + } catch (_) { + return false; + } + + final timeoutMs = session.expectedTimeoutMs; + final confirmTimeout = timeoutMs != null && timeoutMs > 0 + ? Duration(milliseconds: timeoutMs) + : const Duration(seconds: 30); + + try { + await session.confirmCompleter!.future.timeout(confirmTimeout); + } catch (_) { + return false; + } + return true; + } + + void _updateVoiceMessageStatus(String messageId, MessageStatus status) { + for (final entry in _conversations.entries) { + final messages = entry.value; + final index = messages.indexWhere((m) => m.messageId == messageId); + if (index == -1) continue; + messages[index] = messages[index].copyWith(status: status); + _messageStore.saveMessages(entry.key, messages); + break; + } + } + + void _handleVoiceMessageSent(Uint8List ackHash, int timeoutMs, {required bool isFlood}) { + final session = _voiceSendSession; + if (session == null) return; + session.handleSent(ackHash, timeoutMs); + if (isFlood) { + // Flooded sends may not emit send-confirmed; unblock voice chunking. + session.handleConfirmed(ackHash); + } + } + + void _handleVoiceSendConfirmed(Uint8List ackHash) { + final session = _voiceSendSession; + if (session == null) return; + session.handleConfirmed(ackHash); + } + Future setContactPath(Contact contact, Uint8List customPath, int pathLen) async { if (!isConnected) return; @@ -839,6 +1083,10 @@ class MeshCoreConnector extends ChangeNotifier { Future sendChannelMessage(Channel channel, String text) async { if (!isConnected || text.isEmpty) return; + if (_voiceSendSession != null) { + debugPrint('Voice send in progress, skipping channel send.'); + return; + } final message = ChannelMessage.outgoing(text, _selfName ?? 'Me', channel.index); _addChannelMessage(channel.index, message); @@ -886,6 +1134,7 @@ class MeshCoreConnector extends ChangeNotifier { latitude: existing.latitude, longitude: existing.longitude, lastSeen: existing.lastSeen, + lastMessageAt: existing.lastMessageAt, ); notifyListeners(); unawaited(_persistContacts()); @@ -1233,7 +1482,13 @@ class MeshCoreConnector extends ChangeNotifier { ); if (existingIndex >= 0) { - _contacts[existingIndex] = contact; + final existing = _contacts[existingIndex]; + final mergedLastMessageAt = existing.lastMessageAt.isAfter(contact.lastMessageAt) + ? existing.lastMessageAt + : contact.lastMessageAt; + _contacts[existingIndex] = contact.copyWith( + lastMessageAt: mergedLastMessageAt, + ); } else { _contacts.add(contact); } @@ -1275,6 +1530,83 @@ class MeshCoreConnector extends ChangeNotifier { return latest; } + bool _setContactLastMessageAt(int index, DateTime timestamp) { + final contact = _contacts[index]; + if (contact.type != advTypeChat) return false; + if (!timestamp.isAfter(contact.lastMessageAt)) return false; + _contacts[index] = contact.copyWith(lastMessageAt: timestamp); + return true; + } + + void _updateContactLastMessageAt( + String contactKeyHex, + DateTime timestamp, { + bool notify = false, + }) { + final index = _contacts.indexWhere((c) => c.publicKeyHex == contactKeyHex); + if (index < 0) return; + if (!_setContactLastMessageAt(index, timestamp)) return; + unawaited(_persistContacts()); + if (notify) { + notifyListeners(); + } + } + + void _updateContactLastMessageAtByName( + String senderName, + DateTime timestamp, { + Uint8List? pathBytes, + bool notify = false, + }) { + final normalized = senderName.trim().toLowerCase(); + final hasName = normalized.isNotEmpty && normalized != 'unknown'; + var updated = false; + var matchedByName = false; + + if (hasName) { + for (var i = 0; i < _contacts.length; i++) { + final contact = _contacts[i]; + if (contact.type != advTypeChat) continue; + if (contact.name.trim().toLowerCase() == normalized) { + matchedByName = true; + updated = _setContactLastMessageAt(i, timestamp) || updated; + } + } + } + + if (!matchedByName && pathBytes != null && pathBytes.isNotEmpty) { + final matches = []; + for (var i = 0; i < _contacts.length; i++) { + final contact = _contacts[i]; + if (contact.type != advTypeChat) continue; + if (_pathMatchesContact(pathBytes, contact.publicKey)) { + matches.add(i); + } + } + if (matches.length == 1) { + updated = _setContactLastMessageAt(matches.first, timestamp) || updated; + } + } + + if (updated) { + unawaited(_persistContacts()); + if (notify) { + notifyListeners(); + } + } + } + + bool _pathMatchesContact(Uint8List pathBytes, Uint8List publicKey) { + if (pathBytes.isEmpty || publicKey.length < pathHashSize) return false; + for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) { + final prefix = pathBytes.sublist(i, i + pathHashSize); + if (_matchesPrefix(publicKey, prefix)) { + return true; + } + } + return false; + } + void _handleIncomingMessage(Uint8List frame) { if (_selfPublicKey == null) return; @@ -1290,6 +1622,12 @@ class MeshCoreConnector extends ChangeNotifier { pathBytes: contact.pathLength < 0 ? Uint8List(0) : contact.path, ); } + if (_tryHandleVoiceChunk(message)) { + return; + } + if (contact != null) { + _updateContactLastMessageAt(contact.publicKeyHex, message.timestamp); + } if (!message.isOutgoing) { final existing = _conversations[message.senderKeyHex]; final incomingTimestamp = message.timestamp.millisecondsSinceEpoch; @@ -1404,22 +1742,179 @@ class MeshCoreConnector extends ChangeNotifier { String _prepareContactOutboundText(Contact contact, String text) { final trimmed = text.trim(); - final isStructuredPayload = trimmed.startsWith('g:') || trimmed.startsWith('m:'); + final isStructuredPayload = + trimmed.startsWith('g:') || trimmed.startsWith('m:') || trimmed.startsWith('V1|'); if (!isStructuredPayload && isContactSmazEnabled(contact.publicKeyHex)) { return Smaz.encodeIfSmaller(text); } return text; } + bool _tryHandleVoiceChunk(Message message) { + if (message.isOutgoing || message.isCli) return false; + final chunk = _voiceMessageService.tryParseChunk(message.text); + if (chunk == null) return false; + _updateContactLastMessageAt( + message.senderKeyHex, + message.timestamp, + notify: true, + ); + final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000; + final key = _voiceAssemblyKey(message.senderKeyHex, timestampSeconds); + final assembly = _voiceAssemblies.putIfAbsent( + key, + () => _VoiceAssembly( + senderKey: message.senderKey, + senderKeyHex: message.senderKeyHex, + timestampSeconds: timestampSeconds, + totalChunks: chunk.count, + ), + ); + if (assembly.totalChunks != chunk.count) { + _voiceAssemblies.remove(key); + return true; + } + assembly.addChunk(chunk); + if (assembly.isComplete) { + _voiceAssemblies.remove(key); + unawaited(_finalizeVoiceAssembly(assembly, message)); + } + _cleanupVoiceAssemblies(); + if (_isSyncingQueuedMessages) { + _handleQueuedMessageReceived(); + } + return true; + } + + String _voiceAssemblyKey(String senderKeyHex, int timestampSeconds) { + return '$senderKeyHex:$timestampSeconds'; + } + + Future _finalizeVoiceAssembly(_VoiceAssembly assembly, Message chunkMessage) async { + final codec2Bytes = assembly.assemble(); + if (codec2Bytes.isEmpty) return; + final existing = _conversations[assembly.senderKeyHex]; + if (existing != null) { + final alreadyAdded = existing.any((message) { + if (!message.isVoice) return false; + final tsSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000; + return tsSeconds == assembly.timestampSeconds; + }); + if (alreadyAdded) return; + } + String? filePath; + int durationMs = 0; + try { + final pcmBytes = _voiceMessageService.decodeCodec2ToPcm(codec2Bytes); + durationMs = _voiceMessageService.durationMsForCodec2Bytes(codec2Bytes); + final fileName = _voiceMessageService.buildVoiceFileName( + senderKeyHex: assembly.senderKeyHex, + timestampSeconds: assembly.timestampSeconds, + ); + filePath = await _voiceMessageService.writeWavFile( + pcmBytes: pcmBytes, + fileName: fileName, + ); + } catch (e) { + debugPrint('Voice decode failed: $e'); + return; + } + + final message = Message( + senderKey: assembly.senderKey, + text: 'Voice message', + timestamp: DateTime.fromMillisecondsSinceEpoch(assembly.timestampSeconds * 1000), + isOutgoing: false, + isCli: false, + status: MessageStatus.delivered, + isVoice: true, + voicePath: filePath, + voiceDurationMs: durationMs, + voiceCodec: VoiceMessageService.codecName, + pathLength: chunkMessage.pathLength, + pathBytes: chunkMessage.pathBytes, + ); + + _addMessage(assembly.senderKeyHex, message); + _maybeMarkActiveContactRead(message); + notifyListeners(); + + if (_appSettingsService != null) { + final settings = _appSettingsService!.settings; + if (settings.notificationsEnabled && settings.notifyOnNewMessage) { + final contact = _contacts.cast().firstWhere( + (c) => c != null && c.publicKeyHex == assembly.senderKeyHex, + orElse: () => null, + ); + _notificationService.showMessageNotification( + contactName: contact?.name ?? 'Unknown', + message: 'Voice message', + contactId: assembly.senderKeyHex, + ); + } + } + } + + void _cleanupVoiceAssemblies() { + if (_voiceAssemblies.isEmpty) return; + final cutoff = DateTime.now().subtract(const Duration(minutes: 3)); + final expiredKeys = []; + for (final entry in _voiceAssemblies.entries) { + if (entry.value.startedAt.isBefore(cutoff)) { + expiredKeys.add(entry.key); + } + } + for (final key in expiredKeys) { + _voiceAssemblies.remove(key); + } + } + + String _channelDisplayName(int channelIndex) { + for (final channel in _channels) { + if (channel.index != channelIndex) continue; + return channel.name.isEmpty ? 'Channel $channelIndex' : channel.name; + } + return 'Channel $channelIndex'; + } + + void _maybeNotifyChannelMessage( + ChannelMessage message, { + String? channelName, + }) { + if (message.isOutgoing || _appSettingsService == null) return; + final channelIndex = message.channelIndex; + if (channelIndex == null) return; + + final settings = _appSettingsService!.settings; + if (!settings.notificationsEnabled || !settings.notifyOnNewChannelMessage) { + return; + } + + final label = channelName ?? _channelDisplayName(channelIndex); + _notificationService.showChannelMessageNotification( + channelName: label, + message: message.text, + channelIndex: channelIndex, + ); + } + void _handleIncomingChannelMessage(Uint8List frame) { final message = ChannelMessage.fromFrame(frame); if (message != null && message.channelIndex != null) { if (_shouldDropSelfChannelMessage(message.senderName, message.pathBytes)) { return; } - _addChannelMessage(message.channelIndex!, message); + _updateContactLastMessageAtByName( + message.senderName, + message.timestamp, + pathBytes: message.pathBytes, + ); + final isNew = _addChannelMessage(message.channelIndex!, message); _maybeMarkActiveChannelRead(message); notifyListeners(); + if (isNew) { + _maybeNotifyChannelMessage(message); + } _handleQueuedMessageReceived(); } else if (_isSyncingQueuedMessages) { _handleQueuedMessageReceived(); @@ -1470,9 +1965,18 @@ class MeshCoreConnector extends ChangeNotifier { channelIndex: channel.index, ); - _addChannelMessage(channel.index, message); + _updateContactLastMessageAtByName( + parsed.senderName, + message.timestamp, + pathBytes: message.pathBytes, + ); + final isNew = _addChannelMessage(channel.index, message); _maybeMarkActiveChannelRead(message); notifyListeners(); + if (isNew) { + final label = channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name; + _maybeNotifyChannelMessage(message, channelName: label); + } return; } } @@ -1484,11 +1988,15 @@ class MeshCoreConnector extends ChangeNotifier { // [2-5] = expected_ack_hash (uint32) // [6-9] = estimated_timeout_ms (uint32) - if (frame.length >= 10 && _retryService != null) { + if (frame.length >= 10) { + final isFlood = frame[1] != 0; final ackHash = Uint8List.fromList(frame.sublist(2, 6)); final timeoutMs = readUint32LE(frame, 6); - _retryService!.updateMessageFromSent(ackHash, timeoutMs); + if (_retryService != null) { + _retryService!.updateMessageFromSent(ackHash, timeoutMs); + } + _handleVoiceMessageSent(ackHash, timeoutMs, isFlood: isFlood); } else { // Fallback to old behavior for (var messages in _conversations.values) { @@ -1517,6 +2025,7 @@ class MeshCoreConnector extends ChangeNotifier { if (_retryService != null) { _retryService!.handleAckReceived(ackHash, tripTimeMs); } + _handleVoiceSendConfirmed(ackHash); } else { // Fallback to old behavior for (var messages in _conversations.values) { @@ -1564,8 +2073,8 @@ class MeshCoreConnector extends ChangeNotifier { Future setChannelOrder(List order) async { _channelOrder = List.from(order); _applyChannelOrder(); - await _channelOrderStore.saveChannelOrder(_channelOrder); notifyListeners(); + await _channelOrderStore.saveChannelOrder(_channelOrder); } bool _shouldTrackUnreadForContactKey(String contactKeyHex) { @@ -1760,17 +2269,26 @@ class MeshCoreConnector extends ChangeNotifier { return contact.pathLength; } - void _addChannelMessage(int channelIndex, ChannelMessage message) { + bool _addChannelMessage(int channelIndex, ChannelMessage message) { _channelMessages.putIfAbsent(channelIndex, () => []); final messages = _channelMessages[channelIndex]!; final existingIndex = _findChannelRepeatIndex(messages, message); + var isNew = true; if (existingIndex >= 0) { + isNew = false; final existing = messages[existingIndex]; - final mergedPathBytes = existing.pathBytes.isEmpty ? message.pathBytes : existing.pathBytes; + final mergedPathBytes = _selectPreferredPathBytes(existing.pathBytes, message.pathBytes); + final mergedPathVariants = _mergePathVariants(existing.pathVariants, message.pathVariants); + final mergedPathLength = _mergePathLength( + existing.pathLength, + message.pathLength, + mergedPathBytes.length, + ); messages[existingIndex] = existing.copyWith( repeatCount: existing.repeatCount + 1, - pathLength: message.pathLength ?? existing.pathLength, + pathLength: mergedPathLength, pathBytes: mergedPathBytes, + pathVariants: mergedPathVariants, ); } else { messages.add(message); @@ -1781,6 +2299,7 @@ class MeshCoreConnector extends ChangeNotifier { channelIndex, messages, ); + return isNew; } int _findChannelRepeatIndex(List messages, ChannelMessage incoming) { @@ -1838,6 +2357,56 @@ class MeshCoreConnector extends ChangeNotifier { return false; } + Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) { + if (incoming.isEmpty) return existing; + if (existing.isEmpty) return incoming; + if (incoming.length > existing.length) return incoming; + return existing; + } + + int? _mergePathLength(int? existing, int? incoming, int observedLength) { + if (existing == null) { + if (incoming == null) return observedLength > 0 ? observedLength : null; + return incoming >= observedLength ? incoming : observedLength; + } + if (incoming == null) { + return existing >= observedLength ? existing : observedLength; + } + final merged = existing >= incoming ? existing : incoming; + return merged >= observedLength ? merged : observedLength; + } + + List _mergePathVariants( + List existing, + List incoming, + ) { + if (incoming.isEmpty) return existing; + if (existing.isEmpty) return incoming; + + final merged = [...existing]; + for (final candidate in incoming) { + var already = false; + for (final current in merged) { + if (_pathsEqual(current, candidate)) { + already = true; + break; + } + } + if (!already && candidate.isNotEmpty) { + merged.add(candidate); + } + } + return merged; + } + + bool _pathsEqual(Uint8List a, Uint8List b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + void _handleDisconnection() { _notifySubscription?.cancel(); _notifySubscription = null; @@ -1853,8 +2422,11 @@ class MeshCoreConnector extends ChangeNotifier { _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; + _voiceAssemblies.clear(); + _voiceSendSession = null; _setState(MeshCoreConnectionState.disconnected); + _scheduleReconnect(); } void _setState(MeshCoreConnectionState newState) { @@ -1869,6 +2441,7 @@ class MeshCoreConnector extends ChangeNotifier { _scanSubscription?.cancel(); _connectionSubscription?.cancel(); _notifySubscription?.cancel(); + _reconnectTimer?.cancel(); _receivedFramesController.close(); super.dispose(); } @@ -1917,3 +2490,93 @@ class _ParsedText { required this.text, }); } + +class _VoiceAssembly { + _VoiceAssembly({ + required this.senderKey, + required this.senderKeyHex, + required this.timestampSeconds, + required this.totalChunks, + }); + + final Uint8List senderKey; + final String senderKeyHex; + final int timestampSeconds; + final int totalChunks; + final DateTime startedAt = DateTime.now(); + final Map _chunks = {}; + + bool get isComplete => _chunks.length == totalChunks; + + void addChunk(VoiceChunk chunk) { + _chunks.putIfAbsent(chunk.index, () => chunk.bytes); + } + + Uint8List assemble() { + if (!isComplete) return Uint8List(0); + final builder = BytesBuilder(copy: false); + for (var i = 0; i < totalChunks; i++) { + final part = _chunks[i]; + if (part == null) return Uint8List(0); + builder.add(part); + } + return builder.takeBytes(); + } +} + +class _VoiceSendSession { + _VoiceSendSession({ + required this.contact, + required this.messageId, + required this.chunks, + required this.timestampSeconds, + }); + + final Contact contact; + final String messageId; + final List chunks; + final int timestampSeconds; + + int currentChunkIndex = -1; + Uint8List? expectedAckHash; + int? expectedTimeoutMs; + Completer? sentCompleter; + Completer? confirmCompleter; + bool _cancelled = false; + + bool get isCancelled => _cancelled; + + void beginChunk(int index) { + currentChunkIndex = index; + expectedAckHash = null; + expectedTimeoutMs = null; + sentCompleter = Completer(); + confirmCompleter = Completer(); + } + + void handleSent(Uint8List ackHash, int timeoutMs) { + if (sentCompleter == null || sentCompleter!.isCompleted) return; + expectedAckHash = Uint8List.fromList(ackHash); + expectedTimeoutMs = timeoutMs > 0 ? timeoutMs : null; + sentCompleter!.complete(); + } + + void handleConfirmed(Uint8List ackHash) { + if (confirmCompleter == null || confirmCompleter!.isCompleted) return; + final expected = expectedAckHash; + if (expected == null) return; + if (!listEquals(expected, ackHash)) return; + confirmCompleter!.complete(); + } + + void cancel() { + if (_cancelled) return; + _cancelled = true; + if (sentCompleter != null && !sentCompleter!.isCompleted) { + sentCompleter!.completeError(StateError('cancelled')); + } + if (confirmCompleter != null && !confirmCompleter!.isCompleted) { + confirmCompleter!.completeError(StateError('cancelled')); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index ea0e144..f1c7c6d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:media_kit_fork/media_kit_fork.dart'; import 'connector/meshcore_connector.dart'; import 'screens/scanner_screen.dart'; @@ -9,9 +10,12 @@ import 'services/path_history_service.dart'; import 'services/app_settings_service.dart'; import 'services/notification_service.dart'; import 'services/ble_debug_log_service.dart'; +import 'services/background_service.dart'; +import 'services/map_tile_cache_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + MediaKit.ensureInitialized(); // Initialize services final storage = StorageService(); @@ -20,6 +24,8 @@ void main() async { final retryService = MessageRetryService(storage); final appSettingsService = AppSettingsService(); final bleDebugLogService = BleDebugLogService(); + final backgroundService = BackgroundService(); + final mapTileCacheService = MapTileCacheService(); // Load settings await appSettingsService.loadSettings(); @@ -27,6 +33,7 @@ void main() async { // Initialize notification service final notificationService = NotificationService(); await notificationService.initialize(); + await backgroundService.initialize(); // Wire up connector with services connector.initialize( @@ -34,6 +41,7 @@ void main() async { pathHistoryService: pathHistoryService, appSettingsService: appSettingsService, bleDebugLogService: bleDebugLogService, + backgroundService: backgroundService, ); await connector.loadContactCache(); @@ -50,6 +58,7 @@ void main() async { storage: storage, appSettingsService: appSettingsService, bleDebugLogService: bleDebugLogService, + mapTileCacheService: mapTileCacheService, )); } @@ -60,6 +69,7 @@ class MeshCoreApp extends StatelessWidget { final StorageService storage; final AppSettingsService appSettingsService; final BleDebugLogService bleDebugLogService; + final MapTileCacheService mapTileCacheService; const MeshCoreApp({ super.key, @@ -69,6 +79,7 @@ class MeshCoreApp extends StatelessWidget { required this.storage, required this.appSettingsService, required this.bleDebugLogService, + required this.mapTileCacheService, }); @override @@ -81,6 +92,7 @@ class MeshCoreApp extends StatelessWidget { ChangeNotifierProvider.value(value: appSettingsService), ChangeNotifierProvider.value(value: bleDebugLogService), Provider.value(value: storage), + Provider.value(value: mapTileCacheService), ], child: Consumer( builder: (context, settingsService, child) { diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 9b64834..e494344 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -1,4 +1,6 @@ class AppSettings { + static const Object _unset = Object(); + final bool clearPathOnMaxRetry; final bool mapShowRepeaters; final bool mapShowChatNodes; @@ -7,8 +9,12 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final Map? mapCacheBounds; + final int mapCacheMinZoom; + final int mapCacheMaxZoom; final bool notificationsEnabled; final bool notifyOnNewMessage; + final bool notifyOnNewChannelMessage; final bool notifyOnNewAdvert; final bool autoRouteRotationEnabled; final String themeMode; @@ -23,8 +29,12 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.mapCacheBounds, + this.mapCacheMinZoom = 10, + this.mapCacheMaxZoom = 15, this.notificationsEnabled = true, this.notifyOnNewMessage = true, + this.notifyOnNewChannelMessage = true, this.notifyOnNewAdvert = true, this.autoRouteRotationEnabled = false, this.themeMode = 'system', @@ -41,8 +51,12 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'map_cache_bounds': mapCacheBounds, + 'map_cache_min_zoom': mapCacheMinZoom, + 'map_cache_max_zoom': mapCacheMaxZoom, 'notifications_enabled': notificationsEnabled, 'notify_on_new_message': notifyOnNewMessage, + 'notify_on_new_channel_message': notifyOnNewChannelMessage, 'notify_on_new_advert': notifyOnNewAdvert, 'auto_route_rotation_enabled': autoRouteRotationEnabled, 'theme_mode': themeMode, @@ -60,8 +74,15 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( + (key, value) => MapEntry(key.toString(), (value as num).toDouble()), + ), + mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10, + mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15, notificationsEnabled: json['notifications_enabled'] as bool? ?? true, notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true, + notifyOnNewChannelMessage: + json['notify_on_new_channel_message'] as bool? ?? true, notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true, autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false, themeMode: json['theme_mode'] as String? ?? 'system', @@ -81,8 +102,12 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + Object? mapCacheBounds = _unset, + int? mapCacheMinZoom, + int? mapCacheMaxZoom, bool? notificationsEnabled, bool? notifyOnNewMessage, + bool? notifyOnNewChannelMessage, bool? notifyOnNewAdvert, bool? autoRouteRotationEnabled, String? themeMode, @@ -97,8 +122,14 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + mapCacheBounds: + mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, + mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom, + mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom, notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage, + notifyOnNewChannelMessage: + notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage, notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert, autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, themeMode: themeMode ?? this.themeMode, diff --git a/lib/models/channel_message.dart b/lib/models/channel_message.dart index 0fdb679..14d4bf0 100644 --- a/lib/models/channel_message.dart +++ b/lib/models/channel_message.dart @@ -32,6 +32,7 @@ class ChannelMessage { final int repeatCount; final int? pathLength; final Uint8List pathBytes; + final List pathVariants; final int? channelIndex; ChannelMessage({ @@ -45,8 +46,13 @@ class ChannelMessage { this.repeatCount = 0, this.pathLength, Uint8List? pathBytes, + List? pathVariants, this.channelIndex, - }) : pathBytes = pathBytes ?? Uint8List(0); + }) : pathBytes = pathBytes ?? Uint8List(0), + pathVariants = _mergePathVariants( + pathBytes ?? Uint8List(0), + pathVariants, + ); String? get senderKeyHex => senderKey != null ? pubKeyToHex(senderKey!) : null; @@ -56,6 +62,7 @@ class ChannelMessage { int? repeatCount, int? pathLength, Uint8List? pathBytes, + List? pathVariants, }) { return ChannelMessage( senderKey: senderKey, @@ -68,6 +75,7 @@ class ChannelMessage { repeatCount: repeatCount ?? this.repeatCount, pathLength: pathLength ?? this.pathLength, pathBytes: pathBytes ?? this.pathBytes, + pathVariants: pathVariants ?? this.pathVariants, channelIndex: channelIndex, ); } @@ -164,7 +172,39 @@ class ChannelMessage { status: ChannelMessageStatus.pending, pathLength: null, pathBytes: Uint8List(0), + pathVariants: const [], channelIndex: channelIndex, ); } + + static List _mergePathVariants( + Uint8List pathBytes, + List? pathVariants, + ) { + final merged = []; + + void addPath(Uint8List bytes) { + if (bytes.isEmpty) return; + for (final existing in merged) { + if (_pathsEqual(existing, bytes)) return; + } + merged.add(bytes); + } + + if (pathVariants != null) { + for (final variant in pathVariants) { + addPath(variant); + } + } + addPath(pathBytes); + return merged; + } + + static bool _pathsEqual(Uint8List a, Uint8List b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 80e2e8a..1f5003d 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -10,6 +10,7 @@ class Contact { final double? latitude; final double? longitude; final DateTime lastSeen; + final DateTime lastMessageAt; Contact({ required this.publicKey, @@ -20,7 +21,8 @@ class Contact { this.latitude, this.longitude, required this.lastSeen, - }); + DateTime? lastMessageAt, + }) : lastMessageAt = lastMessageAt ?? lastSeen; String get publicKeyHex => pubKeyToHex(publicKey); @@ -47,6 +49,30 @@ class Contact { bool get hasLocation => latitude != null && longitude != null; + Contact copyWith({ + Uint8List? publicKey, + String? name, + int? type, + int? pathLength, + Uint8List? path, + double? latitude, + double? longitude, + DateTime? lastSeen, + DateTime? lastMessageAt, + }) { + return Contact( + publicKey: publicKey ?? this.publicKey, + name: name ?? this.name, + type: type ?? this.type, + pathLength: pathLength ?? this.pathLength, + path: path ?? this.path, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + lastSeen: lastSeen ?? this.lastSeen, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + ); + } + String get pathIdList { if (path.isEmpty) return ''; final parts = []; diff --git a/lib/models/message.dart b/lib/models/message.dart index 4c347d4..3ed7354 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -10,6 +10,10 @@ class Message { final bool isOutgoing; final bool isCli; final MessageStatus status; + final bool isVoice; + final String? voicePath; + final int? voiceDurationMs; + final String? voiceCodec; // NEW: Retry logic fields final String? messageId; @@ -30,6 +34,10 @@ class Message { required this.isOutgoing, this.isCli = false, this.status = MessageStatus.pending, + this.isVoice = false, + this.voicePath, + this.voiceDurationMs, + this.voiceCodec, this.messageId, this.retryCount = 0, this.estimatedTimeoutMs, @@ -55,6 +63,10 @@ class Message { int? pathLength, Uint8List? pathBytes, bool? isCli, + bool? isVoice, + String? voicePath, + int? voiceDurationMs, + String? voiceCodec, }) { return Message( senderKey: senderKey, @@ -63,6 +75,10 @@ class Message { isOutgoing: isOutgoing, isCli: isCli ?? this.isCli, status: status ?? this.status, + isVoice: isVoice ?? this.isVoice, + voicePath: voicePath ?? this.voicePath, + voiceDurationMs: voiceDurationMs ?? this.voiceDurationMs, + voiceCodec: voiceCodec ?? this.voiceCodec, messageId: messageId, retryCount: retryCount ?? this.retryCount, estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs, @@ -101,6 +117,7 @@ class Message { isOutgoing: false, isCli: false, status: MessageStatus.delivered, + isVoice: false, pathBytes: Uint8List(0), ); } @@ -118,6 +135,7 @@ class Message { isOutgoing: true, isCli: false, status: MessageStatus.pending, + isVoice: false, pathLength: pathLength, pathBytes: pathBytes, ); diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index b6ec3a1..746f4b8 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../services/app_settings_service.dart'; import '../services/notification_service.dart'; +import 'map_cache_screen.dart'; class AppSettingsScreen extends StatelessWidget { const AppSettingsScreen({super.key}); @@ -133,6 +134,31 @@ class AppSettingsScreen extends StatelessWidget { : null, ), const Divider(height: 1), + SwitchListTile( + secondary: Icon( + Icons.forum_outlined, + color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + ), + title: Text( + 'Channel Message Notifications', + style: TextStyle( + color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + ), + ), + subtitle: Text( + 'Show notification when receiving channel messages', + style: TextStyle( + color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + ), + ), + value: settingsService.settings.notifyOnNewChannelMessage, + onChanged: settingsService.settings.notificationsEnabled + ? (value) { + settingsService.setNotifyOnNewChannelMessage(value); + } + : null, + ), + const Divider(height: 1), SwitchListTile( secondary: Icon( Icons.cell_tower, @@ -267,6 +293,24 @@ class AppSettingsScreen extends StatelessWidget { trailing: const Icon(Icons.chevron_right), onTap: () => _showTimeFilterDialog(context, settingsService), ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.download_outlined), + title: const Text('Offline Map Cache'), + subtitle: Text( + settingsService.settings.mapCacheBounds == null + ? 'No area selected' + : 'Area selected (zoom ${settingsService.settings.mapCacheMinZoom}' + '-${settingsService.settings.mapCacheMaxZoom})', + ), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MapCacheScreen()), + ); + }, + ), ], ), ); diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index ab16b91..b04cab8 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -168,6 +168,9 @@ class _ChannelChatScreenState extends State { final isOutgoing = message.isOutgoing; final gifId = _parseGifId(message.text); final poi = _parsePoiMessage(message.text); + final displayPath = message.pathBytes.isNotEmpty + ? message.pathBytes + : (message.pathVariants.isNotEmpty ? message.pathVariants.first : Uint8List(0)); return Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), @@ -223,10 +226,10 @@ class _ChannelChatScreenState extends State { message.text, style: const TextStyle(fontSize: 14), ), - if (message.pathBytes.isNotEmpty) ...[ + if (displayPath.isNotEmpty) ...[ const SizedBox(height: 4), Text( - 'via ${_formatPathPrefixes(message.pathBytes)}', + 'via ${_formatPathPrefixes(displayPath)}', style: TextStyle(fontSize: 11, color: Colors.grey[600]), ), ], diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index bbd7b97..16ef349 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -7,6 +7,7 @@ import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../services/map_tile_cache_service.dart'; import '../connector/meshcore_protocol.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; @@ -23,8 +24,14 @@ class ChannelMessagePathScreen extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { - final hops = _buildPathHops(message.pathBytes, connector.contacts); - final hasHopDetails = message.pathBytes.isNotEmpty; + final primaryPath = _selectPrimaryPath(message.pathBytes, message.pathVariants); + final hops = _buildPathHops(primaryPath, connector.contacts); + final hasHopDetails = primaryPath.isNotEmpty; + final observedLabel = _formatObservedHops( + primaryPath.length, + message.pathLength, + ); + final extraPaths = _otherPaths(primaryPath, message.pathVariants); return Scaffold( appBar: AppBar( @@ -35,13 +42,7 @@ class ChannelMessagePathScreen extends StatelessWidget { tooltip: 'View map', onPressed: hasHopDetails ? () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - ChannelMessagePathMapScreen(message: message), - ), - ); + _openPathMap(context); } : null, ), @@ -50,8 +51,17 @@ class ChannelMessagePathScreen extends StatelessWidget { body: ListView( padding: const EdgeInsets.all(16), children: [ - _buildSummaryCard(context), + _buildSummaryCard(context, observedLabel: observedLabel), const SizedBox(height: 16), + if (extraPaths.isNotEmpty) ...[ + Text( + 'Other Observed Paths', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + _buildPathVariants(context, extraPaths), + const SizedBox(height: 16), + ], Text( 'Repeater Hops', style: Theme.of(context).textTheme.titleSmall, @@ -71,7 +81,10 @@ class ChannelMessagePathScreen extends StatelessWidget { ); } - Widget _buildSummaryCard(BuildContext context) { + Widget _buildSummaryCard( + BuildContext context, { + String? observedLabel, + }) { return Card( child: Padding( padding: const EdgeInsets.all(12), @@ -88,12 +101,37 @@ class ChannelMessagePathScreen extends StatelessWidget { if (message.repeatCount > 0) _buildDetailRow('Repeats', message.repeatCount.toString()), _buildDetailRow('Path', _formatPathLabel(message.pathLength)), + if (observedLabel != null) _buildDetailRow('Observed', observedLabel), ], ), ), ); } + Widget _buildPathVariants( + BuildContext context, + List variants, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < variants.length; i++) + Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + dense: true, + title: Text( + 'Observed path ${i + 1} • ${_formatHopCount(variants[i].length)}', + ), + subtitle: Text(_formatPathPrefixes(variants[i])), + trailing: const Icon(Icons.map_outlined, size: 20), + onTap: () => _openPathMap(context, initialPath: variants[i]), + ), + ), + ], + ); + } + List _buildHopTiles(List<_PathHop> hops) { return [ for (final hop in hops) @@ -138,6 +176,22 @@ class ChannelMessagePathScreen extends StatelessWidget { return '$pathLength hops'; } + String? _formatObservedHops(int observedCount, int? pathLength) { + if (observedCount <= 0 && (pathLength == null || pathLength <= 0)) { + return null; + } + if (pathLength == null || pathLength < 0) { + return observedCount > 0 ? '$observedCount hops' : null; + } + if (observedCount == 0) { + return '0 of $pathLength hops'; + } + if (observedCount == pathLength) { + return '$observedCount hops'; + } + return '$observedCount of $pathLength hops'; + } + Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2), @@ -153,21 +207,71 @@ class ChannelMessagePathScreen extends StatelessWidget { ), ); } + + void _openPathMap(BuildContext context, {Uint8List? initialPath}) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelMessagePathMapScreen( + message: message, + initialPath: initialPath, + ), + ), + ); + } + } -class ChannelMessagePathMapScreen extends StatelessWidget { +class ChannelMessagePathMapScreen extends StatefulWidget { final ChannelMessage message; + final Uint8List? initialPath; const ChannelMessagePathMapScreen({ super.key, required this.message, + this.initialPath, }); + @override + State createState() => + _ChannelMessagePathMapScreenState(); +} + +class _ChannelMessagePathMapScreenState extends State { + Uint8List? _selectedPath; + + @override + void initState() { + super.initState(); + _selectedPath = widget.initialPath; + } + + @override + void didUpdateWidget(ChannelMessagePathMapScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.message != widget.message || + !_pathsEqual(oldWidget.initialPath ?? Uint8List(0), + widget.initialPath ?? Uint8List(0))) { + _selectedPath = widget.initialPath; + } + } + @override Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { - final hops = _buildPathHops(message.pathBytes, connector.contacts); + final tileCache = context.read(); + final primaryPath = + _selectPrimaryPath(widget.message.pathBytes, widget.message.pathVariants); + final observedPaths = + _buildObservedPaths(primaryPath, widget.message.pathVariants); + final selectedPath = _resolveSelectedPath( + _selectedPath, + observedPaths, + primaryPath, + ); + final selectedIndex = _indexForPath(selectedPath, observedPaths); + final hops = _buildPathHops(selectedPath, connector.contacts); final points = hops .where((hop) => hop.hasLocation) .map((hop) => hop.position!) @@ -186,6 +290,7 @@ class ChannelMessagePathMapScreen extends StatelessWidget { points.isNotEmpty ? points.first : const LatLng(0, 0); final initialZoom = points.isNotEmpty ? 13.0 : 2.0; final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null; + final mapKey = ValueKey(_formatPathPrefixes(selectedPath)); return Scaffold( appBar: AppBar( @@ -194,6 +299,7 @@ class ChannelMessagePathMapScreen extends StatelessWidget { body: Stack( children: [ FlutterMap( + key: mapKey, options: MapOptions( initialCenter: initialCenter, initialZoom: initialZoom, @@ -209,9 +315,10 @@ class ChannelMessagePathMapScreen extends StatelessWidget { ), children: [ TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.meshcore.open', + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, maxZoom: 19, ), if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), @@ -220,6 +327,17 @@ class ChannelMessagePathMapScreen extends StatelessWidget { ), ], ), + if (observedPaths.length > 1) + _buildPathSelector( + context, + observedPaths, + selectedIndex, + (index) { + setState(() { + _selectedPath = observedPaths[index].pathBytes; + }); + }, + ), if (points.isEmpty) Center( child: Card( @@ -238,6 +356,65 @@ class ChannelMessagePathMapScreen extends StatelessWidget { ); } + Widget _buildPathSelector( + BuildContext context, + List<_ObservedPath> paths, + int selectedIndex, + ValueChanged onSelected, + ) { + final selectedPath = paths[selectedIndex]; + final label = selectedPath.isPrimary + ? 'Path ${selectedIndex + 1} (Primary)' + : 'Path ${selectedIndex + 1}'; + return Positioned( + left: 16, + right: 16, + top: 16, + child: SafeArea( + child: Card( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Observed Path', + style: TextStyle(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 4), + DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: selectedIndex, + items: [ + for (int i = 0; i < paths.length; i++) + DropdownMenuItem( + value: i, + child: Text( + '${paths[i].isPrimary ? 'Path ${i + 1} (Primary)' : 'Path ${i + 1}'}' + ' • ${_formatHopCount(paths[i].pathBytes.length)}', + ), + ), + ], + onChanged: (value) { + if (value == null) return; + onSelected(value); + }, + ), + ), + const SizedBox(height: 4), + Text( + '$label • ${_formatPathPrefixes(selectedPath.pathBytes)}', + style: TextStyle(color: Colors.grey[700], fontSize: 12), + ), + ], + ), + ), + ), + ), + ); + } + List _buildHopMarkers(List<_PathHop> hops) { return [ for (final hop in hops) @@ -356,6 +533,16 @@ class _PathHop { } } +class _ObservedPath { + final Uint8List pathBytes; + final bool isPrimary; + + const _ObservedPath({ + required this.pathBytes, + required this.isPrimary, + }); +} + List<_PathHop> _buildPathHops(Uint8List pathBytes, List contacts) { final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { @@ -375,7 +562,10 @@ List<_PathHop> _buildPathHops(Uint8List pathBytes, List contacts) { Contact? _matchContactForPrefix(List contacts, int prefix) { final matches = contacts - .where((contact) => contact.publicKey.isNotEmpty && contact.publicKey[0] == prefix) + .where((contact) => + (contact.type == advTypeRepeater || contact.type == advTypeRoom) && + contact.publicKey.isNotEmpty && + contact.publicKey[0] == prefix) .toList(); if (matches.isEmpty) return null; @@ -410,6 +600,16 @@ String _formatPrefix(int prefix) { return prefix.toRadixString(16).padLeft(2, '0').toUpperCase(); } +String _formatPathPrefixes(Uint8List pathBytes) { + return pathBytes + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(','); +} + +String _formatHopCount(int count) { + return '$count ${count == 1 ? 'hop' : 'hops'}'; +} + String _resolveName(Contact? contact) { if (contact == null) return 'Unknown Repeater'; final name = contact.name.trim(); @@ -418,3 +618,81 @@ String _resolveName(Contact? contact) { } return name; } + +Uint8List _selectPrimaryPath(Uint8List pathBytes, List variants) { + Uint8List primary = pathBytes; + for (final variant in variants) { + if (variant.length > primary.length) { + primary = variant; + } + } + return primary; +} + +List _otherPaths(Uint8List primary, List variants) { + final others = []; + for (final variant in variants) { + if (variant.isEmpty) continue; + if (!_pathsEqual(primary, variant)) { + others.add(variant); + } + } + return others; +} + +List<_ObservedPath> _buildObservedPaths( + Uint8List primary, + List variants, +) { + final observed = <_ObservedPath>[]; + + void addPath(Uint8List pathBytes, bool isPrimary) { + if (pathBytes.isEmpty) return; + for (final existing in observed) { + if (_pathsEqual(existing.pathBytes, pathBytes)) return; + } + observed.add(_ObservedPath(pathBytes: pathBytes, isPrimary: isPrimary)); + } + + addPath(primary, true); + for (final variant in variants) { + addPath(variant, false); + } + + return observed; +} + +Uint8List _resolveSelectedPath( + Uint8List? selected, + List<_ObservedPath> observedPaths, + Uint8List fallback, +) { + if (selected != null) { + for (final path in observedPaths) { + if (_pathsEqual(path.pathBytes, selected)) { + return path.pathBytes; + } + } + } + if (observedPaths.isNotEmpty) { + return observedPaths.first.pathBytes; + } + return fallback; +} + +int _indexForPath(Uint8List selected, List<_ObservedPath> paths) { + for (int i = 0; i < paths.length; i++) { + if (_pathsEqual(paths[i].pathBytes, selected)) { + return i; + } + } + return 0; +} + +bool _pathsEqual(Uint8List a, Uint8List b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; +} diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 145a639..051d584 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; @@ -6,11 +7,21 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../models/channel.dart'; +import '../utils/route_transitions.dart'; +import '../widgets/quick_switch_bar.dart'; import '../widgets/unread_badge.dart'; import 'channel_chat_screen.dart'; +import 'contacts_screen.dart'; +import 'map_screen.dart'; +import 'settings_screen.dart'; class ChannelsScreen extends StatefulWidget { - const ChannelsScreen({super.key}); + final bool hideBackButton; + + const ChannelsScreen({ + super.key, + this.hideBackButton = false, + }); @override State createState() => _ChannelsScreenState(); @@ -31,7 +42,21 @@ class _ChannelsScreenState extends State { appBar: AppBar( title: const Text('Channels'), centerTitle: true, + automaticallyImplyLeading: !widget.hideBackButton, actions: [ + IconButton( + icon: const Icon(Icons.tune), + tooltip: 'Settings', + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + IconButton( + icon: const Icon(Icons.bluetooth_disabled), + tooltip: 'Disconnect', + onPressed: () => _disconnect(context), + ), IconButton( icon: const Icon(Icons.refresh), onPressed: () => context.read().getChannels(), @@ -69,20 +94,23 @@ class _ChannelsScreenState extends State { } return ReorderableListView.builder( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 88), + buildDefaultDragHandles: false, itemCount: channels.length, - onReorder: (oldIndex, newIndex) async { + onReorder: (oldIndex, newIndex) { if (newIndex > oldIndex) newIndex -= 1; final reordered = List.from(channels); final item = reordered.removeAt(oldIndex); reordered.insert(newIndex, item); - await connector.setChannelOrder( - reordered.map((c) => c.index).toList(), + unawaited( + connector.setChannelOrder( + reordered.map((c) => c.index).toList(), + ), ); }, itemBuilder: (context, index) { final channel = channels[index]; - return _buildChannelTile(context, connector, channel); + return _buildChannelTile(context, connector, channel, index); }, ); }, @@ -91,6 +119,13 @@ class _ChannelsScreenState extends State { onPressed: () => _showAddChannelDialog(context), child: const Icon(Icons.add), ), + bottomNavigationBar: SafeArea( + top: false, + child: QuickSwitchBar( + selectedIndex: 1, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + ), + ), ); } @@ -98,11 +133,17 @@ class _ChannelsScreenState extends State { BuildContext context, MeshCoreConnector connector, Channel channel, + int index, ) { final unreadCount = connector.getUnreadCountForChannel(channel); return Card( key: ValueKey('channel_${channel.index}'), + margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( + dense: true, + minVerticalPadding: 0, + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + visualDensity: const VisualDensity(vertical: -2), leading: CircleAvatar( backgroundColor: channel.isPublicChannel ? Colors.green.withValues(alpha: 0.2) @@ -120,36 +161,26 @@ class _ChannelsScreenState extends State { channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name, style: const TextStyle(fontWeight: FontWeight.w500), ), - subtitle: Text( - channel.name.startsWith('#') - ? 'Hashtag channel' - : channel.isPublicChannel - ? 'Public channel' - : 'Private channel', - ), + subtitle: Text( + channel.name.startsWith('#') + ? 'Hashtag channel' + : channel.isPublicChannel + ? 'Public channel' + : 'Private channel', + ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (unreadCount > 0) ...[ UnreadBadge(count: unreadCount), - const SizedBox(width: 8), + const SizedBox(width: 4), ], - IconButton( - icon: const Icon(Icons.edit_outlined), - onPressed: () => _showEditChannelDialog(context, connector, channel), - ), - PopupMenuButton( - onSelected: (value) { - if (value == 'delete') { - _confirmDeleteChannel(context, connector, channel); - } - }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 'delete', - child: Text('Delete'), - ), - ], + ReorderableDelayedDragStartListener( + index: index, + child: Icon( + Icons.drag_handle, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), @@ -162,10 +193,91 @@ class _ChannelsScreenState extends State { ), ); }, + onLongPress: () => _showChannelActions(context, connector, channel), ), ); } + void _showChannelActions( + BuildContext context, + MeshCoreConnector connector, + Channel channel, + ) { + showModalBottomSheet( + context: context, + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.edit_outlined), + title: const Text('Edit channel'), + onTap: () { + Navigator.pop(context); + _showEditChannelDialog(context, connector, channel); + }, + ), + ListTile( + leading: const Icon(Icons.delete_outline, color: Colors.red), + title: const Text('Delete channel', style: TextStyle(color: Colors.red)), + onTap: () { + Navigator.pop(context); + _confirmDeleteChannel(context, connector, channel); + }, + ), + ], + ), + ), + ); + } + + void _handleQuickSwitch(int index, BuildContext context) { + if (index == 1) return; + switch (index) { + case 0: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute( + const ContactsScreen(hideBackButton: true), + ), + ); + break; + case 2: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute( + const MapScreen(hideBackButton: true), + ), + ); + break; + } + } + + Future _disconnect(BuildContext context) async { + final connector = context.read(); + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Disconnect'), + content: const Text('Are you sure you want to disconnect from this device?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Disconnect'), + ), + ], + ), + ); + + if (confirmed == true) { + await connector.disconnect(); + } + } + void _showAddChannelDialog(BuildContext context) { final connector = context.read(); final nameController = TextEditingController(); diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 2509eb3..1cf33cc 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1,10 +1,13 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:latlong2/latlong.dart'; +import 'package:record/record.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; @@ -12,12 +15,14 @@ import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; import '../models/message.dart'; +import '../services/voice_message_service.dart'; import '../services/path_history_service.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; import '../utils/emoji_utils.dart'; import '../widgets/gif_message.dart'; import '../widgets/gif_picker.dart'; +import '../widgets/voice_message.dart'; class ChatScreen extends StatefulWidget { final Contact contact; @@ -32,6 +37,16 @@ class _ChatScreenState extends State { final _textController = TextEditingController(); final _scrollController = ScrollController(); bool _forceFlood = false; + final AudioRecorder _voiceRecorder = AudioRecorder(); + StreamSubscription? _voiceStreamSubscription; + BytesBuilder _voiceBuffer = BytesBuilder(copy: false); + Timer? _voiceRecordTimer; + bool _isRecordingVoice = false; + Message? _pendingVoiceMessage; + Uint8List? _pendingVoiceCodec2Bytes; + int? _pendingVoiceTimestampSeconds; + int? _pendingVoiceDurationMs; + String? _pendingVoicePath; @override void initState() { @@ -47,6 +62,11 @@ class _ChatScreenState extends State { context.read().setActiveContact(null); _textController.dispose(); _scrollController.dispose(); + _voiceRecordTimer?.cancel(); + _voiceStreamSubscription?.cancel(); + unawaited(_voiceRecorder.stop()); + _voiceRecorder.dispose(); + unawaited(_clearPendingVoicePreview(deleteFile: true, notify: false)); super.dispose(); } @@ -56,35 +76,29 @@ class _ChatScreenState extends State { appBar: AppBar( title: Consumer2( builder: (context, pathService, connector, _) { - final paths = pathService.getRecentPaths(widget.contact.publicKeyHex); final contact = _resolveContact(connector); - final showRecentPath = paths.isNotEmpty && contact.pathLength >= 0; final unreadCount = connector.getUnreadCountForContactKey(widget.contact.publicKeyHex); final unreadLabel = 'Unread: $unreadCount'; + final pathLabel = _forceFlood ? 'Flood (forced)' : _currentPathLabel(contact); + final canShowPathDetails = !_forceFlood && contact.path.isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(contact.name), - if (showRecentPath) + if (canShowPathDetails) GestureDetector( behavior: HitTestBehavior.opaque, - onLongPress: () => _showFullPathDialog(context, paths.first.pathBytes), + onLongPress: () => _showFullPathDialog(context, contact.path), child: Text( - '${paths.first.displayText} • $unreadLabel', + '$pathLabel • $unreadLabel', overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.normal), ), ) - else if (contact.pathLength >= 0) - Text( - '${contact.pathLength} ${contact.pathLength == 1 ? 'hop' : 'hops'} • $unreadLabel', - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 11, fontWeight: FontWeight.normal), - ) else Text( - 'No path • $unreadLabel', + '$pathLabel • $unreadLabel', overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.normal), ), @@ -207,10 +221,14 @@ class _ChatScreenState extends State { Widget _buildInputBar(MeshCoreConnector connector) { final maxBytes = maxContactMessageBytes(); + final isVoiceBusy = connector.isVoiceSending; + final voiceSupported = Platform.isAndroid || Platform.isIOS; + final hasPendingVoice = _pendingVoiceMessage != null; + final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + color: colorScheme.surface, border: Border( top: BorderSide(color: Theme.of(context).dividerColor), ), @@ -218,59 +236,93 @@ class _ChatScreenState extends State { child: SafeArea( child: Row( children: [ + if (voiceSupported) + IconButton( + icon: Icon(_isRecordingVoice ? Icons.stop_circle : Icons.mic), + onPressed: (isVoiceBusy || hasPendingVoice) ? null : () => _toggleVoiceRecording(connector), + tooltip: _isRecordingVoice ? 'Stop recording' : 'Record voice', + ), IconButton( icon: const Icon(Icons.gif_box), - onPressed: () => _showGifPicker(context), + onPressed: (_isRecordingVoice || isVoiceBusy || hasPendingVoice) + ? null + : () => _showGifPicker(context), tooltip: 'Send GIF', ), Expanded( - child: ValueListenableBuilder( - valueListenable: _textController, - builder: (context, value, child) { - final gifId = _parseGifId(value.text); - if (gifId != null) { - return Row( - children: [ - Expanded( - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - fallbackTextColor: - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - width: 160, - height: 110, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], - ); - } + child: hasPendingVoice + ? _buildVoicePreview(colorScheme) + : ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, child) { + final gifId = _parseGifId(value.text); + if (gifId != null) { + return Row( + children: [ + Expanded( + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: colorScheme.surfaceContainerHighest, + fallbackTextColor: + colorScheme.onSurface.withValues(alpha: 0.6), + width: 160, + height: 110, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => _textController.clear(), + ), + ], + ); + } - return TextField( - controller: _textController, - inputFormatters: [ - Utf8LengthLimitingTextInputFormatter(maxBytes), - ], - decoration: const InputDecoration( - hintText: 'Type a message...', - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + return TextField( + controller: _textController, + enabled: !_isRecordingVoice && !isVoiceBusy, + inputFormatters: [ + Utf8LengthLimitingTextInputFormatter(maxBytes), + ], + decoration: const InputDecoration( + hintText: 'Type a message...', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + textInputAction: TextInputAction.send, + onSubmitted: (_isRecordingVoice || isVoiceBusy) + ? null + : (_) => _sendMessage(connector), + ); + }, ), - textInputAction: TextInputAction.send, - onSubmitted: (_) => _sendMessage(connector), - ); - }, - ), ), const SizedBox(width: 8), - IconButton.filled( - icon: const Icon(Icons.send), - onPressed: () => _sendMessage(connector), - ), + if (isVoiceBusy) + IconButton.filled( + icon: const Icon(Icons.stop_circle), + onPressed: () => _cancelVoiceSend(connector), + tooltip: 'Cancel voice send', + ) + else if (hasPendingVoice) ...[ + IconButton( + icon: const Icon(Icons.close), + onPressed: () => _clearPendingVoicePreview(deleteFile: true), + tooltip: 'Discard voice message', + ), + IconButton.filled( + icon: const Icon(Icons.send), + onPressed: () => _sendPendingVoice(connector), + tooltip: 'Send voice message', + ), + ] + else + IconButton.filled( + icon: const Icon(Icons.send), + onPressed: (_isRecordingVoice || isVoiceBusy) + ? null + : () => _sendMessage(connector), + ), ], ), ), @@ -325,6 +377,209 @@ class _ChatScreenState extends State { }); } + void _cancelVoiceSend(MeshCoreConnector connector) { + connector.cancelVoiceSend(); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Voice send canceled')), + ); + } + + Future _toggleVoiceRecording(MeshCoreConnector connector) async { + if (_isRecordingVoice) { + await _stopVoiceRecording(connector); + } else { + await _startVoiceRecording(); + } + } + + Future _startVoiceRecording() async { + if (_isRecordingVoice) return; + final hasPermission = await _voiceRecorder.hasPermission(); + if (!hasPermission) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Microphone permission denied')), + ); + return; + } + + _voiceBuffer = BytesBuilder(copy: false); + try { + final stream = await _voiceRecorder.startStream( + const RecordConfig( + encoder: AudioEncoder.pcm16bits, + sampleRate: VoiceMessageService.sampleRate, + numChannels: VoiceMessageService.channels, + ), + ); + _voiceStreamSubscription = stream.listen((data) { + _voiceBuffer.add(data); + }); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to start recording: $e')), + ); + return; + } + _voiceRecordTimer?.cancel(); + _voiceRecordTimer = Timer( + const Duration(seconds: VoiceMessageService.maxRecordSeconds), + () => _stopVoiceRecording(context.read()), + ); + setState(() { + _isRecordingVoice = true; + }); + } + + Future _stopVoiceRecording(MeshCoreConnector connector) async { + if (!_isRecordingVoice) return; + _voiceRecordTimer?.cancel(); + await _voiceRecorder.stop(); + await _voiceStreamSubscription?.cancel(); + _voiceStreamSubscription = null; + final pcmBytes = _voiceBuffer.takeBytes(); + setState(() { + _isRecordingVoice = false; + }); + if (pcmBytes.isEmpty) return; + await _prepareVoicePreview(connector, pcmBytes); + } + + Future _prepareVoicePreview(MeshCoreConnector connector, Uint8List pcmBytes) async { + final voiceService = VoiceMessageService.instance; + try { + final codec2Bytes = voiceService.encodePcmToCodec2(pcmBytes); + if (codec2Bytes.isEmpty) return; + final timestampSeconds = connector.reserveVoiceTimestampSeconds(); + final durationMs = voiceService.durationMsForCodec2Bytes(codec2Bytes); + final decodedPcm = voiceService.decodeCodec2ToPcm(codec2Bytes); + final fileName = voiceService.buildVoiceFileName( + senderKeyHex: widget.contact.publicKeyHex, + timestampSeconds: timestampSeconds, + outgoing: true, + ); + final voicePath = await voiceService.writeWavFile( + pcmBytes: decodedPcm, + fileName: fileName, + ); + + final previewMessage = Message( + senderKey: widget.contact.publicKey, + text: 'Voice message', + timestamp: DateTime.fromMillisecondsSinceEpoch(timestampSeconds * 1000), + isOutgoing: true, + isCli: false, + status: MessageStatus.pending, + isVoice: true, + voicePath: voicePath, + voiceDurationMs: durationMs, + voiceCodec: VoiceMessageService.codecName, + ); + + if (!mounted) return; + setState(() { + _pendingVoiceMessage = previewMessage; + _pendingVoiceCodec2Bytes = codec2Bytes; + _pendingVoiceTimestampSeconds = timestampSeconds; + _pendingVoiceDurationMs = durationMs; + _pendingVoicePath = voicePath; + }); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Voice message failed: $e')), + ); + } + } + + Widget _buildVoicePreview(ColorScheme colorScheme) { + final message = _pendingVoiceMessage; + if (message == null) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: VoiceMessageBubble( + message: message, + backgroundColor: colorScheme.surfaceContainerHighest, + textColor: colorScheme.onSurface, + metaColor: colorScheme.onSurface.withValues(alpha: 0.7), + isOutgoing: true, + ), + ); + } + + Future _sendPendingVoice(MeshCoreConnector connector) async { + final codec2Bytes = _pendingVoiceCodec2Bytes; + final voicePath = _pendingVoicePath; + final durationMs = _pendingVoiceDurationMs; + final timestampSeconds = _pendingVoiceTimestampSeconds; + + if (codec2Bytes == null || + codec2Bytes.isEmpty || + voicePath == null || + voicePath.isEmpty || + durationMs == null || + timestampSeconds == null) { + return; + } + if (!connector.isConnected) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Not connected to a MeshCore device')), + ); + return; + } + if (connector.isVoiceSending) { + return; + } + + await connector.sendVoiceMessage( + contact: widget.contact, + codec2Bytes: codec2Bytes, + voicePath: voicePath, + durationMs: durationMs, + timestampSeconds: timestampSeconds, + ); + unawaited(_clearPendingVoicePreview(deleteFile: false)); + } + + Future _clearPendingVoicePreview({required bool deleteFile, bool notify = true}) async { + final path = _pendingVoicePath; + if (notify && mounted) { + setState(() { + _pendingVoiceMessage = null; + _pendingVoiceCodec2Bytes = null; + _pendingVoiceTimestampSeconds = null; + _pendingVoiceDurationMs = null; + _pendingVoicePath = null; + }); + } else { + _pendingVoiceMessage = null; + _pendingVoiceCodec2Bytes = null; + _pendingVoiceTimestampSeconds = null; + _pendingVoiceDurationMs = null; + _pendingVoicePath = null; + } + if (deleteFile && path != null && path.isNotEmpty) { + try { + final file = File(path); + if (await file.exists()) { + await file.delete(); + } + } catch (_) { + return; + } + } + } + void _showPathHistory(BuildContext context) { final connector = Provider.of(context, listen: false); @@ -1024,14 +1279,15 @@ class _ChatScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - leading: const Icon(Icons.copy), - title: const Text('Copy'), - onTap: () { - Navigator.pop(sheetContext); - _copyMessageText(message.text); - }, - ), + if (!message.isVoice) + ListTile( + leading: const Icon(Icons.copy), + title: const Text('Copy'), + onTap: () { + Navigator.pop(sheetContext); + _copyMessageText(message.text); + }, + ), ListTile( leading: const Icon(Icons.delete_outline), title: const Text('Delete'), @@ -1040,7 +1296,9 @@ class _ChatScreenState extends State { await _deleteMessage(message); }, ), - if (message.isOutgoing && message.status == MessageStatus.failed) + if (message.isOutgoing && + message.status == MessageStatus.failed && + !message.isVoice) ListTile( leading: const Icon(Icons.refresh), title: const Text('Retry'), @@ -1154,7 +1412,15 @@ class _MessageBubble extends StatelessWidget { ), const SizedBox(height: 4), ], - if (poi != null) + if (message.isVoice) + VoiceMessageBubble( + message: message, + backgroundColor: bubbleColor, + textColor: textColor, + metaColor: metaColor, + isOutgoing: isOutgoing, + ) + else if (poi != null) _buildPoiMessage(context, poi, textColor, metaColor) else if (gifId != null) GifMessage( diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 8425725..79ee0ff 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -6,11 +6,17 @@ import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; import '../models/contact_group.dart'; import '../storage/contact_group_store.dart'; +import '../utils/contact_search.dart'; +import '../utils/emoji_utils.dart'; +import '../utils/route_transitions.dart'; +import '../widgets/quick_switch_bar.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/unread_badge.dart'; -import '../utils/emoji_utils.dart'; +import 'channels_screen.dart'; import 'chat_screen.dart'; +import 'map_screen.dart'; import 'repeater_hub_screen.dart'; +import 'settings_screen.dart'; enum ContactSortOption { lastSeen, @@ -19,8 +25,22 @@ enum ContactSortOption { type, } +enum _ContactMenuAction { + sortRecentMessages, + sortName, + sortType, + toggleLastSeenFilter, + toggleUnreadOnly, + newGroup, +} + class ContactsScreen extends StatefulWidget { - const ContactsScreen({super.key}); + final bool hideBackButton; + + const ContactsScreen({ + super.key, + this.hideBackButton = false, + }); @override State createState() => _ContactsScreenState(); @@ -30,6 +50,7 @@ class _ContactsScreenState extends State { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; ContactSortOption _sortOption = ContactSortOption.lastSeen; + bool _forceLastSeenSort = true; bool _showUnreadOnly = false; final ContactGroupStore _groupStore = ContactGroupStore(); List _groups = []; @@ -60,275 +81,309 @@ class _ContactsScreenState extends State { @override Widget build(BuildContext context) { + final connector = context.watch(); + + if (!connector.isConnected) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + Navigator.popUntil(context, (route) => route.isFirst); + } + }); + } + + final theme = Theme.of(context); + return Scaffold( appBar: AppBar( - title: const Text('Contacts'), - centerTitle: true, - actions: [ - PopupMenuButton( - icon: const Icon(Icons.sort), - tooltip: 'Sort by', - onSelected: (option) { - setState(() { - _sortOption = option; - }); - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: ContactSortOption.lastSeen, - child: Row( - children: [ - Icon( - Icons.access_time, - size: 20, - color: _sortOption == ContactSortOption.lastSeen - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 12), - Text( - 'Last Seen', - style: TextStyle( - fontWeight: _sortOption == ContactSortOption.lastSeen - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), + titleSpacing: 16, + centerTitle: false, + automaticallyImplyLeading: !widget.hideBackButton, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Contacts'), + Text( + '${connector.contacts.length} contacts', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, ), - PopupMenuItem( - value: ContactSortOption.recentMessages, - child: Row( - children: [ - Icon( - Icons.chat_bubble, - size: 20, - color: _sortOption == ContactSortOption.recentMessages - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 12), - Text( - 'Recent Messages', - style: TextStyle( - fontWeight: _sortOption == ContactSortOption.recentMessages - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: ContactSortOption.name, - child: Row( - children: [ - Icon( - Icons.sort_by_alpha, - size: 20, - color: _sortOption == ContactSortOption.name - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 12), - Text( - 'Name', - style: TextStyle( - fontWeight: _sortOption == ContactSortOption.name - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - PopupMenuItem( - value: ContactSortOption.type, - child: Row( - children: [ - Icon( - Icons.category, - size: 20, - color: _sortOption == ContactSortOption.type - ? Theme.of(context).primaryColor - : null, - ), - const SizedBox(width: 12), - Text( - 'Type', - style: TextStyle( - fontWeight: _sortOption == ContactSortOption.type - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ], - ), - ), - ], - ), - IconButton( - icon: Icon( - Icons.mark_chat_unread_outlined, - color: _showUnreadOnly ? Theme.of(context).primaryColor : null, ), - tooltip: _showUnreadOnly ? 'Showing unread only' : 'Show unread only', - onPressed: () { - setState(() { - _showUnreadOnly = !_showUnreadOnly; - }); - }, + ], + ), + actions: [ + IconButton( + icon: connector.isLoadingContacts + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + tooltip: 'Refresh', + onPressed: connector.isLoadingContacts ? null : () => connector.getContacts(), ), IconButton( - icon: const Icon(Icons.group_add), - tooltip: 'New group', - onPressed: () { - final contacts = context.read().contacts; - _showGroupEditor(context, contacts); - }, + icon: const Icon(Icons.bluetooth_disabled), + tooltip: 'Disconnect', + onPressed: () => _disconnect(context, connector), ), - Consumer( - builder: (context, connector, child) { - return IconButton( - icon: connector.isLoadingContacts - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh), - onPressed: connector.isLoadingContacts - ? null - : () => connector.getContacts(), + IconButton( + icon: const Icon(Icons.tune), + tooltip: 'Settings', + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + PopupMenuButton<_ContactMenuAction>( + tooltip: 'Contacts options', + onSelected: (action) { + switch (action) { + case _ContactMenuAction.sortRecentMessages: + setState(() { + _sortOption = ContactSortOption.recentMessages; + _forceLastSeenSort = false; + }); + break; + case _ContactMenuAction.sortName: + setState(() { + _sortOption = ContactSortOption.name; + _forceLastSeenSort = false; + }); + break; + case _ContactMenuAction.sortType: + setState(() { + _sortOption = ContactSortOption.type; + _forceLastSeenSort = false; + }); + break; + case _ContactMenuAction.toggleLastSeenFilter: + setState(() { + _forceLastSeenSort = !_forceLastSeenSort; + if (_forceLastSeenSort) { + _sortOption = ContactSortOption.lastSeen; + } + }); + break; + case _ContactMenuAction.toggleUnreadOnly: + setState(() { + _showUnreadOnly = !_showUnreadOnly; + }); + break; + case _ContactMenuAction.newGroup: + _showGroupEditor(context, connector.contacts); + break; + } + }, + itemBuilder: (context) { + final labelStyle = theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, ); + return [ + PopupMenuItem<_ContactMenuAction>( + enabled: false, + child: Text('Sort by', style: labelStyle), + ), + CheckedPopupMenuItem<_ContactMenuAction>( + value: _ContactMenuAction.sortRecentMessages, + checked: _sortOption == ContactSortOption.recentMessages, + child: const Text('Recent messages'), + ), + CheckedPopupMenuItem<_ContactMenuAction>( + value: _ContactMenuAction.sortName, + checked: _sortOption == ContactSortOption.name, + child: const Text('Name'), + ), + CheckedPopupMenuItem<_ContactMenuAction>( + value: _ContactMenuAction.sortType, + checked: _sortOption == ContactSortOption.type, + child: const Text('Type'), + ), + const PopupMenuDivider(), + PopupMenuItem<_ContactMenuAction>( + enabled: false, + child: Text('Filters', style: labelStyle), + ), + CheckedPopupMenuItem<_ContactMenuAction>( + value: _ContactMenuAction.toggleLastSeenFilter, + checked: _forceLastSeenSort, + child: const Text('Last seen'), + ), + CheckedPopupMenuItem<_ContactMenuAction>( + value: _ContactMenuAction.toggleUnreadOnly, + checked: _showUnreadOnly, + child: const Text('Unread only'), + ), + PopupMenuItem<_ContactMenuAction>( + value: _ContactMenuAction.newGroup, + child: const Text('New group'), + ), + ]; }, ), ], ), - body: Consumer( - builder: (context, connector, child) { - final contacts = connector.contacts; - - if (contacts.isEmpty && connector.isLoadingContacts && _groups.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } - - if (contacts.isEmpty && _groups.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.people_outline, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - 'No contacts yet', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - const SizedBox(height: 8), - Text( - 'Contacts will appear when devices advertise', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), - ), - ], - ), - ); - } - - final filteredAndSorted = _filterAndSortContacts(contacts, connector); - final filteredGroups = - _showUnreadOnly ? const [] : _filterAndSortGroups(_groups, contacts); - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'Search contacts...', - prefixIcon: const Icon(Icons.search), - suffixIcon: _searchQuery.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchController.clear(); - setState(() { - _searchQuery = ''; - }); - }, - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - onChanged: (value) { - setState(() { - _searchQuery = value.toLowerCase(); - }); - }, - ), - ), - Expanded( - child: filteredAndSorted.isEmpty && filteredGroups.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - _showUnreadOnly - ? 'No unread contacts' - : 'No contacts or groups found', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - ], - ), - ) - : RefreshIndicator( - onRefresh: () => connector.getContacts(), - child: ListView.builder( - itemCount: filteredGroups.length + filteredAndSorted.length, - itemBuilder: (context, index) { - if (index < filteredGroups.length) { - final group = filteredGroups[index]; - return _buildGroupTile(context, group, contacts); - } - final contact = filteredAndSorted[index - filteredGroups.length]; - final unreadCount = connector.getUnreadCountForContact(contact); - return _ContactTile( - contact: contact, - unreadCount: unreadCount, - onTap: () => _openChat(context, contact), - onLongPress: () => _showContactOptions(context, connector, contact), - ); - }, - ), - ), - ), - ], - ); - }, + body: _buildContactsBody(context, connector), + bottomNavigationBar: SafeArea( + top: false, + child: QuickSwitchBar( + selectedIndex: 0, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + ), ), ); } + Future _disconnect( + BuildContext context, + MeshCoreConnector connector, + ) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Disconnect'), + content: const Text('Are you sure you want to disconnect from this device?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Disconnect'), + ), + ], + ), + ); + + if (confirmed == true) { + await connector.disconnect(); + } + } + + Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) { + final contacts = connector.contacts; + + if (contacts.isEmpty && connector.isLoadingContacts && _groups.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (contacts.isEmpty && _groups.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.people_outline, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No contacts yet', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + const SizedBox(height: 8), + Text( + 'Contacts will appear when devices advertise', + style: TextStyle(fontSize: 14, color: Colors.grey[500]), + ), + ], + ), + ); + } + + final filteredAndSorted = _filterAndSortContacts(contacts, connector); + final filteredGroups = + _showUnreadOnly ? const [] : _filterAndSortGroups(_groups, contacts); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Search contacts...', + prefixIcon: const Icon(Icons.search), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + setState(() { + _searchQuery = ''; + }); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + onChanged: (value) { + setState(() { + _searchQuery = value.toLowerCase(); + }); + }, + ), + ), + Expanded( + child: filteredAndSorted.isEmpty && filteredGroups.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.search_off, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + _showUnreadOnly + ? 'No unread contacts' + : 'No contacts or groups found', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ], + ), + ) + : RefreshIndicator( + onRefresh: () => connector.getContacts(), + child: ListView.builder( + itemCount: filteredGroups.length + filteredAndSorted.length, + itemBuilder: (context, index) { + if (index < filteredGroups.length) { + final group = filteredGroups[index]; + return _buildGroupTile(context, group, contacts); + } + final contact = filteredAndSorted[index - filteredGroups.length]; + final unreadCount = connector.getUnreadCountForContact(contact); + return _ContactTile( + contact: contact, + lastSeen: _resolveLastSeen(contact), + unreadCount: unreadCount, + onTap: () => _openChat(context, contact), + onLongPress: () => _showContactOptions(context, connector, contact), + ); + }, + ), + ), + ), + ], + ); + } + List _filterAndSortGroups(List groups, List contacts) { final query = _searchQuery.trim().toLowerCase(); - final contactNames = {}; + final contactsByKey = {}; for (final contact in contacts) { - contactNames[contact.publicKeyHex] = contact.name.toLowerCase(); + contactsByKey[contact.publicKeyHex] = contact; } final filtered = groups.where((group) { if (query.isEmpty) return true; if (group.name.toLowerCase().contains(query)) return true; for (final key in group.memberKeys) { - final name = contactNames[key]; - if (name != null && name.contains(query)) return true; + final contact = contactsByKey[key]; + if (contact != null && matchesContactQuery(contact, query)) return true; } return false; }).toList(); @@ -340,7 +395,7 @@ class _ContactsScreenState extends State { List _filterAndSortContacts(List contacts, MeshCoreConnector connector) { var filtered = contacts.where((contact) { if (_searchQuery.isEmpty) return true; - return contact.name.toLowerCase().contains(_searchQuery); + return matchesContactQuery(contact, _searchQuery); }).toList(); if (_showUnreadOnly) { @@ -349,9 +404,10 @@ class _ContactsScreenState extends State { }).toList(); } - switch (_sortOption) { + final sortOption = _forceLastSeenSort ? ContactSortOption.lastSeen : _sortOption; + switch (sortOption) { case ContactSortOption.lastSeen: - filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); + filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a))); break; case ContactSortOption.recentMessages: filtered.sort((a, b) { @@ -377,6 +433,13 @@ class _ContactsScreenState extends State { return filtered; } + DateTime _resolveLastSeen(Contact contact) { + if (contact.type != advTypeChat) return contact.lastSeen; + return contact.lastMessageAt.isAfter(contact.lastSeen) + ? contact.lastMessageAt + : contact.lastSeen; + } + Widget _buildGroupTile(BuildContext context, ContactGroup group, List contacts) { final memberContacts = _resolveGroupContacts(group, contacts); final subtitle = _formatGroupMembers(memberContacts); @@ -432,6 +495,28 @@ class _ContactsScreenState extends State { } } + void _handleQuickSwitch(int index, BuildContext context) { + if (index == 0) return; + switch (index) { + case 1: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute( + const ChannelsScreen(hideBackButton: true), + ), + ); + break; + case 2: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute( + const MapScreen(hideBackButton: true), + ), + ); + break; + } + } + void _showRepeaterLogin(BuildContext context, Contact repeater) { showDialog( context: context, @@ -542,7 +627,7 @@ class _ContactsScreenState extends State { final filteredContacts = filterQuery.isEmpty ? sortedContacts : sortedContacts - .where((contact) => contact.name.toLowerCase().contains(filterQuery)) + .where((contact) => matchesContactQuery(contact, filterQuery)) .toList(); return AlertDialog( title: Text(isEditing ? 'Edit Group' : 'New Group'), @@ -728,12 +813,14 @@ class _ContactsScreenState extends State { class _ContactTile extends StatelessWidget { final Contact contact; + final DateTime lastSeen; final int unreadCount; final VoidCallback onTap; final VoidCallback onLongPress; const _ContactTile({ required this.contact, + required this.lastSeen, required this.unreadCount, required this.onTap, required this.onLongPress, @@ -757,7 +844,7 @@ class _ContactTile extends StatelessWidget { const SizedBox(height: 4), ], Text( - _formatLastSeen(contact.lastSeen), + _formatLastSeen(lastSeen), style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), if (contact.hasLocation) @@ -814,10 +901,13 @@ class _ContactTile extends StatelessWidget { final now = DateTime.now(); final diff = now.difference(lastSeen); - if (diff.inMinutes < 1) return 'Just now'; - if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; - if (diff.inHours < 24) return '${diff.inHours}h ago'; - if (diff.inDays < 7) return '${diff.inDays}d ago'; - return '${lastSeen.month}/${lastSeen.day}'; + if (diff.isNegative || diff.inMinutes < 5) return 'Last seen now'; + if (diff.inMinutes < 60) return 'Last seen ${diff.inMinutes} mins ago'; + if (diff.inHours < 24) { + final hours = diff.inHours; + return hours == 1 ? 'Last seen 1 hour ago' : 'Last seen $hours hours ago'; + } + final days = diff.inDays; + return days == 1 ? 'Last seen 1 day ago' : 'Last seen $days days ago'; } } diff --git a/lib/screens/device_screen.dart b/lib/screens/device_screen.dart index 26d694e..d99aa57 100644 --- a/lib/screens/device_screen.dart +++ b/lib/screens/device_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../utils/route_transitions.dart'; +import '../widgets/quick_switch_bar.dart'; import 'channels_screen.dart'; import 'contacts_screen.dart'; import 'map_screen.dart'; @@ -17,6 +19,7 @@ class DeviceScreen extends StatefulWidget { class _DeviceScreenState extends State { bool _showBatteryVoltage = false; + int _quickIndex = 0; @override Widget build(BuildContext context) { @@ -31,14 +34,26 @@ class _DeviceScreenState extends State { }); } + final theme = Theme.of(context); + return PopScope( canPop: false, child: Scaffold( appBar: AppBar( - title: Text(connector.deviceDisplayName), - centerTitle: true, - automaticallyImplyLeading: false, + titleSpacing: 16, + centerTitle: false, + title: _buildAppBarTitle(connector, theme), actions: [ + IconButton( + icon: const Icon(Icons.tune), + tooltip: 'Settings', + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsScreen(), + ), + ), + ), IconButton( icon: const Icon(Icons.bluetooth_disabled), tooltip: 'Disconnect', @@ -46,20 +61,15 @@ class _DeviceScreenState extends State { ), ], ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + body: SafeArea( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), children: [ - // Connection status card - _buildStatusCard(connector, context), - - const SizedBox(height: 24), - - // Navigation grid - Expanded( - child: _buildNavigationGrid(context), - ), + _buildConnectionCard(connector, context), + const SizedBox(height: 16), + _buildSectionLabel(theme, 'Quick switch'), + const SizedBox(height: 12), + _buildQuickSwitchBar(context), ], ), ), @@ -69,54 +79,114 @@ class _DeviceScreenState extends State { ); } - Widget _buildStatusCard(MeshCoreConnector connector, BuildContext context) { + Widget _buildAppBarTitle(MeshCoreConnector connector, ThemeData theme) { + final colorScheme = theme.colorScheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'MeshCore', + style: theme.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + color: colorScheme.onSurfaceVariant, + ), + ), + Text( + connector.deviceDisplayName, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ); + } + + Widget _buildSectionLabel(ThemeData theme, String text) { + return Text( + text, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: 0.6, + color: theme.colorScheme.onSurfaceVariant, + ), + ); + } + + Widget _buildConnectionCard( + MeshCoreConnector connector, + BuildContext context, + ) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + return Card( + elevation: 0, + color: colorScheme.surfaceVariant, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon(Icons.bluetooth_connected, color: Colors.green, size: 32), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - connector.deviceDisplayName, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - connector.deviceIdLabel, - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: Colors.green.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(16), - ), - child: const Text( - 'Connected', - style: TextStyle( - color: Colors.green, - fontWeight: FontWeight.w500, - ), + CircleAvatar( + radius: 24, + backgroundColor: colorScheme.primaryContainer, + child: Icon( + Icons.wifi_tethering_rounded, + color: colorScheme.onPrimaryContainer, ), ), - const SizedBox(height: 8), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + connector.deviceDisplayName, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + connector.deviceIdLabel, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Chip( + avatar: Icon( + Icons.check_circle, + size: 18, + color: colorScheme.onSecondaryContainer, + ), + label: const Text('Connected'), + backgroundColor: colorScheme.secondaryContainer, + labelStyle: theme.textTheme.labelMedium?.copyWith( + color: colorScheme.onSecondaryContainer, + fontWeight: FontWeight.w600, + ), + visualDensity: VisualDensity.compact, + ), _buildBatteryIndicator(connector, context), ], ), @@ -126,7 +196,22 @@ class _DeviceScreenState extends State { ); } - Widget _buildBatteryIndicator(MeshCoreConnector connector, BuildContext context) { + Widget _buildQuickSwitchBar(BuildContext context) { + return QuickSwitchBar( + selectedIndex: _quickIndex, + onDestinationSelected: (index) { + _openQuickDestination(index, context); + }, + ); + } + + + Widget _buildBatteryIndicator( + MeshCoreConnector connector, + BuildContext context, + ) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; final percent = connector.batteryPercent; final millivolts = connector.batteryMillivolts; final percentLabel = percent != null ? '$percent%' : '--%'; @@ -136,31 +221,24 @@ class _DeviceScreenState extends State { final displayLabel = _showBatteryVoltage ? voltageLabel : percentLabel; final icon = _batteryIcon(percent); - return InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () { + return ActionChip( + avatar: Icon( + icon, + size: 16, + color: colorScheme.onSecondaryContainer, + ), + label: Text(displayLabel), + labelStyle: theme.textTheme.labelMedium?.copyWith( + color: colorScheme.onSecondaryContainer, + fontWeight: FontWeight.w600, + ), + backgroundColor: colorScheme.secondaryContainer, + visualDensity: VisualDensity.compact, + onPressed: () { setState(() { _showBatteryVoltage = !_showBatteryVoltage; }); }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 18, color: Colors.grey[700]), - const SizedBox(width: 4), - Text( - displayLabel, - style: TextStyle( - fontSize: 12, - color: Colors.grey[700], - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), ); } @@ -170,89 +248,44 @@ class _DeviceScreenState extends State { return Icons.battery_full; } - Widget _buildNavigationGrid(BuildContext context) { - final items = [ - _NavItem( - icon: Icons.people_outline, - label: 'Contacts', - color: Colors.blue, - onTap: () => Navigator.push( + void _openQuickDestination(int index, BuildContext context) { + if (_quickIndex != index) { + setState(() { + _quickIndex = index; + }); + } + switch (index) { + case 0: + Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const ContactsScreen()), - ), - ), - _NavItem( - icon: Icons.tag, - label: 'Channels', - color: Colors.green, - onTap: () => Navigator.push( + buildQuickSwitchRoute( + const ContactsScreen(hideBackButton: true), + ), + ); + break; + case 1: + Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const ChannelsScreen()), - ), - ), - _NavItem( - icon: Icons.map_outlined, - label: 'Map', - color: Colors.orange, - onTap: () => Navigator.push( + buildQuickSwitchRoute( + const ChannelsScreen(hideBackButton: true), + ), + ); + break; + case 2: + Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const MapScreen()), - ), - ), - _NavItem( - icon: Icons.settings_outlined, - label: 'Settings', - color: Colors.grey, - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ), - ), - ]; - - return GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 1.2, - ), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - return _buildNavCard(item); - }, - ); + buildQuickSwitchRoute( + const MapScreen(hideBackButton: true), + ), + ); + break; + } } - Widget _buildNavCard(_NavItem item) { - return Card( - child: InkWell( - onTap: item.onTap, - borderRadius: BorderRadius.circular(12), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - item.icon, - size: 48, - color: item.color, - ), - const SizedBox(height: 12), - Text( - item.label, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ); - } - - Future _disconnect(BuildContext context, MeshCoreConnector connector) async { + Future _disconnect( + BuildContext context, + MeshCoreConnector connector, + ) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -276,17 +309,3 @@ class _DeviceScreenState extends State { } } } - -class _NavItem { - final IconData icon; - final String label; - final Color color; - final VoidCallback onTap; - - _NavItem({ - required this.icon, - required this.label, - required this.color, - required this.onTap, - }); -} diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart new file mode 100644 index 0000000..dafd69c --- /dev/null +++ b/lib/screens/map_cache_screen.dart @@ -0,0 +1,390 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../services/app_settings_service.dart'; +import '../services/map_tile_cache_service.dart'; + +class MapCacheScreen extends StatefulWidget { + const MapCacheScreen({super.key}); + + @override + State createState() => _MapCacheScreenState(); +} + +class _MapCacheScreenState extends State { + final MapController _mapController = MapController(); + + LatLngBounds? _selectedBounds; + int _minZoom = MapTileCacheService.defaultMinZoom; + int _maxZoom = MapTileCacheService.defaultMaxZoom; + int _estimatedTiles = 0; + bool _isDownloading = false; + int _completedTiles = 0; + int _failedTiles = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _loadSettings(); + }); + } + + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } + + void _loadSettings() { + final settings = context.read().settings; + final bounds = MapTileCacheService.boundsFromJson(settings.mapCacheBounds); + final minZoom = settings.mapCacheMinZoom.clamp(3, 18); + final maxZoom = settings.mapCacheMaxZoom.clamp(3, 18); + final safeMin = minZoom <= maxZoom ? minZoom : maxZoom; + final safeMax = minZoom <= maxZoom ? maxZoom : minZoom; + setState(() { + _minZoom = safeMin; + _maxZoom = safeMax; + _selectedBounds = bounds; + }); + _updateEstimate(); + if (bounds != null) { + _mapController.fitCamera( + CameraFit.bounds( + bounds: bounds, + padding: const EdgeInsets.all(48), + ), + ); + } + } + + void _updateEstimate() { + if (_selectedBounds == null) { + setState(() { + _estimatedTiles = 0; + }); + return; + } + final cacheService = context.read(); + final count = + cacheService.estimateTileCount(_selectedBounds!, _minZoom, _maxZoom); + setState(() { + _estimatedTiles = count; + }); + } + + Future _setBoundsFromView() async { + final bounds = _mapController.camera.visibleBounds; + await _saveBounds(bounds); + } + + Future _saveBounds(LatLngBounds bounds) async { + setState(() { + _selectedBounds = bounds; + }); + final settings = context.read(); + await settings.setMapCacheBounds(MapTileCacheService.boundsToJson(bounds)); + _updateEstimate(); + } + + Future _clearBounds() async { + setState(() { + _selectedBounds = null; + _estimatedTiles = 0; + }); + final settings = context.read(); + await settings.setMapCacheBounds(null); + } + + Future _saveZoomRange() async { + final settings = context.read(); + await settings.setMapCacheZoomRange(_minZoom, _maxZoom); + _updateEstimate(); + } + + Future _startDownload() async { + final bounds = _selectedBounds; + if (bounds == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Select an area to cache first')), + ); + return; + } + + if (_estimatedTiles == 0) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No tiles to download for this area')), + ); + return; + } + + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Download tiles'), + content: Text( + 'Download $_estimatedTiles tiles for offline use?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext, true), + child: const Text('Download'), + ), + ], + ), + ); + + if (confirmed != true) return; + + setState(() { + _isDownloading = true; + _completedTiles = 0; + _failedTiles = 0; + }); + + final cacheService = context.read(); + final result = await cacheService.downloadRegion( + bounds: bounds, + minZoom: _minZoom, + maxZoom: _maxZoom, + onProgress: (progress) { + if (!mounted) return; + setState(() { + _completedTiles = progress.completed; + _failedTiles = progress.failed; + }); + }, + ); + + if (!mounted) return; + + setState(() { + _isDownloading = false; + _completedTiles = result.downloaded + result.failed; + _failedTiles = result.failed; + }); + + final message = result.failed > 0 + ? 'Cached ${result.downloaded} tiles (${result.failed} failed)' + : 'Cached ${result.downloaded} tiles'; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + Future _clearCache() async { + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Clear offline cache'), + content: const Text('Remove all cached map tiles?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext, true), + child: const Text('Clear'), + ), + ], + ), + ); + if (confirmed != true) return; + + final cacheService = context.read(); + await cacheService.clearCache(); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Offline cache cleared')), + ); + } + + @override + Widget build(BuildContext context) { + final tileCache = context.read(); + final selectedBounds = _selectedBounds; + final progressValue = _estimatedTiles == 0 + ? 0.0 + : (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble(); + + return Scaffold( + appBar: AppBar( + title: const Text('Offline Map Cache'), + centerTitle: true, + ), + body: Column( + children: [ + Expanded( + child: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: const MapOptions( + initialCenter: LatLng(0, 0), + initialZoom: 2.0, + minZoom: 2.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + if (selectedBounds != null) + PolygonLayer( + polygons: [ + Polygon( + points: _boundsToPolygon(selectedBounds), + borderStrokeWidth: 2, + color: Colors.blue.withValues(alpha: 0.2), + borderColor: Colors.blue, + ), + ], + ), + ], + ), + Positioned( + top: 12, + right: 12, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + selectedBounds == null + ? 'No area selected' + : _formatBounds(selectedBounds), + style: const TextStyle(fontSize: 12), + ), + ), + ), + ), + ], + ), + ), + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Cache Area', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.crop_free), + label: const Text('Use Current View'), + onPressed: _isDownloading ? null : _setBoundsFromView, + ), + ), + const SizedBox(width: 12), + TextButton( + onPressed: + _isDownloading || selectedBounds == null ? null : _clearBounds, + child: const Text('Clear'), + ), + ], + ), + const SizedBox(height: 12), + const Text( + 'Zoom Range', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + RangeSlider( + values: + RangeValues(_minZoom.toDouble(), _maxZoom.toDouble()), + min: 3, + max: 18, + divisions: 15, + labels: RangeLabels('$_minZoom', '$_maxZoom'), + onChanged: _isDownloading + ? null + : (values) { + setState(() { + _minZoom = values.start.round(); + _maxZoom = values.end.round(); + }); + }, + onChangeEnd: _isDownloading + ? null + : (_) { + _saveZoomRange(); + }, + ), + Text('Estimated tiles: $_estimatedTiles'), + if (_isDownloading) ...[ + const SizedBox(height: 8), + LinearProgressIndicator(value: progressValue), + const SizedBox(height: 4), + Text('Downloaded $_completedTiles / $_estimatedTiles'), + ], + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.download), + label: const Text('Download Tiles'), + onPressed: _isDownloading || selectedBounds == null + ? null + : _startDownload, + ), + ), + const SizedBox(width: 12), + OutlinedButton( + onPressed: _isDownloading ? null : _clearCache, + child: const Text('Clear Cache'), + ), + ], + ), + if (_failedTiles > 0 && !_isDownloading) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + 'Failed downloads: $_failedTiles', + style: TextStyle(color: Colors.orange[700]), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + List _boundsToPolygon(LatLngBounds bounds) { + return [ + bounds.northWest, + bounds.northEast, + bounds.southEast, + bounds.southWest, + ]; + } + + String _formatBounds(LatLngBounds bounds) { + return 'N ${bounds.north.toStringAsFixed(4)}, ' + 'S ${bounds.south.toStringAsFixed(4)}, ' + 'E ${bounds.east.toStringAsFixed(4)}, ' + 'W ${bounds.west.toStringAsFixed(4)}'; + } +} diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index de1a200..f3c497d 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -9,18 +9,27 @@ import '../models/channel.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; import '../services/map_marker_service.dart'; +import '../services/map_tile_cache_service.dart'; +import '../utils/contact_search.dart'; +import '../utils/route_transitions.dart'; +import '../widgets/quick_switch_bar.dart'; +import 'channels_screen.dart'; import 'chat_screen.dart'; +import 'contacts_screen.dart'; +import 'settings_screen.dart'; class MapScreen extends StatefulWidget { final LatLng? highlightPosition; final String? highlightLabel; final double highlightZoom; + final bool hideBackButton; const MapScreen({ super.key, this.highlightPosition, this.highlightLabel, this.highlightZoom = 15.0, + this.hideBackButton = false, }); @override @@ -60,6 +69,7 @@ class _MapScreenState extends State { Widget build(BuildContext context) { return Consumer2( builder: (context, connector, settingsService, child) { + final tileCache = context.read(); final settings = settingsService.settings; final contacts = connector.contacts; final highlightPosition = widget.highlightPosition; @@ -124,6 +134,22 @@ class _MapScreenState extends State { appBar: AppBar( title: const Text('Node Map'), centerTitle: true, + automaticallyImplyLeading: !widget.hideBackButton, + actions: [ + IconButton( + icon: const Icon(Icons.tune), + tooltip: 'Settings', + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + IconButton( + icon: const Icon(Icons.bluetooth_disabled), + tooltip: 'Disconnect', + onPressed: () => _disconnect(context, connector), + ), + ], ), body: !hasMapContent ? _buildEmptyState() @@ -173,8 +199,10 @@ class _MapScreenState extends State { ), children: [ TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.meshcore.open', + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, maxZoom: 19, ), MarkerLayer( @@ -199,6 +227,13 @@ class _MapScreenState extends State { _buildLegend(contactsWithLocation.length, sharedMarkers.length), ], ), + bottomNavigationBar: SafeArea( + top: false, + child: QuickSwitchBar( + selectedIndex: 2, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + ), + ), floatingActionButton: FloatingActionButton( onPressed: () => _showFilterDialog(context, settingsService), child: const Icon(Icons.filter_list), @@ -556,6 +591,55 @@ class _MapScreenState extends State { ); } + void _handleQuickSwitch(int index, BuildContext context) { + if (index == 2) return; + switch (index) { + case 0: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute( + const ContactsScreen(hideBackButton: true), + ), + ); + break; + case 1: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute( + const ChannelsScreen(hideBackButton: true), + ), + ); + break; + } + } + + Future _disconnect( + BuildContext context, + MeshCoreConnector connector, + ) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Disconnect'), + content: const Text('Are you sure you want to disconnect from this device?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Disconnect'), + ), + ], + ), + ); + + if (confirmed == true) { + await connector.disconnect(); + } + } + void _showMarkerInfo(_SharedMarker marker) { showDialog( context: context, @@ -792,8 +876,7 @@ class _MapScreenState extends State { ), ...allContacts .where((contact) => - query.isEmpty || - contact.name.toLowerCase().contains(query)) + query.isEmpty || matchesContactQuery(contact, query)) .map((contact) { return ListTile( leading: const Icon(Icons.person), diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index b558ed8..b0e31e1 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../widgets/device_tile.dart'; -import 'device_screen.dart'; +import 'contacts_screen.dart'; /// Screen for scanning and connecting to MeshCore devices class ScannerScreen extends StatelessWidget { @@ -161,7 +161,7 @@ class ScannerScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => const DeviceScreen(), + builder: (context) => const ContactsScreen(), ), ); } diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index ac8e64c..e79f973 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -73,6 +73,21 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setMapCacheBounds(Map? value) async { + await updateSettings(_settings.copyWith(mapCacheBounds: value)); + } + + Future setMapCacheZoomRange(int minZoom, int maxZoom) async { + final safeMin = minZoom <= maxZoom ? minZoom : maxZoom; + final safeMax = minZoom <= maxZoom ? maxZoom : minZoom; + await updateSettings( + _settings.copyWith( + mapCacheMinZoom: safeMin, + mapCacheMaxZoom: safeMax, + ), + ); + } + Future setNotificationsEnabled(bool value) async { await updateSettings(_settings.copyWith(notificationsEnabled: value)); } @@ -81,6 +96,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(notifyOnNewMessage: value)); } + Future setNotifyOnNewChannelMessage(bool value) async { + await updateSettings(_settings.copyWith(notifyOnNewChannelMessage: value)); + } + Future setNotifyOnNewAdvert(bool value) async { await updateSettings(_settings.copyWith(notifyOnNewAdvert: value)); } diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart new file mode 100644 index 0000000..fce77a1 --- /dev/null +++ b/lib/services/background_service.dart @@ -0,0 +1,82 @@ +import 'dart:isolate'; +import 'dart:io'; + +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; + +class BackgroundService { + bool _initialized = false; + + Future initialize() async { + if (!Platform.isAndroid || _initialized) return; + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'meshcore_background', + channelName: 'MeshCore Background', + channelDescription: 'Keeps MeshCore running in the background.', + channelImportance: NotificationChannelImportance.LOW, + priority: NotificationPriority.LOW, + iconData: const NotificationIconData( + resType: ResourceType.mipmap, + resPrefix: ResourcePrefix.ic, + name: 'launcher', + ), + ), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: false, + playSound: false, + ), + foregroundTaskOptions: const ForegroundTaskOptions( + interval: 5000, + autoRunOnBoot: false, + allowWakeLock: true, + allowWifiLock: false, + ), + ); + _initialized = true; + } + + Future start() async { + if (!Platform.isAndroid) return; + if (!_initialized) { + await initialize(); + } + final running = await FlutterForegroundTask.isRunningService; + if (running) return; + await FlutterForegroundTask.startService( + notificationTitle: 'MeshCore running', + notificationText: 'Keeping BLE connected', + callback: startCallback, + ); + } + + Future stop() async { + if (!Platform.isAndroid) return; + final running = await FlutterForegroundTask.isRunningService; + if (!running) return; + await FlutterForegroundTask.stopService(); + } +} + +@pragma('vm:entry-point') +void startCallback() { + FlutterForegroundTask.setTaskHandler(_MeshCoreTaskHandler()); +} + +class _MeshCoreTaskHandler extends TaskHandler { + @override + void onStart(DateTime timestamp, SendPort? sendPort) {} + + @override + void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {} + + @override + void onDestroy(DateTime timestamp, SendPort? sendPort) {} + + @override + void onNotificationButtonPressed(String id) {} + + @override + void onNotificationPressed() { + FlutterForegroundTask.launchApp('/'); + } +} diff --git a/lib/services/codec2_ffi.dart b/lib/services/codec2_ffi.dart new file mode 100644 index 0000000..4f3bce7 --- /dev/null +++ b/lib/services/codec2_ffi.dart @@ -0,0 +1,152 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +const int _codec2Mode1300 = 4; + +class Codec2Ffi { + Codec2Ffi._(this._lib) + : _codec2Create = _lib + .lookupFunction<_codec2_create_c, _codec2_create_d>('codec2_create'), + _codec2Destroy = _lib + .lookupFunction<_codec2_destroy_c, _codec2_destroy_d>('codec2_destroy'), + _codec2Encode = _lib + .lookupFunction<_codec2_encode_c, _codec2_encode_d>('codec2_encode'), + _codec2Decode = _lib + .lookupFunction<_codec2_decode_c, _codec2_decode_d>('codec2_decode'), + _codec2SamplesPerFrame = _lib.lookupFunction<_codec2_samples_per_frame_c, + _codec2_samples_per_frame_d>('codec2_samples_per_frame'), + _codec2BytesPerFrame = _lib.lookupFunction<_codec2_bytes_per_frame_c, + _codec2_bytes_per_frame_d>('codec2_bytes_per_frame'); + + static final Codec2Ffi instance = Codec2Ffi._(_openLibrary()); + + final DynamicLibrary _lib; + final _codec2_create_d _codec2Create; + final _codec2_destroy_d _codec2Destroy; + final _codec2_encode_d _codec2Encode; + final _codec2_decode_d _codec2Decode; + final _codec2_samples_per_frame_d _codec2SamplesPerFrame; + final _codec2_bytes_per_frame_d _codec2BytesPerFrame; + + Codec2Session createSession() { + final handle = _codec2Create(_codec2Mode1300); + if (handle == nullptr) { + throw StateError('codec2_create returned null'); + } + return Codec2Session._( + handle: handle, + destroy: _codec2Destroy, + encode: _codec2Encode, + decode: _codec2Decode, + samplesPerFrame: _codec2SamplesPerFrame, + bytesPerFrame: _codec2BytesPerFrame, + ); + } + + static DynamicLibrary _openLibrary() { + if (Platform.isAndroid) { + return DynamicLibrary.open('libcodec2.so'); + } + if (Platform.isIOS || Platform.isMacOS) { + return DynamicLibrary.process(); + } + throw UnsupportedError('Codec2 is only supported on Android and iOS.'); + } +} + +class Codec2Session { + Codec2Session._({ + required this.handle, + required this.destroy, + required this.encode, + required this.decode, + required this.samplesPerFrame, + required this.bytesPerFrame, + }); + + final Pointer handle; + final _codec2_destroy_d destroy; + final _codec2_encode_d encode; + final _codec2_decode_d decode; + final _codec2_samples_per_frame_d samplesPerFrame; + final _codec2_bytes_per_frame_d bytesPerFrame; + + int get samplesPerFrameValue => samplesPerFrame(handle); + int get bytesPerFrameValue => bytesPerFrame(handle); + + Uint8List encodePcmFrame(Int16List pcmFrame) { + final bytesOut = calloc(bytesPerFrameValue); + final pcmIn = calloc(samplesPerFrameValue); + try { + final sampleCount = samplesPerFrameValue; + final pcmBuffer = pcmIn.asTypedList(sampleCount); + final copyLen = pcmFrame.length < sampleCount ? pcmFrame.length : sampleCount; + pcmBuffer.setRange(0, copyLen, pcmFrame); + if (copyLen < sampleCount) { + for (var i = copyLen; i < sampleCount; i++) { + pcmBuffer[i] = 0; + } + } + encode(handle, bytesOut, pcmIn); + return Uint8List.fromList(bytesOut.asTypedList(bytesPerFrameValue)); + } finally { + calloc.free(bytesOut); + calloc.free(pcmIn); + } + } + + Int16List decodeCodecFrame(Uint8List codecFrame) { + final pcmOut = calloc(samplesPerFrameValue); + final bytesIn = calloc(bytesPerFrameValue); + try { + final codecBuffer = bytesIn.asTypedList(bytesPerFrameValue); + codecBuffer.setRange(0, bytesPerFrameValue, codecFrame); + decode(handle, pcmOut, bytesIn); + return Int16List.fromList(pcmOut.asTypedList(samplesPerFrameValue)); + } finally { + calloc.free(bytesIn); + calloc.free(pcmOut); + } + } + + void dispose() { + destroy(handle); + } +} + +typedef _codec2_create_c = Pointer Function(Int32 mode); +typedef _codec2_create_d = Pointer Function(int mode); + +typedef _codec2_destroy_c = Void Function(Pointer codec2State); +typedef _codec2_destroy_d = void Function(Pointer codec2State); + +typedef _codec2_encode_c = Void Function( + Pointer codec2State, + Pointer bytes, + Pointer speechIn, +); +typedef _codec2_encode_d = void Function( + Pointer codec2State, + Pointer bytes, + Pointer speechIn, +); + +typedef _codec2_decode_c = Void Function( + Pointer codec2State, + Pointer speechOut, + Pointer bytes, +); +typedef _codec2_decode_d = void Function( + Pointer codec2State, + Pointer speechOut, + Pointer bytes, +); + +typedef _codec2_samples_per_frame_c = Int32 Function(Pointer codec2State); +typedef _codec2_samples_per_frame_d = int Function(Pointer codec2State); + +typedef _codec2_bytes_per_frame_c = Int32 Function(Pointer codec2State); +typedef _codec2_bytes_per_frame_d = int Function(Pointer codec2State); diff --git a/lib/services/map_tile_cache_service.dart b/lib/services/map_tile_cache_service.dart new file mode 100644 index 0000000..47910f3 --- /dev/null +++ b/lib/services/map_tile_cache_service.dart @@ -0,0 +1,241 @@ +import 'dart:math' as math; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +const String kMapTileUrlTemplate = + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + +class MapTileCacheProgress { + final int completed; + final int total; + final int failed; + + const MapTileCacheProgress({ + required this.completed, + required this.total, + required this.failed, + }); +} + +class MapTileCacheResult { + final int total; + final int downloaded; + final int failed; + + const MapTileCacheResult({ + required this.total, + required this.downloaded, + required this.failed, + }); +} + +class MapTileCacheService { + static const String cacheKey = 'map_tile_cache'; + static const String userAgentPackageName = 'com.meshcore.open'; + static const int defaultMinZoom = 10; + static const int defaultMaxZoom = 15; + + final BaseCacheManager cacheManager; + late final TileProvider tileProvider; + + MapTileCacheService({BaseCacheManager? cacheManager}) + : cacheManager = cacheManager ?? + CacheManager( + Config( + cacheKey, + stalePeriod: const Duration(days: 365), + maxNrOfCacheObjects: 200000, + ), + ) { + tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager); + } + + Map get defaultHeaders => { + 'User-Agent': 'flutter_map ($userAgentPackageName)', + }; + + Future clearCache() async { + await cacheManager.emptyCache(); + } + + int estimateTileCount(LatLngBounds bounds, int minZoom, int maxZoom) { + final safeMin = math.min(minZoom, maxZoom); + final safeMax = math.max(minZoom, maxZoom); + int total = 0; + + for (int zoom = safeMin; zoom <= safeMax; zoom++) { + final tileBounds = _tileBoundsForBounds(bounds, zoom); + final xCount = tileBounds.maxX - tileBounds.minX + 1; + final yCount = tileBounds.maxY - tileBounds.minY + 1; + total += xCount * yCount; + } + return total; + } + + Future downloadRegion({ + required LatLngBounds bounds, + required int minZoom, + required int maxZoom, + int concurrentDownloads = 8, + Map? headers, + void Function(MapTileCacheProgress progress)? onProgress, + }) async { + final safeMin = math.min(minZoom, maxZoom); + final safeMax = math.max(minZoom, maxZoom); + final total = estimateTileCount(bounds, safeMin, safeMax); + final authHeaders = headers ?? defaultHeaders; + final safeConcurrency = math.max(1, concurrentDownloads); + int completed = 0; + int failed = 0; + + final pending = >[]; + Future queueDownload(String url) async { + final future = cacheManager + .downloadFile(url, key: url, authHeaders: authHeaders) + .then((_) { + completed += 1; + }).catchError((_) { + completed += 1; + failed += 1; + }).whenComplete(() { + onProgress?.call(MapTileCacheProgress( + completed: completed, + total: total, + failed: failed, + )); + }); + + pending.add(future); + if (pending.length >= safeConcurrency) { + await Future.wait(pending); + pending.clear(); + } + } + + for (int zoom = safeMin; zoom <= safeMax; zoom++) { + final tileBounds = _tileBoundsForBounds(bounds, zoom); + for (int x = tileBounds.minX; x <= tileBounds.maxX; x++) { + for (int y = tileBounds.minY; y <= tileBounds.maxY; y++) { + final url = _buildTileUrl(x, y, zoom); + await queueDownload(url); + } + } + } + + if (pending.isNotEmpty) { + await Future.wait(pending); + } + + return MapTileCacheResult( + total: total, + downloaded: completed - failed, + failed: failed, + ); + } + + static Map boundsToJson(LatLngBounds bounds) { + return { + 'north': bounds.north, + 'south': bounds.south, + 'east': bounds.east, + 'west': bounds.west, + }; + } + + static LatLngBounds? boundsFromJson(Map? json) { + if (json == null) return null; + final north = (json['north'] as num?)?.toDouble(); + final south = (json['south'] as num?)?.toDouble(); + final east = (json['east'] as num?)?.toDouble(); + final west = (json['west'] as num?)?.toDouble(); + if (north == null || south == null || east == null || west == null) { + return null; + } + return LatLngBounds.unsafe( + north: north, + south: south, + east: east, + west: west, + ); + } + + _TileBounds _tileBoundsForBounds(LatLngBounds bounds, int zoom) { + final north = _clampLatitude(bounds.north); + final south = _clampLatitude(bounds.south); + final maxIndex = (1 << zoom) - 1; + + final minX = _lonToTileX(bounds.west, zoom, maxIndex); + final maxX = _lonToTileX(bounds.east, zoom, maxIndex); + final minY = _latToTileY(north, zoom, maxIndex); + final maxY = _latToTileY(south, zoom, maxIndex); + + return _TileBounds( + minX: math.min(minX, maxX), + maxX: math.max(minX, maxX), + minY: math.min(minY, maxY), + maxY: math.max(minY, maxY), + ); + } + + int _lonToTileX(double lon, int zoom, int maxIndex) { + final n = 1 << zoom; + final value = ((lon + 180.0) / 360.0 * n).floor(); + return value.clamp(0, maxIndex) as int; + } + + int _latToTileY(double lat, int zoom, int maxIndex) { + final n = 1 << zoom; + final rad = lat * math.pi / 180.0; + final value = ((1 - + math.log(math.tan(rad) + 1 / math.cos(rad)) / math.pi) / + 2 * + n) + .floor(); + return value.clamp(0, maxIndex) as int; + } + + double _clampLatitude(double lat) { + const maxLat = 85.05112878; + return lat.clamp(-maxLat, maxLat) as double; + } + + String _buildTileUrl(int x, int y, int zoom) { + return kMapTileUrlTemplate + .replaceAll('{z}', zoom.toString()) + .replaceAll('{x}', x.toString()) + .replaceAll('{y}', y.toString()); + } +} + +class CachedNetworkTileProvider extends TileProvider { + final BaseCacheManager cacheManager; + + CachedNetworkTileProvider({required this.cacheManager, super.headers}); + + @override + ImageProvider getImage(TileCoordinates coordinates, TileLayer options) { + final url = getTileUrl(coordinates, options); + return CachedNetworkImageProvider( + url, + cacheManager: cacheManager, + headers: headers, + ); + } +} + +class _TileBounds { + final int minX; + final int maxX; + final int minY; + final int maxY; + + const _TileBounds({ + required this.minX, + required this.maxX, + required this.minY, + required this.maxY, + }); +} diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 0a5b29b..1d694df 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -103,6 +103,7 @@ class MessageRetryService extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: contact.lastSeen, + lastMessageAt: contact.lastMessageAt, ); } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 2461081..cc13ccb 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -139,6 +139,55 @@ class NotificationService { ); } + Future showChannelMessageNotification({ + required String channelName, + required String message, + int? channelIndex, + }) async { + if (!_isInitialized) { + await initialize(); + } + + const androidDetails = AndroidNotificationDetails( + 'channel_messages', + 'Channel Messages', + channelDescription: 'New channel message notifications', + importance: Importance.high, + priority: Priority.high, + icon: '@mipmap/ic_launcher', + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + final preview = _truncateMessage(message, 30); + final body = preview.isEmpty + ? 'Received new message' + : 'Received new message: $preview'; + + await _notifications.show( + channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + channelName, + body, + notificationDetails, + payload: 'channel:$channelIndex', + ); + } + + String _truncateMessage(String message, int maxLength) { + final trimmed = message.trim(); + if (trimmed.length <= maxLength) return trimmed; + return '${trimmed.substring(0, maxLength)}...'; + } + void _onNotificationTapped(NotificationResponse response) { final payload = response.payload; if (payload != null) { diff --git a/lib/services/voice_message_service.dart b/lib/services/voice_message_service.dart new file mode 100644 index 0000000..0b7c60e --- /dev/null +++ b/lib/services/voice_message_service.dart @@ -0,0 +1,220 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +import 'codec2_ffi.dart'; + +class VoiceMessageService { + static const int sampleRate = 8000; + static const int channels = 1; + static const int bitsPerSample = 16; + static const int maxRecordSeconds = 5; + static const int chunkRawBytes = 90; + static const String codecName = 'codec2_1300'; + static const String chunkPrefix = 'V1|'; + + static final VoiceMessageService instance = VoiceMessageService._(); + + VoiceMessageService._(); + + Future ensureVoiceDir() async { + final docs = await getApplicationDocumentsDirectory(); + final dir = Directory(path.join(docs.path, 'voice')); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + + String buildVoiceFileName({ + required String senderKeyHex, + required int timestampSeconds, + bool outgoing = false, + }) { + final suffix = outgoing ? 'out' : 'in'; + return 'voice_${senderKeyHex}_${timestampSeconds}_$suffix.wav'; + } + + List buildVoiceChunks(Uint8List codec2Bytes) { + if (codec2Bytes.isEmpty) return []; + final chunks = []; + for (var offset = 0; offset < codec2Bytes.length; offset += chunkRawBytes) { + final end = (offset + chunkRawBytes).clamp(0, codec2Bytes.length).toInt(); + chunks.add(Uint8List.fromList(codec2Bytes.sublist(offset, end))); + } + final count = chunks.length; + return List.generate(count, (index) { + final encoded = _base64UrlEncodeNoPad(chunks[index]); + return '$chunkPrefix$index/$count|$encoded'; + }); + } + + VoiceChunk? tryParseChunk(String text) { + final trimmed = text.trim(); + if (!trimmed.startsWith(chunkPrefix)) return null; + final match = RegExp(r'^V1\|(\d+)/(\d+)\|([A-Za-z0-9_-]+)$').firstMatch(trimmed); + if (match == null) return null; + final idx = int.tryParse(match.group(1) ?? ''); + final count = int.tryParse(match.group(2) ?? ''); + final payload = match.group(3); + if (idx == null || count == null || payload == null) return null; + if (idx < 0 || count <= 0 || idx >= count) return null; + try { + final bytes = _base64UrlDecode(payload); + return VoiceChunk(index: idx, count: count, bytes: bytes); + } catch (_) { + return null; + } + } + + Uint8List encodePcmToCodec2(Uint8List pcmBytes) { + final session = Codec2Ffi.instance.createSession(); + try { + final samplesPerFrame = session.samplesPerFrameValue; + final pcmSamples = _toInt16(pcmBytes); + final frameCount = (pcmSamples.length + samplesPerFrame - 1) ~/ samplesPerFrame; + final builder = BytesBuilder(copy: false); + + for (var frameIndex = 0; frameIndex < frameCount; frameIndex++) { + final start = frameIndex * samplesPerFrame; + final end = (start + samplesPerFrame).clamp(0, pcmSamples.length).toInt(); + final frame = Int16List(samplesPerFrame); + final copyLen = end - start; + if (copyLen > 0) { + frame.setRange(0, copyLen, pcmSamples.sublist(start, end)); + } + final encoded = session.encodePcmFrame(frame); + builder.add(encoded); + } + + return builder.takeBytes(); + } finally { + session.dispose(); + } + } + + Uint8List decodeCodec2ToPcm(Uint8List codec2Bytes) { + final session = Codec2Ffi.instance.createSession(); + try { + final bytesPerFrame = session.bytesPerFrameValue; + if (bytesPerFrame <= 0) return Uint8List(0); + final frameCount = codec2Bytes.length ~/ bytesPerFrame; + final builder = BytesBuilder(copy: false); + + for (var frameIndex = 0; frameIndex < frameCount; frameIndex++) { + final start = frameIndex * bytesPerFrame; + final frameBytes = codec2Bytes.sublist(start, start + bytesPerFrame); + final decoded = session.decodeCodecFrame(frameBytes); + builder.add(Uint8List.view( + decoded.buffer, + decoded.offsetInBytes, + decoded.lengthInBytes, + )); + } + + return builder.takeBytes(); + } finally { + session.dispose(); + } + } + + int durationMsForCodec2Bytes(Uint8List codec2Bytes) { + final session = Codec2Ffi.instance.createSession(); + try { + final bytesPerFrame = session.bytesPerFrameValue; + final samplesPerFrame = session.samplesPerFrameValue; + if (bytesPerFrame <= 0 || samplesPerFrame <= 0) return 0; + final frameCount = codec2Bytes.length ~/ bytesPerFrame; + final frameDurationMs = (samplesPerFrame * 1000 / sampleRate).round(); + return frameCount * frameDurationMs; + } finally { + session.dispose(); + } + } + + Future writeWavFile({ + required Uint8List pcmBytes, + required String fileName, + }) async { + final dir = await ensureVoiceDir(); + final filePath = path.join(dir.path, fileName); + final wavHeader = _buildWavHeader( + pcmDataSize: pcmBytes.length, + sampleRate: sampleRate, + channels: channels, + bitsPerSample: bitsPerSample, + ); + final file = File(filePath); + final builder = BytesBuilder(copy: false); + builder.add(wavHeader); + builder.add(pcmBytes); + await file.writeAsBytes(builder.takeBytes(), flush: true); + return filePath; + } + + Uint8List _buildWavHeader({ + required int pcmDataSize, + required int sampleRate, + required int channels, + required int bitsPerSample, + }) { + final byteRate = sampleRate * channels * (bitsPerSample ~/ 8); + final blockAlign = channels * (bitsPerSample ~/ 8); + final buffer = BytesBuilder(copy: false); + buffer.add(ascii.encode('RIFF')); + buffer.add(_le32(36 + pcmDataSize)); + buffer.add(ascii.encode('WAVE')); + buffer.add(ascii.encode('fmt ')); + buffer.add(_le32(16)); + buffer.add(_le16(1)); + buffer.add(_le16(channels)); + buffer.add(_le32(sampleRate)); + buffer.add(_le32(byteRate)); + buffer.add(_le16(blockAlign)); + buffer.add(_le16(bitsPerSample)); + buffer.add(ascii.encode('data')); + buffer.add(_le32(pcmDataSize)); + return buffer.takeBytes(); + } + + Uint8List _le16(int value) { + final data = ByteData(2)..setUint16(0, value, Endian.little); + return data.buffer.asUint8List(); + } + + Uint8List _le32(int value) { + final data = ByteData(4)..setUint32(0, value, Endian.little); + return data.buffer.asUint8List(); + } + + Int16List _toInt16(Uint8List bytes) { + final evenLength = bytes.lengthInBytes - (bytes.lengthInBytes % 2); + if (evenLength <= 0) return Int16List(0); + return Int16List.view(bytes.buffer, bytes.offsetInBytes, evenLength ~/ 2); + } + + String _base64UrlEncodeNoPad(Uint8List bytes) { + return base64Url.encode(bytes).replaceAll('=', ''); + } + + Uint8List _base64UrlDecode(String encoded) { + final paddedLength = (encoded.length + 3) ~/ 4 * 4; + final padded = encoded.padRight(paddedLength, '='); + return base64Url.decode(padded); + } +} + +class VoiceChunk { + final int index; + final int count; + final Uint8List bytes; + + VoiceChunk({ + required this.index, + required this.count, + required this.bytes, + }); +} diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index f40377e..0d08ed7 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -65,6 +65,7 @@ class ChannelMessageStore { 'repeatCount': msg.repeatCount, 'pathLength': msg.pathLength, 'pathBytes': base64Encode(msg.pathBytes), + 'pathVariants': msg.pathVariants.map(base64Encode).toList(), 'repeats': msg.repeats.map(_repeatToJson).toList(), }; } @@ -87,6 +88,9 @@ class ChannelMessageStore { pathBytes: json['pathBytes'] != null ? Uint8List.fromList(base64Decode(json['pathBytes'] as String)) : Uint8List(0), + pathVariants: (json['pathVariants'] as List?) + ?.map((entry) => Uint8List.fromList(base64Decode(entry as String))) + .toList(), repeats: (json['repeats'] as List?) ?.map((entry) => _repeatFromJson(entry as Map)) .toList() ?? diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 18ce056..31078ab 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -37,10 +37,13 @@ class ContactStore { 'latitude': contact.latitude, 'longitude': contact.longitude, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, }; } Contact _fromJson(Map json) { + final lastSeenMs = json['lastSeen'] as int? ?? 0; + final lastMessageMs = json['lastMessageAt'] as int?; return Contact( publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', @@ -51,7 +54,8 @@ class ContactStore { : Uint8List(0), latitude: (json['latitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(), - lastSeen: DateTime.fromMillisecondsSinceEpoch(json['lastSeen'] as int? ?? 0), + lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + lastMessageAt: DateTime.fromMillisecondsSinceEpoch(lastMessageMs ?? lastSeenMs), ); } } diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index ecb8299..bedc83e 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -41,6 +41,10 @@ class MessageStore { 'timestamp': msg.timestamp.millisecondsSinceEpoch, 'isOutgoing': msg.isOutgoing, 'isCli': msg.isCli, + 'isVoice': msg.isVoice, + 'voicePath': msg.voicePath, + 'voiceDurationMs': msg.voiceDurationMs, + 'voiceCodec': msg.voiceCodec, 'status': msg.status.index, 'messageId': msg.messageId, 'retryCount': msg.retryCount, @@ -65,6 +69,10 @@ class MessageStore { timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int), isOutgoing: json['isOutgoing'] as bool, isCli: isCli, + isVoice: json['isVoice'] as bool? ?? false, + voicePath: json['voicePath'] as String?, + voiceDurationMs: json['voiceDurationMs'] as int?, + voiceCodec: json['voiceCodec'] as String?, status: MessageStatus.values[json['status'] as int], messageId: json['messageId'] as String?, retryCount: json['retryCount'] as int? ?? 0, diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart new file mode 100644 index 0000000..31def4e --- /dev/null +++ b/lib/utils/contact_search.dart @@ -0,0 +1,26 @@ +import '../models/contact.dart'; + +bool matchesContactQuery(Contact contact, String query) { + final normalizedQuery = query.trim().toLowerCase(); + if (normalizedQuery.isEmpty) return true; + + if (contact.name.toLowerCase().contains(normalizedQuery)) { + return true; + } + + final hexPrefix = _extractHexPrefix(normalizedQuery); + if (hexPrefix == null) return false; + + return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); +} + +String? _extractHexPrefix(String query) { + var cleaned = query; + if (cleaned.startsWith('0x')) { + cleaned = cleaned.substring(2); + } + cleaned = cleaned.replaceAll(' ', ''); + if (cleaned.length < 2) return null; + if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; + return cleaned; +} diff --git a/lib/utils/route_transitions.dart b/lib/utils/route_transitions.dart new file mode 100644 index 0000000..7de5a9c --- /dev/null +++ b/lib/utils/route_transitions.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +Route buildQuickSwitchRoute(Widget page) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => page, + transitionDuration: const Duration(milliseconds: 220), + reverseTransitionDuration: const Duration(milliseconds: 200), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final curved = CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + reverseCurve: Curves.easeInCubic, + ); + return FadeTransition( + opacity: curved, + child: SlideTransition( + position: Tween( + begin: const Offset(0.02, 0), + end: Offset.zero, + ).animate(curved), + child: child, + ), + ); + }, + ); +} diff --git a/lib/widgets/quick_switch_bar.dart b/lib/widgets/quick_switch_bar.dart new file mode 100644 index 0000000..d612f90 --- /dev/null +++ b/lib/widgets/quick_switch_bar.dart @@ -0,0 +1,83 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class QuickSwitchBar extends StatelessWidget { + final int selectedIndex; + final ValueChanged onDestinationSelected; + + const QuickSwitchBar({ + super.key, + required this.selectedIndex, + required this.onDestinationSelected, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final labelStyle = theme.textTheme.labelMedium ?? const TextStyle(); + + return SizedBox( + width: double.infinity, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + border: Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.4), + ), + ), + child: NavigationBarTheme( + data: NavigationBarThemeData( + backgroundColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + shadowColor: Colors.transparent, + indicatorColor: colorScheme.primaryContainer, + labelTextStyle: MaterialStateProperty.resolveWith((states) { + final isSelected = states.contains(MaterialState.selected); + return labelStyle.copyWith( + fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ); + }), + iconTheme: MaterialStateProperty.resolveWith((states) { + final isSelected = states.contains(MaterialState.selected); + return IconThemeData( + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurfaceVariant, + ); + }), + ), + child: NavigationBar( + height: 60, + selectedIndex: selectedIndex, + onDestinationSelected: onDestinationSelected, + destinations: const [ + NavigationDestination( + icon: Icon(Icons.people_outline), + label: 'Contacts', + ), + NavigationDestination( + icon: Icon(Icons.tag), + label: 'Channels', + ), + NavigationDestination( + icon: Icon(Icons.map_outlined), + label: 'Map', + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/voice_message.dart b/lib/widgets/voice_message.dart new file mode 100644 index 0000000..a976ac0 --- /dev/null +++ b/lib/widgets/voice_message.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:media_kit_fork/media_kit_fork.dart'; + +import '../models/message.dart'; + +class VoiceMessageBubble extends StatefulWidget { + final Message message; + final Color backgroundColor; + final Color textColor; + final Color metaColor; + final bool isOutgoing; + + const VoiceMessageBubble({ + super.key, + required this.message, + required this.backgroundColor, + required this.textColor, + required this.metaColor, + required this.isOutgoing, + }); + + @override + State createState() => _VoiceMessageBubbleState(); +} + +class _VoiceMessageBubbleState extends State { + late final Player _player; + StreamSubscription? _durationSubscription; + StreamSubscription? _completeSubscription; + Duration _duration = Duration.zero; + + @override + void initState() { + super.initState(); + _player = Player(); + final voicePath = widget.message.voicePath; + if (voicePath != null && voicePath.isNotEmpty) { + _player.open(Media(Uri.file(voicePath).toString()), play: false); + } + _durationSubscription = _player.stream.duration.listen((value) { + if (!mounted) return; + if (value > Duration.zero && value != _duration) { + setState(() { + _duration = value; + }); + } + }); + _completeSubscription = _player.stream.completed.listen((completed) { + if (!completed) return; + _player.seek(Duration.zero); + _player.pause(); + }); + } + + @override + void dispose() { + _durationSubscription?.cancel(); + _completeSubscription?.cancel(); + _player.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final hasAudio = widget.message.voicePath != null && widget.message.voicePath!.isNotEmpty; + final fallbackDuration = Duration(milliseconds: widget.message.voiceDurationMs ?? 0); + final displayDuration = _duration > Duration.zero ? _duration : fallbackDuration; + + return StreamBuilder( + stream: _player.stream.playing, + initialData: false, + builder: (context, playingSnapshot) { + final isPlaying = playingSnapshot.data ?? false; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + IconButton( + icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow), + color: widget.textColor, + onPressed: hasAudio + ? () { + if (isPlaying) { + _player.pause(); + } else { + _player.play(); + } + } + : null, + ), + Expanded( + child: StreamBuilder( + stream: _player.stream.position, + initialData: Duration.zero, + builder: (context, positionSnapshot) { + final position = positionSnapshot.data ?? Duration.zero; + final progress = displayDuration.inMilliseconds > 0 + ? position.inMilliseconds / displayDuration.inMilliseconds + : 0.0; + return LinearProgressIndicator( + value: progress.clamp(0.0, 1.0), + backgroundColor: widget.metaColor.withValues(alpha: 0.2), + valueColor: AlwaysStoppedAnimation(widget.textColor), + minHeight: 4, + ); + }, + ), + ), + const SizedBox(width: 8), + Text( + _formatDuration(displayDuration), + style: TextStyle( + color: widget.metaColor, + fontSize: 11, + ), + ), + ], + ), + ], + ); + }, + ); + } + + String _formatDuration(Duration duration) { + final totalSeconds = duration.inSeconds; + final minutes = totalSeconds ~/ 60; + final seconds = totalSeconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..209cfb0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,14 @@ #include "generated_plugin_registrant.h" +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); + media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); + g_autoptr(FlPluginRegistrar) record_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); + record_linux_plugin_register_with_registrar(record_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..b8244e7 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + media_kit_libs_linux + record_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 7b9bdcf..2fa4dc8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,10 +7,18 @@ import Foundation import flutter_blue_plus_darwin import flutter_local_notifications +import media_kit_libs_macos_audio +import path_provider_foundation +import record_macos import shared_preferences_foundation +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + MediaKitLibsMacosAudioPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosAudioPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 75819f3..810210c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -33,6 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -106,7 +138,7 @@ packages: source: hosted version: "1.3.3" ffi: - dependency: transitive + dependency: "direct main" description: name: ffi sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" @@ -190,6 +222,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.7" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + sha256: "6cf10a27f5e344cd2ecad0752d3a5f4ec32846d82fda8753b3fe2480ebb832a3" + url: "https://pub.dev" + source: hosted + version: "6.5.0" flutter_lints: dependency: "direct dev" description: @@ -256,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + url: "https://pub.dev" + source: hosted + version: "4.7.2" intl: dependency: transitive description: @@ -344,6 +400,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + media_kit_fork: + dependency: "direct main" + description: + name: media_kit_fork + sha256: aa6e7bb6153545f64a3bcfcdeaf5d245328deedc004a17731bd0e0cf7b566981 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + media_kit_libs_android_audio: + dependency: transitive + description: + name: media_kit_libs_android_audio + sha256: "8f8f9759e537e12d66f08bc4d5279eb1bb21a0ccc519ff3442c68a9f3b6dd68b" + url: "https://pub.dev" + source: hosted + version: "1.3.8" + media_kit_libs_audio: + dependency: "direct main" + description: + name: media_kit_libs_audio + sha256: "81bf506c234e81e3ec536ba72f8f700a928543c14c345220210cae0411636316" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + media_kit_libs_ios_audio: + dependency: transitive + description: + name: media_kit_libs_ios_audio + sha256: "78ccf04e27d6b4ba00a355578ccb39b772f00d48269a6ac3db076edf2d51934f" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + media_kit_libs_linux: + dependency: transitive + description: + name: media_kit_libs_linux + sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + media_kit_libs_macos_audio: + dependency: transitive + description: + name: media_kit_libs_macos_audio + sha256: "3be21844df98f286de32808592835073cdef2c1a10078bac135da790badca950" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + media_kit_libs_windows_audio: + dependency: transitive + description: + name: media_kit_libs_windows_audio + sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 + url: "https://pub.dev" + source: hosted + version: "1.0.9" meta: dependency: transitive description: @@ -368,14 +480,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - path: + octo_image: dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -440,6 +584,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" proj4dart: dependency: transitive description: @@ -456,6 +608,70 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + record: + dependency: "direct main" + description: + name: record + sha256: "6bad72fb3ea6708d724cf8b6c97c4e236cf9f43a52259b654efeb6fd9b737f1f" + url: "https://pub.dev" + source: hosted + version: "6.1.2" + record_android: + dependency: transitive + description: + name: record_android + sha256: "9aaf3f151e61399b09bd7c31eb5f78253d2962b3f57af019ac5a2d1a3afdcf71" + url: "https://pub.dev" + source: hosted + version: "1.4.5" + record_ios: + dependency: transitive + description: + name: record_ios + sha256: "69fcd37c6185834e90254573599a9165db18a2cbfa266b6d1e46ffffeb06a28c" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + record_linux: + dependency: transitive + description: + name: record_linux + sha256: "235b1f1fb84e810f8149cc0c2c731d7d697f8d1c333b32cb820c449bf7bb72d8" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + record_macos: + dependency: transitive + description: + name: record_macos + sha256: "842ea4b7e95f4dd237aacffc686d1b0ff4277e3e5357865f8d28cd28bc18ed95" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + record_platform_interface: + dependency: transitive + description: + name: record_platform_interface + sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed + url: "https://pub.dev" + source: hosted + version: "1.4.0" + record_web: + dependency: transitive + description: + name: record_web + sha256: "3feeffbc0913af3021da9810bb8702a068db6bc9da52dde1d19b6ee7cb9edb51" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + record_windows: + dependency: transitive + description: + name: record_windows + sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78" + url: "https://pub.dev" + source: hosted + version: "1.0.7" rxdart: dependency: transitive description: @@ -464,6 +680,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236 + url: "https://pub.dev" + source: hosted + version: "2.0.1" shared_preferences: dependency: "direct main" description: @@ -533,6 +757,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -557,6 +821,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -597,6 +869,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + uri_parser: + dependency: transitive + description: + name: uri_parser + sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce" + url: "https://pub.dev" + source: hosted + version: "3.0.2" uuid: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7868d7b..032d08d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,15 @@ dependencies: crypto: ^3.0.3 pointycastle: ^3.7.4 http: ^1.2.0 + ffi: ^2.1.3 + cached_network_image: ^3.4.1 + flutter_cache_manager: ^3.4.1 + media_kit_fork: ^0.0.2 + media_kit_libs_audio: ^1.0.5 + path: ^1.9.0 + path_provider: ^2.1.2 + record: ^6.1.2 + flutter_foreground_task: ^6.1.2 dev_dependencies: flutter_test: diff --git a/third_party/codec2/.clang-format b/third_party/codec2/.clang-format new file mode 100644 index 0000000..f2dd0de --- /dev/null +++ b/third_party/codec2/.clang-format @@ -0,0 +1,168 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + - Regex: '.*' + Priority: 3 + SortPriority: 0 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Auto +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +... + diff --git a/third_party/codec2/.github/workflows/cmake-sm1000.yml b/third_party/codec2/.github/workflows/cmake-sm1000.yml new file mode 100644 index 0000000..ab24b5d --- /dev/null +++ b/third_party/codec2/.github/workflows/cmake-sm1000.yml @@ -0,0 +1,43 @@ +name: Build SM1000 + +on: [pull_request] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Debug + +jobs: + build: + # The CMake configure and build commands are platform agnostic and should work equally + # well on Windows or Mac. You can convert this to a matrix build if you need + # cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + + - name: Install packages + shell: bash + run: | + sudo apt-get update + sudo apt-get install octave octave-common octave-signal liboctave-dev gnuplot sox p7zip-full python3-numpy valgrind + + - name: Install ST Standard Peripheral Library (SM1000) + working-directory: ${{github.workspace}}/stm32 + shell: bash + run: git clone https://github.com/whimsicalraps/STM32F4xx_DSP_StdPeriph_Lib + + - name: Install SM1000 prerequisites + working-directory: ${{github.workspace}}/stm32 + shell: bash + run: sudo apt install gcc-arm-none-eabi + + - name: Build SM1000 + working-directory: ${{github.workspace}}/stm32 + shell: bash + run: | + mkdir build_stm32 + cd build_stm32 + cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/STM32_Toolchain.cmake -DPERIPHLIBDIR=${{github.workspace}}/stm32/STM32F4xx_DSP_StdPeriph_Lib .. + make diff --git a/third_party/codec2/.github/workflows/cmake.yml b/third_party/codec2/.github/workflows/cmake.yml new file mode 100644 index 0000000..8550384 --- /dev/null +++ b/third_party/codec2/.github/workflows/cmake.yml @@ -0,0 +1,58 @@ +name: Build Codec2 for Linux + +on: [pull_request] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Debug + +jobs: + build: + # The CMake configure and build commands are platform agnostic and should work equally + # well on Windows or Mac. You can convert this to a matrix build if you need + # cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + + - name: Install packages + shell: bash + run: | + sudo apt-get update + sudo apt-get install octave octave-common octave-signal liboctave-dev gnuplot sox p7zip-full python3-numpy valgrind clang-format texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra texlive-science texmaker texlive-bibtex-extra + + - name: Create Build Directory + shell: bash + run: mkdir $GITHUB_WORKSPACE/build_linux + + - name: Configure codec2 CMake + shell: bash + working-directory: ${{github.workspace}}/build_linux + run: cmake -DUNITTEST=1 $GITHUB_WORKSPACE + + - name: Build LPCNet and Run ctests + shell: bash + run: | + cd $HOME + git clone https://github.com/drowe67/LPCNet.git + cd LPCNet && mkdir -p build_linux && cd build_linux + cmake .. && make && ctest + + - name: Build codec2 with LPCNet + working-directory: ${{github.workspace}}/build_linux + shell: bash + run: | + cmake -DLPCNET_BUILD_DIR=$HOME/LPCNet/build_linux -DUNITTEST=1 $GITHUB_WORKSPACE + make -j4 + + - name: Run ctests + working-directory: ${{github.workspace}}/build_linux + shell: bash + run: ctest --output-on-failure + + - name: Test library installation + working-directory: ${{github.workspace}}/build_linux + shell: bash + run: cmake --install . --prefix "$HOME/codec2_install" && rm -rf "$HOME/codec2_install" diff --git a/third_party/codec2/.gitignore b/third_party/codec2/.gitignore new file mode 100644 index 0000000..edcb225 --- /dev/null +++ b/third_party/codec2/.gitignore @@ -0,0 +1,8 @@ +build_linux +stm32/build_stm32 +stm32/libstm32f4.a +stm32/unittest/lib/python/__pycache__/ +stm32/unittest/src/libstm32f4.a +stm32/unittest/src/*.map +stm32/unittest/test_run/ +*.pyc diff --git a/third_party/codec2/CMakeLists.txt b/third_party/codec2/CMakeLists.txt new file mode 100644 index 0000000..e62c58a --- /dev/null +++ b/third_party/codec2/CMakeLists.txt @@ -0,0 +1,1434 @@ +# +# Codec2 - Next-Generation Digital Voice for Two-Way Radio +# +# CMake configuration contributed by Richard Shaw (KF5OIM) +# Please report questions, comments, problems, or patches to the freetel +# mailing list: https://lists.sourceforge.net/lists/listinfo/freetel-codec2 +# + +# Note: this has to be at the beginning of the file in order for CMake to +# actually recognize this override (vs. simply telling the macOS build toolchain +# to mandate the current release). +set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9" CACHE STRING "Minimum OS X deployment version") + +cmake_minimum_required(VERSION 3.13) +project(CODEC2 + VERSION 1.2.0 + DESCRIPTION "Next-Generation Digital Voice for Two-Way Radio" + HOMEPAGE_URL "https://www.rowetel.com/codec2.html" + LANGUAGES C + ) + +include(GNUInstallDirs) +mark_as_advanced(CLEAR + CMAKE_INSTALL_BINDIR + CMAKE_INSTALL_INCLUDEDIR + CMAKE_INSTALL_LIBDIR +) + +# +# Prevent in-source builds +# If an in-source build is attempted, you will still need to clean up a few +# files manually. +# +set(CMAKE_DISABLE_SOURCE_CHANGES ON) +set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) +if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + message(FATAL_ERROR "In-source builds in ${CMAKE_BINARY_DIR} are not " + "allowed, please remove ./CMakeCache.txt and ./CMakeFiles/, create a " + "separate build directory and run cmake from there.") +endif("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") + +# Set default build type +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +endif() + +# Build universal ARM64 and x86_64 binaries on Mac. +if(BUILD_OSX_UNIVERSAL) +set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") +endif(BUILD_OSX_UNIVERSAL) +set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9" CACHE STRING "Minimum OS X deployment version") + +# +# Find the git hash if this is a working copy. +# +if(EXISTS ${CMAKE_SOURCE_DIR}/.git) + find_package(Git) + if(Git_FOUND) + execute_process( + COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE res + OUTPUT_VARIABLE CODEC2_HASH + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + message(STATUS "Codec2 current git hash: ${CODEC2_HASH}") + add_definitions(-DGIT_HASH="${CODEC2_HASH}") + else() + message(WARNING "Git not found. Can not determine current commit hash.") + add_definitions(-DGIT_HASH="Unknown") + endif() +else() + add_definitions(-DGIT_HASH="None") +endif() + +set(ARCHIVE_NAME "codec2-${CODEC2_VERSION_MAJOR}.${CODEC2_VERSION_MINOR}.${CODEC2_VERSION_PATCH}") +add_custom_target(dist + COMMAND git archive --prefix=${ARCHIVE_NAME}/ HEAD + | bzip2 > ${CMAKE_BINARY_DIR}/${ARCHIVE_NAME}.tar.bz2 + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + +# Set default C flags. +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-strict-overflow") + +# Check for what C standard is supported. +if(NOT WIN32) +include(CheckCCompilerFlag) +CHECK_C_COMPILER_FLAG("-std=gnu11" COMPILER_SUPPORTS_GNU11) +CHECK_C_COMPILER_FLAG("-std=gnu99" COMPILER_SUPPORTS_GNU99) + +if(COMPILER_SUPPORTS_GNU11) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu11") +elseif(COMPILER_SUPPORTS_GNU99) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99") +else() + message(SEND_ERROR "Compiler doesn't seem to support at least gnu99, might cause problems" ) +endif() +endif(NOT WIN32) + +# -fPIC is implied on MinGW... +if((NOT WIN32) AND (NOT MICROCONTROLLER_BUILD)) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC") +endif() + +set(CMAKE_C_FLAGS_DEBUG "-g -O2 -DDUMP") +set(CMAKE_C_FLAGS_RELEASE "-O3") + +# +# Setup Windows/MinGW specifics here. +# +if(MINGW) + message(STATUS "System is MinGW.") +endif(MINGW) + +# +# Default options +# +option(BUILD_SHARED_LIBS + "Build shared library. Set to OFF for static library." ON) +option(UNITTEST "Build unittest binaries." OFF) + +# LPCNet needs to be bootstrapped because codec2 and freedvlpcnet are +# cross dependent. +option(LPCNET "Build codec2 with LPCNet support." OFF) +set(LPCNET_BUILD_DIR FALSE CACHE PATH "Location of lpcnet build tree.") +# Setting LPCNET_BUILD_DIR implies LPCNET=ON +if(LPCNET_BUILD_DIR) + set(LPCNET ON) +endif() + + +include(CheckIncludeFiles) +check_include_files("stdlib.h" HAVE_STDLIB_H) +check_include_files("string.h" HAVE_STRING_H) + +include(CheckSymbolExists) +# Check if _GNU_SOURCE is available. +if (NOT DEFINED _GNU_SOURCE) + check_symbol_exists(__GNU_LIBRARY__ "features.h" _GNU_SOURCE) + + if (NOT _GNU_SOURCE) + unset(_GNU_SOURCE CACHE) + check_symbol_exists(_GNU_SOURCE "features.h" _GNU_SOURCE) + endif() +endif() + +if (_GNU_SOURCE) + add_definitions(-D_GNU_SOURCE=1) +endif() + +check_symbol_exists(floor math.h HAVE_FLOOR) +check_symbol_exists(ceil math.h HAVE_CEIL) +check_symbol_exists(pow math.h HAVE_POW) +check_symbol_exists(sqrt math.h HAVE_SQRT) +check_symbol_exists(sin math.h HAVE_SIN) +check_symbol_exists(cos math.h HAVE_COS) +check_symbol_exists(atan2 math.h HAVE_ATAN2) +check_symbol_exists(log10 math.h HAVE_LOG10) +check_symbol_exists(round math.h HAVE_ROUND) +check_symbol_exists(getopt getopt.h HAVE_GETOPT) + +configure_file ("${PROJECT_SOURCE_DIR}/cmake/config.h.in" + "${PROJECT_BINARY_DIR}/config.h" ) +# Output path is such that #include in codec2.h works +set(CODEC2_VERSION_PATH "${PROJECT_BINARY_DIR}/codec2") +configure_file ("${PROJECT_SOURCE_DIR}/cmake/version.h.in" + "${CODEC2_VERSION_PATH}/version.h" ) +include_directories(${PROJECT_BINARY_DIR}) + +# CMake Package setup +#include(CMakePackageConfigHelpers) +#configure_package_config_file(cmake/codec2-config.cmake.in +# ${CMAKE_CURRENT_BINARY_DIR}/codec2-config.cmake +# INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/codec2 +# PATH_VARS CMAKE_INSTALL_INCLUDEDIR +#) + + +# +# Find lpcnet library +# +if(LPCNET) + if(LPCNET_BUILD_DIR) + # Theoretically this shouldn't be needed as we're also defining LPCNET_BUILD_DIR + # in the PATHS section below. But on Fedora 37, CMake can't find LPCNet (at least + # in Docker) without this. + set(lpcnetfreedv_DIR ${LPCNET_BUILD_DIR}) + + find_package(lpcnetfreedv REQUIRED + PATHS ${LPCNET_BUILD_DIR} + NO_DEFAULT_PATH + CONFIGS lpcnetfreedv.cmake + ) + if(lpcnetfreedv_FOUND) + message(STATUS "liblpcnetfreedv found in build tree.") + add_definitions("-D__LPCNET__") + else() + message(FATAL_ERROR "LPCNet include/library not found in build tree.") + endif() + else() + find_package(lpcnetfreedv REQUIRED) + if(lpcnetfreedv_FOUND) + add_definitions("-D__LPCNET__") + message(STATUS "liblpcnetfreedv found.") + else() + message(FATAL_ERROR "lpcnetfreedv library not found.") + endif() + endif() +endif() + + +# +# codec2 library and demo apps +# +add_subdirectory(src) +add_subdirectory(demo) + + +if(UNITTEST) + # Pthread Library + find_package(Threads REQUIRED) + message(STATUS "Threads library flags: ${CMAKE_THREAD_LIBS_INIT}") + + add_subdirectory(unittest) +endif(UNITTEST) + +message(STATUS "Build type is: " ${CMAKE_BUILD_TYPE}) +string(TOUPPER ${CMAKE_BUILD_TYPE} _FLAGS) +if(_FLAGS STREQUAL "NONE") + message(STATUS "Compiler Flags: " ${CMAKE_C_FLAGS}) +else() + message(STATUS "Compiler Flags: " ${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${_FLAGS}}) +endif() +message(STATUS "Libraries linked: " ${CMAKE_REQUIRED_LIBRARIES}) + +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Next-Generation Digital Voice for Two-Way Radio") +set(CPACK_PACKAGE_VENDOR "CMake") +set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.md") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/COPYING") +set(CPACK_PACKAGE_VERSION_MAJOR ${CODEC2_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${CODEC2_VERSION_MINOR}) +if(CODEC2_VERSION_PATCH) + set(CPACK_PACKAGE_VERSION_PATCH ${CODEC2_VERSION_PATCH}) +else() + set(CPACK_PACKAGE_VERSION_PATCH 0) +endif() + +# Return the date (yyyy-mm-dd) +string(TIMESTAMP DATE_RESULT "%Y-%m-%d" UTC) +message(STATUS "Compilation date = XX${DATE_RESULT}XX") + +set(CPACK_PACKAGE_VERSION_PATCH "${CPACK_PACKAGE_VERSION_PATCH}-${DATE_RESULT}-${CODEC2_HASH}") + +if(WIN32) + # + # Cpack NSIS installer configuration for Windows. + # See: http://nsis.sourceforge.net/Download + # + + # Detect if we're doing a 32-bit or 64-bit windows build. + if(${CMAKE_SIZEOF_VOID_P} EQUAL 8) + set(CMAKE_CL_64 TRUE) + endif() + configure_file(cmake/GetDependencies.cmake.in cmake/GetDependencies.cmake + @ONLY + ) + install(SCRIPT ${CMAKE_BINARY_DIR}/cmake/GetDependencies.cmake) + set(CPACK_PACKAGE_INSTALL_DIRECTORY "Codec2") + set(CPACK_CREATE_DESKTOP_LINKS "") + set(CPACK_NSIS_DISPLAY_NAME "${CPACK_PACKAGE_INSTALL_DIRECTORY}") + set(CPACK_NSIS_URL_INFO_ABOUT "http://rowetel.com/codec2.html") + set(CPACK_NSIS_MODIFY_PATH ON) + include(CPack) +elseif(UNIX AND NOT APPLE) + # Linux packaging + SET(CPACK_GENERATOR "DEB") + SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Mooneer Salem ") #required + SET(CPACK_DEB_COMPONENT_INSTALL ON) + SET(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) + SET(CPACK_DEBIAN_ENABLE_COMPONENT_DEPENDS ON) + SET(CPACK_DEBIAN_LIB_PACKAGE_NAME "codec2") + SET(CPACK_DEBIAN_PACKAGE_DEPENDS "lpcnet (>= 0.3.0)") + include(CPack) + cpack_add_component(lib REQUIRED) + cpack_add_component(dev DEPENDS lib) +endif(WIN32) + +######################################################################## +# Create Pkg Config File +######################################################################## +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/codec2.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/codec2.pc + @ONLY +) + +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/codec2.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + COMPONENT "codec2_devel" +) + +################################################################## +# Tests +################################################################## + +if(UNITTEST) + include(CTest) + enable_testing() + + add_test(NAME test_clang_format + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}; + clang-format --dry-run --Werror src/*.c src/*.h unittest/*.c demo/*.c") + + add_test(NAME test_codec2_doc + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/doc; + make clean; + CODEC2_SRC=${CMAKE_CURRENT_SOURCE_DIR} CODEC2_BINARY=${CMAKE_CURRENT_BINARY_DIR} JOBNAME=test make") + + add_test(NAME test_freedv_get_hash + COMMAND sh -c "${CMAKE_CURRENT_BINARY_DIR}/unittest/thash") + +if(UNIX) # Uses pthreads + add_test(NAME test_fifo + COMMAND $ + ) +endif() + + # 16<->8 kHz float resamplers + add_test(NAME test_fdmdv_16to8 + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + ${CMAKE_CURRENT_BINARY_DIR}/unittest/t16_8; + DISPLAY=\"\" echo \"diff_fft_mag('in8.raw','out8.raw'); quit;\" | octave-cli -qf + ") + set_tests_properties(test_fdmdv_16to8 PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + # 16<->8 kHz short (int16) resamplers + add_test(NAME test_fdmdv_16to8_short + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + ${CMAKE_CURRENT_BINARY_DIR}/unittest/t16_8_short; + DISPLAY=\"\" echo \"diff_fft_mag('in8_short.raw','out8_short.raw'); quit;\" | octave-cli -qf + ") + set_tests_properties(test_fdmdv_16to8_short PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + # 48<->8 kHz float resamplers + add_test(NAME test_fdmdv_48to8_short + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + ${CMAKE_CURRENT_BINARY_DIR}/unittest/t48_8_short; + DISPLAY=\"\" echo \"diff_fft_mag('in8.raw','out8.raw'); quit;\" | octave-cli -qf + ") + set_tests_properties(test_fdmdv_48to8_short PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + # 48<->8 kHz short resamplers + add_test(NAME test_fdmdv_48to8 + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + ${CMAKE_CURRENT_BINARY_DIR}/unittest/t48_8; + DISPLAY=\"\" echo \"diff_fft_mag('in8.raw','out8.raw'); quit;\" | octave-cli -qf + ") + set_tests_properties(test_fdmdv_48to8 PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + # Basic sanity check of Quisk complex band pass filter. Note complex filtering cosw(wn) gives + # just the +ve freq exp(jwn) so output power is 0.5 input power + add_test(NAME test_quisk_filter + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + ${CMAKE_CURRENT_BINARY_DIR}/unittest/mksine in.raw 1500 1; + cat in.raw | ${CMAKE_CURRENT_BINARY_DIR}/unittest/tquisk_filter | + sox -t .s16 -r 8000 -c 1 - -t .s16 out.raw vol 2; + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" echo \"diff_fft_mag('in.raw','out.raw'); quit;\" | octave-cli -qf + ") + set_tests_properties(test_quisk_filter PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + add_test(NAME test_CML_ldpcut + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; CTEST_SINGLE=1 octave-cli -qf ldpcut.m") + set_tests_properties(test_CML_ldpcut PROPERTIES PASS_REGULAR_EXPRESSION "Nerr: 0") + + add_test(NAME test_CML_ldpcut_one_stuffing + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; CTEST_ONE_STUFFING=1 octave-cli -qf ldpcut.m") + set_tests_properties(test_CML_ldpcut_one_stuffing PROPERTIES PASS_REGULAR_EXPRESSION "Ferrs: 0") + + # Golay (23,11) unit tests + add_test(NAME test_golay23 COMMAND sh -c "${CMAKE_CURRENT_BINARY_DIR}/unittest/golay23") + add_test(NAME test_golay23_runtime_tables COMMAND sh -c "${CMAKE_CURRENT_BINARY_DIR}/unittest/golay23_runtime_tables") + + # check channel simulator measures correct Peak to Average Power Ratio (about 0dB) with a sine wave input signal + add_test(NAME test_ch_papr + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + ./unittest/mksine - 1000 10 | ./src/ch - /dev/null --ctest") + + add_test(NAME test_codec2_700c_octave_port + COMMAND sh -c " + cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./c2sim ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw --phase0 --postfilter --dump hts1a --lpc 10 --dump_pitch_e hts1a_pitche.txt; + cd ${CMAKE_CURRENT_BINARY_DIR}/unittest; ./tnewamp1 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw; + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" octave-cli -qf --eval 'tnewamp1(\"${CMAKE_CURRENT_BINARY_DIR}/src/hts1a\", \"${CMAKE_CURRENT_BINARY_DIR}/unittest\")'") + set_tests_properties(test_codec2_700c_octave_port PROPERTIES PASS_REGULAR_EXPRESSION "fails: 0") + + # ------------------------------------------------------------------------- + # FDMDV Modem + # ------------------------------------------------------------------------- + + add_test(NAME test_FDMDV_modem_octave_ut + COMMAND sh -c " + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave/; + DISPLAY=\"\" octave-cli -qf fdmdv_ut.m") + set_tests_properties(test_FDMDV_modem_octave_ut PROPERTIES PASS_REGULAR_EXPRESSION "errors......: 0") + + add_test(NAME test_FDMDV_modem_octave_mod_demod + COMMAND sh -c " + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave/; + echo \"fdmdv_mod('test.raw',1400); fdmdv_demod('test.raw',1400); quit\" | DISPLAY=\"\" octave-cli") + set_tests_properties(test_FDMDV_modem_octave_mod_demod PROPERTIES PASS_REGULAR_EXPRESSION "0 errors") + + add_test(NAME test_FDMDV_modem_octave_port + COMMAND sh -c "$ && DISPLAY=\"\" octave-cli --no-gui -qf ${CMAKE_CURRENT_SOURCE_DIR}/octave/tfdmdv.m" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/octave) + set_tests_properties(test_FDMDV_modem_octave_port PROPERTIES PASS_REGULAR_EXPRESSION "fails: 0") + + add_test(NAME test_FDMDV_modem_octave_c + COMMAND sh -c " + cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./fdmdv_get_test_bits - 14000 | ./fdmdv_mod - - | + ./fdmdv_demod - - 14 demod_dump.txt | ./fdmdv_put_test_bits - ; + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave/; + DISPLAY=\"\" octave-cli -qf fdmdv_ut.m") + set_tests_properties(test_FDMDV_modem_octave_c PROPERTIES PASS_REGULAR_EXPRESSION "errors......: 0") + + # ------------------------------------------------------------------------- + # COHPSK Modem + # ------------------------------------------------------------------------- + + add_test(NAME test_COHPSK_modem_octave_port + COMMAND sh -c "$ && DISPLAY=\"\" octave-cli --no-gui -qf ${CMAKE_CURRENT_SOURCE_DIR}/octave/tcohpsk.m" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/octave) + set_tests_properties(test_COHPSK_modem_octave_port PROPERTIES PASS_REGULAR_EXPRESSION "fails: 0") + + add_test(NAME test_COHPSK_modem_AWGN_BER + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./cohpsk_get_test_bits - 5600 | + ./cohpsk_mod - - | + ./ch - - --No -30 --Fs 7500 | + ./cohpsk_demod - - | + ./cohpsk_put_test_bits -" + ) + + add_test(NAME test_COHPSK_modem_freq_offset + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./cohpsk_get_test_bits - 5600 | + ./cohpsk_mod - - | + ./ch - - --No -40 -f -30 --Fs 7500 | + ./cohpsk_demod - - | + ./cohpsk_put_test_bits -" + ) + + # ------------------------------------------------------------------------- + # OFDM Modem + # ------------------------------------------------------------------------- + + add_test(NAME test_OFDM_qam16 + COMMAND sh -c "${CMAKE_CURRENT_BINARY_DIR}/unittest/tqam16") + + add_test(NAME test_OFDM_modem_octave_port + COMMAND sh -c "PATH_TO_TOFDM=${CMAKE_CURRENT_BINARY_DIR}/unittest/tofdm DISPLAY=\"\" octave-cli --no-gui -qf ${CMAKE_CURRENT_SOURCE_DIR}/octave/tofdm.m" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/octave) + set_tests_properties(test_OFDM_modem_octave_port PROPERTIES PASS_REGULAR_EXPRESSION "fails: 0") + + add_test(NAME test_OFDM_modem_octave_port_Nc_31 + COMMAND sh -c "NC=31 PATH_TO_TOFDM=${CMAKE_CURRENT_BINARY_DIR}/unittest/tofdm DISPLAY=\"\" octave-cli --no-gui -qf ${CMAKE_CURRENT_SOURCE_DIR}/octave/tofdm.m" + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/octave) + set_tests_properties(test_OFDM_modem_octave_port_Nc_31 PROPERTIES PASS_REGULAR_EXPRESSION "fails: 0") + + add_test(NAME test_OFDM_modem_octave_qam16_uncoded + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo \"ofdm_tx('test_qam16.raw','qam16c1',3,12,'awgn','bursts',3); ofdm_rx('test_qam16.raw','qam16c1', 'passber', 0.05, 'packetsperburst', 1); quit\" | + DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_octave_qam16_uncoded PROPERTIES PASS_REGULAR_EXPRESSION "Pass") + + add_test(NAME test_OFDM_modem_esno_est_octave + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo 'pkg load signal; esno_est; esno_est_tests_octave' | + PATH_TO_UNITEST=${CMAKE_CURRENT_BINARY_DIR}/unittest/ DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_esno_est_octave PROPERTIES PASS_REGULAR_EXPRESSION "AWGN Pass.*MPP Pass") + + add_test(NAME test_OFDM_modem_esno_est_c + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo 'pkg load signal; esno_est; esno_est_tests_c' | + PATH_TO_UNITEST=${CMAKE_CURRENT_BINARY_DIR}/unittest/ DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_esno_est_c PROPERTIES PASS_REGULAR_EXPRESSION "AWGN Pass.*MPP Pass") + + + # ---------------------------------- Data Mode burst acquisition tests ---------------------------------- + + add_test(NAME test_OFDM_modem_octave_burst_acq + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo \"ctest=1; ofdm_acquisition; quit\" | DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_octave_burst_acq PROPERTIES PASS_REGULAR_EXPRESSION "P.acq. = 1.00") + + add_test(NAME test_OFDM_modem_octave_datac0_postamble + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo \"ofdm_tx('test_datac0.raw','datac0',1,100,'awgn','bursts',3); ofdm_rx('test_datac0.raw','datac0','packetsperburst',1,'postambletest','passber', 1E-6); quit\" | + DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_octave_datac0_postamble PROPERTIES PASS_REGULAR_EXPRESSION "Pass") + + # Check C port of burst acquisition + add_test(NAME test_OFDM_modem_burst_acq_port + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo \"tofdm_acq; quit\" | PATH_TO_UNITTEST=${CMAKE_CURRENT_BINARY_DIR}/unittest DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_burst_acq_port PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + # Give uncoded Octave burst data modem a workout on a poor channel (0dB SNR MPP) + add_test(NAME test_OFDM_modem_octave_datac0_mpp + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo \"ofdm_tx('test_datac0.raw','datac0',1,0,'mpp','bursts',10); \ + ofdm_rx('test_datac0.raw','datac0','packetsperburst',1,'passpacketcount',9);\ + quit\" | + DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_octave_datac0_mpp PROPERTIES PASS_REGULAR_EXPRESSION "Pass") + + # Same for coded Octave burst data modem - look out for bit rot as simulations evolve .... + add_test(NAME test_OFDM_modem_octave_datac0_mpp_coded + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + echo \"ofdm_ldpc_tx('test_datac0.raw','datac0',1,0,'mpp','bursts',10); \ + ofdm_ldpc_rx('test_datac0.raw','datac0','packetsperburst',1,'passpacketcount',9);\ + quit\" | + DISPLAY=\"\" octave-cli") + set_tests_properties(test_OFDM_modem_octave_datac0_mpp_coded PROPERTIES PASS_REGULAR_EXPRESSION "Pass") + + # Check Octave and C compressed waveforms are about the same + add_test(NAME test_OFDM_modem_datac0_compression + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + ./check_comp.sh ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/src") + + # ---------------------------------- ofdm_mod/demod level C modem tests ---------------------------------- + + # noise free uncoded 700D test, including reading and writing payload bits + add_test(NAME test_OFDM_modem_700D + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_get_test_bits - | + ./ofdm_mod | + ./ofdm_demod --testframes > /dev/null") + + # noise free coded 700D test, including reading and writing payload bits + add_test(NAME test_OFDM_modem_700D_ldpc + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_get_test_bits - --length 112 | + ./ofdm_mod --ldpc | + ./ofdm_demod --ldpc --testframes > /dev/null") + + # noise free 2020 test, including reading and writing payload bits. fsk_*_test_bits + # used as it does it's own frame sync + add_test(NAME test_OFDM_modem_2020_ldpc + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./fsk_get_test_bits - 5000 | + ./ofdm_mod --ldpc --mode 2020 | + ./ofdm_demod --ldpc --mode 2020 | + ./fsk_put_test_bits - -q") + + add_test(NAME test_OFDM_modem_AWGN_BER + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --in /dev/zero --ldpc --testframes 60 --txbpf | + ./ch - - --No -20 -f -50 | + ./ofdm_demod --out /dev/null --testframes --ldpc --verbose 1" + ) + + add_test(NAME test_OFDM_modem_fading_BER + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/src ./ofdm_fade.sh ${CMAKE_CURRENT_BINARY_DIR}/unittest") + + add_test(NAME test_OFDM_modem_phase_est_bw + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/src ./ofdm_phase_est_bw.sh ${CMAKE_CURRENT_BINARY_DIR}/unittest") + + add_test(NAME test_OFDM_modem_time_sync_700D + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/src ./ofdm_time_sync.sh 700D") + +if(LPCNET) + add_test(NAME test_OFDM_modem_time_sync_2020 + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/src ./ofdm_time_sync.sh 2020") +endif() + + # 700E at a little above AWGN operating point + add_test(NAME test_OFDM_modem_700E_AWGN + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --in /dev/zero --testframes 10 --ldpc --mode 700E | + ./ch - - --No -22 | + ./ofdm_demod --mode 700E --ldpc --testframes -v 2 > /dev/null") + + # 2020B AWGN test + add_test(NAME test_OFDM_modem_2020B_AWGN + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --in /dev/zero --testframes 10 --mode 2020B --ldpc --clip --txbpf | + ./ch - - --No -19 | + ./ofdm_demod --mode 2020B --testframes --ldpc -v 2 > /dev/null") + + # ------------------------------------------------------------------------- + # OFDM Data modes + # ------------------------------------------------------------------------- + + # To integrate a new mode/waveform we prototype in Octave, get the core OFDM modem + # running in C (ofdm_mod & ofdm_demod), then the FreeDV API (frredv_tx & freedv_rx). + # Here we test Octave and the C versions of the OFDM modem working together, to help + # prevent any bit rot between them + + # DATAC0 burst mode Octave Tx, C Rx + add_test(NAME test_OFDM_modem_datac0_octave_burst + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" octave-cli -qf --eval \"ofdm_ldpc_tx('${CMAKE_CURRENT_BINARY_DIR}/test.raw','datac0',1,100,'awgn','bursts',3)\"; + cd ${CMAKE_CURRENT_BINARY_DIR}; + cat test.raw | ./src/ofdm_demod --mode datac0 --out /dev/null --testframes --ldpc --verbose 1 --packetsperburst 1") + set_tests_properties(test_OFDM_modem_datac0_octave_burst PROPERTIES PASS_REGULAR_EXPRESSION "Coded PER: 0.0000 Tpkts: 3") + + # DATAC1 C Tx, Octave Rx + add_test(NAME test_OFDM_modem_datac1_octave + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + ./src/ofdm_mod --mode datac1 --in /dev/zero --testframes 20 --verbose 1 --ldpc > test.raw; + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" octave-cli -qf --eval 'ofdm_ldpc_rx(\"${CMAKE_CURRENT_BINARY_DIR}/test.raw\",\"datac1\")'") + set_tests_properties(test_OFDM_modem_datac1_octave PROPERTIES PASS_REGULAR_EXPRESSION "Coded PER: 0.0000 Pckts: 4") + + # DATAC3 C Tx, Octave Rx + add_test(NAME test_OFDM_modem_datac3_octave + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + ./src/ofdm_mod --mode datac3 --in /dev/zero --testframes 20 --verbose 1 --ldpc > test.raw; + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" octave-cli -qf --eval 'ofdm_ldpc_rx(\"${CMAKE_CURRENT_BINARY_DIR}/test.raw\",\"datac3\")'") + set_tests_properties(test_OFDM_modem_datac3_octave PROPERTIES PASS_REGULAR_EXPRESSION "Coded PER: 0.0000 Pckts: 5") + + # DATAC1 C Tx, C Rx, uncoded + add_test(NAME test_OFDM_modem_datac1 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --mode datac1 --in /dev/zero --testframes 10 --verbose 1 | + ./ofdm_demod --mode datac1 --out /dev/null --testframes --verbose 1") + + # DATAC1 C Tx, C Rx, coded + add_test(NAME test_OFDM_modem_datac1_ldpc + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --mode datac1 --in /dev/zero --testframes 10 --ldpc --verbose 1 | + ./ofdm_demod --mode datac1 --out /dev/null --testframes --ldpc --verbose 1") + + # DATAC0 C Tx, C Rx, coded, burst mode + add_test(NAME test_OFDM_modem_datac0_ldpc_burst + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --mode datac0 --in /dev/zero --testframes 1 --verbose 1 --ldpc --bursts 3 | + ./ch - - --No -17 | + ./ofdm_demod --mode datac0 --out /dev/null --testframes --ldpc --verbose 2 --packetsperburst 1") + + # DATAC4 C Tx, Octave Rx, burst mode + add_test(NAME test_OFDM_modem_datac4_octave + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + ./src/ofdm_mod --mode datac4 --in /dev/zero --testframes 1 --verbose 1 --ldpc --bursts 5 > test.raw; + cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" octave-cli -qf --eval 'ofdm_ldpc_rx(\"${CMAKE_CURRENT_BINARY_DIR}/test.raw\",\"datac4\",\"packetsperburst\",1)'") + set_tests_properties(test_OFDM_modem_datac3_octave PROPERTIES PASS_REGULAR_EXPRESSION "Coded PER: 0.0000 Pckts: 5") + + # DATAC13 Octave Tx, C Rx, burst mode + add_test(NAME test_OFDM_modem_datac13_octave + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" octave-cli -qf --eval 'ofdm_ldpc_tx(\"${CMAKE_CURRENT_BINARY_DIR}/src/test.raw\",\"datac13\",1,3,\"awgn\",\"bursts\",5)'; + cd ${CMAKE_CURRENT_BINARY_DIR}/src; + cat test.raw | ./ofdm_demod --mode datac13 --out /dev/null --testframes --ldpc --verbose 2 --packetsperburst 1") + + # DATAC14 Octave Tx, C Rx, burst mode + add_test(NAME test_OFDM_modem_datac14_octave + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + DISPLAY=\"\" octave-cli -qf --eval 'ofdm_ldpc_tx(\"${CMAKE_CURRENT_BINARY_DIR}/src/test.raw\",\"datac14\",1,3,\"awgn\",\"bursts\",5)'; + cd ${CMAKE_CURRENT_BINARY_DIR}/src; + cat test.raw | ./ofdm_demod --mode datac14 --out /dev/null --testframes --ldpc --verbose 2 --packetsperburst 1") + + # DATAC4 C Tx, C Rx, burst mode + add_test(NAME test_OFDM_modem_datac4_ldpc_burst + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --mode datac4 --in /dev/zero --testframes 1 --verbose 1 --ldpc --bursts 10 | + ./ch - - --No -17 | + ./ofdm_demod --mode datac4 --out /dev/null --testframes --ldpc --verbose 2 --packetsperburst 1") + + # DATAC13 C Tx, C Rx, burst mode + add_test(NAME test_OFDM_modem_datac13_ldpc_burst + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --mode datac13 --in /dev/zero --testframes 1 --verbose 1 --ldpc --bursts 10 | + ./ch - - --No -17 | + ./ofdm_demod --mode datac13 --out /dev/null --testframes --ldpc --verbose 2 --packetsperburst 1") + + # DATAC14 C Tx, C Rx, burst mode + add_test(NAME test_OFDM_modem_datac14_ldpc_burst + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --mode datac14 --in /dev/zero --testframes 1 --verbose 1 --ldpc --bursts 10 | + ./ch - - --No -17 | + ./ofdm_demod --mode datac14 --out /dev/null --testframes --ldpc --verbose 2 --packetsperburst 1") + + # ------------------------------------------------------------------------- + # LDPC + # ------------------------------------------------------------------------- + + # tests ldpc_enc/ldpc_noise/ldpc_dec + add_test(NAME test_ldpc_enc_dec + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code HRA_112_112 --testframes 200 | + ./ldpc_noise - - 0.5 | + ./ldpc_dec - /dev/null --code HRA_112_112 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_HRA_56_56 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code HRA_56_56 --testframes 200 | + ./ldpc_noise - - 0.5 | + ./ldpc_dec - /dev/null --code HRA_56_56 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_212_158 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_212_158 --testframes 200 | + ./ldpc_noise - - -2.0 | + ./ldpc_dec - /dev/null --code H_212_158 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_HRAb_396_504 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code HRAb_396_504 --testframes 200 | + ./ldpc_noise - - -2.0 | + ./ldpc_dec - /dev/null --code HRAb_396_504 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_256_768_22 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_256_768_22 --testframes 200 | + ./ldpc_noise - - 3.0 | + ./ldpc_dec - /dev/null --code H_256_768_22 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_256_512_4 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_256_512_4 --testframes 200 | + ./ldpc_noise - - 0.5 | + ./ldpc_dec - /dev/null --code H_256_512_4 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_HRAa_1536_512 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code HRAa_1536_512 --testframes 200 | + ./ldpc_noise - - -2 | + ./ldpc_dec - /dev/null --code HRAa_1536_512 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_128_256_5 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_128_256_5 --testframes 200 | + ./ldpc_noise - - 0.5 | + ./ldpc_dec - /dev/null --code H_128_256_5 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_4096_8192_3d + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_4096_8192_3d --testframes 100 | + ./ldpc_noise - - 0.0 | + ./ldpc_dec - /dev/null --code H_4096_8192_3d --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_16200_9720 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_16200_9720 --testframes 10 | + ./ldpc_noise - - 0.5 | + ./ldpc_dec - /dev/null --code H_16200_9720 --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_1024_2048_4f + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_1024_2048_4f --testframes 100 | + ./ldpc_noise - - 0.0 | + ./ldpc_dec - /dev/null --code H_1024_2048_4f --sd --testframes" + ) + + add_test(NAME test_ldpc_enc_dec_H_2064_516_sparse + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --sd --code H_2064_516_sparse --testframes 100 | + ./ldpc_noise - - -2.0 | + ./ldpc_dec - /dev/null --code H_2064_516_sparse --sd --testframes" + ) + + # ------------------------------------------------------------------------- + # FreeDV API + # ------------------------------------------------------------------------- + + # Test 1600 using number of frames decoded and correct rx txt channel output + add_test(NAME test_freedv_api_1600 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 1600 ../../raw/ve9qrp_10s.raw - | ./freedv_rx 1600 - /dev/null --txtrx 1600.txt; + cat 1600.txt") + set_tests_properties(test_freedv_api_1600 PROPERTIES PASS_REGULAR_EXPRESSION "frames decoded: 503 .*cq cq hello") + + add_test(NAME test_freedv_api_700C + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700C ../../raw/ve9qrp_10s.raw - | ./freedv_rx 700C - /dev/null") + set_tests_properties(test_freedv_api_700C PROPERTIES PASS_REGULAR_EXPRESSION "frames decoded: 125") + + add_test(NAME test_freedv_api_700D_backwards_compatability + COMMAND sh -c "$ 700D ${CMAKE_CURRENT_SOURCE_DIR}/raw/testframes_700d.raw /dev/null --testframes --discard" + ) + + # speech output on valid signal (at least 70000 samples), to exercise freedv_bits_to_speech() speech output logic + add_test(NAME test_freedv_api_700D_speech + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700D ../../raw/ve9qrp_10s.raw - | + ./ch - - --No -20 | + ./freedv_rx 700D - /dev/null --squelch -2 -vv") + set_tests_properties(test_freedv_api_700D_speech PROPERTIES PASS_REGULAR_EXPRESSION "frames decoded: 62 output speech samples: 7") + + # no random speech output due to trial sync when listening to noise + add_test(NAME test_freedv_api_700D_burble + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700D ../../raw/ve9qrp.raw - | + ./ch - - --No -8 | + ./freedv_rx 700D - /dev/null --squelch -2 -vv") + set_tests_properties(test_freedv_api_700D_burble PROPERTIES PASS_REGULAR_EXPRESSION "output speech samples: 0") + + add_test(NAME test_freedv_api_700D_AWGN_BER + COMMAND sh -c "dd bs=2560 count=120 if=/dev/zero | $ 700D - - --testframes | $ - - --No -20 -f -10 | $ 700D - /dev/null --testframes --discard" + ) + + # exercises complex rx codepath, albeit with just real samples + add_test(NAME test_freedv_api_700D_AWGN_BER_USECOMPLEX + COMMAND sh -c "dd bs=2560 count=120 if=/dev/zero | $ 700D - - --testframes | $ - - --No -20 -f -10 | $ 700D - /dev/null --testframes --discard --usecomplex" + ) + + # check real part of freedv_comptx() matches freedv_tx() + add_test(NAME test_freedv_api_700D_real_comp + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/demo:${CMAKE_CURRENT_BINARY_DIR}/unittest; + ./check_real_comp.sh" + ) + + # exercises freedv_comptx() + add_test(NAME test_freedv_api_700D_comptx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/unittest; + cat ${CMAKE_CURRENT_SOURCE_DIR}/raw/ve9qrp_10s.raw | + ./freedv_700d_comptx | + ./freedv_700d_comprx tx > /dev/null" + ) + + # exercises freedv_comprx() + add_test(NAME test_freedv_api_700D_comprx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/unittest; + cat ${CMAKE_CURRENT_SOURCE_DIR}/raw/ve9qrp_10s.raw | + ./freedv_700d_comptx | + ./freedv_700d_comprx rx > /dev/null" + ) + +if(LPCNET) + + add_test(NAME test_freedv_api_2020_to_ofdm_demod + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 2020 ../../wav/wia_16kHz.wav - --testframes | + ./ofdm_demod --mode 2020 --verbose 1 --ldpc --testframes > /dev/null" + ) + + add_test(NAME test_freedv_api_2020_from_ofdm_mod + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ofdm_mod --in /dev/zero --mode 2020 --verbose 1 --ldpc --testframes 10 | + ./freedv_rx 2020 - /dev/null --testframes" + ) + + add_test(NAME test_freedv_api_2020_awgn + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + dd bs=32000 count=10 if=/dev/zero | + ./freedv_tx 2020 - - --testframes | + ./ch - - --No -24 | + ./freedv_rx 2020 - /dev/null --testframes" + ) + + add_test(NAME test_freedv_api_2020B_mpp + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + dd bs=32000 count=60 if=/dev/zero | + ./freedv_tx 2020B - - --testframes --clip 1 | + ./ch - - --No -25 --mpp --fading_dir ../unittest | + ./freedv_rx 2020B - /dev/null --testframes" + ) + +endif() + + add_test(NAME test_freedv_api_2400A + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 2400A ../../raw/ve9qrp_10s.raw - | ./freedv_rx 2400A - /dev/null") + set_tests_properties(test_freedv_api_2400A PROPERTIES PASS_REGULAR_EXPRESSION "frames decoded: 250") + add_test(NAME test_freedv_api_2400B + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 2400B ../../raw/ve9qrp_10s.raw - | ./freedv_rx 2400B - /dev/null") + set_tests_properties(test_freedv_api_2400B PROPERTIES PASS_REGULAR_EXPRESSION "frames decoded: 250") + add_test(NAME test_freedv_api_800XA + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 800XA ../../raw/ve9qrp_10s.raw - | ./freedv_rx 800XA - /dev/null") + set_tests_properties(test_freedv_api_800XA PROPERTIES PASS_REGULAR_EXPRESSION "frames decoded: 125") + + add_test(NAME test_freedv_api_rawdata_800XA + COMMAND sh -c "./tfreedv_800XA_rawdata" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/unittest + ) + + add_test(NAME test_freedv_api_rawdata_2400A + COMMAND sh -c "./tfreedv_2400A_rawdata" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/unittest + ) + + add_test(NAME test_freedv_api_rawdata_2400B + COMMAND sh -c "./tfreedv_2400B_rawdata" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/unittest + ) + + add_test(NAME test_peak_levels + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/src ./check_peak.sh") + set_tests_properties(test_peak_levels PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") +if(LPCNET) + add_test(NAME test_peak_levels_lpcnet + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/src ./check_peak.sh LPCNet") + set_tests_properties(test_peak_levels_lpcnet PROPERTIES FAIL_REGULAR_EXPRESSION "FAIL") +endif() + + # ------------------------------------------------------------------------- + # Reliable Text + # ------------------------------------------------------------------------- + add_test(NAME test_freedv_reliable_text_truncate_string + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 1600 ../../raw/ve9qrp.raw - --reliabletext AB1CDEFGH > 1600_reliable.raw 2>/dev/null; + ./freedv_rx 1600 1600_reliable.raw /dev/null --txtrx 1600_reliable.txt --reliabletext 2>/dev/null; + grep 'AB1CDEFG' 1600_reliable.txt | wc -l") + set_tests_properties(test_freedv_reliable_text_truncate_string PROPERTIES PASS_REGULAR_EXPRESSION "20") + + add_test(NAME test_freedv_reliable_text_ideal_1600 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 1600 ../../raw/ve9qrp.raw - --reliabletext AB1CDEF > 1600_reliable.raw 2>/dev/null; + ./freedv_rx 1600 1600_reliable.raw /dev/null --txtrx 1600_reliable.txt --reliabletext 2>/dev/null; + cat 1600_reliable.txt | wc -l") + set_tests_properties(test_freedv_reliable_text_ideal_1600 PROPERTIES PASS_REGULAR_EXPRESSION "20") + + add_test(NAME test_freedv_reliable_text_ideal_700D + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700D ../../raw/ve9qrp.raw - --reliabletext AB1CDEF --txbpf 1 --clip 1 > 700D_reliable.raw 2>/dev/null; + ./freedv_rx 700D 700D_reliable.raw /dev/null --txtrx 700D_reliable.txt --reliabletext 2>/dev/null; + cat 700D_reliable.txt | wc -l") + set_tests_properties(test_freedv_reliable_text_ideal_700D PROPERTIES PASS_REGULAR_EXPRESSION "21") + + add_test(NAME test_freedv_reliable_text_ideal_700E + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700E ../../raw/ve9qrp.raw - --reliabletext AB1CDEF --txbpf 1 --clip 1 > 700E_reliable.raw 2>/dev/null; + ./freedv_rx 700E 700E_reliable.raw /dev/null --txtrx 700E_reliable.txt --reliabletext 2>/dev/null; + cat 700E_reliable.txt | wc -l") + set_tests_properties(test_freedv_reliable_text_ideal_700E PROPERTIES PASS_REGULAR_EXPRESSION "21") + + add_test(NAME test_freedv_reliable_text_awgn_1600 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 1600 ../../raw/ve9qrp.raw - --reliabletext AB1CDEF | ./ch - - --No -25 -f -5 > 1600_reliable.raw 2>/dev/null; + ./freedv_rx 1600 1600_reliable.raw /dev/null --txtrx 1600_reliable.txt --reliabletext 2>/dev/null; + if [ `cat 1600_reliable.txt | wc -l` -ge 10 ]; then echo 1; fi") + set_tests_properties(test_freedv_reliable_text_awgn_1600 PROPERTIES PASS_REGULAR_EXPRESSION "1") + + add_test(NAME test_freedv_reliable_text_awgn_700D + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700D ../../raw/ve9qrp.raw - --reliabletext AB1CDEF --txbpf 1 --clip 1 | ./ch - - --No -12 -f -5 > 700D_reliable.raw 2>/dev/null; + ./freedv_rx 700D 700D_reliable.raw /dev/null --txtrx 700D_reliable.txt --reliabletext 2>/dev/null; + if [ `cat 700D_reliable.txt | wc -l` -ge 10 ]; then echo 1; fi") + set_tests_properties(test_freedv_reliable_text_awgn_700D PROPERTIES PASS_REGULAR_EXPRESSION "1") + + add_test(NAME test_freedv_reliable_text_awgn_700E + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700E ../../raw/ve9qrp.raw - --reliabletext AB1CDEF --txbpf 1 --clip 1 | ./ch - - --No -15 -f -5 > 700E_reliable.raw 2>/dev/null; + ./freedv_rx 700E 700E_reliable.raw /dev/null --txtrx 700E_reliable.txt --reliabletext 2>/dev/null; + if [ `cat 700E_reliable.txt | wc -l` -ge 10 ]; then echo 1; fi") + set_tests_properties(test_freedv_reliable_text_awgn_700E PROPERTIES PASS_REGULAR_EXPRESSION "1") + + add_test(NAME test_freedv_reliable_text_fade_1600 + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; ./reliable_text_fade.sh 1600 -28 3 0 '${CMAKE_CURRENT_BINARY_DIR}/src'") + + add_test(NAME test_freedv_reliable_text_fade_700D + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; ./reliable_text_fade.sh 700D -19 8 1 '${CMAKE_CURRENT_BINARY_DIR}/src'") + + add_test(NAME test_freedv_reliable_text_fade_700E + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; ./reliable_text_fade.sh 700E -22 8 1 '${CMAKE_CURRENT_BINARY_DIR}/src'") + +if(LPCNET) + add_test(NAME test_freedv_reliable_text_ideal_2020 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 2020 ../../raw/ve9qrp.raw - --reliabletext AB1CDEF > 2020_reliable.raw 2>/dev/null; + ./freedv_rx 2020 2020_reliable.raw /dev/null --txtrx 2020_reliable.txt --reliabletext 2>/dev/null; + cat 2020_reliable.txt | wc -l") + set_tests_properties(test_freedv_reliable_text_ideal_2020 PROPERTIES PASS_REGULAR_EXPRESSION "9") + + add_test(NAME test_freedv_reliable_text_awgn_2020 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 2020 ../../raw/ve9qrp.raw - --reliabletext AB1CDEF | ./ch - - --No -22 -f -5 > 2020_reliable.raw 2>/dev/null; + ./freedv_rx 2020 2020_reliable.raw /dev/null --txtrx 2020_reliable.txt --reliabletext 2>/dev/null; + if [ `cat 2020_reliable.txt | wc -l` -ge 9 ]; then echo 1; fi") + set_tests_properties(test_freedv_reliable_text_awgn_1600 PROPERTIES PASS_REGULAR_EXPRESSION "1") + + add_test(NAME test_freedv_reliable_text_fade_2020 + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; ./reliable_text_fade.sh 2020 -26 4 0 '${CMAKE_CURRENT_BINARY_DIR}/src'") +endif(LPCNET) + + # ------------------------------------------------------------------------- + # FreeDV API memory leaks + # ------------------------------------------------------------------------- + +if (NOT APPLE) + add_test(NAME test_memory_leak_FreeDV_1600_tx + COMMAND sh -c " valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./freedv_tx 1600 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw /dev/null" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + set_tests_properties(test_memory_leak_FreeDV_1600_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_1600_rx + COMMAND sh -c "./freedv_tx 1600 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw t.raw; \ + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./freedv_rx 1600 t.raw /dev/null" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + set_tests_properties(test_memory_leak_FreeDV_1600_rx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_700D_tx + COMMAND sh -c " valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./freedv_tx 700D ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw /dev/null" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + set_tests_properties(test_memory_leak_FreeDV_700D_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_700D_rx + COMMAND sh -c "./freedv_tx 700D ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw t.raw; \ + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./freedv_rx 700D t.raw /dev/null" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + set_tests_properties(test_memory_leak_FreeDV_700D_rx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_700C_tx + COMMAND sh -c " valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./freedv_tx 700C ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw /dev/null" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + set_tests_properties(test_memory_leak_FreeDV_700C_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_700C_rx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 700C ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw t.raw; \ + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./freedv_rx 700C t.raw /dev/null" + ) + set_tests_properties(test_memory_leak_FreeDV_700C_rx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_FSK_LDPC_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_data_raw_tx --testframes 10 FSK_LDPC /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_FSK_LDPC_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_DATAC0_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_data_raw_tx --testframes 10 DATAC0 /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_DATAC0_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_DATAC1_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_data_raw_tx --testframes 10 DATAC1 /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_DATAC1_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_DATAC3_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_data_raw_tx --testframes 10 DATAC3 /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_DATAC3_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_DATAC4_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_data_raw_tx --testframes 10 DATAC4 /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_DATAC4_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_DATAC13_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_data_raw_tx --testframes 10 DATAC13 /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_DATAC13_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_DATAC14_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_data_raw_tx --testframes 10 DATAC14 /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_DATAC14_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_700E_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_tx --testframes 10 700E /dev/zero /dev/null") + set_tests_properties(test_memory_leak_FreeDV_700E_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + +if(LPCNET) + add_test(NAME test_memory_leak_FreeDV_2020_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_tx 2020 ../../wav/wia_16kHz.wav /dev/null" + ) + set_tests_properties(test_memory_leak_FreeDV_2020_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_2020_rx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 2020 ../../wav/wia_16kHz.wav t.raw; \ + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_rx 2020 t.raw /dev/null" + ) + set_tests_properties(test_memory_leak_FreeDV_2020_rx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_2020B_tx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_tx 2020B ../../wav/wia_16kHz.wav /dev/null" + ) + set_tests_properties(test_memory_leak_FreeDV_2020B_tx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") + + add_test(NAME test_memory_leak_FreeDV_2020B_rx + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_tx 2020B ../../wav/wia_16kHz.wav t.raw; \ + valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \ + ./freedv_rx 2020B t.raw /dev/null" + ) + set_tests_properties(test_memory_leak_FreeDV_2020B_rx PROPERTIES PASS_REGULAR_EXPRESSION "ERROR SUMMARY: 0 errors") +endif(LPCNET) +endif(NOT APPLE) + + # ------------------------------------------------------------------------- + # Codec 2 modes + # ------------------------------------------------------------------------- + + add_test(NAME test_codec2_mode_dot_c2 + COMMAND sh -c "./c2enc 700C ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw hts1a.c2 && ./c2dec 1600 hts1a.c2 /dev/null" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + set_tests_properties(test_codec2_mode_dot_c2 PROPERTIES PASS_REGULAR_EXPRESSION "mode 8") + + add_test(NAME test_codec2_mode_3200 + COMMAND sh -c "./c2enc 3200 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw - | ./c2dec 3200 - - | sox -t .s16 -r 8000 - hts1a_3200.wav" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + add_test(NAME test_codec2_mode_2400 + COMMAND sh -c "./c2enc 2400 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw - | ./c2dec 2400 - - | sox -t .s16 -r 8000 - hts1a_2400.wav" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + add_test(NAME test_codec2_mode_1400 + COMMAND sh -c "./c2enc 1400 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw - | ./c2dec 1400 - - | sox -t .s16 -r 8000 - hts1a_1400.wav" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + add_test(NAME test_codec2_mode_1300 + COMMAND sh -c "./c2enc 1300 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw - | ./c2dec 1300 - - | sox -t .s16 -r 8000 - hts1a_1300.wav" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + add_test(NAME test_codec2_mode_1200 + COMMAND sh -c "./c2enc 1200 ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw - | ./c2dec 1200 - - | sox -t .s16 -r 8000 - hts1a_1200.wav" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + add_test(NAME test_codec2_mode_700C + COMMAND sh -c "./c2enc 700C ${CMAKE_CURRENT_SOURCE_DIR}/raw/hts1a.raw - | ./c2dec 700C - - | sox -t .s16 -r 8000 - hts1a_700C.wav" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/src + ) + + add_test(NAME test_vq_mbest + COMMAND sh -c "./tvq_mbest; \ + cat target.f32 | \ + ./vq_mbest -k 4 -q vq1.f32,vq2.f32 --st 1 --en 2 --mbest 2 -v > /dev/null;" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/unittest + ) + set_tests_properties(test_vq_mbest PROPERTIES PASS_REGULAR_EXPRESSION "MSE: 0.00") + + add_test(NAME test_700c_eq + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/unittest; + PATH=$PATH:${CMAKE_CURRENT_BINARY_DIR}/src ./test_700c_eq.sh") + + # ------------------------------------------------------------------------- + # FSK Modem + # ------------------------------------------------------------------------- + + # Octave FSK Modem, to make sure we don't break reference simulation + add_test(NAME test_fsk_lib + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; DISPLAY=\"\" octave-cli -qf fsk_lib_demo.m") + set_tests_properties(test_fsk_lib PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + add_test(NAME test_fsk_modem_octave_port + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; + PATH_TO_TFSK=${CMAKE_CURRENT_BINARY_DIR}/unittest/tfsk octave-cli -qf tfsk.m") + set_tests_properties(test_fsk_modem_octave_port PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + add_test(NAME test_fsk_modem_mod_demod + COMMAND sh -c "$ - 10000 | + $ 2 8000 100 1200 100 - - | + $ -l 2 8000 100 - - | + $ -p 99 -q -" + ) + + # 2FSK modem at Eb/No = 9dB, SNR = Eb/No+10log10(Rb/B) = 9 + 10*log10(100/3000) = -5.7dB + # Ideal BER = 0.0094, set thresh 50% higher + add_test(NAME test_fsk_2fsk_ber + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./fsk_get_test_bits - 10000 | ./fsk_mod 2 8000 100 1000 100 - - | + ./ch - - --No -26 | + ./fsk_demod 2 8000 100 - - | ./fsk_put_test_bits -b 0.015 -q - ") + # 4FSK modem at Eb/No = 6dB, SNR = Eb/No+10log10(Rb/B) = 6 + 10*log10(2*100/3000) = -5.7dB + # Ideal BER = 0.016, set thresh 50% higher + add_test(NAME test_fsk_4fsk_ber + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./fsk_get_test_bits - 10000 | ./fsk_mod 4 8000 100 1000 100 - - | + ./ch - - --No -26 | + ./fsk_demod 4 8000 100 - - | ./fsk_put_test_bits -b 0.025 - ") + # shift FSK signal to -ve frequencies + add_test(NAME test_fsk_4fsk_ber_negative_freq + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./fsk_get_test_bits - 10000 | ./fsk_mod 4 8000 100 1000 200 - - | + ./ch - - --No -26 --ssbfilt 0 --complexout -f -3000 | + ./fsk_demod -c -p 8 4 8000 100 - - | + ./fsk_put_test_bits -b 0.025 -q - ") + # Low SNR 4FSK uncoded PER/BER test: + # 4FSK modem at Eb/No = 2dB, SNR = Eb/No+10log10(Rb/B) = 6 + 10*log10(2*100/3000) = -15.7dB + # Theoretical BER is 0.14. + # Pass condition is 10% PER + add_test(NAME test_fsk_4fsk_lockdown + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + bits=512; tx_packets=20; rx_packets=18; tx_tone_sep=50; Rs=25; + ./fsk_get_test_bits - $(($bits*$tx_packets)) $bits | + ./fsk_mod 4 8000 $Rs 1000 $tx_tone_sep - - | + ./ch - - --No -16 --ssbfilt 0 -f -3000 --complexout | + ./fsk_demod -c -p 8 --mask $tx_tone_sep -t1 --nsym 100 4 8000 $Rs - - 2>stats.txt | + ./fsk_put_test_bits -t 0.25 -b 0.20 -p $rx_packets -f $bits -q -") + + # Octave 4FSK LLR reference simulation - make sure this keeps working + add_test(NAME test_fsk_lib_4fsk_ldpc + COMMAND sh -c "cd ${CMAKE_CURRENT_SOURCE_DIR}/octave; DISPLAY=\"\" octave-cli -qf fsk_lib_ldpc_demo.m") + set_tests_properties(test_fsk_lib_4fsk_ldpc PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + # Command line Unique Word (UW) framer in hard decision mode + add_test(NAME test_fsk_framer + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./fsk_get_test_bits - 300 | + ./framer - - 100 51 | + ./deframer - - 100 51 --hard | + ./fsk_put_test_bits -") + set_tests_properties(test_fsk_framer PROPERTIES PASS_REGULAR_EXPRESSION "PASS") + + # Command line Unique Word (UW) framer with LLRs and LDPC (no noise) + add_test(NAME test_fsk_framer_ldpc + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --code HRA_112_112 --testframes 10 | ./framer - - 224 51 | + ./tollr | ./deframer - - 224 51 | ./ldpc_dec - /dev/null --code HRA_112_112 --testframes") + + # mFSK soft decision rx_filter to LLR mapping + add_test(NAME test_fsk_llr + COMMAND sh -c "${CMAKE_CURRENT_BINARY_DIR}/unittest/tfsk_llr") + + # 4FSK LDPC modem with framer at Rs=100 (uncoded Rb=200), rate 0.8 code + # SNR = Eb/No + 10*log10(Rb/B) = 5 + 10*log10(200/3000) = -6.7dB + # Coded Ebc/No = Eb/No - 10*log1010(0.8) = 5 - 10*log10(0.8) = 6.0dB + # (calculation ignores small UW overhead). See also test_freedv_fsk_ldpc below + # which is the same thing bundled up into a FreeDV "mode" + add_test(NAME test_fsk_4fsk_ldpc + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./ldpc_enc /dev/zero - --code HRAb_396_504 --testframes 200 | + ./framer - - 504 5186 | + ./fsk_mod 4 8000 100 1000 100 - - | + ./ch - - --No -25 | + ./fsk_demod -s 4 8000 100 - - | + ./deframer - - 504 5186 | + ./ldpc_dec - /dev/null --code HRAb_396_504 --testframes") + + # 800XA framer test + add_test(NAME test_fsk_vhf_framer + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./c2enc 700C ../../raw/ve9qrp_10s.raw - | + ./vhf_frame_c2 B - - | + ./fsk_mod -p 10 4 8000 400 400 400 - - | + ./fsk_demod -p 10 4 8000 400 - - | + ./vhf_deframe_c2 B - /dev/null") + set_tests_properties(test_fsk_vhf_framer PROPERTIES PASS_REGULAR_EXPRESSION "total_uw_err: 0") + + # VHF Ethernet-style packet system + add_test(NAME test_freedv_data_channel + COMMAND sh -c "./tfreedv_data_channel" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/unittest + ) + + # --------------------------------------------------------- + # FreeDV API raw data + # --------------------------------------------------------- + + # Burst mode with test frames: 3 bursts, each burst is two frames long + add_test(NAME test_freedv_data_raw_ofdm_datac0_burst + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_data_raw_tx --framesperburst 2 --bursts 3 --testframes 6 DATAC0 /dev/zero - | + ./freedv_data_raw_rx --framesperburst 2 --testframes DATAC0 - /dev/null --vv") + set_tests_properties(test_freedv_data_raw_ofdm_datac0_burst PROPERTIES PASS_REGULAR_EXPRESSION "Coded FER: 0.0000 Tfrms: 6 Tfers: 0") + + add_test(NAME test_freedv_data_raw_ofdm_data_custom + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_data_raw_tx --bursts 3 --testframes 3 custom /dev/zero - | + ./freedv_data_raw_rx --testframes custom - /dev/null --vv") + set_tests_properties(test_freedv_data_raw_ofdm_data_custom PROPERTIES PASS_REGULAR_EXPRESSION "Coded FER: 0.0000 Tfrms: 3 Tfers: 0") + + # Burst mode with data file I/O: + add_test(NAME test_freedv_data_raw_ofdm_datac0_burst_file + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + head -c $((14*10)) binaryIn.bin; + ./freedv_data_raw_tx DATAC0 binaryIn.bin - --bursts 10 | + ./freedv_data_raw_rx DATAC0 - binaryOut.bin -v; + diff binaryIn.bin binaryOut.bin") + + add_test(NAME test_freedv_data_raw_ofdm_datac1_burst_file + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + head -c $((510*10)) binaryIn.bin; + ./freedv_data_raw_tx DATAC1 binaryIn.bin - --bursts 10 | + ./freedv_data_raw_rx DATAC1 - binaryOut.bin -v; + diff binaryIn.bin binaryOut.bin") + + add_test(NAME test_freedv_data_raw_ofdm_datac3_burst_file + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + head -c $((126*10)) binaryIn.bin; + ./freedv_data_raw_tx DATAC3 binaryIn.bin - --bursts 10 | + ./freedv_data_raw_rx DATAC3 - binaryOut.bin -v; + diff binaryIn.bin binaryOut.bin") + + add_test(NAME test_freedv_data_raw_ofdm_datac4_burst_file + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + head -c $((54*10)) binaryIn.bin; + ./freedv_data_raw_tx DATAC4 binaryIn.bin - --bursts 10 | + ./freedv_data_raw_rx DATAC4 - binaryOut.bin -v; + diff binaryIn.bin binaryOut.bin") + + add_test(NAME test_freedv_data_raw_ofdm_datac13_burst_file + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + head -c $((14*10)) binaryIn.bin; + ./freedv_data_raw_tx DATAC13 binaryIn.bin - --bursts 10 | + ./freedv_data_raw_rx DATAC13 - binaryOut.bin -v; + diff binaryIn.bin binaryOut.bin") + + add_test(NAME test_freedv_data_raw_ofdm_datac14_burst_file + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + head -c $((3*10)) binaryIn.bin; + ./freedv_data_raw_tx DATAC14 binaryIn.bin - --bursts 10 | + ./freedv_data_raw_rx DATAC14 - binaryOut.bin -v; + diff binaryIn.bin binaryOut.bin") + + # FSK LDPC default 100 bit/s 2FSK, enough noise for several % raw BER to give + # FEC/acquisition a work out, bursts of 1 frame as that stresses acquisition + add_test(NAME test_freedv_data_raw_fsk_ldpc_100 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_data_raw_tx --testframes 10 --bursts 10 FSK_LDPC /dev/zero - | + ./ch - - --No -5 --ssbfilt 0 | + ./freedv_data_raw_rx --testframes -v FSK_LDPC - /dev/null") + set_tests_properties(test_freedv_data_raw_fsk_ldpc_100 PROPERTIES PASS_REGULAR_EXPRESSION "Frms.: ( 9|10)") + + # FSK LDPC 1000 bit/s 2FSK, Fs=40kHz, as different configs can upset acquisition + add_test(NAME test_freedv_data_raw_fsk_ldpc_1k + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_data_raw_tx --Fs 40000 --Rs 1000 --tone1 1000 --shift 1000 --testframes 10 --bursts 10 FSK_LDPC /dev/zero - | + ./ch - - --No -10 --ssbfilt 0 | + ./freedv_data_raw_rx --testframes -v --Fs 40000 --Rs 1000 FSK_LDPC - /dev/null") + set_tests_properties(test_freedv_data_raw_fsk_ldpc_1k PROPERTIES PASS_REGULAR_EXPRESSION "Frms.: 10") + + # FSK LDPC 10000 bit/s 2FSK, Fs=100kHz, each of the 10 bursts has 100 frames + add_test(NAME test_freedv_data_raw_fsk_ldpc_10k + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_data_raw_tx --Fs 100000 --Rs 10000 --tone1 10000 --shift 10000 --framesperburst 100 --bursts 10 --testframes 1000 FSK_LDPC /dev/zero - | + ./ch - - --No -16 --ssbfilt 0 | + ./freedv_data_raw_rx --testframes -v --Fs 100000 --Rs 10000 FSK_LDPC - /dev/null") + set_tests_properties(test_freedv_data_raw_fsk_ldpc_10k PROPERTIES PASS_REGULAR_EXPRESSION "Frms.: 1000") + + # FSK LDPC Rs=1000 bit/s (Rb=2000) 4FSK, Fs=40kHz, this needs --mask and 2Rs shift to work reliably + add_test(NAME test_freedv_data_raw_fsk_ldpc_2k + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; + ./freedv_data_raw_tx -a 8192 -m 4 --Fs 40000 --Rs 1000 --tone1 10000 --shift 2000 --testframes 10 --bursts 10 FSK_LDPC /dev/zero - | + ./ch - - --No -22 --ssbfilt 0 | + ./freedv_data_raw_rx -m 4 --testframes -v --Fs 40000 --Rs 1000 FSK_LDPC --mask 2000 - /dev/null") + set_tests_properties(test_freedv_data_raw_fsk_ldpc_2k PROPERTIES PASS_REGULAR_EXPRESSION "Frms.: 10") + + # --------------------------------------------------------- + # tests for demos + # --------------------------------------------------------- + + add_test(NAME test_demo_c2demo + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + ./demo/c2demo ../raw/hts1a.raw hts1a_out.raw; + ls -l hts1a_out.raw") + set_tests_properties(test_demo_c2demo PROPERTIES PASS_REGULAR_EXPRESSION "48000") + + add_test(NAME test_demo_700d + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + cat ../raw/ve9qrp_10s.raw | ./demo/freedv_700d_tx | + ./demo/freedv_700d_rx > ve9qrp_10s_700d.raw; + ls -l ve9qrp_10s_700d.raw") + set_tests_properties(test_demo_700d PROPERTIES PASS_REGULAR_EXPRESSION "158720") + + add_test(NAME test_demo_700d_python + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + cat ../raw/ve9qrp_10s.raw | ./demo/freedv_700d_tx | + ../demo/freedv_700d_rx.py > ve9qrp_10s_700d.raw; + ls -l ve9qrp_10s_700d.raw") + set_tests_properties(test_demo_700d_python PROPERTIES PASS_REGULAR_EXPRESSION "161280") + + add_test(NAME test_demo_datac1 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + head -c $((510*10)) binaryIn.bin; + cat binaryIn.bin | ./demo/freedv_datac1_tx | + ./demo/freedv_datac1_rx > binaryOut.bin; + diff binaryIn.bin binaryOut.bin") + + # test Rx of two modes in parallel, with AWGN noise and sample clock offsets + add_test(NAME test_demo_datac0c1 + COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}; + ./demo/freedv_datac0c1_tx | + ./src/ch - - --No -24 -f 20 | + sox -t .s16 -c 1 -r 8000 - -t .s16 -c 1 -r 8008 - | + ./demo/freedv_datac0c1_rx") + set_tests_properties(test_demo_datac0c1 PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0 Frames: 10 DATAC1 Frames: 10") + + # Set common properties for tests that need Octave/CML + set_tests_properties( + test_CML_ldpcut + test_CML_ldpcut_one_stuffing + test_OFDM_modem_octave_port + test_OFDM_modem_octave_port_Nc_31 + test_OFDM_modem_octave_datac0_mpp_coded + test_OFDM_modem_datac0_octave_burst + test_OFDM_modem_datac1_octave + test_OFDM_modem_datac3_octave + test_OFDM_modem_datac4_octave + test_OFDM_modem_datac13_octave + test_OFDM_modem_datac14_octave + test_fsk_lib_4fsk_ldpc + test_OFDM_modem_datac0_compression + PROPERTIES + ENVIRONMENT "CML_PATH=${CMAKE_CURRENT_BINARY_DIR}/cml" + ) +endif(UNITTEST) diff --git a/third_party/codec2/COPYING b/third_party/codec2/COPYING new file mode 100644 index 0000000..cc40a46 --- /dev/null +++ b/third_party/codec2/COPYING @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see + . + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/third_party/codec2/README.md b/third_party/codec2/README.md new file mode 100644 index 0000000..a41b034 --- /dev/null +++ b/third_party/codec2/README.md @@ -0,0 +1,293 @@ +# Codec 2 README + +Codec 2 is an open source (LGPL 2.1) low bit rate speech codec: http://rowetel.com/codec2.html written in C99 standard C. + +Also included: + + + The FreeDV API for digital voice over radio. FreeDV is an open source digital voice protocol that integrates modems, codecs, and FEC [README_freedv](README_freedv.md) + + HF OFDM and FSK modems, FEC used in the FreeDV API + + APIs for packet data over radio [README_data](README_data.md) + + An STM32 embedded version of FreeDV 1600/700D/700E for the [SM1000](stm32/README.md) + +## Old Code and Deprecated Features + +In July 2023 this repo was refactored, older code can be found in https://github.com/drowe67/codec2-dev. + +We are currently conducting a major re-development of Codec 2, new speech coding, modems, and FreeDV modes are under active development. We have limited resources available for maintenance or development on features that may soon be replaced (with the exception of major bugs). We'd rather put our efforts into new features! Modes and Features we are not actively maintaining at present, and likely to be superseded in the near future include: +1. FreeDV 1600, 700C, 700D, 700E, 2020. +1. Codec 2 modes with the exception of Codec 2 3200 for M17. +1. Our fork of LPCNet. +1. The stm32/SM1000 firmware (paused until new modes available). + +The code supporting these modes won't be going away any time soon (and we will continue to include any modes/code in popular use), but we have chosen not to actively develop it at this time. + +## Pull Requests, Feature Requests + +We have a process for considering Feature Requests and Pull Requests that we will guide you through. + +If you have a Feature Request, please answer the questions in the [Feature Request Form](https://freedv.org/wp-content/uploads/sites/8/2024/03/FreeDV-027-Feature-Request-Form-V1.1.pdf), and submit your answers as a GitHub Issue. + +Before writing any code or submitting a PR - **please discuss** the PR with developers by raising a GitHub Issue. We have many years of experience and a carefully considered plan for Codec 2 development, and can guide you on work that will most benefit this project. + +Some key guidelines about the code in the `codec2` repo: +1. Only code that is required to build, test, or document libcodec2 goes in codec2. +2. Experimental work, code used for algorithm development, should probably go into some other repo. +3. Only widely used “production” code goes in codec2. If it has an user base of < 2 (e.g. personal projects, early R&D) - it should probably be application code or a fork. + +## Ports to non C99 Compilers + +For Windows applications (built with MSVC or any compiler) we recommend linking with our [cross-compiled](#building-for-windows) Codec 2 DLLs. This lets you enjoy the benefits of our carefully developed, tested and maintained codebase without having to develop and maintain your own fork. + +We have standardized on C99 and develop and test using gcc on a Linux platform. Our focus needs to be on what’s unique about our project – the speech codec and modem waveforms, and we lack the resources to support multiple compilers. If you want to build Codec 2 using a non-standard compiler like MSVC and certain embedded compilers you will need to maintain your own Codec 2 fork (a very large commitment). If you decide to fork Codec 2 to a non C99 compiler - please ensure you port the ctests and that they all pass. If the tests have not been ported or do not pass - it's not Codec 2. + +## Quickstart + +1. Install packages (Debian/Ubuntu): + ``` + sudo apt install git build-essential cmake + ``` + Fedora/RH distros: + ``` + sudo dnf groupinstall "Development Tools" "C Development Tools and Libraries" + sudo dnf install cmake + ``` + +1. Build Codec 2: + ``` + git clone https://github.com/drowe67/codec2.git + cd codec2 + mkdir build_linux + cd build_linux + cmake .. + make + ``` + +1. Listen to Codec 2: + ``` + cd codec2/build_linux + ./demo/c2demo ../raw/hts1a.raw hts1a_c2.raw + aplay -f S16_LE ../raw/hts1a.raw + aplay -f S16_LE hts1a_c2.raw + ``` +1. Compress, decompress and then play a file using Codec 2 at 2400 bit/s: + ``` + ./src/c2enc 2400 ../raw/hts1a.raw hts1a_c2.bit + ./src/c2dec 2400 hts1a_c2.bit hts1a_c2_2400.raw + ``` + which can be played with: + ``` + aplay -f S16_LE hts1a_c2_2400.raw + ``` + Or using Codec 2 using 700C (700 bits/s): + ``` + ./src/c2enc 700C ../raw/hts1a.raw hts1a_c2.bit + ./src/c2dec 700C hts1a_c2.bit hts1a_c2_700.raw + aplay -f S16_LE hts1a_c2_700.raw + ``` +1. If you prefer a one-liner without saving to files: + ``` + ./src/c2enc 1300 ../raw/hts1a.raw - | ./src/c2dec 1300 - - | aplay -f S16_LE + ``` + +1. Or you can use your microphone and headphones to encode and listen to the result on the fly: + ``` + br=1300; arecord -f S16_LE -c 1 -r 8000 | ./src/c2enc $br - - | ./src/c2dec $br - - | aplay -f S16_LE - + ``` + +## FreeDV 2020 support (building with LPCNet) + +1. Build LPCNet: + ``` + cd ~ + git clone https://github.com/drowe67/LPCNet + cd LPCNet && mkdir build_linux && cd build_linux + cmake .. + make + ``` + +1. Build Codec 2 with LPCNet support: + ``` + cd ~/codec2/build_linux && rm -Rf * + cmake -DLPCNET_BUILD_DIR=~/LPCNet/build_linux .. + make + ``` + +## Documentation + +An algorithm description can be found in `doc/codec2.pdf`. + +## Programs + ++ See `demo` directory for simple examples of using Codec and the FreeDV API. + ++ `c2demo` encodes a file of speech samples, then decodes them and saves the result. + ++ `c2enc` encodes a file of speech samples to a compressed file of encoded bits. `c2dec` decodes a compressed file of bits to a file of speech samples. + ++ `c2sim` is a simulation/development version of Codec 2. It allows selective use of the various Codec 2 algorithms. For example switching phase modelling or quantisation on and off. + ++ `freedv_tx` & `freedv_rx` are command line implementations of the FreeDV protocol, which combines Codec 2, modems, and Forward Error Correction (FEC). + ++ `cohpsk_*` are coherent PSK (COHPSK) HF modem command line programs. + ++ `fdmdv_*` are differential PSK HF modem command line programs (README_fdmdv). + ++ `fsk_*` are command line programs for a non-coherent FSK modem (README_fsk). + ++ `ldpc_*` are LDPC encoder/decoder command line programs, based on the CML library. + ++ `ofdm_*` are OFDM PSK HF modem command line programs (README_ofdm). + +## Building and Running Unit Tests + +CTest is used as a test framework, with support from [GNU Octave](https://www.gnu.org/software/octave/) scripts. + +1. Install GNU Octave and libraries on Ubuntu with: + ``` + sudo apt install octave octave-common octave-signal liboctave-dev gnuplot python3-numpy sox valgrind clang-format texmaker texlive-bibtex-extra texlive-science + ``` +1. To build and run the tests: + ``` + cd ~/codec2 + rm -Rf build_linux && mkdir build_linux + cd build_linux + cmake -DUNITTEST=1 .. + make + ``` + +1. To just run tests without rebuilding: + ``` + ctest + ``` + +1. To get a verbose run (e.g. for test debugging): + ``` + ctest -V + ``` + +1. To just run a single test: + ``` + ctest -R test_OFDM_modem_octave_port + ``` + +1. To list the available tests: + ``` + ctest -N + ``` + +1. Many Octave scripts rely on the CML LDPC library. To run these from the Octave CLI, you need to set + the `CML_PATH` environment variable. A convenient way to do this is using a `.octaverc` file + in your `codec/octave` directory. For example on a Linux machine, create a `.octaverc` file: + ``` + setenv("CML_PATH","../build_linux/cml") + ``` + +## Directories +``` +cmake - cmake support files +demo - Simple Codec 2 and FreeDv API demo applications +doc - documentation +octave - Octave scripts used to support ctests +src - C source code for Codec 2, FDMDV modem, COHPSK modem, FreeDV API +raw - speech files in raw format (16 bits signed linear 8 kHz) +stm32 - STM32F4 microcontroller and SM1000 FreeDV Adaptor support +unittest - Code to perform and support testing. Part of Debug build. +wav - speech files in wave file format +``` +## GDB and Dump Files + +1. To compile with debug symbols for using gdb: + ``` + cd ~/codec2 + rm -Rf build_linux && mkdir build_linux + cd build_linux + CFLAGS=-g cmake .. + make + ``` + +## Building for Windows + +We develop and test on Linux to the [C99 standard](#ports-to-non-c99-compilers). We recommend using MinGW to cross compile for Windows. + +On Ubuntu Linux: + ``` + sudo apt-get install mingw-w64 + mkdir build_windows && cd build_windows + cmake .. -DCMAKE_TOOLCHAIN_FILE=/home/david/freedv-dev/cmake/Toolchain-Ubuntu-mingw32.cmake -DUNITTEST=FALSE -DGENERATE_CODEBOOK=/home/david/codec2/build_linux/src/generate_codebook + make + ``` + +This will create a working `libcodec2.dll` file for use with other applications (e.g. FreeDV GUI which is in wide spread use on Windows). Please note the utility/development command line applications (e.g. `freedv_rx.exe`) may not work exactly the same on the Windows CLI compared to running on a Unix machine/shell. For example pipes may not function as expected, and ctests are not supported. Our primary development and test environment is Linux, and we lack the resources to support and maintain these applications for other operating systems. + +## Including Codec 2 in an Android project + +In an Android Studio 'NDK' project (a project that uses 'native' code) +Codec 2 can be added to the project in the following way. + +1. Add the Codec 2 source tree to your app (e.g. in app/src/main/codec2) + (e.g. as a git sub-module). + +1. Add Codec 2 to the CMakeList.txt (app/src/main/cpp/CMakeLists.txt): + + ``` + # Sets lib_src_DIR to the path of the target CMake project. + set( codec2_src_DIR ../codec2/ ) + # Sets lib_build_DIR to the path of the desired output directory. + set( codec2_build_DIR ../codec2/ ) + file(MAKE_DIRECTORY ${codec2_build_DIR}) + + add_subdirectory( ${codec2_src_DIR} ${codec2_build_DIR} ) + + include_directories( + ${codec2_src_DIR}/src + ${CMAKE_CURRENT_BINARY_DIR}/../codec2 + ) + ``` + +1. Add Codec 2 to the target_link_libraries in the same file. + +## Building Codec 2 for Microcontrollers + +Codec 2 requires a hardware Floating Point Unit (FPU) to run in real time. + +Two build options have been added to support building on microcontrollers: +1. Setting the `cmake` variable MICROCONTROLLER_BUILD disables position independent code (-fPIC is not used). This was required for the IMRT1052 used in Teensy 4/4.1). + +1. On ARM machines, setting the C Flag \_\_EMBEDDED\_\_ and linking with the ARM CMSIS library will improve performance on ARM-based microcontrollers. \_\_REAL\_\_ and FDV\_ARM\_MATH are additional ARM-specific options that can be set to improve performance if required, especially with OFDM modes. + +A CMakeLists.txt example for a microcontroller is below: + +``` +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(MICROCONTROLLER_BUILD 1) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mlittle-endian -ffunction-sections -fdata-sections -g -O3") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -ffunction-sections -fdata-sections") + +add_definitions(-DCORTEX_M7 -D__EMBEDDED__) +add_definitions(-DFREEDV_MODE_EN_DEFAULT=0 -DFREEDV_MODE_1600_EN=1 -DFREEDV_MODE_700D_EN=1 -DFREEDV_MODE_700E_EN=1 -DCODEC2_MODE_EN_DEFAULT=0 -DCODEC2_MODE_1300_EN=1 -DCODEC2_MODE_700C_EN=1) + +FetchContent_Declare(codec2 + GIT_REPOSITORY https://github.com/drowe67/codec2.git + GIT_TAG origin/main + GIT_SHALLOW ON + GIT_PROGRESS ON +) +FetchContent_GetProperties(codec2) +if(NOT ${codec2_POPULATED}) + FetchContent_Populate(codec2) +endif() +set(CMAKE_REQUIRED_FLAGS "") + +set(LPCNET OFF CACHE BOOL "") +add_subdirectory(${codec2_SOURCE_DIR} ${codec2_BINARY_DIR} EXCLUDE_FROM_ALL) +``` + +## Building Debian packages + +To build Debian packages, simply run the "cpack" command after running "make". This will generate the following packages: + ++ codec2: Contains the .so and .a files for linking/executing applications dependent on Codec2. +* codec2-dev: Contains the header files for development using Codec2. + +Once generated, they can be installed with "dpkg -i" (once LPCNet is installed). If LPCNet is not desired, CMakeLists.txt can be modified to remove that dependency. diff --git a/third_party/codec2/README_cohpsk.md b/third_party/codec2/README_cohpsk.md new file mode 100644 index 0000000..aafc71d --- /dev/null +++ b/third_party/codec2/README_cohpsk.md @@ -0,0 +1,43 @@ +# README_cohpsk.md + +## Introduction + +## Quickstart + +1. BER test in AWGN channel with just less that 2% average bit error rate: + + ``` + $ ./cohpsk_get_test_bits - 5600 | ./cohpsk_mod - - | ./ch - - --No -30 --Fs 7500 | ./cohpsk_demod - - | ./cohpsk_put_test_bits - + + SNR3k(dB): 3.41 C/No: 38.2 PAPR: 8.1 + BER: 0.017 Nbits: 5264 Nerrors: 92 + + ``` + +2. Plot some of the demod internal states, used to chase down freq offset problemL + + ``` + $ cd build_linux/src + $ ./cohpsk_get_test_bits - 5600 | ./cohpsk_mod - - | ./ch - - --No -40 -f -20 --Fs 7500 | ./cohpsk_demod -o cohpsk_demod.txt - - | ./cohpsk_put_test_bits - + $ cd ../../octave + $ octave --no-gui + $ cohpsk_demod_plot("../build_linux/src/cohpsk_demod.txt") + ``` + +3. Run Octave<-> tests + + ``` + $ cd ~/codec2/build_linux/unittest + $ ./tochpsk + $ cd ~/codec2/octave + $ octave --no-gui + octave> tcohpsk + ``` + +## References + +## C Code + +## Octave Scripts + + diff --git a/third_party/codec2/README_data.md b/third_party/codec2/README_data.md new file mode 100644 index 0000000..49fb9e3 --- /dev/null +++ b/third_party/codec2/README_data.md @@ -0,0 +1,378 @@ +# README_data.md + +# Introduction + +FreeDV can be used to send data over radio channels. Two APis are supported: ++ VHF packet data channel which uses Ethernet style framing. ++ Raw frames of modem data over VHF and HF channels. + +## Credits + +The VHF data channel was developed by Jeroen Vreeken. + +## Quickstart + +Raw modem frame API: + +1. Let's send a 128 byte frame containing some text over the modem: + ```sh + padding=$(head -c 115 < /dev/zero | tr '\0' '-'); echo "Hello World" $padding > in.txt + ./src/freedv_data_raw_tx --bursts 1 datac3 in.txt - | ./src/freedv_data_raw_rx --framesperburst 1 datac3 - - + Hello World -------- + ``` + Note we've padded the input frame to 126 bytes, the DATAC3 framesize (less CRC). + +VHF packet data API: + +1. Simple test using mode 2400A and VHF packet data + + ```sh + $ cd ~/codec2/build_linux + $ ./src/freedv_data_tx 2400A - --frames 15 | ./src/freedv_data_rx 2400A - + + ``` + You can listen to the modem signal using: + ```sh + $ ./src/freedv_data_tx 2400A - --frames 15 | aplay -f S16_LE -r 48000 + + ``` + +2. Same for 2400B and 800XA + + ```sh + $ ./src/freedv_data_tx 2400B - --frames 15 | ./src/freedv_data_rx 2400B - + $ ./src/freedv_data_tx 800XA - --frames 15 | ./src/freedv_data_rx 800XA - + + ``` + +3. Using a different callsign and secondary station id + + ```sh + $ ./src/freedv_data_tx 2400A - --callsign T3ST --ssid 15 --frames 15 | src/freedv_data_rx 2400A - + ``` + +# Raw Data using the FreeDV API + +The raw data API can be used to send frames of bytes over radio channels. The frames are protected with FEC and have a 16-bit checksum to verify correct transmission. However the raw data API may lose frames due to channel impairments, loss of sync, or acquisition delays. The caller must handle these situations. The caller is also responsible for segmentation/re-assembly of the modem frames into larger blocks of data. + +Several modes are available which support FSK and OFDM modulation. FSK is aimed at VHF And UHF applications, and the OFDM modes have been optimised for multipath HF radio channels. + +For simple examples of how use the FreeDV API with raw data frames, see the demo programs [freedv_data1_tx.c](demo/freedv_data1_tx.c) and [freedv_data1_rx.c](src/freedv_data1_rx.c) The full featured sample programs [freedv_data_raw_tx.c](src/freedv_data_raw_tx.c) and [freedv_data_raw_rx.c](src/freedv_data_raw_rx.c) can be used to experiment with the raw data API. + +## FSK LDPC Raw Data Mode + +The FSK_LDPC mode uses 2 or 4 FSK in combination with powerful LDPC codes, and was designed for VHF or UHF AWGN channels. Parameters such as the number of FSK tones, sample rate, symbol rate, and LDPC code can be selected at initialisation time. The frame format is: +``` +| Preamble | UW | payload data | CRC | parity | UW | payload data | CRC | parity | ........... | + | frame 1 -------------------------| frame 2 -------------------------| ... frame n | +``` +Only one preamble is transmitted for each data burst, which can contain as many frames as you like. Each frame starts with a 32 bit Unique Word (UW), then the FEC codeword consisting of the data and parity bits. At the end of the data bits, we reserve 16 bits for a CRC. + +Here is an example of sending some text: +``` +$ cd codec2/build_linux/src +$ echo 'Hello World ' | + ./freedv_data_raw_tx FSK_LDPC - - 2>/dev/null | + ./freedv_data_raw_rx FSK_LDPC - - 2>/dev/null | + hexdump -C +00000000 48 65 6c 6c 6f 20 57 6f 72 6c 64 20 20 20 20 20 |Hello World | +00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | ..| +00000020 +``` +Notes: +1. The input data is padded to 30 bytes. The (512,256) code sends 256 data bits every frame, we reserve 16 for a CRC, so there are 240 bits, or 30 bytes of payload data required for one frame. +1. The '2>/dev/null' command redirects stderr to nowhere, removing some of the debug information the test programs usually display to make this example easier to read. + +When testing, it's convenient to use an internal source of test data. Here is an example where we send a single burst of 10 test frames: +``` +$ cd codec2/build_linux/src +$ ./freedv_data_raw_tx --testframes 10 FSK_LDPC /dev/zero - | ./freedv_data_raw_rx --testframes FSK_LDPC - /dev/null +Nbits: 50 N: 4000 Ndft: 1024 +bits_per_modem_frame: 256 bytes_per_modem_frame: 32 +bytes_per_modem_frame: 32 +Frequency: Fs: 8.0 kHz Rs: 0.1 kHz Tone1: 1.0 kHz Shift: 0.2 kHz M: 2 + +frames processed: 131 output bytes: 320 output_packets: 10 +BER......: 0.0000 Tbits: 5440 Terrs: 0 +Coded BER: 0.0000 Tbits: 2560 Terrs: 0 +``` +The default is 100 bits/s 2FSK. The (512,256) code sends 256 data bits (32 bytes) with every codeword, the remaining 256 bits reserved for parity. The `--testframes` mode reports `320 output bytes` (10 frames where sent), and `Tbits: 2560`, so all of our data made it through. + +In real world operation, 16 of the data bits are reserved for a CRC, leaving 240 payload data bits per frame. Taking into account the overhead of the UW, CRC, and parity bits, we send 240 payload data bits for every out of 544, so the payload data rate in this example is (240/512)*(100 bits/s) = 44.1 bits/s. + +We can add some channel noise using the `ch` tool and see how it performs: +``` +$ ./freedv_data_raw_tx --testframes 1 --bursts 10 FSK_LDPC /dev/zero - | + ./ch - - --No -5 --ssbfilt 0 | + ./freedv_data_raw_rx --testframes -v FSK_LDPC - /dev/null + +frames processed: 336 output bytes: 320 output_packets: 10 +BER......: 0.0778 Tbits: 5440 Terrs: 423 +SNR3k(dB): -13.00 C/No: 21.8 PAPR: 7.5 +Coded BER: 0.0000 Tbits: 2560 Terrs: 0 +``` +The `ch` stderr reporting is mixed up with the testframes results but we can see that over a channel with a -13dB SNR, we obtained a raw bit error rate of 0.0778 (nearly 8%). However the LDPC code cleaned that up nicely and still received all 10 packets with no errors. + +Here is an example running 4FSK at 20000 bits/s (10000 symbols/s), at a sample rate of 200 kHz: +``` +$./freedv_data_raw_tx -m 4 --Fs 200000 --Rs 10000 --tone1 10000 --shift 10000 --testframes 100 --bursts 10 FSK_LDPC /dev/zero - | + ./ch - - --No -12 --ssbfilt 0 | + ./freedv_data_raw_rx -m 4 --testframes -v --Fs 200000 --Rs 10000 FSK_LDPC --mask 10000 - /dev/null + + frames processed: 5568 output bytes: 30144 output_packets: 942 +BER......: 0.0691 Tbits: 528224 Terrs: 36505 +Coded BER: 0.0022 Tbits: 248576 Terrs: 535 +``` +Some notes on this example: +1. We transmit 10 bursts, each of 100 frames in length, 1000 packets total. There are a couple of frames silence between each burst. This gives the acquisition algorithms a good work out. +1. Only 942 packets make it though this rather noisy channel, a 6% Packet Error Rate (PER). In a real world application, a higher protocol layer would need to detect this, and arrange for re-transmission of missing packets. If the SNR was a few dB better, all 1000 packets would likely make it through. If it was 1dB worse, nothing would get through; LDPC codes have a very sharp "knee" in the PER versus SNR curve. +1. Our first tone `--tone` is at 10kHz, and each tone is spaced `--shift` by 10kHz, so we have FSK tones at 10,20,30, and 40 kHz. For good performance, FSK tones must be spaced by at least the symbol rate Rs. +1. Although the `ch` utility is designed for 8kHz sample rate operation, it just operates on sampled signals, so it's OK to use at higher sample rates. It does have some internal filtering so best to keep your signal well away from 0 and (sample rate)/2. The SNR measurement is calibrated to a 3000 Hz noise bandwidth, so won't make much sense at other sample rates. The third argument `-12` sets the noise level of the channel. +1. The `--mask` frequency offset algorithm is used, which gives better results on noisy channels, especially for 4FSK. + +## Reading Further + +1. Examples in the [ctests](CMakeLists.txt). +1. [FSK_LDPC blog post](http://www.rowetel.com/?p=7467) + +# OFDM Raw Data modes for HF Radio + +These modes use an OFDM modem with powerful LDPC codes and are designed for sending data over HF radio channels with multipath fading. The current modes supported are: + +| FreeDV Mode | RF bandwidth (Hz) | Payload data rate bits/s | Payload bytes/frame | FEC | Duration (sec) | MPP test | Use case | +| :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | +| DATAC0 | 500 | 291 | 14 | (256,128) | 0.44 | 70/100 at 0dB | Reverse link ACK packets | +| DATAC1 | 1700 | 980 | 510 | (8192,4096) | 4.18 | 92/100 at 5dB | Forward link data (medium SNR) | +| DATAC3 | 500 | 321 | 126 | (2048,1024) | 3.19 | 74/100 at 0dB | Forward link data (low SNR) | +| DATAC4 | 250 | 87 | 56 | (1472,448) | 5.17 | 90/100 at -4dB | Forward link data (low SNR) | +| DATAC13 | 200 | 64 | 14 | (384,128) | 2.0 | 90/100 at -4dB | Reverse link ACK packets (low SNR) | +| DATAC14 | 250 | 58 | 3 | (112,56) | 0.69 | 90/100 at -2dB | Reverse link ACK packets (low SNR) | + +Notes: +1. 16 bits (2 bytes) per frame are reserved for a 16 bit CRC, e.g. for `datac3` we have 128 byte frames, and 128-2=126 bytes/frame of payload data. +1. SNR is the target operating point SNR for each mode. +1. "MPP test" is the number of packets received/transmitted on a simulated MultiPath Poor channel (1Hz Doppler spread, 2ms delay) at the operating point SNR. + +From the callers point of view, the frame format of each burst is: +``` +| Preamble | payload data | CRC | payload data | CRC | ........... | Postamble | + | frame 1 -----------| frame 2 -----------| ... frame N | +``` +In the next layer down, each frame is comprised of several OFDM "modem frames", that contain pilot, unique word, and FEC symbols to handle synchronisation and error correction over the challenging HF channel. The preamble and postamble are used to locate the burst and estimate it's frequency offset. Having both a pre and postamble increases the probability of successful detection of the burst in a fading channel. Here are some single frame bursts on a MPP channel at 5dB SNR: + +![](doc/pre_post_amble_mpp.png) + +You can see what a mess the MPP channel makes. Sometimes we find the pre-amble, other times the post-amble. Using both increases the probability of detecting the burst, it's a form of time diversity. If the probability of missing the pre-amble is P(fail)=0.1, then the probability of missing the pre and post-amble is P(fail)*P(fail)=0.01. If we find either we can work out where the burst starts and start demodulating. + +Here is an example of sending 3 bursts of 2 frames/burst, a total of 6 frames: +``` +./src/freedv_data_raw_tx --framesperburst 2 --bursts 3 --testframes 6 DATAC0 /dev/zero - | +./src/freedv_data_raw_rx --framesperburst 2 --testframes DATAC0 - /dev/null --vv + +BER......: 0.0000 Tbits: 1536 Terrs: 0 +Coded BER: 0.0000 Tbits: 768 Terrs: 0 +Coded PER: 0.0000 Tpkts: 6 Tpers: 0 +``` + +Lets add some noise and a 20 Hz frequency offset: +``` +./src/freedv_data_raw_tx --framesperburst 2 --bursts 3 --testframes 6 DATAC0 /dev/zero - | +./src/ch - - --No -14 -f 20 | +./src/freedv_data_raw_rx --framesperburst 2 --testframes DATAC0 - /dev/null + +mark:space: 0.79 SNR offset: -1.03 +ch: SNR3k(dB): -0.96 C/No....: 33.82 +ch: peak.....: 16394.23 RMS.....: 9814.35 CPAPR.....: 4.46 +ch: Nsamples.: 33440 clipped.: 0.00% OutClipped: 0.03% +modembufs: 35 bytes: 84 Frms.: 6 SNRAv: -1.15 +BER......: 0.0319 Tbits: 1536 Terrs: 49 +Coded BER: 0.0000 Tbits: 768 Terrs: 0 +Coded FER: 0.0000 Tfrms: 6 Tfers: 0 +``` +We still received 6 frames OK (Tpkts field), but in this case there was a raw BER of about 3% which the FEC cleaned up nicely (Coded BER 0.0). Just above that we can see the "SNR offset" and "ch: SNR3k" fields. In the silence between bursts the modem signal has zero power, which biases the SNR measured by the `ch` channel simulation tool. This bias is the "SNR offset". So the true SNR for this test is actually: +``` +SNR = -1.15 - (-1.03) = -0.12 dB +``` +The same offset applies the the Peak to Average Power measurement (CPAPR) returned by the `ch` tool, but in the other direction. So the unbiased CPAPR is: +``` +CPAPR = 4.46 - 1.03 = 3.43 dB +``` +CPAPR refers to the PAPR of the complex valued signal. + +In the `raw` directory is a real world off-air sample of a signal sent between Adelaide and Melbourne (800km) using about 20W on 40m. This can be decoded with: +``` +./src/freedv_data_raw_rx datac1 --framesperburst 1 --testframes ../raw/test_datac1_006.raw /dev/null --vv + +BER......: 0.0134 Tbits: 73728 Terrs: 986 +Coded BER: 0.0000 Tbits: 36864 Terrs: 0 +Coded PER: 0.0000 Tpkts: 9 Tpers: 0 +``` + +It's also useful to listen to the file, you can hear co-channel SSB, the bursts starting and stopping, and some fading: +``` +aplay -f S16_LE ../raw/test_datac1_006.raw +``` + +Here is a spectrogram (waterfall on it's side - time flows from left to right, frequency on the Y axis): + +![](doc/test_datac1_006_spectrogram.png) + +The multipath channel carves notches out of the signal, and the level rises and falls. The 27 carriers of the `datac1` channel can also be observed. The SSB is the fuzz along the top. The SNR varied between 8 and 16dB. The fading is even more obvious on the scatter diagram: + +![](doc/test_datac1_006_scatter.png) + +The X shape is due to the level of each carrier changing with the fading. In some cases a carrier is faded down to zero. The FEC helps clean up any errors due to faded carriers. + +## Modem Performance and Throughput + +The following curves illustrate the OFDM raw data mode performance and throughput over AWGN and MPP channels: + +![](doc/c_tx_comp.png) +![](doc/c_tx_comp_thruput.png) + +The signalling modes (`datac0` and `datac13`) tend to have a "long PER tail" at they are short in duration compared to the fading period. The throughput curve can be used as a guide for "gear shifting" between modes. These curves were generated by [snr_curves.sh](../unittest/raw_data_curves/snr_curves.sh) + +## SNR estimation and clipping + +The modem estimates the SNR of every received packet, which can be useful for selecting the best mode to maximise bit rate while minimising packet error rate. + +Clipping (compression) is enabled by default on each modem waveform to maximise the Peak to Average Power Ratio (PAPR). Power amplifiers are usually rated in terms of peak power (PEP). For a given peak power, clipping increases SNR over the channel by 3-4dB. + +Clipping works by introducing controlled distortion, which affects the SNR estimator in the modem. When clipping is enabled, the SNR reported will start to roll off. If clipping is disabled, the modem will report a more accurate SNR. + +This command line demonstrates the effect: +``` +./src/freedv_data_raw_tx datac3 /dev/zero - --testframes 10 --bursts 10 --clip 1 | ./src/ch - - --No -100 --fading_dir unittest | ./src/freedv_data_raw_rx datac3 - /dev/null --testframes --framesperburst 1 -v +``` +Try adjusting `--clip` and `No` argument of `ch` (noise level) for different modes. Note the SNR estimates returned from `freedv_data_raw_rx` compared to the SNR from the channel simulator `ch`. You will notice clipping also increases the RMS power and reduces the PER for a given channel noise power. CPAPR will also reduce with clipping enabled. + +The following plots illustrate the SNR estimates versus actual channel SNR with and without compression (clipping). Note that even with the uncompressed waveform there is a small offset of around 1dB, possibly due to modem implementation loss or noise in the frequency, phase, or timing estimators. + +![](doc/snrest_snr_ctx.png) +![](doc/snrest_snr_ctxc.png) + +## Reading Further + +1. See the raw data example in Quickstart section above. +1. For simple examples of how use the FreeDV API, see the demo programs [freedv_datac1_tx.c](demo/freedv_datac1_tx.c) and [freedv_datac1_rx.c](demo/freedv_datac1_rx.c) +1. [freedv_data_raw_tx.c](src/freedv_data_raw_tx.c) and [freedv_data_raw_rx.c](src/freedv_data_raw_rx.c) are more full deatured example programs. +1. The modem waveforms designs are described in this [spreadsheet](doc/modem_codec_frame_design.ods). +1. Examples in the [ctests](CMakeLists.txt) (look for "FreeDV API raw data") +1. [Codec 2 HF Data Modes Part 1 blog post](http://www.rowetel.com/?p=7167) +1. [HF Data Acquisition](https://github.com/drowe67/codec2/pull/171) GitHub Pull Request +1. [datac4 & datac13](https://github.com/drowe67/codec2/pull/364) GitHub Pull Request +1. [FreeDATA](https://freedata.app/) uses these modems + +# VHF Packet Data Channel + +The FreeDV VHF data channel operates on a packet level. The FreeDV modems however typically operate on a fixed frame base. This means that data packets have to be sent in multiple frames. + +The packet format is modeled after Ethernet. As a result, any protocol that is compatible with Ethernet can potentially be used over a FreeDV data link. (There are of course practical limits. Browsing the world wide web with just a few hundred bits per second will not be a pleasant experience.) + +## Header optimization + +When there are no packets available for transmission a small 'filler' packet with just the sender's address will be sent. +When there is a packet available not all of the header needs to be sent. The sender's address can often be left out if it was already sent in a previous frame. Likewise when the packet has no specific destination but is targeted at a multicast address, this can also be transmitted in a single bit as opposed to a 6 byte broadcast address. + + +## Addressing + +Since the format is based on Ethernet, a 6 byte sender and destination address is used. It is possible to encode an ITU compatible callsign in these bytes. See http://dmlinking.net/eth_ar.html for more info. Or have a look at freedv_data_tx.c and freedv_data_rx.c for an actual implementation. + +## Packet types + +The 2 byte EtherType field is used to distinguish between various protocols. + +## Checks + +Not all channels are perfect, and especially since a packet is split up over multiple frames, bits might get lost. Each packet therefore has a CRC which is checked before it is accepted. Note there is No FEC on 2400A/2400B/800XA. + +## Available modes + +The data channel is available for modes 2400A, 2400B and 800XA. + +## API + +The data channel is part of the regular FreeDV API. + +### Initialization + +After creating a new freedv instance with freedv_open(), a few more calls need to be done before the data channel is usable. + + ``` + void freedv_set_data_header (struct freedv *freedv, unsigned char *header); + ``` + +The address that will be used for 'filler' packets must be set. The freedv_set_data_header() function must be called with a 6 byte header. + + ``` + typedef void (*freedv_callback_datarx)(void *, unsigned char *packet, size_t size); + typedef void (*freedv_callback_datatx)(void *, unsigned char *packet, size_t *size); + void freedv_set_callback_data (struct freedv *freedv, freedv_callback_datarx datarx, freedv_callback_datatx datatx, void *callback_state); + ``` + +Using freedv_set_callback_data() two callback functions can be provided. The datarx callback will be used whenever a new data packet has been successfully received. The datatx callback will be used when a new data packet is required for transmission. + +### Operation + + ``` + void freedv_datatx (struct freedv *f, short mod_out[]); + ``` + +During normal operation the freedv_datatx() function can be used whenever a data frame has to be sent. If no data is available it will request new data using the datatx callback. The callback function is allowed to set 'size' to zero if no data is available or if it wishes to send an address frame. + +For reception the regular freedv_rx() functions can be used as received data will automatically be reported using the datarx callback. Be aware that these functions return the actual number of received speech samples. When a data frame is received the return value will be zero. This may lead to 'gaps' in the audio stream which will have to be filled with silence. + +### Examples + +The freedv_data_tx and freedv_data_rx test programs implement the minimum needed to send and receive data packets. + +## Mixing voice and data + +Encoding only voice data is easy with the FreeDV API. Simply use the freedv_tx() function and provide it with speech samples. +Likewise encoding only data is also easy. Make sure to provide a source of data frames using the freedv_set_callback_data() function, and use the freedv_datatx() function to generate frames. + +However there are many use cases where one would like to transmit a mix of voice and data. For example one might want to transmit their callsign in a machine readable format, or a short position report. There are a few ways to do this: + +### Data bursts at start and/or end of transmission + +This method simply transmits voice frames during the transmission, except for a few moments. For example when the user keys the radio the software uses the freedv_datatx() function for a number of frames before switching to regular voice frames. +Likewise when the user releases the key the software may hold it for a number of frames to transmit data before it releases the actual radio. + +Be careful though: depending on your setup (radio, PC, soundcard, etc) the generated frames and the keying of your radio might not be perfectly in sync and the first or last frames might be lost in the actual transmission. Make sure to take this into account when using this method. + +### Data and voice interleaved + +Another method is to generate a mixed stream of frames. Compared to a small burst at the beginning or end a lot more data can be sent. We only need a way to choose between voice or data such that the recovered speech at the other side is not impacted. + +#### Detect voice activity + +When it is possible to determine activity in the voice signal (and it almost always is) this presence can be used to insert a data frame by calling freedv_datatx() instead of freedv_tx()/freedv_codectx(). This method is used in the freedv_mixed_tx demo program. When the option --codectx is given the codec2 library is used to determine the activity. + + ``` + $ ./src/freedv_mixed_tx 2400A ../raw/hts1a.raw - --codectx | src/freedv_data_rx 2400A - + $ ./src/freedv_mixed_tx 2400A ../raw/hts1a.raw - | src/freedv_data_rx 2400A - + ``` + +The advantage of this method is that the audio is not distorted, there was nothing (or near nothing) to distort. A drawback is that constant voice activity may mean there are insufficient frames for data. + +### Receiving mixed voice and data + +Receiving and decoding a mixed voice and data stream is (almost) as easy as receiving a regular voice-only transmission. +One simply uses the regular API calls for reception of speech samples. In addition, the callback functions are used for data. +There is one caveat though: when a data frame is received the API functions (like freedv_rx) will return zero as this is the amount of codec/voice data received. +For proper playback silence (or comfort noise) should be inserted for the duration of a frame to restore the timing of the original source speech samples. +An example of how this is done is provided in freedv_mixed_rx + + ``` + $ ./src/freedv_mixed_tx 2400A ../raw/hts1a.raw - | src/freedv_mixed_rx 2400A - ./hts1a_out.raw + ``` + +### Insert a data frame periodically + +This is a very simple method, simply insert a data frame every n frames, (e.g. once every 10 seconds). Since single FreeDV frames are relatively short (tens of milliseconds) the effect on received audio will be minor. The advantage of this method is that one can create a guaranteed amount of data bandwidth. A drawback is some interruption in the audio that may be noticed. + +### Combination of the above. + +A combination of the two methods may also be used. Send data when no voice is active and insert a frame when this does not occur for a long time. + diff --git a/third_party/codec2/README_fdmdv.md b/third_party/codec2/README_fdmdv.md new file mode 100644 index 0000000..b1d6bb9 --- /dev/null +++ b/third_party/codec2/README_fdmdv.md @@ -0,0 +1,106 @@ +# README_fdmdv + +## Introduction + +A 1400 bit/s (nominal) Frequency Division Multiplexed Digital Voice (FDMDV) modem based on [FreeDV 1600 Specification](https://freedv.org/freedv-specification). Used for FreeDV 1600. + +The FDMDV modem was first implemented in GNU Octave, then ported to C. Algorithm development is generally easier in Octave, but for real-time work we need the C version. Automated units tests ensure the operation of the Octave and C versions are identical. + +## Quickstart + +Built as part of codec2, see [README](README.md) for build instructions. + +1. Generate some test bits and modulate them: + ``` + $ cd codec2/build_linux/src + $ ./fdmdv_get_test_bits test.c2 1400 + $ ./fdmdv_mod test.c2 test.raw + $ play -t .s16 -r 8000 test.raw + ``` + +1. Two seconds of test frame data modulated and sent out of sound device: + ``` + $ ./fdmdv_get_test_bits - 2800 | ./fdmdv_mod - - | play -t .s16 -r 8000 - + ``` + +1. Send 14000 modulated bits (10 seconds) to the demod and count errors: + ``` + $ ./fdmdv_get_test_bits - 14000 | ./fdmdv_mod - - | ./fdmdv_demod - - 14 demod_dump.txt | ./fdmdv_put_test_bits - + bits 13664 errors 0 BER 0.0000 + ``` + Use Octave to look at plots of 1 second (1400 bits) of modem operation: + ``` + $ cd codec2/octave + $ octave-cli + octave:1> fdmdv_demod_c("../build_linux/src/demod_dump.txt",14000) + ``` + +1. Test with timing slips due to sample clock offset of 1000ppm: + ``` + $ ./fdmdv_get_test_bits - 30000 | ./fdmdv_mod - - | sox -t raw -t .s16 -r 8000 - -t .s16 -r 7990 - | ./fdmdv_demod - - 14 demod_dump.txt | ./fdmdv_put_test_bits - + octave:98> fdmdv_demod_c("../build_linux/src/demod_dump.txt",28000) + 27552 bits 0 errors BER: 0.0000 + ``` + +1. Run Octave simulation of entire modem and AWGN channel: + ``` + $ cd codec2/octave + $ octave-cli + octave:1> fdmdv_ut + ``` + + +## References + +1. [FreeDV 1600 Specification](https://freedv.org/freedv-specification) +1. [Testing a FDMDV Modem](http://www.rowetel.com/blog/?p=2433) + +## C Code + +| File | Description | +| --- | --- | +| src/fdmdv_mod.c | C version of modulator that takes a file of bits and converts it to a raw file of modulated samples | +| src/fdmdv_demod.c | C version of demodulator that takes a raw file of modulated samples and outputs a file of bits. Optionally dumps demod states to a text file which can be plotted using the Octave script fdmdv_demod_c.m | +| src/codec2_fdmdv.h | Header file that exposes FDMDV C API functions | +| src/fdmdv.c | C functions that implement the FDMDV modem | +| src/fdmdv-internal.h | Internal states and constants for FDMDV modem, shouldn't be exposed to application program | +| unittest/tfdmdv.c | Used to conjunction with unittest/tfdmdv.m to automatically test C FDMDV functions against Octave versions | + +## Octave Scripts + +Note these require some Octave packages to be installed, see [README](README.md) + +| File | Description | +| --- | --- | +| fdmdv.m | Functions and variables that implement the Octave version of the FDMDV modem | +| fdmdv_ut.m | Unit test for fdmdv Octave code, useful while developing algorithm. Includes tx/rx plus basic channel simulation | +| fdmdv_mod.m | Octave version of modulator that outputs a raw file. The modulator is driven by a test frame of bits. This can then be played over a real channel or through a channel simulator like PathSim. The sample rate can be changed using "sox" to simulate differences in tx/rx sample clocks | +| fdmdv_demod.m | Demodulator program that takes a raw file as input, and works out the bit error rate using known test frames. Can be used to test the demod performance with off-air signals, or signals that have been passed through a channel simulator | +| fdmdv_demod_c.m | Takes an output text file from the C demod fdmdv_demod.c and produces plots and measures BER. Useful for evaluating fdmdv_demod.c performance. The plots produced are identical to the Octave version fdmdv_demod.m, allowing direct comparison of the C and Octave versions | +| tfdmdv.m | Automatic tests that compare the Octave and C versions of the FDMDV modem functions. First run unittest/tfdmdv, this will generate a text file with test vectors from the C version. Then run the Octave script tfdmdv and it will generate Octave versions of the test vectors and compare each vector with the C equivalent. It plots the vectors and errors (green). It also produces an automatic checklist based on test results. If the Octave or C modem code is changed, this script should be used to ensure the C and Octave versions remain identical. This process has been wrapped up in the `ctest -R test_FDMDV_modem_octave_port`. | + +1. Typical fdmdv_ut run: + ``` + octave:6> fdmdv_ut + Eb/No (meas): 7.30 (8.29) dB + bits........: 2464 + errors......: 20 + BER.........: 0.0081 + PAPR........: 13.54 dB + SNR.........: 4.0 dB + ``` + It also outputs lots of nice plots that show the operation of the modem. + + For a 1400 bit/s DQPSK modem we expect about 1% BER for Eb/No = 7.3dB, which corresponds to SNR = 4dB (3kHz noise BW). The extra dB of measured power is due to the DBPSK pilot. Currently the noise generation code doesn't take the pilot power into account, so in this example the real SNR is actually 5dB. + +1. To generate 10 seconds of modulated signal: + ``` + octave:8> fdmdv_mod("test.raw",1400*10); + ``` + To demodulate 2 seconds of the test.raw file generated above: + ``` + octave:9> fdmdv_demod("test.raw",1400*2); + 2464 bits 0 errors BER: 0.0000 + ``` + It also produces several plots showing the internal states of the demod. Useful for debugging and observing what happens with various channels. + diff --git a/third_party/codec2/README_freedv.md b/third_party/codec2/README_freedv.md new file mode 100644 index 0000000..29f8aed --- /dev/null +++ b/third_party/codec2/README_freedv.md @@ -0,0 +1,217 @@ +# FreeDV Technology + +FreeDV is an open source digital voice protocol that integrates modems, speech codecs, and FEC. + +On transmit, FreeDV converts speech to a modem signal you can send over a radio channel. On receive, FreeDV takes off air modem signals and converts them to speech samples. + +FreeDV is available as a GUI application, an open source library (FreeDV API), and in hardware (the SM1000 FreeDV adaptor). FreeDV is part of the Codec 2 project. + +This document gives an overview of the technology inside FreeDV, and some additional notes on building/using the FreeDV 2020 and 2400A/2400B modes. + +![FreeDV mode knob](http://www.rowetel.com/images/codec2/mode_dv.jpg) + +## FreeDV API + +The general programming model is: + ``` + speech samples -> FreeDV encode -> modulated samples (send over radio) -> FreeDV decode -> speech samples + ``` + +The `codec2/demo` directory provides simple FreeDV API demo programs written in C and Python to help you get started, for example: + +``` +cd codec2/build_linux +cat ../raw/ve9qrp_10s.raw | ./demo/freedv_700d_tx | ./demo/freedv_700d_rx | aplay -f S16_LE +``` + +The current demo programs are as follows: + +| Program | Description | +| --- | --- | +| [c2demo.c](demo/c2demo.c) | Encode and decode speech with Codec 2 | +| [freedv_700d_tx.c](demo/freedv_700d_tx.c) | Transmit a voice signal using the FreeDV API | +| [freedv_700d_rx.c](demo/freedv_700d_rx.c) | Receive a voice signal using the FreeDV API | +| [freedv_700d_rx.py](demo/freedv_700d_rx.py) | Receive a voice signal using the FreeDV API in Python | +| [freedv_datac1_tx.c](demo/freedv_datac1_tx.c) | Transmit raw data frames using the FreeDV API | +| [freedv_datac1_rx.c](demo/freedv_datac1_rx.c) | Receive raw data frames using the FreeDV API | +| [freedv_datac0c1_tx.c](demo/freedv_datac0c1_tx.c) | Transmit two types of raw data frames using the FreeDV API | +| [freedv_datac0c1_rx.c](demo/freedv_datac0c1_rx.c) | Receive two types of raw data frames using the FreeDV API | + +So also [freedv_api.h](src/freedv_api.h) and [freedv_api.c](src/freedv_api.c) for the full list of API functions. Only a small set of these functions are needed for basic FreeDV use, please see the demo programs for minimal examples. + +The full featured command line demo programs [freedv_tx.c](src/freedv_tx.c) & [freedv_rx.c](src/freedv_rx.c) demonstrate many features of the API: + +``` +$ ./freedv_tx 1600 ../../raw/hts1.raw - | ./freedv_rx 1600 - - | aplay -f S16_LE +$ cat freedv_rx_log.txt +``` + +Speech samples are input to the API as 16 bit signed integers. Modulated samples can be in real 16 bit signed integer or complex float. The expected sample rates can be found with `freedv_get_speech_sample_rate()` and `freedv_get_modem_sample_rate()`. These are typically 8000 Hz but can vary depending on the current FreeDV mode. + +## FreeDV HF Modes + +These are designed for use with a HF SSB radio. + +| Mode | Date | Codec | Modem | RF BW | Raw bits/s | FEC | Text bits/s | SNR min | Multipath | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1600 | 2012 | Codec2 1300 | 14 DQPSK + 1 DBPSK pilot carrier | 1125 | 1600 | Golay (23,12) | 25 | 4 | poor | +| 700C | 2017 | Codec2 700C | 14 carrier coherent QPSK + diversity | 1500 | 1400 | - | - | 2 | good | +| 700D | 2018 | Codec2 700C | 17 carrier coherent OFDM/QPSK | 1000 | 1900 | LDPC (224,112) | 25 | -2 | fair | +| 700E | 2020 | Codec2 700C | 21 carrier coherent OFDM/QPSK | 1500 | 3000 | LDPC (112,56) | 25 | 1 | good | +| 2020 | 2019 | LPCNet 1733 | 31 carrier coherent OFDM/QPSK | 1600 | 3000 | LDPC (504,396) | 22 | 2 | poor | +| 2020B | 2022 | LPCNet 1733 | 29 carrier coherent OFDM/QPSK | 2100 | 4100 | LDPC (112,56) unequal | 22.2 | 3 | good | +| 2020C | 2022 | LPCNet 1733 | 29 carrier coherent OFDM/QPSK | 2100 | 4100 | LDPC (212,158) | 22.2 | 5 | good | + +Notes: + +1. *Raw bits/s* is the number of payload bits/s carried over the channel by the modem. This consists of codec frames, FEC parity bits, unprotected text, and synchronisation information such as pilot and unique word bits. The estimates are open to interpretation for the OFDM waveforms due to pilot symbol and cyclic prefix considerations (see spreadsheet). + +1. *RF BW* is the bandwidth of the RF signal over the air. FreeDV is more bandwidth efficient than SSB. + +1. *Multipath* is the relative resilience of the mode to multipath fading, the biggest problem digital voice faces on HF radio channels. Analog SSB would be rated as "good". + +1. *Text* is a side channel for low bit rate text such as your location and call sign. It is generally unprotected by FEC, and encoded with varicode. The exception is if reliable_text support is turned on (see reliable_text.c/h); this results in text protected by LDPC(112,56) FEC with interleaving. + +1. *SNR Min* is for an AWGN channel (no multipath/fading). + +1. All of the modems use multiple parallel carriers running at a low symbol rate of around 50 Hz. This helps combat the effects of multipath channels. + +1. Some of the Codec 2 modes (2400/1300/700C etc) happen to match the name of a FreeDV mode. For example FreeDV 700C uses Codec 2 700C for voice compression. However FreeDV 700D *also* uses Codec 2 700C for voice compression, but has a very different modem waveform to FreeDV 700C. Sorry for the confusing nomenclature. + +1. Coherent demodulation gives much better performance than differential, at the cost of some additional complexity. Pilot symbols are transmitted regularly to allow the demod to estimate the reference phase of each carrier. + +1. The 1600 and 700C waveforms use parallel tone modems, later modes use OFDM. OFDM gives tighter carrier packing which allows higher bit rates, but tends to suffer more from frequency offsets and delay spread. + +1. At medium to high SNRs, FreeDV 700C performs well (better than 700D) on fast fading multipath channels with large delay spread due its parallel tone design and high pilot symbol rate. It employs transmit diversity which delivers BER performance similar to modes using FEC. FreeDV 700C also has a short frame (40ms), so syncs fast with low latency. Fast sync is useful on marginal channels that move between unusable and barely usable. + +1. FreeDV 700D uses an OFDM modem and was optimised for low SNR channels, with strong FEC but a low pilot symbol rate and modest (2ms) cyclic prefix which means its performance degrades on multipath channels with fast (> 1Hz) fading. The use of strong FEC makes this mode quite robust to other channel impairments, such as static crashes, urban HF noise, and in-band interference. + +1. FEC was added fairly recently to FreeDV modes. The voice codecs we use work OK at bit error rates of a few %, and packet error rates of 10%. Raw bit error rates on multipath channels often exceed 10%. For reasonable latency (say 40ms) we need small codewords. Thus to be useful we require a FEC code that works at over 10% raw BER, has 1% output (coded) bit error rate, and a codeword of around 100 bits. Digital voice has unusual requirements, most FEC codes are designed for data which is intolerant of any bit errors, and few operate over 10% raw BER. Powerful FEC codes have long block lengths (1000's of bits) which leads to long latency. However LDPC codes come close, and can also "clean up" other channel errors caused by static and interference. The use of OFDM means we now have "room" for the extra bits required for FEC, so there is little cost in adding it, apart from latency. + +1. 2020B uses unequal error protection, only 11 bits from each 52 bit vocoder frame are protected by FEC. This provides strong protection of the most important bits. The effect is a gentle "slope" in the speech quality versus SNR curve, but with some audible errors even at high SNRs. 2020C has a LDPC code that protects all bits - it will have no audible errors at high SNRs, but will fall over at about 5dB SNR. 2020B and 2020C have a modem waveform similar to 700E - a high pilot symbol rate to operate on fast fading channels. Compared to 2020, B&C have a shorter frame duration (90ms), lower latency and faster sync, but require a few more dB SNR. + +## FreeDV VHF Modes + +These modes use constant amplitude modulation like FSK or FM, and are designed for VHF and above. However 800XA can be run over HF or VHF on a SSB radio. + +| Mode | Date | Codec2 | Modem | RF BW | Raw bits/s | FEC | Text bits/s | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 2400A | 2016 | 1300 | 4FSK | 5kHz | 2400 | Golay (23,12) | 50 | +| 2400B | 2016 | 1300 | baseband/analog FM | analog FM | 2400 | Golay (23,12) | 50 | +| 800XA | 2017 | 700C | 4FSK | 2000 | 800 | - | N | +| FSK_LDPC | 2020 | - | 2 or 4 FSK | user defined | user defined | LDPC | - | - | + +The FSK_LDPC mode is used for data, and has user defined bit rate and a variety of LDPC codes available. It is discussed in [README_data](README_data.md) + +## FreeDV 2400A and 2400B modes + +FreeDV 2400A and FreeDV 2400B are modes designed for VHF radio. FreeDV 2400A is designed for SDR radios (it has a 5 kHz RF bandwidth), however FreeDV 2400B is designed to pass through commodity FM radios. + +Demos of FreeDV 2400A and 2400B: +``` +$ ./freedv_tx 2400A ../../raw/ve9qrp_10s.raw - | ./freedv_rx 2400A - - | play -t .s16 -r 8000 - +$ ./freedv_tx 2400B ../../raw/ve9qrp_10s.raw - | ./freedv_rx 2400B - - | play -t .s16 -r 8000 - +``` +Note for FreeDV 2400A/2400B the modem signal sample rate is 48kHz. To +listen to the modem tones from FreeDV 2400B, or play them into a FM HT +mic input: +``` +$ ./freedv_tx 2400B ../../raw/ve9qrp_10s.raw - | play -t .s16 -r 48000 - +``` +Simulate FreeDV 2400B passing through a 300 to 3000 Hz audio path using sox to filter: +``` +$ ./freedv_tx 2400B ../../raw/ve9qrp_10s.raw - | sox -t .s16 -r 48000 - -t .s16 - sinc 300-3000 | ./freedv_rx 2400B - - | play -t .s16 -r 8000 - +``` + +## FreeDV 2020 support (building with LPCNet) + +1. Build LPCNet: + ``` + $ cd ~ + $ git clone https://github.com/drowe67/LPCNet + $ cd LPCNet && mkdir build_linux && cd build_linux + $ cmake .. + $ make + ``` + +1. Build Codec 2 with LPCNet support: + ``` + $ cd ~/codec2/build_linux && rm -Rf * + $ cmake -DLPCNET_BUILD_DIR=~/LPCNet/build_linux .. + $ make + ``` + +## FreeDV 2020 tests with FreeDV API + +``` +$ cat ~/LPCNet/wav/wia.wav | ~/LPCNet/build_linux/src/lpcnet_enc -s | ./ofdm_mod --mode 2020 --ldpc --verbose 1 | ./ofdm_demod --mode 2020 --verbose 1 --ldpc | ~/LPCNet/build_linux/src/lpcnet_dec -s | aplay -f S16_LE -r 16000 +``` +Listen the reference tx: +``` +$ cat ~/LPCNet/wav/wia.wav | ~/LPCNet/build_linux/src/lpcnet_enc -s | ./ofdm_mod --mode 2020 --ldpc --verbose 1 | aplay -f S16_LE +``` + +Listen the freedv_tx: +``` +$ ./freedv_tx 2020 ~/LPCNet/wav/wia.wav - | aplay -f S16_LE +``` + +FreeDV API tx, with reference rx from above: +``` +$ ./freedv_tx 2020 ~/LPCNet/wav/wia.wav - | ./ofdm_demod --mode 2020 --verbose 1 --ldpc | ~/LPCNet/build_linux/src/lpcnet_dec -s | aplay -f S16_LE -r 16000 +``` + +FreeDV API tx and rx: +``` +$ ./freedv_tx 2020 ~/LPCNet/wav/all.wav - | ./freedv_rx 2020 - - | aplay -f S16_LE -r 16000 +$ ./freedv_tx 2020 ~/LPCNet/wav/all.wav - --testframes | ./freedv_rx 2020 - /dev/null --testframes -vv +``` + +Simulated HF slow fading channel, 10.8dB SNR: +``` +$ ./freedv_tx 2020 ~/LPCNet/wav/all.wav - | ./ch - - --No -30 --slow | ./freedv_rx 2020 - - | aplay -f S16_LE -r 16000 +``` +It falls down quite a bit with fast fading (--fast): + +AWGN (noise but no fading) channel, 2.8dB SNR: +``` +$ ./freedv_tx 2020 ~/LPCNet/wav/all.wav - | ./ch - - --No -22 | ./freedv_rx 2020 - - | aplay -f S16_LE -r 16000 +``` + +## Command lines for PER testing 700D/700E PER with clipper + +AWGN: +``` +$ ./src/freedv_tx 700D ../raw/ve9qrp.raw - --clip 0 --testframes | ./src/ch - - --No -16 | ./src/freedv_rx 700D - /dev/null --testframes +``` +MultiPath Poor (MPP): +``` +$ ./src/freedv_tx 700D ../raw/ve9qrp.raw - --clip 0 --testframes | ./src/ch - - --No -24 --mpp --fading_dir unittest | ./src/freedv_rx 700D - /dev/null --testframes +``` + +Adjust `--clip [0|1]` and `No` argument of `ch` to obtain a PER of just less than 0.1, and note the SNR and PAPR reported by `ch`. The use of the `ve9qrp` samples makes the test run for a few minutes, in order to get reasonable multipath channel results. + +Low SNR MPP channel 2020B command line: +``` +cat ~/LPCNet/wav/all.wav | ~/LPCNet/build_linux/src/lpcnet_enc -x | ./src/ofdm_mod --mode 2020B --ldpc --clip --txbpf | ./src/ch - - --No -22 --mpd | ./src/ofdm_demod --mode 2020B --verbose 1 --ldpc | ~/LPCNet/build_linux/src/lpcnet_dec -x | aplay -f S16_LE -r 16000 +``` + +## Reading Further + +1. [FreeDV web site](http://freedv.org) +1. [FreeDV GUI User Manual](https://github.com/drowe67/freedv-gui/blob/master/USER_MANUAL.md) +1. [Codec 2](http://rowetel.com/codec2.html) +1. FreeDV can also be used for data [README_data](https://github.com/drowe67/codec2/blob/master/README_data.md) +1. [FreeDV 1600 specification](https://freedv.org/freedv-specification) +1. [FreeDV 700C blog post](http://www.rowetel.com/wordpress/?p=5456) +1. [FreeDV 700D Released blog post](http://www.rowetel.com/wordpress/?p=6103) +1. [FreeDV 2020 blog post](http://www.rowetel.com/wordpress/?p=6747) +1. [FreeDV 2400A blog post](http://www.rowetel.com/?p=5119) +1. [FreeDV 2400A & 2400B](http://www.rowetel.com/?p=5219) +1. Technical information on various modem waveforms in the [modem codec frame design spreadsheet](https://github.com/drowe67/codec2/blob/master/doc/modem_codec_frame_design.ods) +1. [Modems for HF Digital Voice Part 1](http://www.rowetel.com/wordpress/?p=5420) +1. [Modems for HF Digital Voice Part 2](http://www.rowetel.com/wordpress/?p=5448) +1. [FDMDV modem README](README_fdmdv.md) +1. [OFDM modem README](README_ofdm.md) +1. Many blog posts in the [rowetel.com blog archives](http://www.rowetel.com/?page_id=6172) + diff --git a/third_party/codec2/README_fsk.md b/third_party/codec2/README_fsk.md new file mode 100644 index 0000000..820b55b --- /dev/null +++ b/third_party/codec2/README_fsk.md @@ -0,0 +1,175 @@ +# README_fsk + +A FSK modem with a non-coherent demodulator. Performance is within a fraction of a dB of ideal. The demodulator automagically estimates the tone frequencies and tracks frequency drift. + +Here is a typical Bit Error Rate (BER) versus Eb/No curve: + +![BER versus Eb/No curve](doc/fsk_modem_ber_8000_100.png) + +Note how close the theory line is to measured performance. + +This modem can demodulate FSK signals that sound like [this sample](doc/lockdown_3s.wav); and is used to receive images from the [edge of space](https://github.com/projecthorus/wenet): + +![HAB image from edge of space](doc/wenet_image.jpg) + +## Credits + +The Octave version of the modem was developed by David Rowe. Brady O'Brien ported the modem to C, and wrote the C/Octave tests. The modem is being maintained by David Rowe. Mark Jessop has helped improve the modem operation by testing against various balloon telemtry waveforms. Bill Cowley has developed the Log Likelihood Ratio (LLR) algorithms for 4FSK. + +## Quickstart + +1. Build codec2: + ``` + $ cd codec2 && mkdir build_linux && cmake .. && make + ``` + +1. Generate 1000 test bits, modulate them using 2FSK using a 8000 Hz sample rate, 100 bits/s, play on your sound card: + ``` + $ cd ~/codec2/build_linux/src + $ ./fsk_get_test_bits - 1000 | ./fsk_mod 2 8000 100 1200 1200 - - | aplay -f S16_LE + ``` + The low tone frequency is 1200Hz, and the upper tone 1200 + 1200 = 2400Hz. + +1. Add the demodulator and measure the bit error rate over 10,000 bits of 100 bit/s 2FSK: + ``` + $ ./fsk_get_test_bits - 10000 | ./fsk_mod 2 8000 100 1200 100 - - | ./fsk_demod 2 8000 100 - - | ./fsk_put_test_bits - + + [0099] BER 0.000, bits tested 9900, bit errors 0 + PASS + ``` + We get a Bit Error Rate (BER) of 0, as there is no channel noise to induce bit errors. + +1. Same thing but this time with 4FSK, and less verbose output: + ``` + $ ./fsk_get_test_bits - 10000 | ./fsk_mod 4 8000 100 1200 100 - - | ./fsk_demod 4 8000 100 - - | ./fsk_put_test_bits -q - + + [0099] BER 0.000, bits tested 9900, bit errors 0 + PASS + ``` + +1. Lets add some channel noise: + ``` + $ ./fsk_get_test_bits - 10000 | ./fsk_mod 2 8000 100 1200 100 - - | ./ch - - --No -26 | ./fsk_demod 2 8000 100 - - | ./fsk_put_test_bits -b 0.015 - + + SNR3k(dB): -5.76 C/No: 29.0 PAPR: 3.0 + [0099] BER 0.010, bits tested 9900, bit errors 103 + PASS + ``` + The `ch` utility takes the FSK modulator signal, and adds calibrated noise to it (the `--No -26` value specifies the noise). Try changing the noise level, and note how the Bit Error Rate (BER) changes. The BER is 0.01, which is right on theory for this sort of FSK demodulator at this SNR (2FSK non-coherent demodulator Eb/No=9dB). + + The SNR is calculated using the signal power divided by the noise power in 3000 Hz. The C/No value is the same thing, but uses a noise bandwidth of 1 Hz. There is less noise power when you look at just 1Hz, so C/No is higher. Peak to Average Power ratio (PAPR) is 3dB as a FSK signal is just a single sine wave, and a sine wave peak is 3dB higher than it's average. + +1. You can visualise the C modem operation with a companion python script, for example: + ``` + $ ./fsk_get_test_bits - 10000 | ./fsk_mod -p 10 4 8000 400 400 400 - - | ./fsk_demod -p 10 -t1 4 8000 400 - /dev/null 2>stats.txt + $ python ../../octave/plot_fsk_demod_stats.py stats.txt + ``` + +1. Send some digital voice using FSK at 800 bits/s, and try the two 2400 bits/s FSK modes: + ``` + $ ./freedv_tx 800XA ../../raw/ve9qrp.raw - | ./freedv_rx 800XA - - -vv | aplay -f S16_LE + $ ./freedv_tx 2400A ../../raw/ve9qrp.raw - | ./freedv_rx 2400A - - -vv | aplay -f S16_LE + $ ./freedv_tx 2400B ../../raw/ve9qrp.raw - | ./freedv_rx 2400B - - -vv | aplay -f S16_LE + ``` + +1. LDPC encoded 4FSK, with framing: + ``` + $ cd ~/codec2/build_linux/src + $ ./ldpc_enc /dev/zero - --code H_256_512_4 --testframes 200 | + ./framer - - 512 5186 | ./fsk_mod 4 8000 100 1000 100 - - | + ./ch - - --No -24 | + ./fsk_demod -s 4 8000 100 - - | + ./deframer - - 512 5186 | + ./ldpc_dec - /dev/null --code H_256_512_4 --testframes + + SNR3k(dB): -7.74 C/No: 27.0 PAPR: 3.0 + Raw Tbits: 100352 Terr: 6701 BER: 0.067 + Coded Tbits: 50176 Terr: 139 BER: 0.003 + Tpkts: 196 Tper: 4 PER: 0.020 + ``` + In this example the unique word is the 16 bit sequence `5186`. See also several ctests using these application. Other codes are also available: + ``` + $ ./ldpc_enc --listcodes + + H2064_516_sparse rate 0.80 (2580,2064) + HRA_112_112 rate 0.50 (224,112) + HRAb_396_504 rate 0.79 (504,396) + H_256_768 rate 0.33 (768,256) + H_256_512_4 rate 0.50 (512,256) + HRAa_1536_512 rate 0.75 (2048,1536) + H_128_256_5 rate 0.50 (256,128) + ``` + If you change the code you also need to change the `frameSizeBits` argument in `framer/deframer` (`512` in the example above). + +1. The FSK/LDPC/framer steps above have been combined in a FreeDV API mode. See "FSK LDPC Raw Data Mode" in [README_data.md](README_data.md). + +1. FSK modem C files in ```codec2/src```: + + | File | Description | + | --- | --- | + | fsk.c/fsk.h | core FSK modem library | + | fsk_mod.c | command line modulator | + | fsk_demod.c | command line demodulator | + | fsk_get_test_bits.c | source of test bits | + | fsk_put_test_bits.c | test bit sync, counts bit errors and packet errors | + | fsk_mod_ext_vco.c | modulator that uses an external FSK oscillator | + | framer.c | adds a unique word to a frame of bits to implement frame sync for LDPC codewords | + | deframer.c | locates and strips a unique word to implement frame sync for LDPC codewords | + | tollr.c | converts bits to LLRs for testing LDPC framing | + +1. GNU Octave files in ```codec2/octave```: + + | File | Description | + | --- | --- | + | fsk_lib.m | Core FSK modem library | + | fsk_lib_demo.m | A demonstration of fsk_lib, runs a single point BER test | + | fsk_demod_file.m | Demodulates FSK signals from a file, useful for debugging FSK waveforms | + | tfsk.m | automated test that compares the C and Octave versions of the modem | + | fsk_lib_ldpc_demo.m | CML library LLR routines and LDPC codes with fsk_lib.m | + + You can run many of them from the Octave command line: + ``` + $ octave --no-gui + octave:1> fsk_lib_demo + ``` + +1. A suite of automated ctests that exercise the C and Octave code: + ``` + $ cd ~/codec2/build_linux + $ ctest -R test_fsk + 1/9 Test #39: test_fsk_lib ...................... Passed 3.37 sec + 3/9 Test #41: test_fsk_modem_octave_port ........ Passed 4.17 sec + 4/9 Test #42: test_fsk_modem_mod_demod .......... Passed 0.06 sec + 5/9 Test #43: test_fsk_2fsk_ber ................. Passed 0.24 sec + 6/9 Test #44: test_fsk_4fsk_ber ................. Passed 0.12 sec + 7/9 Test #45: test_fsk_4fsk_ber_negative_freq ... Passed 0.07 sec + 8/9 Test #46: test_fsk_4fsk_lockdown ............ Passed 2.84 sec + 9/9 Test #47: test_fsk_vhf_framer ............... Passed 0.06 sec + ``` + These are written in ```codec2/CmakeLists.txt```, inspect them to find out how we test the modem. + +1. ```fsk_demod_file.m``` is useful for peering inside the modem, for example when debugging. + ``` + $ cd ~/codec2/build_linux/src + $ ./fsk_get_test_bits - 1000 | ./fsk_mod 2 8000 100 1000 1000 - ../../octave/fsk.s16 + $ octave --no-gui + octave:1> fsk_demod_file("fsk.s16",format="s16",8000,100,2) + ``` + +## Further Reading + + Here are some links to projects and blog posts that use this modem: + + 1. [Horus Binary](https://github.com/projecthorus/horusbinary) High Altitude Balloon (HAB) telemetry protocol, 3 second updates, works at 7dB lower SNR that RTTY. + 1. [Testing HAB Telemetry, Horus binary waveform](http://www.rowetel.com/?p=5906) + 1. A really useful reference on a variety of modulation techniques from [Atlanta DSP](http://www.atlantarf.com/FSK_Modulation.php). I keep this handy when experimenting with modems. + 1. The [RTTY modem project](http://www.rowetel.com/?p=4629) that kicked off the FSK modem work. + 1. [Wenet](https://github.com/projecthorus/wenet) - high speed SSTV images from balloons at the edge of space + 1. [Wenet High speed SSTV images](http://www.rowetel.com/?p=5344) + 1. [FreeDV 2400A and 2400B](http://www.rowetel.com/?p=5219), digital speech for VHF/UHF radios. + 1. [HF FSK with Rpitx](http://www.rowetel.com/?p=6317), a zero hardware FSK transmitter using a Pi + 1. [Eb/No and SNR worked Example](http://www.rowetel.com/wordpress/?p=4621) + 1. [FSK LLR LDPC Code Experiments](https://github.com/drowe67/codec2/pull/129) + 1. [FreeDV API FSK LDPC Raw Data Mode](README_data.md) + + diff --git a/third_party/codec2/README_ofdm.md b/third_party/codec2/README_ofdm.md new file mode 100644 index 0000000..62c2bdf --- /dev/null +++ b/third_party/codec2/README_ofdm.md @@ -0,0 +1,252 @@ +# README_ofdm + +An Orthogonal Frequency Division Multiplexed (OFDM) modem designed for digital voice over HF SSB. Typical configuration for FreeDV 700D is 700 bit/s voice, a rate 0.5 LDPC code, and 1400 bit/s raw data rate over the channel. + +The OFDM modem was first implemented in GNU Octave, then ported to C. Algorithm development is generally easier in Octave, but for real time work we need the C version. Automated units tests ensure the operation of the Octave and C versions are identical. + +## Credits + +Steve, David, Don, Richard + +## References + +1. Spreadsheet describing the [waveform design](doc/modem_codec_frame_design.ods) The OFDM tab descrives the baseline 700D OFDM waveform. + +1. This modem can be used for sending [raw data frames](README_data.md) over HF channels. + +1. [Towards FreeDV 700D](https://www.rowetel.com/?p=5573) + +1. [FreeDV 700D - First Over The Air Tests](https://www.rowetel.com/?p=5630) + +1. [Steve Ports an OFDM modem from Octave to C](https://www.rowetel.com/?p=5824) + +1. [Modems for HF Digital Voice Part 1](http://www.rowetel.com/wordpress/?p=5420) + +1. [Modems for HF Digital Voice Part 2](http://www.rowetel.com/wordpress/?p=5448) + +# Examples + +Built as part of codec2-dev, see [README](README.md) for build instructions. + +1. Generate 10 seconds of test frame bits, modulate, and play audio + out of sound device (SoX v14.4.2): + ``` + $ build_linux/src$ ./ofdm_mod --in /dev/zero --testframes 10 | play --type s16 --rate 8000 --channels 2 - + ``` + +1. Generate 10 seconds of uncoded test frame bits, modulate, demodulate, count errors: + ``` + $ build_linux/src$ ./ofdm_mod --in /dev/zero --testframes 10 | ./ofdm_demod --out /dev/null --testframes --verbose 1 --log demod_dump.txt + ``` + Use Octave to look at plots of C modem operation: + ``` + $ cd ../../octave + $ octave --no-gui + octave:1> ofdm_demod_c("../build_linux/src/demod_dump.txt") + ``` + +1. Run Octave versions of mod and demod (called tx and rx to avoid namespace clashes in Octave): + ``` + $ cd ~/octave + $ octave --no-gui + octave:1> ofdm_tx("ofdm_test.raw","700D",10) + octave:1> ofdm_rx("ofdm_test.raw") + ``` + The Octave modulator ofdm_tx can simulate channel impairments, for + example AWGN noise at 4dB SNR: + ``` + octave:1> ofdm_tx("ofdm_test.raw", "700D", 10, 4) + ``` + The Octave versions use the same test frames as C so can interoperate. + ``` + build_linux/src$ ./ofdm_demod --in ../../octave/ofdm_test.raw --out /dev/null --testframes --verbose 1 + ``` + +1. Run mod/demod with LDPC FEC; 60 seconds, 3dB SNR: + ``` + octave:6> ofdm_ldpc_tx('ofdm_test.raw',"700D",60,3) + octave:7> ofdm_ldpc_rx('ofdm_test.raw',"700D") + ``` + C demodulator/LDPC decoder: + ``` + build_linux/src$ ./ofdm_demod --in ../../octave/ofdm_test.raw --out /dev/null --verbose 1 --testframes --ldpc + ``` + +1. Pass Codec 2 700C compressed speech through OFDM modem: + ``` + build_linux/src$ ./c2enc 700C ../../raw/ve9qrp_10s.raw - --bitperchar | ./ofdm_mod --ldpc | ./ofdm_demod --ldpc | ./c2dec 700C - - --bitperchar | play --type s16 --rate 8000 --channels 1 - + ``` + +1. Listen to signal through simulated fading channel in C: + ``` + build_linux/src$ ./c2enc 700C ../../raw/ve9qrp_10s.raw - --bitperchar | ./ofdm_mod --ldpc | ./ch - - --No -20 --mpg -f -5 | aplay -f S16 + ``` + +1. Run test frames through simulated channel in C: + ``` + build_linux/src$ ./ofdm_mod --in /dev/zero --ldpc --testframes 20 | ./ch - - --No -24 -f -10 --mpp | ./ofdm_demod --out /dev/null --testframes --verbose 1 --ldpc + ``` + +1. Run codec voice through simulated fast fading channel, just where it starts to fall over: + ``` + build_linux/src$ ./c2enc 700C ../../raw/ve9qrp.raw - --bitperchar | ./ofdm_mod --ldpc | ./ch - - --No -24 -f -10 --mpp | ./ofdm_demod --ldpc --verbose 1 | ./c2dec 700C - - --bitperchar | aplay -f S16 + ``` + +1. FreeDV 1600 on the same channel conditions, roughly same quality at 8dB higher SNR: + ``` + build_linux/src$ ./freedv_tx 1600 ../../raw/ve9qrp_10s.raw - | ./ch - - --No -30 -f -10 --mpp | ./freedv_rx 1600 - - | aplay -f S16 + ``` + +1. Using FreeDV API test programs: + ``` + build_linux/src$ ./freedv_tx 700D ../../raw/hts1a.raw - --testframes | ./freedv_rx 700D - /dev/null --testframes + build_linux/src$ ./freedv_tx 700D ../../raw/hts1a.raw - | ./freedv_rx 700D - - | aplay -f S16 + build_linux/src$ ./freedv_tx 700D ../../raw/ve9qrp.raw - | ./ch - - --No -26 -f -10 --mpp | ./freedv_rx 700D - - | aplay -f S16 + ``` + +## FreeDV 2020 extensions + +1. 20.5ms symbol period, 31 carrier waveform, (504,396) code, but only 312 data bits used, so we don't send unused data bits. This means we need less carriers (so more power per carrier), and code rate is increased slightly: + ``` + build_linux/src$ ./ofdm_mod --in /dev/zero --testframes 300 --mode 2020 --ldpc 1 --verbose 1 | ./ch - - --No -22 -f 10 --ssbfilt 1 | ./ofdm_demod --out /dev/null --testframes --mode 2020 --verbose 1 --ldpc + + SNR3k(dB): 2.21 C/No: 37.0 PAPR: 9.6 + BER......: 0.0505 Tbits: 874020 Terrs: 44148 + Coded BER: 0.0096 Tbits: 649272 Terrs: 6230 + ``` + +## Acquisition tests + +1. Acquisition (getting sync) can be problematic in fading channels. Some special tests have been developed, that measure acquisition time on off air 700D samples at different time offsets: + ``` + octave:61> ofdm_ldpc_rx("../wav/vk2tpm_004.wav", "700D", "", 5, 4) + build_linux/src$ ./ofdm_demod --in ../../wav/vk2tpm_004.wav --out /dev/null --verbose 2 --ldpc --start_secs 5 --len_secs 4 + ``` + +1. Different time offsets effectively tests the ability to sync on fading channel in different states. Stats for a series of these tests can be obtained with: + ``` + octave:61> ofdm_time_sync("../wav/vk2tpm_004.wav", 30) + + pass: 30 fails: 0 mean: 1.35 var 0.51 + ``` + +## Octave Acceptance Tests + +Here are some useful tests for the Octave, uncoded modem. + +The rate 1/2 LDPC code can correct up to about 10% raw BER, so a good test is to run the modem at Eb/No operating points that produce just less that BER=0.1. The BER2 measure truncates the effect of any start up transients, e.g. as the frequency offset is tracked out. + +1. HF Multipath: + ``` + octave:580> ofdm_tx("ofdm_test.raw","700D",60,2,'mpm',20) + octave:581> ofdm_rx("ofdm_test.raw") + BER2.: 0.0803 Tbits: 84728 Terrs: 6803 + ``` + +1. AWGN: + ``` + octave:582> ofdm_tx("ofdm_test.raw","700D",60,-2,'awgn') + octave:583> ofdm_rx("ofdm_test.raw") + BER2.: 0.0885 Tbits: 84252 Terrs: 7459 + ``` + +## C Acceptance Tests + +Here are some useful tests for the LDPC coded C version of the modem, useful to verify any changes. + +1. AWGN channel, -2dB: + ``` + ./ofdm_mod --in /dev/zero --ldpc --testframes 60 --txbpf | ./ch - - --No -20 -f -10 | ./ofdm_demod --out /dev/null --testframes --verbose 1 --ldpc + + SNR3k(dB): -1.85 C/No: 32.9 PAPR: 9.8 + BER......: 0.0815 Tbits: 98532 Terrs: 8031 + Coded BER: 0.0034 Tbits: 46368 Terrs: 157 + ``` + +1. Fading HF channel: + ``` + ./ofdm_mod --in /dev/zero --ldpc --testframes 60 --txbpf | ./ch - - --No -24 -f -10 --fast | ./ofdm_demod --out /dev/null --testframes --verbose 1 --ldpc + + SNR3k(dB): 2.15 C/No: 36.9 PAPR: 9.8 + BER......: 0.1015 Tbits: 88774 Terrs: 9012 + Coded BER: 0.0445 Tbits: 41776 Terrs: 1860 + ``` + + Note: 10% Raw BER operating point on both channels, as per design. + +# Data Modes + +The OFDM modem can also support datac1/datac2/datac3 modes for packet data. The OFDM modem was originally designed for very short (28 bit) voice codec packets. For data, packets of hundreds to thousands of bits a desirable so we can use long, powerful FEC codewords, and reduce overhead. The datac1/datac2/datac3 QPSK modes are currently under development. + +Here is an example of running the datac3 mode in a low SNR AWGN channel: + +``` +./src/ofdm_mod --mode datac3 --ldpc --in /dev/zero --testframes 60 --verbose 1 | ./src/ch - - --No -20 | ./src/ofdm_demod --mode datac3 --ldpc --out /dev/null --testframes -v 1 + +SNR3k(dB): -3.54 C/No: 31.2 PAPR: 10.4 +BER......: 0.1082 Tbits: 36096 Terrs: 3905 Tpackets: 47 +Coded BER: 0.0000 Tbits: 12032 Terrs: 0 +``` +Note despite the raw BER of 10%, 47/50 packets are received error free. + +# C Code + +| File | Description | +| :-- | :-- | +| ofdm.c | OFDM library | +| codec2_ofdm.h | API header file for OFDM library | +| ofdm_get_test_bits | Generate OFDM test frames | +| ofdm_mod | OFDM modulator command line program | +| ofdm_demod | OFDM demodulator command line program, supports uncoded (raw) and LDPC coded test frames, LDPC decoding of codec data, and can output LLRs to external LDPC decoder | +| ofdm_put_test_bits | Measure BER in OFDM test frames | +| unittest/tofdm | Run C port of modem to compare with octave version (see octave/tofdm) | +| ch | C channel simulator | + +# Octave Scripts + +| File | Description | +| :-- | :-- | +| ofdm_lib | OFDM library | +| ofdm_dev | Used for modem development, run various simulations | +| ofdm_tx | Modulate test frames to a file of sample, cam add channel impairments | +| ofdm_rx | Demod from a sample file and count errors | +| tofdm | Compares Octave and C ports of modem | +| ofdm_ldpc_tx | OFDM modulator with LDPC FEC | +| ofdm_ldpc_rx | OFDM demodulator with LDPC FEC | + +## Specifications + +Nominal FreeDV 700D configuration: + +| Parameter | Value | +| :-- | :-- | +| Modem | OFDM, pilot assisted coherent QPSK | +| Payload bits/s | 700 | +| Text bits/s | 25 (note 4) | +| Unique Word | 10 bits | +| Carriers | 17 | +| RF bandwidth | 944 Hz | +| Symbol period | 18ms +| Cyclic Prefix | 2ms (note 1) +| Pilot rate | 1 in every 8 symbols | +| Frame Period | 160ms | +| FEC | rate 1/2 (224,112) LDPC | +| Operating point | | +| AWGN | Eb/No -0.5dB SNR(3000Hz): -2.5dB (note 2) | +| HF Multipath | Eb/No 4.0dB SNR(3000Hz): 2.0dB (note 3) | +| Freq offset | +/- 60 Hz (sync range) | +| Freq drift | +/- 0.2 Hz/s (for 0.5 dB loss) | +| Sample clock error | 1000 ppm | + +Notes: + +1. Modem can cope with up to 2ms of multipath +1. + ``` + Ideal SNR(3000) = Eb/No + 10*log10(Rb/B) + = -1 + 10*log10(1400/3000) + = -4.3 dB + ``` + So we have about 1.8dB overhead for synchronisation, implementation loss, and the text channel. +1. "CCIR Poor" HF Multipath channel used for testing is two path, 1Hz Doppler, 1ms delay. +1. The text channel is an auxiliary channel, unprotected by FEC, that typically carries callsign/location information for Ham stations. diff --git a/third_party/codec2/cmake/GetDependencies.cmake.in b/third_party/codec2/cmake/GetDependencies.cmake.in new file mode 100644 index 0000000..0d25f67 --- /dev/null +++ b/third_party/codec2/cmake/GetDependencies.cmake.in @@ -0,0 +1,24 @@ +# As this script is run in a new cmake instance, it does not have access to +# the existing cache variables. Pass them in via the configure_file command. +set(CMAKE_BINARY_DIR @CMAKE_BINARY_DIR@) +set(CMAKE_SOURCE_DIR @CMAKE_SOURCE_DIR@) +set(UNIX @UNIX@) +set(WIN32 @WIN32@) +set(CMAKE_CROSSCOMPILING @CMAKE_CROSSCOMPILING@) +set(CMAKE_FIND_LIBRARY_SUFFIXES @CMAKE_FIND_LIBRARY_SUFFIXES@) +set(CMAKE_FIND_LIBRARY_PREFIXES @CMAKE_FIND_LIBRARY_PREFIXES@) +set(CMAKE_SYSTEM_LIBRARY_PATH @CMAKE_SYSTEM_LIBRARY_PATH@) +set(CMAKE_FIND_ROOT_PATH @CMAKE_FIND_ROOT_PATH@) +set(CODEC2_DLL ${CMAKE_BINARY_DIR}/src/libcodec2.dll) + +include(${CMAKE_SOURCE_DIR}/cmake/GetPrerequisites.cmake) +get_prerequisites(${CODEC2_DLL} _deps 1 0 "" "") +foreach(_runtime ${_deps}) + message("Looking for ${_runtime}") + find_library(RUNTIME_${_runtime} ${_runtime}) + message("${RUNTIME_${_runtime}}") + if(RUNTIME_${_runtime}) + file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/bin" + TYPE EXECUTABLE FILES "${RUNTIME_${_runtime}}") + endif() +endforeach() diff --git a/third_party/codec2/cmake/config.h.in b/third_party/codec2/cmake/config.h.in new file mode 100644 index 0000000..60ee7d6 --- /dev/null +++ b/third_party/codec2/cmake/config.h.in @@ -0,0 +1,23 @@ +/*-------------------------------------------------------------------------- + ** This file is autogenerated from config.h.in + ** during the cmake configuration of your project. If you need to make changes + ** edit the original file NOT THIS FILE. + ** --------------------------------------------------------------------------*/ +#ifndef _CONFIGURATION_HEADER_GUARD_H_ +#define _CONFIGURATION_HEADER_GUARD_H_ + +#define SIZEOF_INT @SIZEOF_INT@ +#cmakedefine HAVE_STDLIB_H @HAVE_STDLIB_H@ +#cmakedefine HAVE_STRING_H @HAVE_STRING_H@ +#cmakedefine HAVE_FLOOR @HAVE_FLOOR@ +#cmakedefine HAVE_CEIL @HAVE_CEIL@ +#cmakedefine HAVE_MEMSET @HAVE_MEMSET@ +#cmakedefine HAVE_POW @HAVE_POW@ +#cmakedefine HAVE_SQRT @HAVE_SQRT@ +#cmakedefine HAVE_SIN @HAVE_SIN@ +#cmakedefine HAVE_COS @HAVE_COS@ +#cmakedefine HAVE_ATAN2 @HAVE_ATAN2@ +#cmakedefine HAVE_LOG10 @HAVE_LOG10@ +#cmakedefine HAVE_ROUND @HAVE_ROUND@ +#cmakedefine HAVE_GETOPT @HAVE_GETOPT@ +#endif diff --git a/third_party/codec2/cmake/version.h.in b/third_party/codec2/cmake/version.h.in new file mode 100644 index 0000000..45b26aa --- /dev/null +++ b/third_party/codec2/cmake/version.h.in @@ -0,0 +1,37 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: version.h + AUTHOR......: Tomas Härdin + DATE CREATED: 03 November 2017 + + Codec 2 VERSION #defines + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2017 Tomas Härdin + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +//this functions both as an include guard and your typical HAVE macro +#ifndef CODEC2_HAVE_VERSION +#define CODEC2_HAVE_VERSION + +#define CODEC2_VERSION_MAJOR @CODEC2_VERSION_MAJOR@ +#define CODEC2_VERSION_MINOR @CODEC2_VERSION_MINOR@ +#cmakedefine CODEC2_VERSION_PATCH @CODEC2_VERSION_PATCH@ +#define CODEC2_VERSION "@CODEC2_VERSION@" + +#endif //CODEC2_HAVE_VERSION \ No newline at end of file diff --git a/third_party/codec2/codec2.pc.in b/third_party/codec2/codec2.pc.in new file mode 100644 index 0000000..1d0a565 --- /dev/null +++ b/third_party/codec2/codec2.pc.in @@ -0,0 +1,10 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +libdir=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_LIBDIR@ +includedir=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: codec2 +Description: A speech codec for 2400 bit/s and below +Requires: +Version: @CODEC2_VERSION@ +Libs: -L${libdir} -lcodec2 +Cflags: -I${includedir} -I${includedir}/codec2 diff --git a/third_party/codec2/codec2.podspec b/third_party/codec2/codec2.podspec new file mode 100644 index 0000000..2c7173a --- /dev/null +++ b/third_party/codec2/codec2.podspec @@ -0,0 +1,18 @@ +Pod::Spec.new do |s| + s.name = 'codec2' + s.version = '1.2.0' + s.summary = 'Codec2 voice codec' + s.description = 'Codec2 voice codec library (LGPL-2.1)' + s.homepage = 'https://www.rowetel.com/codec2.html' + s.license = { :type => 'LGPL-2.1', :file => 'COPYING' } + s.author = { 'David Rowe' => 'david@rowetel.com' } + s.source = { :path => '.' } + s.platform = :ios, '12.0' + s.source_files = 'src/**/*.{c,h}' + s.public_header_files = 'src/codec2.h' + s.pod_target_xcconfig = { + 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/src" "$(PODS_TARGET_SRCROOT)/include"', + } + s.compiler_flags = '-std=gnu11' + s.libraries = 'm' +end diff --git a/third_party/codec2/demo/CMakeLists.txt b/third_party/codec2/demo/CMakeLists.txt new file mode 100644 index 0000000..fb902a5 --- /dev/null +++ b/third_party/codec2/demo/CMakeLists.txt @@ -0,0 +1,17 @@ +add_definitions(-DFLOATING_POINT -DVAR_ARRAYS) +include_directories(../src) + +add_executable(c2demo c2demo.c) +target_link_libraries(c2demo codec2) +add_executable(freedv_700d_tx freedv_700d_tx.c) +target_link_libraries(freedv_700d_tx codec2) +add_executable(freedv_700d_rx freedv_700d_rx.c) +target_link_libraries(freedv_700d_rx codec2) +add_executable(freedv_datac1_tx freedv_datac1_tx.c) +target_link_libraries(freedv_datac1_tx codec2) +add_executable(freedv_datac1_rx freedv_datac1_rx.c) +target_link_libraries(freedv_datac1_rx codec2) +add_executable(freedv_datac0c1_tx freedv_datac0c1_tx.c) +target_link_libraries(freedv_datac0c1_tx codec2) +add_executable(freedv_datac0c1_rx freedv_datac0c1_rx.c) +target_link_libraries(freedv_datac0c1_rx codec2) diff --git a/third_party/codec2/demo/c2demo.c b/third_party/codec2/demo/c2demo.c new file mode 100644 index 0000000..6a043bd --- /dev/null +++ b/third_party/codec2/demo/c2demo.c @@ -0,0 +1,77 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: c2demo.c + AUTHOR......: David Rowe + DATE CREATED: 15/11/2010 + + Encodes and decodes a file of raw speech samples using Codec 2. + Demonstrates use of Codec 2 function API. + + cd codec2/build_linux + ./demo/c2demo ../raw/hts1a.raw his1a_out.raw + aplay -f S16_LE hts1a_out.raw + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2010 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +#include +#include + +#include "codec2.h" + +int main(int argc, char *argv[]) { + struct CODEC2 *codec2; + FILE *fin; + FILE *fout; + + if (argc != 3) { + printf("usage: %s InputRawSpeechFile OutputRawSpeechFile\n", argv[0]); + exit(1); + } + + if ((fin = fopen(argv[1], "rb")) == NULL) { + fprintf(stderr, "Error opening input speech file: %s\n", argv[1]); + exit(1); + } + + if ((fout = fopen(argv[2], "wb")) == NULL) { + fprintf(stderr, "Error opening output speech file: %s\n", argv[2]); + exit(1); + } + + /* Note only one set of Codec 2 states is required for an encoder + and decoder pair. */ + codec2 = codec2_create(CODEC2_MODE_1300); + size_t nsam = codec2_samples_per_frame(codec2); + short speech_samples[nsam]; + /* Bits from the encoder are packed into bytes */ + unsigned char compressed_bytes[codec2_bytes_per_frame(codec2)]; + + while (fread(speech_samples, sizeof(short), nsam, fin) == nsam) { + codec2_encode(codec2, compressed_bytes, speech_samples); + codec2_decode(codec2, speech_samples, compressed_bytes); + fwrite(speech_samples, sizeof(short), nsam, fout); + } + + codec2_destroy(codec2); + fclose(fin); + fclose(fout); + + return 0; +} diff --git a/third_party/codec2/demo/freedv_700d_rx.c b/third_party/codec2/demo/freedv_700d_rx.c new file mode 100644 index 0000000..b2c4cfe --- /dev/null +++ b/third_party/codec2/demo/freedv_700d_rx.c @@ -0,0 +1,55 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: freedv_700d_rx.c + AUTHOR......: David Rowe + DATE CREATED: April 2021 + + Demo receive program for FreeDV API (700D mode), see freedv_700d_tx.c for + instructions. + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2021 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +#include +#include +#include + +#include "freedv_api.h" + +int main(int argc, char *argv[]) { + struct freedv *freedv; + + freedv = freedv_open(FREEDV_MODE_700D); + assert(freedv != NULL); + + /* note API functions to tell us how big our buffers need to be */ + short speech_out[freedv_get_n_max_speech_samples(freedv)]; + short demod_in[freedv_get_n_max_modem_samples(freedv)]; + + size_t nin, nout; + nin = freedv_nin(freedv); + while (fread(demod_in, sizeof(short), nin, stdin) == nin) { + nout = freedv_rx(freedv, speech_out, demod_in); + nin = freedv_nin(freedv); /* call me on every loop! */ + fwrite(speech_out, sizeof(short), nout, stdout); + } + + freedv_close(freedv); + return 0; +} diff --git a/third_party/codec2/demo/freedv_700d_rx.py b/third_party/codec2/demo/freedv_700d_rx.py new file mode 100755 index 0000000..5962a73 --- /dev/null +++ b/third_party/codec2/demo/freedv_700d_rx.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +''' + Demo receive program for FreeDV API 700D mode. + + cd ~/codec2/build_linux + cat ../raw/ve9qrp_10s.raw | ./demo/freedv_700d_tx | ../demo/freedv_700d_rx.py | aplay -f S16_LE + + Credits: Thanks DJ2LS, xssfox, VK5QI +''' + +import ctypes +from ctypes import * +import sys +import pathlib +import platform + +if platform.system() == 'Darwin': + libname = pathlib.Path().absolute() / "src/libcodec2.dylib" +else: + libname = pathlib.Path().absolute() / "src/libcodec2.so" + +# See: https://docs.python.org/3/library/ctypes.html + +c_lib = ctypes.CDLL(libname) + +c_lib.freedv_open.argype = [c_int] +c_lib.freedv_open.restype = c_void_p + +c_lib.freedv_get_n_max_speech_samples.argtype = [c_void_p] +c_lib.freedv_get_n_max_speech_samples.restype = c_int + +c_lib.freedv_nin.argtype = [c_void_p] +c_lib.freedv_nin.restype = c_int + +c_lib.freedv_rx.argtype = [c_void_p, c_char_p, c_char_p] +c_lib.freedv_rx.restype = c_int + +FREEDV_MODE_700D = 7 # from freedv_api.h +freedv = cast(c_lib.freedv_open(FREEDV_MODE_700D), c_void_p) + +n_max_speech_samples = c_lib.freedv_get_n_max_speech_samples(freedv) +speech_out = create_string_buffer(2*n_max_speech_samples) + +while True: + nin = c_lib.freedv_nin(freedv) + demod_in = sys.stdin.buffer.read(nin*2) + if len(demod_in) == 0: quit() + nout = c_lib.freedv_rx(freedv, speech_out, demod_in) + sys.stdout.buffer.write(speech_out[:nout*2]) diff --git a/third_party/codec2/demo/freedv_700d_tx.c b/third_party/codec2/demo/freedv_700d_tx.c new file mode 100644 index 0000000..4c22594 --- /dev/null +++ b/third_party/codec2/demo/freedv_700d_tx.c @@ -0,0 +1,68 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: freedv_700d_tx.c + AUTHOR......: David Rowe + DATE CREATED: April 2021 + + Demo transmit program using the FreeDV API (700D mode). + + usage: + + cd ~/codec2/build_linux + cat ../raw/ve9qrp_10s.raw | ./demo/freedv_700d_tx | ./demo/freedv_700d_rx | +aplay -f S16_LE + + Listen to the modulated Tx signal: + + cat ../raw/ve9qrp_10s.raw | ./demo/freedv_700d_tx | aplay -f S16_LE + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2021 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +#include +#include +#include +#include + +#include "freedv_api.h" + +int main(int argc, char *argv[]) { + struct freedv *freedv; + + freedv = freedv_open(FREEDV_MODE_700D); + assert(freedv != NULL); + + /* handy functions to set buffer sizes */ + int n_speech_samples = freedv_get_n_speech_samples(freedv); + short speech_in[n_speech_samples]; + int n_nom_modem_samples = freedv_get_n_nom_modem_samples(freedv); + short mod_out[n_nom_modem_samples]; + + /* OK main loop --------------------------------------- */ + + while (fread(speech_in, sizeof(short), n_speech_samples, stdin) == + n_speech_samples) { + freedv_tx(freedv, mod_out, speech_in); + fwrite(mod_out, sizeof(short), n_nom_modem_samples, stdout); + } + + freedv_close(freedv); + + return 0; +} diff --git a/third_party/codec2/demo/freedv_datac0c1_rx.c b/third_party/codec2/demo/freedv_datac0c1_rx.c new file mode 100644 index 0000000..fd3a335 --- /dev/null +++ b/third_party/codec2/demo/freedv_datac0c1_rx.c @@ -0,0 +1,130 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: freedv_datac0c1_rx.c + AUTHOR......: David Rowe + DATE CREATED: Dec 2021 + + Demonstrates receiving frames of raw data bytes using the FreeDV + API. Two parallel receivers are running, so we can receive either + DATAC0 or DATAC1 frames. Demonstrates a common use case for HF data + - the ability to receive signalling as well as payload data frames. + + usage: + + cd codec2/build_linux + ./demo/freedv_datacc01_tx | ./demo/freedv_datac0c1_rx + + Give it a hard time with some channel noise, frequency offset, and sample + clock offsets: + + ./demo/freedv_datac0c1_tx | ./src/cohpsk_ch - - -24 -f 20 --Fs 8000 | + sox -t .s16 -c 1 -r 8000 - -t .s16 -c 1 -r 8008 - | + ./demo/freedv_datac0c1_rx + + Replace the final line with "aplay -f S16" to listen to the + simulated Rx signal. + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2021 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +#include +#include +#include +#include +#include + +#include "freedv_api.h" + +#define NBUF 160 + +int run_receiver(struct freedv *freedv, short buf[], short demod_in[], int *pn, + uint8_t bytes_out[]); + +int main(int argc, char *argv[]) { + // set up DATAC0 Rx + struct freedv *freedv_c0 = freedv_open(FREEDV_MODE_DATAC0); + assert(freedv_c0 != NULL); + freedv_set_frames_per_burst(freedv_c0, 1); + freedv_set_verbose(freedv_c0, 0); + int bytes_per_modem_frame_c0 = freedv_get_bits_per_modem_frame(freedv_c0) / 8; + uint8_t bytes_out_c0[bytes_per_modem_frame_c0]; + short demod_in_c0[freedv_get_n_max_modem_samples(freedv_c0)]; + + // set up DATAC1 Rx + struct freedv *freedv_c1 = freedv_open(FREEDV_MODE_DATAC1); + assert(freedv_c1 != NULL); + freedv_set_frames_per_burst(freedv_c1, 1); + freedv_set_verbose(freedv_c1, 0); + int bytes_per_modem_frame_c1 = freedv_get_bits_per_modem_frame(freedv_c1) / 8; + uint8_t bytes_out_c1[bytes_per_modem_frame_c1]; + short demod_in_c1[freedv_get_n_max_modem_samples(freedv_c1)]; + + // number of samples in demod_in buffer for each Rx + int n_c0 = 0; + int n_c1 = 0; + // number of frames received in each mode + int c0_frames = 0; + int c1_frames = 0; + + short buf[NBUF]; + + // read a fixed buffer from stdin, use that to fill c0 and c1 demod_in buffers + while (fread(buf, sizeof(short), NBUF, stdin) == NBUF) { + if (run_receiver(freedv_c0, buf, demod_in_c0, &n_c0, bytes_out_c0)) { + fprintf(stderr, "DATAC0 frame received!\n"); + c0_frames++; + } + if (run_receiver(freedv_c1, buf, demod_in_c1, &n_c1, bytes_out_c1)) { + fprintf(stderr, "DATAC1 frame received!\n"); + c1_frames++; + } + } + + fprintf(stderr, "DATAC0 Frames: %d DATAC1 Frames: %d\n", c0_frames, + c1_frames); + + freedv_close(freedv_c0); + freedv_close(freedv_c1); + + return 0; +} + +int run_receiver(struct freedv *freedv, short buf[], short demod_in[], int *pn, + uint8_t bytes_out[]) { + int n = *pn; + int nbytes_out = 0; + int nin; + + // NBUF new samples into DATAC1 Rx + memcpy(&demod_in[n], buf, sizeof(short) * NBUF); + n += NBUF; + assert(n <= freedv_get_n_max_modem_samples(freedv)); + nin = freedv_nin(freedv); + while (n > nin) { + nbytes_out = freedv_rawdatarx(freedv, bytes_out, demod_in); + // nin samples were read + n -= nin; + assert(n >= 0); + memmove(demod_in, &demod_in[nin], sizeof(short) * n); + nin = freedv_nin(freedv); + } + + *pn = n; + return nbytes_out; +} diff --git a/third_party/codec2/demo/freedv_datac0c1_tx.c b/third_party/codec2/demo/freedv_datac0c1_tx.c new file mode 100644 index 0000000..311742f --- /dev/null +++ b/third_party/codec2/demo/freedv_datac0c1_tx.c @@ -0,0 +1,109 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: freedv_datac0c1_tx.c + AUTHOR......: David Rowe + DATE CREATED: Dec 2021 + + Transmitting alternate frames of two different raw data modes. See + freedv_datac0c1_rx.c + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2021 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +#include +#include +#include +#include + +#include "freedv_api.h" +#include "ofdm_internal.h" + +#define FRAMES 10 + +void send_burst(struct freedv *freedv); + +int main(void) { + struct freedv *freedv_c0, *freedv_c1; + + freedv_c0 = freedv_open(FREEDV_MODE_DATAC0); + assert(freedv_c0 != NULL); + freedv_c1 = freedv_open(FREEDV_MODE_DATAC1); + assert(freedv_c1 != NULL); + + // send frames in different modes in random order + int c0_frames = 0; + int c1_frames = 0; + while ((c0_frames < FRAMES) || (c1_frames < FRAMES)) { + if (rand() & 1) { + if (c0_frames < FRAMES) { + send_burst(freedv_c0); + c0_frames++; + } + } else { + if (c1_frames < FRAMES) { + send_burst(freedv_c1); + c1_frames++; + } + } + } + + freedv_close(freedv_c0); + freedv_close(freedv_c1); + + return 0; +} + +void send_burst(struct freedv *freedv) { + size_t bits_per_frame = freedv_get_bits_per_modem_frame(freedv); + size_t bytes_per_modem_frame = bits_per_frame / 8; + size_t payload_bytes_per_modem_frame = + bytes_per_modem_frame - 2; /* 16 bits used for the CRC */ + size_t n_mod_out = freedv_get_n_tx_modem_samples(freedv); + uint8_t bytes_in[bytes_per_modem_frame]; + short mod_out_short[n_mod_out]; + + /* generate a test frame */ + uint8_t testframe_bits[bits_per_frame]; + ofdm_generate_payload_data_bits(testframe_bits, bits_per_frame); + freedv_pack(bytes_in, testframe_bits, bits_per_frame); + + /* send preamble */ + int n_preamble = freedv_rawdatapreambletx(freedv, mod_out_short); + fwrite(mod_out_short, sizeof(short), n_preamble, stdout); + + /* The raw data modes require a CRC in the last two bytes */ + uint16_t crc16 = freedv_gen_crc16(bytes_in, payload_bytes_per_modem_frame); + bytes_in[bytes_per_modem_frame - 2] = crc16 >> 8; + bytes_in[bytes_per_modem_frame - 1] = crc16 & 0xff; + + /* modulate and send a data frame */ + freedv_rawdatatx(freedv, mod_out_short, bytes_in); + fwrite(mod_out_short, sizeof(short), n_mod_out, stdout); + + /* send postamble */ + int n_postamble = freedv_rawdatapostambletx(freedv, mod_out_short); + fwrite(mod_out_short, sizeof(short), n_postamble, stdout); + + /* create some silence between bursts */ + int inter_burst_delay_ms = 200; + int samples_delay = FREEDV_FS_8000 * inter_burst_delay_ms / 1000; + short sil_short[samples_delay]; + for (int i = 0; i < samples_delay; i++) sil_short[i] = 0; + fwrite(sil_short, sizeof(short), samples_delay, stdout); +} diff --git a/third_party/codec2/demo/freedv_datac1_rx.c b/third_party/codec2/demo/freedv_datac1_rx.c new file mode 100644 index 0000000..522de42 --- /dev/null +++ b/third_party/codec2/demo/freedv_datac1_rx.c @@ -0,0 +1,63 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: freedv_datac1_rx.c + AUTHOR......: David Rowe + DATE CREATED: April 2021 + + Demonstrates receiving frames of raw data bytes using the FreeDV API. + + See freedv_datac1_tx.c for instructions. + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2021 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +#include +#include +#include +#include + +#include "freedv_api.h" + +int main(int argc, char *argv[]) { + struct freedv *freedv; + + freedv = freedv_open(FREEDV_MODE_DATAC1); + assert(freedv != NULL); + freedv_set_frames_per_burst(freedv, 1); + freedv_set_verbose(freedv, 2); + + int bytes_per_modem_frame = freedv_get_bits_per_modem_frame(freedv) / 8; + uint8_t bytes_out[bytes_per_modem_frame]; + short demod_in[freedv_get_n_max_modem_samples(freedv)]; + + size_t nin, nbytes_out; + nin = freedv_nin(freedv); + while (fread(demod_in, sizeof(short), nin, stdin) == nin) { + nbytes_out = freedv_rawdatarx(freedv, bytes_out, demod_in); + nin = freedv_nin(freedv); /* must call this every loop */ + if (nbytes_out) { + /* don't output CRC */ + fwrite(bytes_out, sizeof(uint8_t), nbytes_out - 2, stdout); + } + } + + freedv_close(freedv); + + return 0; +} diff --git a/third_party/codec2/demo/freedv_datac1_tx.c b/third_party/codec2/demo/freedv_datac1_tx.c new file mode 100644 index 0000000..a7a2d10 --- /dev/null +++ b/third_party/codec2/demo/freedv_datac1_tx.c @@ -0,0 +1,98 @@ +/*---------------------------------------------------------------------------*\ + + FILE........: freedv_datac1_tx.c + AUTHOR......: David Rowe + DATE CREATED: April 2021 + + Demonstrates transmitting frames of raw data bytes using the FreeDV API datac1 + mode. The data on stdin is transmitted as a sequence of modulated bursts. + + Format of each burst: ...|preamble|data frame|postamble|silence|.... + + There is just one data frame per burst in this demo. + + usage: + + cd codec2/build_linux + head -c $((510*10)) binaryIn.bin + cat binaryIn.bin | ./demo/freedv_datac1_tx | ./demo/freedv_datac1_rx > +binaryOut.bin diff binaryIn.bin binaryOut.bin + + Listen to the modulated Tx signal: + + cat binaryIn.bin | ./demo/freedv_datac1_tx | aplay -f S16_LE + +\*---------------------------------------------------------------------------*/ + +/* + Copyright (C) 2021 David Rowe + + All rights reserved. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License version 2.1, as + published by the Free Software Foundation. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, see . +*/ + +#include +#include +#include +#include + +#include "freedv_api.h" + +int main(int argc, char *argv[]) { + struct freedv *freedv; + + freedv = freedv_open(FREEDV_MODE_DATAC1); + assert(freedv != NULL); + + size_t bytes_per_modem_frame = freedv_get_bits_per_modem_frame(freedv) / 8; + size_t payload_bytes_per_modem_frame = + bytes_per_modem_frame - 2; /* 16 bits used for the CRC */ + size_t n_mod_out = freedv_get_n_tx_modem_samples(freedv); + uint8_t bytes_in[bytes_per_modem_frame]; + short mod_out_short[n_mod_out]; + + for (int b = 0; b < 10; b++) { + /* send preamble */ + int n_preamble = freedv_rawdatapreambletx(freedv, mod_out_short); + fwrite(mod_out_short, sizeof(short), n_preamble, stdout); + + /* read our input data frame from stdin */ + size_t nread = + fread(bytes_in, sizeof(uint8_t), payload_bytes_per_modem_frame, stdin); + if (nread != payload_bytes_per_modem_frame) break; + + /* The raw data modes require a CRC in the last two bytes */ + uint16_t crc16 = freedv_gen_crc16(bytes_in, payload_bytes_per_modem_frame); + bytes_in[bytes_per_modem_frame - 2] = crc16 >> 8; + bytes_in[bytes_per_modem_frame - 1] = crc16 & 0xff; + + /* modulate and send a data frame */ + freedv_rawdatatx(freedv, mod_out_short, bytes_in); + fwrite(mod_out_short, sizeof(short), n_mod_out, stdout); + + /* send postamble */ + int n_postamble = freedv_rawdatapostambletx(freedv, mod_out_short); + fwrite(mod_out_short, sizeof(short), n_postamble, stdout); + + /* create some silence between bursts */ + int inter_burst_delay_ms = 200; + int samples_delay = FREEDV_FS_8000 * inter_burst_delay_ms / 1000; + short sil_short[samples_delay]; + for (int i = 0; i < samples_delay; i++) sil_short[i] = 0; + fwrite(sil_short, sizeof(short), samples_delay, stdout); + } + + freedv_close(freedv); + + return 0; +} diff --git a/third_party/codec2/doc/Makefile b/third_party/codec2/doc/Makefile new file mode 100644 index 0000000..5d05ea4 --- /dev/null +++ b/third_party/codec2/doc/Makefile @@ -0,0 +1,35 @@ +# Makefile for codec2.pdf +# +# usage: +# Build codec2 with -DUNITEST=1 (see README) +# cd ~/codec2/doc +# make + +DOCNAME ?= codec2 + +# Set these externally to override defaults. JOBNAME sets the output file basename, +# and avoids over writing codec2.pdf (e.g. when we are running a doc build test, but don't actually +# want to change codec2.pdf in the repo) + +CODEC2_SRC ?= $(HOME)/codec2 +CODEC2_BINARY ?= $(HOME)/codec2/build_linux +JOBNAME ?= $(DOCNAME) + +PATH := $(PATH):$(CODEC2_BINARY)/src + +PLOT_FILES := hts2a_37_sn.tex hts2a_37_sw.tex hts2a_37_lpc_lsp.tex hts2a_37_lpc_pf.tex + +$(DOCNAME).pdf: $(PLOT_FILES) $(DOCNAME).tex $(DOCNAME)_refs.bib + pdflatex -shell-escape -file-line-error -jobname=$(JOBNAME) $(DOCNAME).tex + bibtex $(JOBNAME).aux + pdflatex -shell-escape -file-line-error -jobname=$(JOBNAME) $(DOCNAME).tex + pdflatex -shell-escape -file-line-error -jobname=$(JOBNAME) $(DOCNAME).tex + +$(PLOT_FILES): + echo $(PATH) + c2sim $(CODEC2_SRC)/raw/hts2a.raw --dump hts2a --lpc 10 --lsp --lpcpf + DISPLAY=""; printf "plamp('hts2a',f=37,epslatex=1)\nq\n" | octave-cli -qf -p $(CODEC2_SRC)/octave + +.PHONY: clean +clean: + rm -f *.blg *.bbl *.aux *.log *.out hts2a* diff --git a/third_party/codec2/doc/c_tx_comp.png b/third_party/codec2/doc/c_tx_comp.png new file mode 100644 index 0000000000000000000000000000000000000000..a95047961d140ee2c7bba3776562badb02aa930d GIT binary patch literal 39874 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 literal 0 HcmV?d00001 diff --git a/third_party/codec2/doc/c_tx_comp_thruput.png b/third_party/codec2/doc/c_tx_comp_thruput.png new file mode 100644 index 0000000000000000000000000000000000000000..f4010ca93056468cc8bb60c44e8adc030e287731 GIT binary patch literal 34357 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

DK2i}?e~dpU5E+x_UBSPetoNL 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%0D57TdE-@F}&dU9O_xv=X{D-lZ{Xf`g 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�I)*2;3#^wezOMPABM^QvD4if;jebR_%I8=_2yI@ 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

hv5!98y*BoFsYL2S3ux$p1!+nf;aMt)X^_ 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

  • {VH)IpN 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

    $6mX!V2$oPg=3zi0x|ee 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 SBLoDCiHYH`u_ gitrevision.txt} + \input{gitrevision.txt} +} + +\newcommand{\branch}{% + \immediate\write18{git branch --show-current > gitbranch.txt} + \input{gitbranch.txt} +} + +\title{Codec 2 Algorithm Description} + +\author{David Rowe\\ \\ Revision: {\gitrevision} on branch: {\branch}} + +\begin{document} + +% Tikz code used to support block diagrams +% credit: https://tex.stackexchange.com/questions/175969/block-diagrams-using-tikz + +\tikzset{ +block/.style = {draw, fill=white, rectangle, minimum height=3em, minimum width=3em}, +tmp/.style = {coordinate}, +circ/.style= {draw, fill=white, circle, node distance=1cm, minimum size=0.6cm}, +input/.style = {coordinate}, +output/.style= {coordinate}, +pinstyle/.style = {pin edge={to-,thin,black}} +} + +% tikz: draws a sine wave +\newcommand{\drawSine}[4]{% x, y, x_scale, y_scale + +\draw plot [smooth] coordinates {(#1-2*#3, #2 ) (#1-1.5*#3,#2+0.707*#4) + (#1-1*#3, #2+1*#4) (#1-0.5*#3,#2+0.707*#4) + (#1 ,#2+0) (#1+0.5*#3,#2-0.707*#4) + (#1+1*#3,#2-1*#4) (#1+1.5*#3,#2-0.707*#4) + (#1+2*#3,#2+0)} +} + +% tikz: draw a summer +\newcommand{\drawSummer}[2]{% x, y + \draw (#1,#2) circle (0.5); + \draw (#1-0.25,#2) -- (#1+0.25,#2); + \draw (#1,#2-0.25) -- (#1,#2+0.25); +} + +\maketitle + +\section{Introduction} + +Codec 2 is an open source speech codec designed for communications quality speech between 700 and 3200 bit/s. The main application is low bandwidth HF/VHF digital radio. It fills a gap in open source voice codecs beneath 5000 bit/s and is released under the GNU Lesser General Public License (LGPL). + +Key feature includes: +\begin{enumerate} +\item A range of modes supporting different bit rates, currently (Nov 2023): 3200, 2400, 1600, 1400, 1300, 1200, 700C. The number is the bit rate, and the supplementary letter the version (700C replaced the earlier 700, 700A, 700B versions). These are referred to as ``Codec 2 3200", ``Codec 2 700C" etc. +\item Modest CPU (a few 10s of MIPs) and memory (a few 10s of kbytes of RAM) requirements such that it can run on stm32 class microcontrollers with hardware FPU. +\item Codec 2 has been designed for digital voice over radio applications, and retains intelligible speech at a few percent bit error rate. +\item An open source reference implementation in the C language for C99/gcc compilers, and a \emph{cmake} build and test framework that runs on Linux. Also included is a cross compiled stm32 reference implementation. +\item Ports to non-C99 compilers (e.g. MSVC, some microcontrollers, native builds on Windows) are left to third party developers - we recommend the tests also be ported and pass before considering the port successful. +\end{enumerate} + +The Codec 2 project was started in 2009 in response to the problem of closed source, patented, proprietary voice codecs in the sub-5 kbit/s range, in particular for use in the Amateur Radio service. + +This document describes Codec 2 at two levels. Section \ref{sect:overview} is a high level description aimed at the Radio Amateur, while Section \ref{sect:details} contains a more detailed description using math and signal processing theory. Combined with the C source code, it is intended to give the reader enough information to understand the operation of Codec 2 in detail and embark on source code level projects, such as improvements, ports to other languages, student or academic research projects. Issues with the current algorithms and topics for further work are also included. Section {\ref{sect:codec2_modes} provides a summary of the Codec 2 modes, and Section \ref{sect:source_files} a guide to the C source files. A glossary of terms and symbols is provided in Section \ref{sect:glossary}, and Section \ref{sect:further_work} has suggestions for further documentation work. + +This production of this document was kindly supported by an ARDC grant \cite{ardc2023}. As an open source project, many people have contributed to Codec 2 over the years - we deeply appreciate all of your support. + +\section{Codec 2 for the Radio Amateur} +\label{sect:overview} + +\subsection{Model Based Speech Coding} + +A speech codec takes speech samples from an A/D converter (e.g. 16 bit samples at 8 kHz or 128 kbits/s) and compresses them down to a low bit rate that can be more easily sent over a narrow bandwidth channel (e.g. 700 bits/s for HF). Speech coding is the art of ``what can we throw away". We need to lower the bit rate of the speech while retaining speech you can understand, and making it sound as natural as possible. + +As such low bit rates we use a speech production ``model". The input speech is analysed, and we extract model parameters, which are then sent over the channel. An example of a model based parameter is the pitch of the person speaking. We estimate the pitch of the speaker, quantise it to a 7 bit number, and send that over the channel every 20ms. + +The model based approach used by Codec 2 allows high compression, with some trade offs such as noticeable artefacts in the decoded speech. Higher bit rate codecs (above 5000 bit/s), such as those use for mobile telephony or voice on the Internet, tend to pay more attention to preserving the speech waveform, or use a hybrid approach of waveform and model based techniques. They sound better but require a higher bit rate. + +Recently, machine learning has been applied to speech coding. This technology promises high quality, artefact free speech quality at low bit rates, but currently (2023) requires significantly more memory and CPU resources than traditional speech coding technology such as Codec 2. However the field is progressing rapidly, and as the cost of CPU and memory decreases (Moore's law) will soon be a viable technology for many low bit rate speech applications. + +\subsection{Speech in Time and Frequency} + +To explain how Codec 2 works, let's look at some speech. Figure \ref{fig:hts2a_time} shows a short 40ms segment of speech in the time and frequency domain. On the time plot we can see the waveform is changing slowly over time as the word is articulated. On the right hand side it also appears to repeat itself - one cycle looks very similar to the last. This cycle time is the ``pitch period", which for this example is around $P=35$ samples. Given we are sampling at $F_s=8000$ Hz, the pitch period is $P/F_s=35/8000=0.0044$ seconds, or 4.4ms. + +\begin{figure} [H] +\caption{ A 40ms segment from the word ``these" from a female speaker, sampled at 8kHz. Top is a plot against time, bottom (blue) is a plot of the same speech against frequency. The waveform repeats itself every 4.3ms ($F_0=230$ Hz); this is the ``pitch period" of this segment. The red crosses are the sine wave amplitudes, explained in the text.} +\label{fig:hts2a_time} +\begin{center} +\input hts2a_37_sn.tex +\\ +\input hts2a_37_sw.tex +\end{center} +\end{figure} + +Now if the pitch period is 4.4ms, the pitch frequency or \emph{fundamental} frequency $F_0$ is about $1/0.0044 \approx 230$ Hz. If we look at the blue frequency domain plot at the bottom of Figure \ref{fig:hts2a_time}, we can see spikes that repeat every 230 Hz. If the signal is repeating itself in the time domain, it also repeats itself in the frequency domain. Those spikes separated by about 230 Hz are harmonics of the fundamental frequency $F_0$. + +Note that each harmonic has its own amplitude, that varies across frequency. The red line plots the amplitude of each harmonic. In this example, there is a peak around 500 Hz and another broader peak around 2300 Hz. The ear perceives speech by the location of these peaks and troughs. + +\subsection{Sinusoidal Speech Coding} + +A sinewave will cause a spike or spectral line on a spectrum plot, so we can see each spike as a small sine wave generator. Each sine wave generator has its own frequency that are all multiples of the fundamental pitch frequency (e.g. $230, 460, 690,...$ Hz). They will also have their own amplitude and phase. If we add all the sine waves together (Figure \ref{fig:sinusoidal_model}) we can produce reasonable quality synthesised speech. This is called sinusoidal speech coding and is the speech production ``model" at the heart of Codec 2. + +\begin{figure}[h] +\caption{The sinusoidal speech model. If we sum a series of sine waves, we can generate a speech signal. Each sinewave has its own amplitude ($A_1,A_2,... A_L$), frequency, and phase (not shown). We assume the frequencies are multiples of the fundamental frequency $F_0$. $L$ is the total number of sinewaves we can fit in 4 kHz.} +\label{fig:sinusoidal_model} +\begin{center} +\begin{tikzpicture}[>=triangle 45,x=1.0cm,y=1.0cm] + +% sine wave sources +\draw (0, 2.0) circle (0.5); \drawSine{0}{ 2.0}{0.2}{0.2}; \draw (-2.0,2.0) node {$A_1, F_0$ Hz}; +\draw (0, 0.5) circle (0.5); \drawSine{0}{ 0.5}{0.2}{0.2}; \draw (-2.0,0.5) node {$A_2, 2F_0$ Hz}; +\draw (0,-2.5) circle (0.5); \drawSine{0}{-2.5}{0.2}{0.2}; \draw (-2.0,-2.5) node {$A_L, LF_0$ Hz}; +\draw [dotted,thick] (0,0) -- (0,-2); + +\drawSummer{2.5}{2}; + +% connecting lines +\draw [->] (0.5,2) -- (2,2); +\draw [->] (0.45,0.7) -- (2.05,1.8); +\draw [->] (0.3,-2.1) -- (2.2,1.6); + +% output speech +\draw [->] (3,2) -- (4,2); +\draw [xshift=4.2cm,yshift=2cm,color=blue] plot[smooth] file {hts2a_37_sn.txt}; + +\end{tikzpicture} +\end{center} +\end{figure} + +The model parameters evolve over time, but can generally be considered constant for a short time window (a few 10s of ms). For example, pitch evolves over time, moving up or down as a word is articulated. + +As the model parameters change over time, we need to keep updating them. This is known as the \emph{frame rate} of the codec, which can be expressed in terms of frequency (Hz) or time (ms). For sampling model parameters, Codec 2 uses a frame rate of 10ms. For transmission over the channel, we reduce this to 20-40ms in order to lower the bit rate. The trade off with a lower frame rate is reduced speech quality. + +The parameters of the sinusoidal model are: +\begin{enumerate} +\item The frequency of each sine wave. As they are all harmonics of $F_0$ we can just send $F_0$ to the decoder, and it can reconstruct the frequency of each harmonic as $F_0,2F_0,3F_0,...,LF_0$. We used 5-7 bits/frame to represent $F_0$ in Codec 2. +\item The amplitude of each sine wave, $A_1,A_2,...,A_L$. These ``spectral amplitudes" are really important as they convey the information the ear needs to understand speech. Most of the bits are used for spectral amplitude information. Codec 2 uses between 18 and 50 bits/frame for spectral amplitude information. +\item Voicing information. Speech can be approximated into voiced speech (vowels) and unvoiced speech (like consonants), or some mixture of the two. The example in Figure \ref{fig:hts2a_time} above is voiced speech. So we need some way to describe voicing to the decoder. This requires just a few bits/frame. +\item The phase of each sine wave. Codec 2 discards the phases of each harmonic at the encoder and reconstructs them at the decoder using an algorithm, so no bits are required for phases. This results in some drop in speech quality. +\end{enumerate} + +\subsection{Codec 2 Encoder and Decoder} + +This section explains how the Codec 2 encoder and decoder work using block diagrams. + +\begin{figure}[h] +\caption{Codec 2 Encoder.} +\label{fig:codec2_encoder} +\begin{center} +\begin{tikzpicture}[auto, node distance=2cm,>=triangle 45,x=1.0cm,y=1.0cm,align=center,text width=2cm] + +\node [input] (rinput) {}; +\node [input, right of=rinput,node distance=0.5cm] (z) {}; +\node [block, right of=z,node distance=1.5cm] (pitch_est) {Pitch Estimator}; +\node [block, below of=pitch_est] (fft) {FFT}; +\node [block, right of=fft,node distance=3cm] (est_am) {Estimate Amplitudes}; +\node [block, below of=est_am] (est_v) {Estimate Voicing}; +\node [block, right of=est_am,node distance=3cm] (quant) {Decimate Quantise}; +\node [output, right of=quant,node distance=2cm] (routput) {}; + +\draw [->] node[align=left] {Input Speech} (rinput) -- (pitch_est); +\draw [->] (z) |- (fft); +\draw [->] (pitch_est) -| (est_am); +\draw [->] (fft) -- (est_am); +\draw [->] (est_am) -- (est_v); +\draw [->] (pitch_est) -| (quant); +\draw [->] (est_am) -- (quant); +\draw [->] (est_v) -| (quant); +\draw [->] (est_v) -| (quant); +\draw [->] (quant) -- (routput) node[right, align=left, text width=1.5cm] {Bit Stream}; + +\end{tikzpicture} +\end{center} +\end{figure} + +The encoder is presented in Figure \ref{fig:codec2_encoder}. Frames of input speech samples are passed to a Fast Fourier Transform (FFT), which converts the time domain samples to the frequency domain. The same frame of input samples is used to estimate the pitch of the current frame. We then use the pitch and frequency domain speech to estimate the amplitude of each sine wave. + +Yet another algorithm is used to determine if the frame is voiced or unvoiced. This works by comparing the spectrum to what we would expect for voiced speech (e.g. lots of spectral lines). If the energy is more random and continuous rather than discrete lines, we consider it unvoiced. + +Up until this point the processing happens at a 10ms frame rate. However, in the next step, we ``decimate`` the model parameters - this means we discard some of the model parameters to lower the frame rate, which helps us lower the bit rate. Decimating to 20ms (throwing away every 2nd set of model parameters) doesn't have much effect, but beyond that the speech quality starts to degrade. So there is a trade off between decimation rate and bit rate over the channel. + +Once we have the desired frame rate, we ``quantise" each model parameter. This means we use a fixed number of bits to represent it, so we can send the bits over the channel. Parameters like pitch and voicing are fairly easy, but quite a bit of DSP goes into quantising the spectral amplitudes. For the higher bit rate Codec 2 modes, we design a filter that matches the spectral amplitudes, then send a quantised version of the filter over the channel. Using the example in Figure \ref{fig:hts2a_time} - the filter would have a band pass peaks at 500 and 2300 Hz. Its frequency response would follow the red line. The filter is time varying - we redesign it for every frame. + +You'll notice the term ``estimate" being used a lot. One of the problems with model based speech coding is the algorithms we use to extract the model parameters are not perfect. Occasionally the algorithms get it wrong. Look at the red crosses on the bottom plot of Figure \ref{fig:hts2a_time}. These mark the amplitude estimate of each harmonic. If you look carefully, you'll see that above 2000Hz, the crosses fall a little short of the exact centre of each harmonic. This is an example of a ``fine" pitch estimator error, a little off the correct value. + +Often the errors interact, for example the fine pitch error shown above will mean the amplitude estimates are a little bit off as well. Fortunately, these errors tend to be temporary and are sometimes not even noticeable to the listener - remember this codec is often used for HF/VHF radio where channel noise is part of the normal experience. + +\begin{figure}[h] +\caption{Codec 2 Decoder} +\label{fig:codec2_decoder} +\begin{center} +\begin{tikzpicture}[auto, node distance=2cm,>=triangle 45,x=1.0cm,y=1.0cm,align=center,text width=2cm] + +\node [input] (rinput) {}; +\node [block, right of=rinput,node distance=2cm] (dequantise) {Dequantise Interpolate}; +\node [block, right of=dequantise,node distance=3cm] (recover) {Recover Amplitudes}; +\node [block, right of=recover,node distance=3cm] (synthesise) {Synthesise Speech}; +\node [block, above of=synthesise] (phase) {Synthesise Phases}; +\node [output, right of=synthesise,node distance=2cm] (routput) {}; + +\draw [->] node[align=left, text width=1.5cm] {Bit Stream} (rinput) -- (dequantise); +\draw [->] (dequantise) -- (recover); +\draw [->] (recover) -- (synthesise); +\draw [->] (recover) |- (phase); +\draw [->] (phase) -- (synthesise); +\draw [->] (synthesise) -- (routput) node[right, align=left, text width=1.5cm] {Output Speech}; + +\end{tikzpicture} +\end{center} +\end{figure} + +Figure \ref{fig:codec2_decoder} shows the operation of the Codec 2 decoder. We take the sequence of bits received from the channel and recover the quantised model parameters, pitch, spectral amplitudes, and voicing. We then resample the model parameters back up to the 10ms frame rate using a technique called interpolation. For example, say we receive a $F_0=200$ Hz pitch value, then 20ms later $F_0=220$ Hz. We can use the average $F_0=210$ Hz for the middle 10ms frame. + +The phases of each harmonic are generated using the other model parameters and some DSP. It turns out that if you know the amplitude spectrum, you can determine a ``reasonable" phase spectrum using some DSP operations, which in practice is implemented with a couple of FFTs. We also use the voicing information - for unvoiced speech we use random phases (a good way to synthesise noise-like signals) - and for voiced speech we make sure the phases are chosen so the synthesised speech transitions smoothly from one frame to the next. + +Frames of speech are synthesised using an inverse FFT. We take a blank array of FFT samples, and at intervals of $F_0$ insert samples with the amplitude and phase of each harmonic. We then inverse FFT to create a frame of time domain samples. These frames of synthesised speech samples are carefully aligned with the previous frame to ensure smooth frame-frame transitions and output to the listener. + +\subsection{Bit Allocation} + +Table \ref{tab:bit_allocation} presents the bit allocation for two popular Codec 2 modes. One additional parameter is the frame energy, which is the average level of the spectral amplitudes, or ``AF gain" of the speech frame. + +At very low bit rates such as 700 bits/s, we use Vector Quantisation (VQ) to represent the spectral amplitudes. We construct a table such that each row of the table has a set of spectral amplitude samples. In Codec 2 700C the table has 512 rows. During the quantisation process, we choose the table row that best matches the spectral amplitudes for this frame, then send the \emph{index} of the table row. The decoder has a similar table, so can use the index to look up the spectral amplitude values. If the table is 512 rows, we can use a 9 bit number to quantise the spectral amplitudes. In Codec 2 700C, we use two tables of 512 entries each (18 bits total), the second one helps fine tune the quantisation from the first table. + +Vector Quantisation can only represent what is present in the tables, so if it sees anything unusual (for example, a different microphone frequency response or background noise), the quantisation can become very rough and speech quality poor. We train the tables at design time using a database of speech samples and a training algorithm - an early form of machine learning. + +Codec 2 3200 uses the method of fitting a filter to the spectral amplitudes, this approach tends to be more forgiving of small variations in the input speech spectrum, but is not as efficient in terms of bit rate. + +\begin{table}[H] +\label{tab:bit_allocation} +\centering +\begin{tabular}{l c c } +\hline +Parameter & 3200 & 700C \\ +\hline +Pitch $F_0$ & 7 & 5 \\ +Spectral Amplitudes $\{A_m\}$ & 50 & 18 \\ +Energy & 5 & 3 \\ +Voicing & 2 & 1 \\ +Bits/frame & 64 & 28 \\ +Frame Rate & 20ms & 40ms \\ +Bit rate & 3200 & 700 \\ +\hline +\end{tabular} +\caption{Bit allocation of the 3200 and 700C modes} +\end{table} + +\section{Detailed Design} +\label{sect:details} + +\subsection{Overview} + +Codec 2 is based on sinusoidal \cite{mcaulay1986speech} and Multi-Band Excitation (MBE) \cite{griffin1988multiband} vocoders that were first developed in the late 1980s. Descendants of the MBE vocoders (IMBE, AMBE etc) have enjoyed widespread use in many applications such as VHF/UHF handheld radios and satellite communications. In the 1990s the author studied sinusoidal speech coding \cite{rowe1997techniques}, which provided the skill set and a practical, patent free baseline for starting the Codec 2 project: + +Some features of the Codec 2 Design: +\begin{enumerate} +\item A pitch estimator based on a 2nd order non-linearity developed by the author. +\item A single voiced/unvoiced binary voicing model. +\item A frequency domain IFFT/overlap-add synthesis model for voiced and unvoiced speech. +\item Phases are not transmitted, they are synthesised at the decoder from the magnitude spectrum and voicing decision. +\item For the higher bit rate modes (1200 to 3200 bits/s), spectral magnitudes are represented using LPCs extracted from time domain analysis and scalar LSP quantisation. +\item For Codec 2 700C, vector quantisation of resampled spectral magnitudes in the log domain. +\item Minimal interframe prediction in order to minimise error propagation and maximise robustness to channel errors. +\item A post filter that enhances the speech quality of the baseline codec, especially for low pitched (male) speakers. +\end{enumerate} + +\subsection{Sinusoidal Analysis} + +Both voiced and unvoiced speech is represented using a harmonic sinusoidal model: +\begin{equation} +\hat{s}(n) = \sum_{m=1}^L A_m cos(\omega_0 m n + \theta_m) +\end{equation} +where the parameters $A_m, \theta_m, m=1...L$ represent the magnitude and phases of each sinusoid, $\omega_0$ is the fundamental frequency in radians/sample, and $L=\lfloor \pi/\omega_0 \rfloor$ is the number of harmonics. + +Figure \ref{fig:analysis} illustrates the processing steps in the sinusoidal analysis system at the core of the Codec 2 encoder. The algorithms described in this section are based on the work in \cite{rowe1997techniques}, with some changes in notation. + +\begin{figure}[h] +\caption{Sinusoidal Analysis} +\label{fig:analysis} +\begin{center} +\begin{tikzpicture}[auto, node distance=2cm,>=triangle 45,x=1.0cm,y=1.0cm, align=center] + +\node [input] (rinput) {}; +\node [tmp, right of=rinput,node distance=0.5cm] (z) {}; +\node [block, right of=z,node distance=1.5cm] (window) {Window}; +\node [block, right of=window,node distance=2.5cm] (dft) {DFT}; +\node [block, right of=dft,node distance=3cm,text width=2cm] (est) {Est Amp and Phase}; +\node [block, below of=window] (nlp) {NLP}; +\node [output, right of=est,node distance=2cm] (routput) {}; + +\draw [->] node[align=left,text width=2cm] {$s(n)$} (rinput) -- (window); +\draw [->] (z) |- (nlp); +\draw [->] (window) -- node[below] {$s_w(n)$} (dft); +\draw [->] (dft) -- node[below] {$S_\omega(k)$} (est); +\draw [->] (nlp) -| node[below] {$\omega_0$} (est) ; +\draw [->] (est) -- (routput) node[right] {$\{A_m\}$ \\ $\{\theta_m\}$}; + +\end{tikzpicture} +\end{center} +\end{figure} + +The time domain speech signal $s(n)$ is divided into overlapping analysis windows (frames) of $N_w=279$ samples. The centre of each analysis window is separated by $N=80$ or 10ms. Codec 2 operates at an internal frame rate of 100 Hz. To analyse the $l$-th frame it is convenient to convert the fixed time reference to a sliding time reference centred on the current analysis window: +\begin{equation} +s_w(n) = s(lN + n) w(n), \quad n = - N_{w2} ... N_{w2} +\end{equation} +where $w(n)$ is a tapered even window of $N_w$ ($N_w$ odd) samples with: +\begin{equation} +N_{w2} = \left \lfloor \frac{N_w}{2} \right \rfloor +\end{equation} +A suitable window function is a shifted Hann window: +\begin{equation} +w(n) = \frac{1}{2} - \frac{1}{2} cos \left(\frac{2 \pi (n- N_{w2})}{N_w-1} \right) +\end{equation} +where the energy in the window is normalised such that: +\begin{equation} +\sum_{n=0}^{N_w-1}w^2(n) = \frac{1}{N_{dft}} +\end{equation} +To analyse $s(n)$ in the frequency domain the $N_{dft}$ point Discrete Fourier Transform (DFT) can be computed: +\begin{equation} +S_w(k) = \sum_{n=-N_{w2}}^{N_{w2}} s_w(n) e^{-j 2 \pi k n / N_{dft}} +\end{equation} +The magnitude and phase of each harmonic is given by: +\begin{equation} +\label{eq:mag_est} +A_m = \sqrt{\sum_{k=a_m}^{b_m-1} |S_w(k)|^2 } +\end{equation} +\begin{equation} +\theta_m = arg \left[ S_w(\lfloor m r \rceil \right] +\end{equation} +where: +\begin{equation} +\begin{split} +a_m &= \lfloor (m - 0.5)r \rceil \\ +b_m &= \lfloor (m + 0.5)r \rceil \\ +r &= \frac{\omega_0 N_{dft}}{2 \pi} +\end{split} +\end{equation} +The DFT indexes $a_m, b_m$ select the band of $S_w(k)$ containing the $m$-th harmonic; $r$ maps the harmonic number $m$ to the nearest DFT index, and $\lfloor x \rceil$ is the rounding operator. This method of estimating $A_m$ is relatively insensitive to small errors in $F0$ estimation and works equally well for voiced and unvoiced speech. Figure $\ref{fig:hts2a_time}$ plots $S_w$ (blue) and $\{A_m\}$ (red) for a sample frame of female speech. + +The phase is sampled at the centre of the band. For all practical Codec 2 modes, the phase is not transmitted to the decoder, so it does not need to be computed. However, speech synthesised using the phase is useful as a control during development and is available using the \emph{c2sim} utility. + +\subsection{Sinusoidal Synthesis} + +Synthesis is achieved by constructing an estimate of the original speech spectrum using the sinusoidal model parameters for the current frame. This information is then transformed to the time domain using an Inverse DFT (IDFT). To produce a continuous time domain waveform the IDFTs from adjacent frames are smoothly interpolated using a weighted overlap add procedure \cite{mcaulay1986speech}. + +\begin{figure}[h] +\caption{Sinusoidal Synthesis. At frame $l$ the windowing function generates $2N$ samples. The first $N$ samples complete the current frame. The second $N$ samples are stored for summing with the next frame.} +\label{fig:synthesis} +\begin{center} +\begin{tikzpicture}[auto, node distance=2cm,>=triangle 45,x=1.0cm,y=1.0cm, align=center] + +\node [input] (rinput) {}; +\node [block, right of=rinput,node distance=1.5cm,text width=1.5cm] (construct) {Construct $S_w(k)$}; +\node [block, right of=construct,node distance=2cm] (idft) {IDFT}; +\node [block, right of=idft,node distance=2.5cm,text width=1.5cm] (window) {Window $t(n)$}; +\node [circ, right of=window,node distance=3cm] (sum) {$+$}; +\node [block, below of=sum,text width=1.5cm] (delay) {1 frame delay}; +\node [output, right of=sum,node distance=1cm] (routput) {}; + +\draw [->] node[left of=rinput,node distance=0.5cm] {$\omega_0$\\$\{A_m\}$\\$\{\theta_m\}$} (rinput) -- (construct); +\draw [->] (construct) --(idft); +\draw [->] (idft) -- node[below] {$\hat{s}_l(n)$} (window); +\draw [->] (window) -- node[above of=window, node distance=0.75cm] + {$\begin{aligned} n =& 0,..,\\[-0.5ex] & N-1 \end{aligned}$} (sum); +\draw [->] (window) |- (delay) node[left of=delay,below, node distance=2cm] + {$\begin{aligned} n =& N,...,\\[-0.5ex] & 2N-1 \end{aligned}$}; +\draw [->] (delay) -- (sum); +\draw [->] (sum) -- (routput) node[right] {$\hat{s}(n+lN)$}; + +\end{tikzpicture} +\end{center} +\end{figure} + +The synthetic speech spectrum is constructed using the sinusoidal model parameters by populating a DFT array $\hat{S}_w(k)$ with weighted impulses at the harmonic centres: +\begin{equation} +\hat{S}_w(k) = \begin{cases} + A_m e^{j\theta_m}, & k = \lfloor m r \rceil, m=1..L \\ + 0, & otherwise + \end{cases} +\end{equation} + +As we wish to synthesise a real time domain signal, $S_w(k)$ is defined to be conjugate symmetric: +\begin{equation} +\hat{S}_w(N_{dft}-k) = \hat{S}_w^{*}(k), \quad k = 1,.. N_{dft}/2-1 +\end{equation} +where $\hat{S}_w^*(k)$ is the complex conjugate of $\hat{S}_w(k)$. This signal is converted to the time domain +using the IDFT: +\begin{equation} +\label{eq:synth_idft} +\hat{s}_l(n) = \frac{1}{N_{dft}}\sum_{k=0}^{N_{dft}-1} \hat{S}_w(k) e^{j 2 \pi k n / N_{dft}} +\end{equation} +Where $N_{dft} > 2N$, to support the overlap add procedure below. + +We introduce the notation $\hat{s}_l(n)$ to denote the synthesised speech for the $l$-th frame. To reconstruct a continuous synthesised speech waveform, we need to smoothly connect adjacent synthesised frames of speech. This is performed by windowing each frame of synthesised speech, then shifting and superimposing adjacent frames using an overlap add algorithm. A triangular window is defined by: +\begin{equation} +t(n) = \begin{cases} + n/N, & 0 \le n < N \\ + 1 - (n-N)/N, & N \le n < 2N \\ + 0, & otherwise + \end{cases} +\end{equation} +The frame size, $N=80$, is the same as the encoder. The shape and overlap of the synthesis window is not important, as long as sections separated by the frame size (frame to frame shift) sum to 1: +\begin{equation} +t(n) + t(N-n) = 1 +\end{equation} +The continuous synthesised speech signal $\hat{s}(n)$ for the $l$-th frame is obtained using: +\begin{equation} +\hat{s}(n+lN) = \begin{cases} + \hat{s}(n+(l-1)N) + \hat{s}_l(N_{dft}-N+1+n)t(n), & n=0,1,...,N-2 \\ + \hat{s}_l(n - N - 1)t(n) & n=N-1,..,2N-1 + \end{cases} +\end{equation} + +From the $N_{dft}$ samples produced by the IDFT (\ref{eq:synth_idft}), after windowing we have $2N$ output samples. The first $N$ output samples $n=0,...N-1$ complete the current frame $l$ and are output from the synthesiser. However we must also compute the contribution to the next frame $n = N,N+1,...,2N-1$. These are stored, and added to samples from the next synthesised frame. + +\subsection{Non-Linear Pitch Estimation} +\label{sect:nlp} + +The Non-Linear Pitch (NLP) pitch estimator was developed by the author, described in detail in chapter 4 of \cite{rowe1997techniques}, and portions of this description are reproduced here. The post processing algorithm used for pitch estimation in Codec 2 is different from \cite{rowe1997techniques} and is described here. The C code \emph{nlp.c} is a useful reference for the fine details of the implementation, and the Octave script \emph{plnlp.m} can by used to plot the internal states and single step through speech, illustrating the operation of the algorithm. + +The core pitch detector is based on a square law non-linearity, that is applied directly to the input speech signal. Given speech is composed of harmonics separated by $F_0$ the non-linearity generates intermodulation products at $F_0$, even if the fundamental is absent from the input signal due to high pass filtering. + +Figure \ref{fig:nlp} illustrates the algorithm. The fundamental frequency $F_0$ is estimated in the range of 50-400 Hz. The algorithm is designed to take blocks of $M = 320$ samples at a sample rate of 8 kHz (40 ms time window). This block length ensures at least two pitch periods lie within the analysis window at the lowest fundamental frequency. + +The speech signal is first squared then notch filtered to remove the DC component from the squared time domain signal. This prevents the large amplitude DC term from interfering with the somewhat smaller amplitude term at the fundamental. This is particularly important for male speakers, who may have low frequency fundamentals close to DC. The notch filter is applied in the time domain and has the experimentally derived transfer function: +\begin{equation} +H_{notch}(z) = \frac{1-z^{-1}}{1-0.95z^{-1}} +\end{equation} + +\begin{figure}[h] +\caption{The Non-Linear Pitch (NLP) algorithm} +\label{fig:nlp} +\begin{center} +\begin{tikzpicture}[auto, node distance=2cm,>=triangle 45,x=1.0cm,y=1.0cm, align=center] + +\node [input] (rinput) {}; +\node [tmp, right of=rinput,node distance=0.5cm] (z) {}; +\node [tmp, below of=z,node distance=1cm] (z1) {}; +\node [circ, right of=z,node distance=1cm] (mult) {$\times$}; +\node [block, right of=mult,node distance=2cm,text width=2cm] (notch) {DC Notch Filter}; +\node [block, right of=notch,node distance=3cm,text width=2cm] (lpf) {Low Pass Filter}; +\node [block, right of=lpf,node distance=2.5cm] (dec5) {$\downarrow 5$}; +\node [block, below of=dec5] (dft) {DFT}; +\node [block, below of=lpf] (peak) {Peak Pick}; +\node [block, below of=notch,text width=2cm] (search) {Sub \\Multiple Search}; +\node [block, left of=search,node distance=3cm] (refine) {Refinement}; +\node [output, left of=refine,node distance=2cm] (routput) {}; + +\draw [->] node[align=left,text width=2cm] {Input Speech} (rinput) -- (mult); +\draw [->] (z) -- (z1) -| (mult); +\draw [->] (mult) -- (notch); +\draw [->] (notch) -- (lpf); +\draw [->] (lpf) -- (dec5); +\draw [->] (dec5) -- (dft); +\draw [->] (dft) -- (peak); +\draw [->] (peak) -- (search); +\draw [->] (search) -- (refine); +\draw [->] (refine) -- (routput) node[left, align=center] {$F_0$}; + +\end{tikzpicture} +\end{center} +\end{figure} + +Before transforming the squared signal to the frequency domain, the signal is low pass filtered and decimated by a factor of 5. This operation is performed to limit the bandwidth of the squared signal to the approximate range of the fundamental frequency. All energy in the squared signal above 400 Hz is superfluous and would lower the resolution of the frequency domain peak picking stage. The low pass filter used for decimation is an FIR type with 48 taps and a cut off frequency of 600 Hz. The decimated signal is then windowed and the $N_{dft} = 512$ point DFT power spectrum $F_w(k)$ is computed by zero padding the decimated signal, where $k$ is the DFT bin. + +The DFT power spectrum of the squared signal $F_w(k)$ generally contains several local maxima. In most cases, the global maxima will correspond to $F_0$, however occasionally the global maxima $|F_w(k_{max})|$ corresponds to a spurious peak or multiple of $F_0$. Thus it is not appropriate to simply choose the global maxima as the fundamental estimate for this frame. Instead, we look at submultiples of the global maxima frequency $k_{max}/2, k_{max}/3,... k_{min}$ for local maxima. If local maxima exists and is above an experimentally derived threshold we choose the submultiple as the $F_0$ estimate. The threshold is biased down for $F_0$ candidates near the previous frames $F_0$ estimate, a form of backwards pitch tracking. + +The accuracy of the pitch estimate in then refined by maximising the function: +\begin{equation} +\label{eq:pitch_refinement} +E(\omega_0)=\sum_{m=1}^L|S_w(\lfloor r m \rceil)|^2 +\end{equation} +where $r=\omega_0 N_{dft}/2 \pi$ maps the harmonic number $m$ to a DFT bin. This function will be maximised when $m \omega_0$ aligns with the peak of each harmonic, corresponding with an accurate pitch estimate. It is evaluated in a small range about the coarse $F_0$ estimate. + +There is nothing particularly unique about this pitch estimator or it's performance. There are occasional artefacts in the synthesised speech that can be traced to ``gross" and ``fine" pitch estimator errors. In the real world no pitch estimator is perfect, partially because the model assumptions around pitch break down (e.g. in transition regions or unvoiced speech). The NLP algorithm could benefit from additional review, tuning and better pitch tracking. However it appears sufficient for the use case of a communications quality speech codec, and is a minor source of artefacts in the synthesised speech. Other pitch estimators could also be used, provided they have practical, real world implementations that offer comparable performance and CPU/memory requirements. + +\subsection{Voicing Estimation} + +Voicing is determined using a variation of the MBE voicing algorithm \cite{griffin1988multiband}. Voiced speech consists of a harmonic series of frequency domain impulses, separated by $\omega_0$. When we multiply a segment of the input speech samples by the window function $w(n)$, we convolve the frequency domain impulses with $W(k)$, the DFT of the $w(n)$. Thus for the $m$-th voiced harmonic, we expect to see a copy of the window function $W(k)$ in each band $Sw(k), k=a_m,...,b_m$. The MBE voicing algorithm starts with the assumption that the band is voiced, and measures the error between $S_w(k)$ and the ideal voiced harmonic $\hat{S}_w(k)$. + +For each band we first estimate the complex harmonic amplitude (magnitude and phase) using \cite{griffin1988multiband}: +\begin{equation} +\label{eq:est_amp_mbe1} +B_m = \frac{\sum_{k=a_m}^{b_m} S_w(k) W^* (k - \lfloor mr \rceil)}{|\sum_{k=a_m}^{b_m} W (k - \lfloor mr \rceil)|^2} +\end{equation} +where $r= \omega_0 N_{dft}/2 \pi$ is a constant that maps the $m$-th harmonic to a DFT bin, and $ \lfloor x \rceil$ is the rounding operator. To avoid non-zero array indexes we define the shifted window function: +\begin{equation} +U(k) = W(k-N_{dft}/2) +\end{equation} +such that $U(N_{dft}/2)=W(0)$. As $w(n)$ is a real and even, $W(k)$ is real and even so we can write: +\begin{equation} +\begin{split} +W^* (k - \lfloor mr \rceil) &= W(k - \lfloor mr \rceil) \\ + &= U(k - \lfloor mr \rceil + Ndft/2) \\ + &= U(k + l) \\ + l &= Ndft/2 - \lfloor mr \rceil \\ + & = \lfloor Ndft/2 - mr \rceil +\end{split} +\end{equation} +for even $Ndft$. We can therefore write \ref{eq:est_amp_mbe1} as: +\begin{equation} +\label{eq:est_amp_mbe} +B_m = \frac{\sum_{k=a_m}^{b_m} S_w(k) U(k + l)}{\sum_{k=a_m}^{b_m} |U (k + l)|^2} +\end{equation} +Note this procedure is different to the $A_m$ magnitude estimation procedure in (\ref{eq:mag_est}), and is only used locally for the MBE voicing estimation procedure. Unlike (\ref{eq:mag_est}), the MBE amplitude estimation (\ref{eq:est_amp_mbe}) assumes the energy in the band of $S_w(k)$ is from the DFT of a sine wave, and $B_m$ is complex valued. + +The synthesised frequency domain speech for this band is defined as: +\begin{equation} +\hat{S}_w(k) = B_m U(k + l), \quad k=a_m,...,b_m-1 +\end{equation} +The error between the input and synthesised speech in this band is then: +\begin{equation} +\begin{split} +E_m &= \sum_{k=a_m}^{b_m-1} |S_w(k) - \hat{S}_w(k)|^2 \\ + &=\sum_{k=a_m}^{b_m-1} |S_w(k) - B_m U(k + l)|^2 +\end{split} +\end{equation} +A Signal to Noise Ratio (SNR) ratio is defined as: +\begin{equation} +\label{eq:voicing_snr} +SNR = \sum_{m=1}^{m_{1000}} \frac{A^2_m}{E_m} +\end{equation} +where $m_{1000}= \lfloor L/4 \rceil$ is the band closest to 1000 Hz, and $\{A_m\}$ are computed from (\ref{eq:mag_est}). If the energy in the bands up to 1000 Hz is a good match to a harmonic series of sinusoids then $\hat{S}_w(k) \approx S_w(k)$ and $E_m$ will be small compared to the energy in the band resulting in a high SNR. Voicing is declared using the following rule: +\begin{equation} +v = \begin{cases} + 1, & SNR > 6 \si{dB} \\ + 0, & otherwise + \end{cases} +\end{equation} +The voicing decision is post processed by several experimentally derived rules to prevent common voicing errors, see the C source code in \emph{sine.c} for details. + +\subsection{Phase Synthesis} + +In Codec 2 the harmonic phases $\{\theta_m\}$ are not transmitted, instead they are synthesised at the decoder from the remaining model parameters, $\{A_m\}$, $\omega_0$, and $v$. The phase model described in this section is referred to as ``zero order" or \emph{phase0} in the source code, as it requires zero model parameters to be transmitted over the channel. + +Consider the source-filter model of speech production: +\begin{equation} +\label{eq:source_filter} +\hat{S}(z)=E(z)H(z) +\end{equation} +where $E(z)$ is an excitation signal with a relatively flat spectrum, and $H(z)$ is a synthesis filter that shapes the magnitude spectrum. The phase of each harmonic is the sum of the excitation and synthesis filter phase: +\begin{equation} +\begin{split} +arg \left[ \hat{S}(e^{j \omega_0 m}) \right] &= arg \left[ E(e^{j \omega_0 m}) H(e^{j \omega_0 m}) \right] \\ +\hat{\theta}_m &= arg \left[ E(e^{j \omega_0 m}) \right] + arg \left[ H(e^{j \omega_0 m}) \right] \\ +&= \phi_m + arg \left[ H(e^{j \omega_0 m}) \right] +\end{split} +\end{equation} + +For voiced speech $E(z)$ is an impulse train (in both the time and frequency domain). We can construct a time domain excitation pulse train using a sum of sinusoids: +\begin{equation} +e(n) = \sum_{m-1}^L cos( m \omega_0 (n - n_0)) +\end{equation} +Where $n_0$ is a time shift that represents the pulse position relative to the centre of the synthesis frame $n=0$. By finding the DTCF transform of $e(n)$ we can determine the phase of each excitation harmonic: +\begin{equation} +\phi_m = - m \omega_0 n_0 +\end{equation} +As we don't transmit any phase information the pulse position $n_0$ is unknown at the decoder. Fortunately, the ear is insensitive to the absolute position of pitch pulses in voiced speech, as long as they evolve smoothly over time (discontinuities in phase are a characteristic of unvoiced speech). + +The excitation pulses occur at a rate of $\omega_0$ (one for each pitch period). The phase of the first harmonic advances by $N \phi_1$ radians over a synthesis frame of $N$ samples. For example if $\omega_1 = \pi /20$ (200 Hz), then over a (10ms $N=80$) sample frame, the phase of the first harmonic would advance $(\pi/20)80 = 4 \pi$ radians or two complete cycles. We therefore derive $n_0$ from the excitation phase of the fundamental, which we treat as a timing reference. Each frame we advance the phase of the fundamental: +\begin{equation} +\phi_1^l = \phi_1^{l-1} + N\omega_0 +\end{equation} +Given $\phi_1$ we can compute $n_0$ and the excitation phase of the other harmonics: +\begin{equation} +\begin{split} +n_0 &= -\phi_1 / \omega_0 \\ +\phi_m &= - m \omega_0 n_0 \\ + &= m \phi_1 \quad \quad m=2,...,L +\end{split} +\end{equation} + +For unvoiced speech $E(z)$ is a white noise signal. At each frame, we sample a random number generator on the interval $-\pi ... \pi$ to obtain the excitation phase of each harmonic. We set $F_0 = 50$ Hz to use a large number of harmonics $L=4000/50=80$ for synthesis to best approximate a noise signal. + +The second phase component is provided by sampling the phase of $H(z)$ at the harmonic centres. The phase spectra of $H(z)$ is derived from the magnitude response using minimum phase techniques. The method for deriving the phase spectra of $H(z)$ differs between Codec 2 modes and is described below in Sections \ref{sect:mode_lpc_lsp} and \ref{sect:mode_newamp1}. This component of the phase tends to disperse the pitch pulse energy in time, especially around spectral peaks (formants). + +The zero phase model tends to make speech with background noise sound "clicky". With high levels of background noise the low level inter-formant parts of the spectrum will contain noise rather than speech harmonics, so modelling them as voiced (i.e. a continuous, non-random phase track) is inaccurate. Some codecs (like MBE) have a mixed voicing model that breaks the spectrum into voiced and unvoiced regions. However (5-12) bits/frame (5-12) are required to transmit the frequency selective voicing information. Mixed excitation also requires accurate voicing estimation (parameter estimators always break occasionally under exceptional conditions). + +In our case we use a post processing approach which requires no additional bits to be transmitted. The decoder measures the average level of the background noise during unvoiced frames. If a harmonic is less than this level it is made unvoiced by randomising it's phases. See the C source code for implementation details. + +Comparing to speech synthesised using original phases $\{\theta_m\}$ the following observations have been made: +\begin{enumerate} +\item Through headphones speech synthesised with this model drops in quality. Through a small loudspeaker it is very close to original phases. +\item If there are voicing errors, the speech can sound clicky or staticy. If voiced speech is mistakenly declared unvoiced, this model tends to synthesise annoying impulses or clicks, as for voiced speech $H(z)$ is relatively flat (broad, high frequency formants), so there is very little dispersion of the excitation impulses through $H(z)$. +\item When combined with amplitude modelling or quantisation, such that $H(z)$ is derived from $\{\hat{A}_m\}$ there is an additional drop in quality. +\item This synthesis model (e.g. a pulse train exciting a LPC filter) is effectively the same as a simple LPC-10 vocoders, and yet (especially when $arg[H(z)]$ is derived from unquantised $\{A_m\}$) sounds much better. Conventional wisdom (AMBE, MELP) says mixed voicing is required for high quality speech. +\item If $H(z)$ is changing rapidly between frames, its phase contribution may also change rapidly. This approach could cause some discontinuities in the phase at the edge of synthesis frames, as no attempt is made to make sure that the phase tracks are continuous (the excitation phases are continuous, but not the final phases after filtering by $H(z)$). +\item The recent crop of neural vocoders produce high quality speech using a similar parameters set, and notably without transmitting phase information. Although many of these vocoders operate in the time domain, this approach can be interpreted as implementing a function $\{ \hat{\theta}_m\} = F(\omega_0, \{Am\},v)$. This validates the general approach used here, and as future work Codec 2 may benefit from being augmented by machine learning. +\end{enumerate} + +\subsection{LPC/LSP based modes} +\label{sect:mode_lpc_lsp} + +In this and the next section we explain how the codec building blocks above are assembled to create a fully quantised Codec 2 mode. This section discusses the higher bit rate (3200 - 1200) modes that use a Linear Predictive Coding (LPC) and Line Spectrum Pairs (LSPs) to quantise and transmit the spectral magnitude information. There is a great deal of information available on these topics so they are only briefly described here. + +\begin{figure} [h] +\caption{LPC spectrum $|H(e^{j \omega})|$ (green line) and LSP frequencies $\{\omega_i\}$ (green crosses) for the speech frame in Figure \ref{fig:hts2a_time}. The original speech spectrum (blue) and $A_m$ estimates (red) are provided as references.} +\label{fig:hts2a_lpc_lsp} +\begin{center} +\input hts2a_37_lpc_lsp.tex +\end{center} +\end{figure} + +The source-filter model of speech production was introduced above in Equation (\ref{eq:source_filter}). A spectrally flat excitation source $E(z)$ excites a filter $H(z)$ which models the magnitude spectrum of the speech. In Linear Predictive Coding (LPC), we define $H(z)$ as an all-pole filter: +\begin{equation} +H(z) = \frac{G}{1-\sum_{k=1}^p a_k z^{-k}} = \frac{G}{A(z)} +\end{equation} +where $\{a_k\}, k=1..10$ is a set of p linear prediction coefficients that characterise the filters frequency response and G is a scalar gain factor. The coefficients are time varying and are extracted from the input speech signal, typically using a least squares approach. An excellent reference for LPC is \cite{makhoul1975linear}. + +To be useful in low bit rate speech coding it is necessary to quantise and transmit the LPC coefficients using a small number of bits. Direct quantisation of these LPC coefficients is inappropriate due to their large dynamic range (8-10 bits/coefficient). Thus for transmission purposes, especially at low bit rates, other forms such as the Line Spectral Pair (LSP) \cite{itakura1975line} frequencies are used to represent the LPC parameters. The LSP frequencies can be derived by decomposing the $p$-th order polynomial $A(z)$, into symmetric and anti-symmetric polynomials $P(z)$ and $Q(z)$, shown here in factored form: +\begin{equation} +\begin{split} +P(z) &= (1+z^{-1}) \prod_{i=1}^{p/2} (1 - 2cos(\omega_{2i-1} z^{-1} + z^{-2} ) \\ +Q(z) &= (1-z^{-1}) \prod_{i=1}^{p/2} (1 - 2cos(\omega_{2i} z^{-1} + z^{-2} ) +\end{split} +\end{equation} +where $\omega_{2i-1}$ and $\omega_{2i}$ are the LSP frequencies, found by evaluating the polynomials on the unit circle. The LSP frequencies are interlaced with each other, where $0<\omega_1 < \omega_2 <,..., < \omega_p < \pi$. The separation of adjacent LSP frequencies is related to the bandwidth of spectral peaks in $H(z)=G/A(z)$. A small separation indicates a narrow bandwidth, as shown in Figure \ref{fig:hts2a_lpc_lsp}. $A(z)$ may be reconstructed from $P(z)$ and $Q(z)$ using: +\begin{equation} +A(z) = \frac{P(z)+Q(z)}{2} +\end{equation} +Thus to transmit the LPC coefficients using LSPs, we first transform the LPC model $A(z)$ to $P(z)$ and $Q(z)$ polynomial form. We then solve $P(z)$ and $Q(z)$ for $z=e^{j \omega}$ to obtain $p$ LSP frequencies $\{\omega_i\}$. The LSP frequencies are then quantised and transmitted over the channel. At the receiver the quantised LSPs are then used to reconstruct an approximation of $A(z)$. More details on LSP analysis can be found in \cite{rowe1997techniques} and many other sources. + +Figure \ref{fig:encoder_lpc_lsp} presents the LPC/LSP mode encoder. Overlapping input speech frames are processed every 10ms ($N=80$ samples). LPC analysis determines a set of $p=10$ LPC coefficients $\{a_k\}$ that describe the spectral envelope of the current frame and the LPC energy $E=G^2$. The LPC coefficients are transformed to $p=10$ LSP frequencies $\{\omega_i\}$. The source code for these algorithms is in \emph{lpc.c} and \emph{lsp.c}. The LSP frequencies are then quantised to a fixed number of bits/frame. Other parameters include the pitch $\omega_0$, LPC energy $E$, and voicing $v$. The quantisation and bit packing source code for each Codec 2 mode can be found in \emph{codec2.c}. Note the spectral magnitudes $\{A_m\}$ are not transmitted but are still computed for use in voicing estimation (\ref{eq:voicing_snr}). + +\begin{figure}[h] +\caption{LPC/LSP Modes Encoder} +\label{fig:encoder_lpc_lsp} +\begin{center} +\begin{tikzpicture}[auto, node distance=2cm,>=triangle 45,x=1.0cm,y=1.0cm, align=center] + +\node [input] (rinput) {}; +\node [tmp, right of=rinput,node distance=0.5cm] (z) {}; +\node [block, right of=z,node distance=1.5cm] (window) {Window}; +\node [tmp, right of=window,node distance=1cm] (z1) {}; +\node [block, right of=z1,node distance=1.5cm] (dft) {DFT}; +\node [block, above of=dft,text width=2cm] (lpc) {LPC Analysis}; +\node [block, right of=lpc,node distance=3cm,text width=2cm] (lsp) {LSP Quantisation}; +\node [tmp, right of=nlp,node distance=1cm] (z2) {}; +\node [tmp, above of=z2,node distance=1cm] (z3) {}; +\node [block, below of=dft,text width=2cm] (est) {Est Amp}; +\node [block, right of=est,node distance=3cm,text width=2cm] (voicing) {Est Voicing}; +\node [block, below of=window] (nlp) {NLP}; +\node [block, below of=lsp,text width=2.5cm] (pack) {Decimation \&\\Bit Packing}; +\node [output, right of=pack,node distance=2cm] (routput) {}; + +\draw [->] node[align=left,text width=2cm] {$s(n)$} (rinput) -- (window); +\draw [->] (z) |- (nlp); +\draw [->] (window) -- (dft); +\draw [->] (z1) |- (lpc); +\draw [->] (lpc) -- (lsp); +\draw [->] (lsp) -- (pack); +\draw [->] (dft) -- (est); +\draw [->] (nlp) -- (est); +\draw [->] (z2) -- (z3) -| (pack); +\draw [->] (est) -- (voicing); +\draw [->] (voicing) -- (pack); +\draw [->] (pack) -- (routput) node[right,align=left,text width=1.5cm] {Bit Stream}; + +\end{tikzpicture} +\end{center} +\end{figure} + +One of the problems with quantising spectral magnitudes in sinusoidal codecs is the time varying number of harmonic magnitudes, as $L=\pi/\omega_0$, and $\omega_0$ varies from frame to frame. As we require a fixed bit rate for our use cases, it is desirable to have a fixed number of parameters. Using a fixed order LPC model is a neat solution to this problem. Another feature of LPC modelling combined with scalar LSP quantisation is some tolerance to variations in the input frequency response, e.g. due to microphone or anti-alias filter shape factors (see section \ref{sect:mode_newamp1} for more information on this issue). + +Some disadvantages \cite{makhoul1975linear} are the LPC spectrum $|H(e^{j \omega})|$ doesn't follow the spectral magnitudes $A_m$ exactly, in other words is requires a non-flat excitation spectrum to accurately model the amplitude spectrum. The slope of the LPC spectrum near 0 and $\pi$ must be 0, which means it does not track perceptually important low frequency information well. For high pitched speakers, LPC tends to place poles around single harmonics, rather than tracking the spectral envelope described by $\{Am\}$. All of these problems can be observed in Figure \ref{fig:hts2a_lpc_lsp}. Thus exciting the LPC model by a simple, spectrally flat $E(z)$ will result in some errors in the reconstructed magnitude speech spectrum. + +In CELP codecs these problems can be accommodated by the (high bit rate) excitation used to construct a non-flat $E(z)$, and some low rate codecs such as MELP supply supplementary low frequency information to ``correct" the LPC model. + +Before bit packing, the Codec 2 parameters are decimated in time. An update rate of 20ms is used for the highest rate modes, which drops to 40ms for Codec 2 1300, with a corresponding drop in speech quality. The number of bits used to quantise the LPC model via LSPs is also reduced in the lower bit rate modes. This has the effect of making the speech less intelligible, and can introduce annoying buzzy or clicky artefacts into the synthesised speech. Lower fidelity spectral magnitude quantisation also results in more noticeable artefacts from phase synthesis. Nevertheless at 1300 bits/s the speech quality is quite usable for HF digital voice, and at 3200 bits/s comparable to closed source codecs at the same bit rate. + +\begin{figure}[H] +\caption{LPC/LSP Modes Decoder} +\label{fig:decoder_lpc_lsp} +\begin{center} +\begin{tikzpicture}[auto, node distance=3cm,>=triangle 45,x=1.0cm,y=1.0cm,align=center] + +\node [input] (rinput) {}; +\node [block, right of=rinput,node distance=1.5cm] (unpack) {Unpack}; +\node [block, right of=unpack,node distance=2.5cm] (interp) {Interpolate}; +\node [block, right of=interp,text width=2cm] (lpc) {LSP to LPC}; +\node [tmp, right of=interp,node distance=1.25cm] (z1) {}; +\node [block, right of=lpc,text width=2cm] (sample) {Sample $A_m$}; +\node [block, below of=lpc,text width=2cm,node distance=2cm] (phase) {Phase Synthesis}; +\node [block, below of=phase,text width=2.5cm,node distance=2cm] (synth) {Sinusoidal\\Synthesis}; +\node [block, right of=phase,text width=2cm] (post) {Post Filter}; +\node [output, left of=synth,node distance=2cm] (routput) {}; + +\draw [->] node[align=left,text width=2cm] {Bit\\Stream} (rinput) -- (unpack); +\draw [->] (unpack) -- (interp); +\draw [->] (interp) -- (lpc); +\draw [->] (lpc) -- (sample); +\draw [->] (sample) -- (post); +\draw [->] (post) |- (synth); +\draw [->] (z1) |- (phase); +\draw [->] (phase) -- (synth); +\draw [->] (post) -- (phase); +\draw [->] (synth) -- (routput) node[align=left,text width=1.5cm] {$\hat{s}(n)$}; + +\end{tikzpicture} +\end{center} +\end{figure} + +Figure \ref{fig:decoder_lpc_lsp} shows the LPC/LSP mode decoder. Frames of bits received at the frame rate are unpacked and resampled to the 10ms internal frame rate using linear interpolation. The spectral magnitude information is resampled by linear interpolation of the LSP frequencies, and converted back to a quantised LPC model $\hat{H}(z)$. The harmonic magnitudes are recovered by averaging the energy of the LPC spectrum over the region of each harmonic: +\begin{equation} +\hat{A}_m = \sqrt{ \sum_{k=a_m}^{b_m-1} | \hat{H}(k) |^2 } +\end{equation} +where $\hat{H}(k)$ is the $N_{dft}$ point DFT of the received LPC model for this frame. For phase synthesis, the $arg[H(z)]$ component is determined by sampling $\hat{H}(k)$ in the centre of each harmonic: +\begin{equation} +arg \left[ H(e^{j \omega_0 m}) \right] = arg \left[ \hat{H}(\lfloor m r \rceil) \right] +\end{equation} + +\begin{figure} [h] +\caption{LPC post filter. LPC spectrum before $|H(e^{j \omega})|$ (green line) and after (red) post filtering. The distance between the spectral peaks and troughs has been increased. The step change at 1000 Hz is +3dB low frequency boost (see source code).} +\label{fig:hts2a_lpc_pf} +\begin{center} +\input hts2a_37_lpc_pf.tex +\end{center} +\end{figure} + +Prior to sampling the amplitude and phase, a frequency domain post filter is applied to the LPC power spectrum. The algorithm is based on the MBE frequency domain post filter \cite[Section 8.6, p 267]{kondoz1994digital}, which is in turn based on the frequency domain post filter from McAulay and Quatieri \cite[Section 4.3, p 148]{kleijn1995speech}. The authors report a significant improvement in speech quality from the post filter, which has also been our experience when applied to Codec 2. The post filter is given by: +\begin{equation} +\label{eq:lpc_lsp_pf} +\begin{split} +P_f(e^{j\omega}) &= g \left( R_w(e^{j \omega} \right))^\beta \\ +R_w(^{j\omega}) &= A(e^{j \omega/ \gamma})/A(e^{j \omega}) +\end{split} +\end{equation} +where $g$ is chosen to normalise the gain of the post filter, and $\beta=0.2$, $\gamma=0.5$ are experimentally derived constants. The post filter raises the spectral peaks (formants), and lowers the inter-formant energy. The $\gamma$ term compensates for spectral tilt, providing equal emphasis at low and high frequencies. The authors suggest the post filter reduces the noise level between formants, an explanation commonly given to post filters used for CELP codecs where significant inter-formant noise exists from the noisy excitation source. However, in harmonic sinusoidal codecs, there is no excitation noise between formants in $E(z)$. Our theory is the post filter also acts to reduce the bandwidth of spectral peaks, modifying the energy distribution across the time domain pitch cycle which improves speech quality, especially for low pitched speakers. + +A disadvantage of the post filter is the need for experimentally derived constants. It performs a non-linear operation on the speech spectrum, and if mis-applied can worsen speech quality. As it's operation is not completely understood, it represents a source of future quality improvement. + +\subsection{Codec 2 700C} +\label{sect:mode_newamp1} + +To efficiently transmit spectral amplitude information, Codec 2 700C uses a set of algorithms collectively denoted \emph{newamp1}. One of these algorithms is the Rate K resampler which transforms the variable length vectors of spectral magnitude samples to fixed length $K$ vectors suitable for vector quantisation. Figure \ref{fig:encoder_newamp1} presents the Codec 2 700C encoder. + +\begin{figure}[H] +\caption{Codec 2 700C (newamp1) Encoder} + +\label{fig:encoder_newamp1} +\begin{center} +\begin{tikzpicture}[auto, node distance=2cm,>=triangle 45,x=1.0cm,y=1.0cm, align=center] + +\node [input] (rinput) {}; +\node [tmp, right of=rinput,node distance=0.5cm] (z) {}; +\node [block, right of=z,node distance=1.5cm] (window) {Window}; +\node [block, right of=window,node distance=2.5cm] (dft) {DFT}; +\node [block, right of=dft,node distance=3cm,text width=1.5cm] (est) {Est Amp}; +\node [block, below of=est,node distance=2cm,text width=2cm] (resample) {Resample Rate $K$}; +\node [block, below of=dft,node distance=2cm,text width=2cm] (eq) {Microphone EQ}; +\node [block, below of=eq,node distance=2cm,text width=2cm] (vq) {Decimate \& VQ}; +\node [block, below of=window] (nlp) {NLP}; +\node [block, below of=nlp] (log) {log $\omega_0$}; +\node [block, below of=resample,node distance=2cm,text width=1.5cm] (voicing) {Est Voicing}; +\node [block, below of=vq,node distance=2cm,text width=2cm] (pack) {Bit Packing}; +\node [tmp, right of=resample,node distance=2cm] (z1) {}; +\node [tmp, below of=vq,node distance=1cm] (z2) {}; +\node [output, right of=pack,node distance=2cm] (routput) {}; + +\draw [->] node[align=left,text width=2cm] {$s(n)$} (rinput) -- (window); +\draw [->] (z) |- (nlp); +\draw [->] (window) -- node[below] {$s_w(n)$} (dft); +\draw [->] (dft) -- node[below] {$S_\omega(k)$} (est); +\draw [->] (est) -- node[right] {$\mathbf{a}$} (resample); +\draw [->] (resample) -- node[below] {$\mathbf{b}$} (eq); +\draw [->] (eq) -- node[left] {$\mathbf{c}$} (vq); +\draw [->] (vq) -- (pack); +\draw [->] (est) -| (z1) |- (voicing); +\draw [->] (nlp) -- (log); +\draw [->] (log) |- (pack); +\draw [->] (voicing) |- (z2) -| (pack); +\draw [->] (pack) -- (routput) node[right] {Bit Stream}; + +\end{tikzpicture} +\end{center} +\end{figure} + +Consider a vector $\mathbf{a}$ of $L$ harmonic spectral magnitudes expressed in dB: +\begin{equation} +\mathbf{a} = \begin{bmatrix} 20log_{10}A_1, 20log_{10}A_2, \ldots 20log_{10}A_L \end{bmatrix} +\end{equation} +\begin{equation} +L=\left \lfloor \frac{F_s}{2F_0} \right \rfloor = \left \lfloor \frac{\pi}{\omega_0} \right \rfloor +\end{equation} +$F_0$ and $L$ are time varying as the pitch track evolves over time. For speech sampled at $F_s=8$ kHz $F_0$ is typically in the range of 50 to 400 Hz, giving $L$ in the range of 10 $\ldots$ 80. + +To quantise and transmit $\mathbf{a}$, it is convenient to resample $\mathbf{a}$ to a fixed length $K$ element vector $\mathbf{b}$ using a resampling function: +\begin{equation} +\mathbf{b} = \begin{bmatrix} B_1, B_2, \ldots B_K \end{bmatrix} = R(\mathbf{a}) +\end{equation} +Where $R$ is a resampling function. To model the response of the human ear $B_k$ are sampled on $K$ non-linearly spaced points on the frequency axis: +\begin{equation} +\begin{split} +f_k &= warp(k,K) \ \textrm{Hz} \quad k=1 \ldots K \\ +warp(1,K) &= 200 \ \textrm{Hz} \\ +warp(K,K) &= 3700 \ \textrm{Hz} +\end{split} +\end{equation} +where $warp()$ is a frequency warping function. Codec 2 700C uses $K=20$, and $warp()$ is defined using the Mel function \cite[p 150]{o1997human} (Figure \ref{fig:mel_fhz}) which samples the spectrum more densely at low frequencies, and less densely at high frequencies: +\begin{equation} \label{eq:mel_f} +mel(f) = 2595log_{10}(1+f/700) +\end{equation} +The inverse mapping of $f$ in Hz from $mel(f)$ is given by: +\begin{equation} \label{eq:f_mel} +f = mel^{-1}(x) = 700(10^{x/2595} - 1); +\end{equation} + +\begin{figure}[h] +\caption{Mel function} +\label{fig:mel_fhz} +\begin{center} +\includegraphics[width=8cm]{ratek_mel_fhz} +\end{center} +\end{figure} + +We wish to use $mel(f)$ to construct $warp(k,K)$, such that there are $K$ evenly spaced points on the $mel(f)$ axis (Figure \ref{fig:mel_k}). Solving for the equation of a straight line we can obtain $mel(f)$ as a function of $k$, and hence $warp(k,K)$ (Figure \ref{fig:warp_fhz_k}): +\begin{equation} +\label{eq:mel_k} +\begin{split} +g &= \frac{mel(3700)-mel(200)}{K-1} \\ +mel(f) &= g(k-1) + mel(200) +\end{split} +\end{equation} +where $g$ is the gradient of the line. Substituting (\ref{eq:f_mel}) into the LHS: +\begin{equation} +\label{eq:warp} +\begin{split} +2595log_{10}(1+f/700) &= g(k-1) + mel(200) \\ +f_k = warp(k,K) &= mel^{-1} ( g(k-1) + mel(200) ) \\ +\end{split} +\end{equation} +and the inverse warp function: +\begin{equation} \label{warp_inv} +k = warp^{-1}(f,K) = \frac{mel(f)-mel(200)}{g} + 1 +\end{equation} + +\begin{figure}[h] +\caption{Linear mapping of $mel(f)$ to Rate $K$ sample index $k$} +\vspace{5mm} +\label{fig:mel_k} +\centering +\begin{tikzpicture} +\tkzDefPoint(1,1){A} +\tkzDefPoint(3,3){B} +\draw[thick] (1,1) node [right]{(1,mel(200))} -- (3,3) node [right]{(K,mel(3700))}; +\draw[thick,->] (0,0) -- (4,0) node [below]{k}; +\draw[thick,->] (0,0) -- (0,4) node [left]{mel(f)}; +\foreach \n in {A,B} + \node at (\n)[circle,fill,inner sep=1.5pt]{}; +\end{tikzpicture} +\end{figure} + +\begin{figure}[h] +\caption{$warp(k,K)$ function for $K=20$} +\label{fig:warp_fhz_k} +\begin{center} +\includegraphics[width=8cm]{warp_fhz_k} +\end{center} +\end{figure} + +The input speech may be subject to arbitrary filtering, for example, due to the microphone frequency response, room acoustics, and anti-aliasing filter. This filtering is fixed or slowly time-varying. The filtering biases the target vectors away from the VQ training material, resulting in significant additional mean square error. The filtering does not greatly affect the input speech quality, however the VQ performance distortion increases and the output speech quality is reduced. This is exacerbated by operating in the log domain, the VQ will try to match very low level, perceptually insignificant energy near 0 and 4000 Hz. A microphone equaliser algorithm has been developed to help adjust to arbitrary microphone filtering. + +For every input frame $l$, the equaliser (EQ) updates the dimension $K$ equaliser vector $\mathbf{e}$: +\begin{equation} +\mathbf{e}^{l} = \mathbf{e}^{l-1} + \beta(\mathbf{b} - \mathbf{t}) +\end{equation} +where $\mathbf{t}$ is a fixed target vector set to the mean of the VQ quantiser, and $\beta$ is a small adaption constant. + +The equalised, mean removed rate $K$ vector $\mathbf{d}$ is vector quantised for transmission over the channel: +\begin{equation} +\begin{split} +\mathbf{c} &= \mathbf{b} - \mathbf{e} \\ +\mathbf{d} &= \mathbf{c} - \bar{\mathbf{c}} \\ +\hat{\mathbf{c}} &= VQ(\mathbf{d}) + Q(\bar{\mathbf{c}}) \\ + &= \hat{\mathbf{d}} + \hat{\bar{\mathbf{c}}} +\end{split} +\end{equation} +Codec 2 700C uses a two stage VQ with 9 bits (512 entries) per stage. The \emph{mbest} multi-stage search algorithm is used to jointly search the two stages (using 5 survivors from the first stage). Note that VQ is performed in the $log$ amplitude (dB) domain. The mean of $\mathbf{c}$ is removed prior to VQ and scalar quantised and transmitted separately as the frame energy. At the decoder, the rate $L$ vector $\hat{\mathbf{a}}$ can then be recovered by resampling $\mathbf{\hat{a}}$: +\begin{equation} +\hat{\mathbf{a}} = S(\hat{\mathbf{c}} + \mathbf{p}) +\end{equation} +where $\mathbf{p}$ is a post filter vector. The post filter vector is generated from the mean-removed rate $K$ vector $\hat{\mathbf{d}}$ in the $log$ frequency domain: +\begin{equation} +\begin{split} +\mathbf{p} &= G + P_{gain} \left( \hat{\mathbf{d}} + \mathbf{r} \right) - \mathbf{r} \\ +\mathbf{r} &= \begin{bmatrix} R_1, R_2, \ldots R_K \end{bmatrix} \\ + R_k &= 20log_{10}(f_k/300) \quad k=1,...,K +\end{split} +\end{equation} +where $G$ is an energy normalisation term, and $1.2 < P_{gain} < 1.5$ describes the amount if post filtering applied. $G$ and $P_{gain}$ are similar to $g$ and $\beta$ in the LPC/LSP post filter (\ref{eq:lpc_lsp_pf}). The $\mathbf{r}$ term is a high pass (pre-emphasis) filter with +20 dB/decade gain after 300 Hz ($f_k$ is given in (\ref{eq:warp})). The post filtering is applied on the pre-emphasised vector, then the pre-emphasis is removed from the final result. Multiplying by $P_{gain}$ in the $log$ domain is similar to the $\alpha$ power function in (\ref{eq:lpc_lsp_pf}); spectral peaks are moved up, and troughs pushed down. This filter enhances the speech quality but also introduces some artefacts. + +Figure \ref{fig:decoder_newamp1} is the block diagram of the decoder signal processing. Cepstral techniques are used to synthesise a phase spectra $arg[H(e^{j \omega}])$ from $\hat{\mathbf{a}}$ using a minimum phase model. + +\begin{figure}[h] +\caption{Codec 2 700C (newamp1) Decoder} +\label{fig:decoder_newamp1} +\begin{center} +\begin{tikzpicture}[auto, node distance=3cm,>=triangle 45,x=1.0cm,y=1.0cm,align=center] + +\node [input] (rinput) {}; +\node [block, right of=rinput,node distance=1.5cm] (unpack) {Unpack}; +\node [block, right of=unpack,node distance=2.5cm] (interp) {Interpolate}; +\node [block, right of=interp,node distance=3cm,text width=2cm] (post) {Post Filter}; +\node [block, below of=post,text width=2cm,node distance=2cm] (resample) {Resample to Rate $L$}; +\node [block, below of=resample,text width=2cm,node distance=2cm] (synth) {Sinusoidal\\Synthesis}; +\node [tmp, below of=resample,node distance=1cm] (z1) {}; +\node [block, left of=synth,text width=2cm] (phase) {Phase Synthesis}; +\node [output,right of=synth,node distance=2cm] (routput) {}; + +\draw [->] node[align=left,text width=2cm] {Bit\\Stream} (rinput) -- (unpack); +\draw [->] (unpack) -- (interp); +\draw [->] (interp) -- node[above] {$\hat{\mathbf{c}}$} (post); +\draw [->] (post) -- node[left] {$\hat{\mathbf{c}} + \mathbf{p}$} (resample); +\draw [->] (interp) |- node[left] {$\hat{\omega_0}, v$} (resample); +\draw [->] (resample) -- node[right] {$\hat{\mathbf{a}}$} (synth); +\draw [->] (resample) -- (z1) -| (phase); +\draw [->] (phase) -- (synth); +\draw [->] (synth) -- (routput) node[align=right,text width=1.5cm] {$\hat{s}(n)$}; + +\end{tikzpicture} +\end{center} +\end{figure} + +Some notes on the Codec 2 700C \emph{newamp1} algorithms: +\begin{enumerate} +\item The amplitudes and Vector Quantiser (VQ) entries are in dB, which matches the ear's logarithmic amplitude response. +\item The mode is capable of communications quality speech and is in common use with FreeDV, but is close to the lower limits of intelligibility, and doesn't do well in some languages (problems have been reported with German and Japanese). +\item The VQ was trained on just 120 seconds of data - way too short. +\item The parameter set (pitch, voicing, log spectral magnitudes) is very similar to that used for the latest neural vocoders. +\item The Rate K algorithms were recently revisited, and several improvements were proposed and prototyped \cite{rowe2023ratek}. +\end{enumerate} + +\section{Summary of Codec 2 Modes} +\label{sect:codec2_modes} + +\begin{table}[H] +\label{tab:codec2_modes} +\centering +\begin{tabular}{p{0.75cm}|p{0.75cm}|p{0.5cm}|p{0.5cm}|p{0.5cm}|p{0.5cm}|p{0.5cm}|p{3cm}} +\hline +Mode & Frm (ms) & Bits & $A_m$ & $E$ & $\omega_0$ & $v$ & Use Cases \\ +\hline +3200 & 20 & 64 & 50 & 5 & 7 & 2 & M17 \\ +2400 & 20 & 50 & 36 & 8 & - & 2 \\ +1600 & 40 & 64 & 36 & 10 & 14 & 4 & M17 \\ +1400 & 40 & 56 & 36 & 16 & - & 4 \\ +1300 & 40 & 52 & 36 & 5 & 7 & 4 & FreeDV 1600 \\ +1200 & 40 & 48 & 27 & 16 & - & 4 & \\ +700C & 40 & 28 & 18 & 4 & 6 & - & FreeDV 700C/D/E \\ +\hline +\end{tabular} +\caption{Codec 2 Modes} +\end{table} + +The 3200 mode quantises the LSP differences $\omega_{i+1}-\omega_i$, which provides low distortion at the expense of robustness to bit errors, as an error in a low order LSP difference will propagate through the frame. The 2400 and 1200 bit/s modes use a joint delta $\omega_0$ and energy VQ, which is efficient but also suffers from error propagation so is not suitable for high BER use cases. + +There is an unfortunate overlap in the naming conventions of Codec 2 and FreeDV. The Codec 2 700C mode is used in the FreeDV 700C, 700D, and 700E modes. + +\section{Summary of Codec 2 Source Files} +\label{sect:source_files} + +Codec 2 is part of the \emph{codec2} repository, which also includes various modems and FreeDV API code. This section lists the files specific to the speech codec. The \emph{cmake} system builds the \emph{libcodec2} library, which is called by user applications via the Codec 2 API in \emph{codec2.h}. See the repository \emph{README} for information on building, demo applications, and an introduction to other features of the \emph{codec2} repository. + +\begin{table}[H] +\label{tab:codec2_file} +\centering +\begin{tabular}{l l} +\hline +File & Description \\ +\hline +c2dec & Sample decoder application \\ +c2enc & Sample encoder application \\ +c2sim & Simulation and development application \\ +codebook & Directory containing quantiser tables \\ +codec2.c & Quantised encoder and decoder functions that implement each mode \\ +codec2\_fft.c & Wrapper for FFT (usually kiss FFT) \\ +defines.h & Constants \\ +lpc.c & LPC functions \\ +mbest.c & Multistage VQ search \\ +newamp1.c & Codec 2 700C \emph{newamp1} mode \\ +nlp.c & Non-linear Pitch (NLP) \\ +sine.c & Sinusoidal analysis, synthesis, voicing estimation \\ +phase.c & Phase synthesis \\ +quantise.c & Quantisation, in particular for LPC/LSP modes \\ +\hline +\end{tabular} +\caption{Codec 2 Source Files} +\end{table} + +\section{Glossary} +\label{sect:glossary} + +\begin{table}[H] +\label{tab:acronyms} +\centering +\begin{tabular}{l l l } +\hline +Acronym & Description \\ +\hline +DFT & Discrete Fourier Transform \\ +DTCF & Discrete Time Continuous Frequency Fourier Transform \\ +EQ & (microphone) Equaliser \\ +IDFT & Inverse Discrete Fourier Transform \\ +LPC & Linear Predictive Coding \\ +LSP & Line Spectrum Pair \\ +MBE & Multi-Band Excitation \\ +MSE & Mean Square Error \\ +NLP & Non Linear Pitch (algorithm) \\ +VQ & Vector Quantiser \\ +\hline +\end{tabular} +\caption{Glossary of Acronyms} +\end{table} + +\begin{table}[H] +\label{tab:symbol_glossary} +\centering +\begin{tabular}{l l l } +\hline +Symbol & Description & Units \\ +\hline +$A(z)$ & LPC (analysis) filter \\ +$a_m$ & Lower DFT index of current band \\ +$b_m$ & Upper DFT index of current band \\ +$\{A_m\}$ & Set of harmonic magnitudes $m=1,...L$ & dB \\ +$\mathbf{a}$ & $\{A_m\}$ in vector form \\ +$B_m$ & Complex spectral amplitudes used for voicing estimation \\ +$E$ & Frame energy \\ +$E(z)$ & Excitation in source-filter model \\ +$F_0$ & Fundamental frequency (pitch) & Hz \\ +$F_s$ & Sample rate (usually 8 kHz) & Hz \\ +$F_w(k)$ & DFT of squared speech signal in NLP pitch estimator \\ +$G$ & LPC gain \\ +$H(z)$ & Synthesis filter in source-filter model \\ +$\hat{H}(z)$ & Synthesis filter approximation after quantisation \\ +$l$ & Frame index \\ +$L$ & Number of harmonics \\ +$N$ & Processing frame size in samples \\ +$n_0$ & Excitation pulse position \\ +$P$ & Pitch period & ms or samples \\ +$P(z), Q(z)$ & LSP polynomials \\ +$P_f(e^{j \omega})$ & LPC post filter \\ +$\{\theta_m\}$ & Set of harmonic phases $m=1,...L$ & dB \\ +$r$ & Maps a harmonic number $m$ to a DFT index \\ +$s(n)$ & Input time domain speech \\ +$\hat{s}(n)$ & Output (synthesised) time domain speech \\ +$s_w(n)$ & Time domain windowed input speech \\ +$S_w(k)$ & Frequency domain windowed input speech \\ +$\hat{S}_w(k)$ & Frequency domain output (synthesised)speech \\ +$t(n)$ & Triangular synthesis window \\ +$\phi_m$ & Phase of excitation harmonic \\ +$\omega_0$ & Fundamental frequency (pitch) & radians/sample \\ +$\{\omega_i\}$ & Set of LSP frequencies \\ +$w(n)$ & Window function \\ +$W(k)$ & DFT of window function \\ +$v$ & Voicing decision for the current frame \\ +\hline +\end{tabular} +\caption{Glossary of Symbols} +\end{table} + +\section{Further Documentation Work} +\label{sect:further_work} + +This section contains ideas for expanding the documentation of Codec 2. Please contact the authors if you are interested in this material or would like to help develop it. + +\begin{enumerate} +\item The \emph{c2sim} utility is presently undocumented. We could add some worked examples aimed at the experimenter - e.g. using c2sim to extract and plot model parameters. Demonstrate how to listen to various stages of quantisation. +\item Several GNU Octave scripts exist that were used to develop Codec 2. We could add information describing how to use the Octave tools to single step through the codec operation. +\end{enumerate} + +\addcontentsline{toc}{chapter}{References} +\bibliographystyle{plain} +\bibliography{codec2_refs} +\end{document} diff --git a/third_party/codec2/doc/codec2_refs.bib b/third_party/codec2/doc/codec2_refs.bib new file mode 100644 index 0000000..756a92b --- /dev/null +++ b/third_party/codec2/doc/codec2_refs.bib @@ -0,0 +1,84 @@ +@article{griffin1988multiband, + title={Multiband excitation vocoder}, + author={Griffin, Daniel W and Lim, Jae S}, + journal={IEEE Transactions on acoustics, speech, and signal processing}, + volume={36}, + number={8}, + pages={1223--1235}, + year={1988}, + publisher={IEEE} +} +@book{rowe1997techniques, + title={Techniques for harmonic sinusoidal coding}, + author={Rowe, David Grant}, + year={1997}, + publisher={Citeseer}, + note = {\url{https://www.rowetel.com/downloads/1997_rowe_phd_thesis.pdf}} +} + +@misc{ardc2023, + title = {{Enhancing HF Digital Voice with FreeDV}}, + year = {2023}, + note = {\url{https://www.ardc.net/apply/grants/2023-grants/enhancing-hf-digital-voice-with-freedv/}} +} + +@article{mcaulay1986speech, + title={Speech analysis/synthesis based on a sinusoidal representation}, + author={McAulay, Robert and Quatieri, Thomas}, + journal={IEEE Transactions on Acoustics, Speech, and Signal Processing}, + volume={34}, + number={4}, + pages={744--754}, + year={1986}, + publisher={IEEE} +} + +@article{makhoul1975linear, + title={Linear prediction: A tutorial review}, + author={Makhoul, John}, + journal={Proceedings of the IEEE}, + volume={63}, + number={4}, + pages={561--580}, + year={1975}, + publisher={IEEE} +} + +@article{itakura1975line, + title={Line spectrum representation of linear predictor coefficients of speech signals}, + author={Itakura, Fumitada}, + journal={The Journal of the Acoustical Society of America}, + volume={57}, + number={S1}, + pages={S35--S35}, + year={1975}, + publisher={AIP Publishing} +} + + +@book{o1997human, + title={Speech Communication - Human and machine}, + author={O‘Shaughnessy, Douglas}, + publisher={Addison-Wesley Publishing Company}, + year={1997} +} + +@misc{rowe2023ratek, + title = {{FreeDV-015 Codec 2 Rate K Resampler}}, + year = {2023}, + note = {\url{https://github.com/drowe67/misc/blob/master/ratek_resampler/ratek_resampler.pdf}} +} + +@book{kondoz1994digital, + title={Digital speech: coding for low bit rate communication systems}, + author={Kondoz, Ahmet M}, + year={1994}, + publisher={John Wiley \& Sons} +} + +@book{kleijn1995speech, + title={Speech coding and synthesis}, + author={Kleijn, W Bastiaan and Paliwal, Kuldip K}, + year={1995}, + publisher={Elsevier Science Inc.} +} \ No newline at end of file diff --git a/third_party/codec2/doc/fsk_modem_ber_8000_100.png b/third_party/codec2/doc/fsk_modem_ber_8000_100.png new file mode 100644 index 0000000000000000000000000000000000000000..d08774d3cb8d8f55c5ee0e5ef92db7ede82caf46 GIT binary patch literal 47376 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%0A+k*7|UJcKzK` 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%0A#q## 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 literal 0 HcmV?d00001 diff --git a/third_party/codec2/doc/lockdown_3s.wav b/third_party/codec2/doc/lockdown_3s.wav new file mode 100644 index 0000000000000000000000000000000000000000..61c7905903faa494c8b1e33bb9698898248e426a GIT binary patch literal 48044 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 literal 0 HcmV?d00001 diff --git a/third_party/codec2/doc/modem_codec_frame_design.ods b/third_party/codec2/doc/modem_codec_frame_design.ods new file mode 100644 index 0000000000000000000000000000000000000000..040ba0160adc96327f3487de7676e81b74d8b332 GIT binary patch literal 44489 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

    nFS!oa7?lK|oLZd)xF$A;5uaX7Ju`1J{0=me zVV+PH$&Be@GrM~Iw)d%+8m)V6?4=$NG=$yizJG@H(ixPr#4;{)LMzWRjgeOvmE@6e zwAz!~_VhZbrUMU5tuTxLLZHnSn*}+^3|4Rlb&*U)uWu!;L(CF+Z;GJFu#kr6?(638f@8fjWo2qsuW3MfMmVQDyerTVTEOeZ6>Wzd|p?uW=xXC>^XSMM0 zgxZdo`{mzH{|J;G4&5cJKmfo#11dmG5eb>N$pATAvL35D`+C{h0g z#nsKk&E3`1=}$QJqT7lfq?!t(ET&lDcQ65?j^@6LCXux<7W39~90|5Tae=l2d6aavxmAk#EgNe1BD~H>^HrbsVEdS~CUj>VQ)S>?ys9gUq nTG4+({IyE?qip=&P~`SEouir}8v37|1c=EAA&``;{1f{>?O*Fw literal 0 HcmV?d00001 diff --git a/third_party/codec2/doc/pre_post_amble_mpp.png b/third_party/codec2/doc/pre_post_amble_mpp.png new file mode 100644 index 0000000000000000000000000000000000000000..80cd5338b45e6f48dd23b0ade6962f72381e5482 GIT binary patch literal 4313 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 literal 0 HcmV?d00001 diff --git a/third_party/codec2/doc/ratek_mel_fhz.png b/third_party/codec2/doc/ratek_mel_fhz.png new file mode 100644 index 0000000000000000000000000000000000000000..c51d40931004ea804665b0c1d90e32677292ab8a GIT binary patch literal 11685 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 literal 0 HcmV?d00001 diff --git a/third_party/codec2/doc/snrest_snr_ctx.png b/third_party/codec2/doc/snrest_snr_ctx.png new file mode 100644 index 0000000000000000000000000000000000000000..618b9e10479f15fea1a19b020de2e5447f56949c GIT binary patch literal 26891 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