This commit is contained in:
zenith 2026-01-02 22:45:56 -05:00
commit f58acebeb1
13 changed files with 101862 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
miniaudio.o

24
LICENSE Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

62
Makefile Normal file
View file

@ -0,0 +1,62 @@
CXX = g++
CXXFLAGS = -std=c++17 -O3 -march=native -Wall -Wextra
LDFLAGS = -lpthread -lncurses -ldl -lm
# dependencies
AICODIX_DSP ?= ../dsp
AICODIX_CODE ?= ../code
MODEM_SRC ?= ../modem
INCLUDES = -I$(AICODIX_DSP) -I$(AICODIX_CODE) -I$(MODEM_SRC)
TARGET = modem73
SRCS = kiss_tnc.cc
HDRS = kiss_tnc.hh miniaudio_audio.hh rigctl_ptt.hh modem.hh tnc_ui.hh
OBJS = miniaudio.o
# defualt to build with UI, headless operations through --headless
UI_FLAGS = -DWITH_UI
.PHONY: all clean install debug help
all: $(TARGET)
miniaudio.o: miniaudio.c miniaudio.h
$(CC) -c -O2 -o $@ miniaudio.c
$(TARGET): $(SRCS) $(HDRS) $(OBJS)
$(CXX) $(CXXFLAGS) $(UI_FLAGS) $(INCLUDES) -o $@ $(SRCS) $(OBJS) $(LDFLAGS)
clean:
rm -f $(TARGET) $(OBJS)
install: $(TARGET)
install -m 755 $(TARGET) /usr/local/bin/
# Debug build
debug: CXXFLAGS = -std=c++17 -g -O0 -Wall -Wextra -DDEBUG
debug: $(TARGET)
# Help
help:
@echo "MODEM73 makefile"
@echo ""
@echo "Targets:"
@echo " all - Build modem"
@echo " clean - Remove build"
@echo " install - Install to /usr/local/bin"
@echo " debug - Build with debug symbols"
@echo ""
@echo "Variables:"
@echo " AICODIX_DSP - Path to aicodix/dsp (default: ../dsp)"
@echo " AICODIX_CODE - Path to aicodix/code (default: ../code)"
@echo " MODEM_SRC - Path to modem source (default: ../modem)"
@echo ""
@echo "Example:"
@echo " make AICODIX_DSP=~/aicodix/dsp AICODIX_CODE=~/aicodix/code"
@echo ""
@echo "Runtime options:"
@echo " ./modem73 # Run with UI"
@echo " ./modem73 -h # Run headless"
@echo " ./modem73 --headless"

101
README.md Normal file
View file

@ -0,0 +1,101 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/LDNR23jg/MODEM73-white.png">
<source media="(prefers-color-scheme: light)" srcset="https://i.ibb.co/wZKznzrF/MODEM73-blk.png">
<img alt="MODEM73" src="https://i.ibb.co/wZKznzrF/MODEM73-blk.png">
</picture>
</p>
MODEM73 is a [KISS](https://en.wikipedia.org/wiki/KISS_(amateur_radio_protocol)) TNC frontend for the [aicodix](https://github.com/aicodix/modem) OFDM modem.
![Screenshot](https://i.ibb.co/4ZhhvcQs/Peek-2026-01-01-10-41.gif)
## Building
1. Install dependencies
```
# Debian/Ubuntu/Pi
sudo apt install git build-essential libncurses-dev g++
```
2. Clone aiocdix DSP libraries and build.
```
# Requires DSP, code, and modem libraries
git clone https://github.com/aicodix/dsp.git
git clone https://github.com/aicodix/code.git
git clone https://github.com/aicodix/modem.git
# Clone modem73
git clone https://github.com/RFnexus/modem73
# Your folders should look like this:
#.../
#├── dsp/ # DSP library (aicodix)
#│ └── ...
#├── code/ # Code library (aicodix)
#│ └── ...
#├── modem/ # Modem library (aicodix)
#│ └── ...
#└── modem73/ # modem73 src
# └── ...
# Build
cd modem73
make AICODIX_DSP=../dsp AICODIX_CODE=../code MODEM_SRC=../modem
# Optional: move to /usr/local/bin
sudo make install
```
## Running & Operations
By default, MODEM73 will listen on port 8001
All of the modes provided by the OFDM modem require a bandwidth of 2400 Hz and work over both FM and SSB.
There are currently four PTT options:
- NONE (speaker/mic over the air)
- Rigctl
- VOX
- Serial
```
# Start in UI mode
./modem73
# Start in headless mode
./modem73 --headless
# See all options with:
./modem73 --help
```
### PTT options
```
# Connect to rigctld for PTT control
./modem73 --rigctl localhost:4532
```
while running `rigctld`
```
./modem73 --ptt vox --vox-freq 1200 --vox-lead 500 --vox-tail 150
# 500ms vox lead and 150ms vox tail
```
```
./modem73 --ptt com --com-port /dev/ttyUSB0 --com-line rts
```

1192
kiss_tnc.cc Normal file

File diff suppressed because it is too large Load diff

248
kiss_tnc.hh Normal file
View file

@ -0,0 +1,248 @@
#pragma once
#include <cstdint>
#include <vector>
#include <queue>
#include <mutex>
#include <atomic>
#include <functional>
#include <string>
#include <iomanip>
#include <iostream>
// KISS protocol
namespace KISS {
constexpr uint8_t FEND = 0xC0;
constexpr uint8_t FESC = 0xDB;
constexpr uint8_t TFEND = 0xDC;
constexpr uint8_t TFESC = 0xDD;
// KISS commands
constexpr uint8_t CMD_DATA = 0x00;
constexpr uint8_t CMD_TXDELAY = 0x01;
constexpr uint8_t CMD_P = 0x02;
constexpr uint8_t CMD_SLOTTIME = 0x03;
constexpr uint8_t CMD_TXTAIL = 0x04;
constexpr uint8_t CMD_FULLDUPLEX = 0x05;
constexpr uint8_t CMD_SETHW = 0x06;
constexpr uint8_t CMD_RETURN = 0xFF;
}
enum class PTTType {
NONE = 0,
RIGCTL = 1,
VOX = 2,
COM = 3
};
struct TNCConfig {
// Network settings
std::string bind_address = "0.0.0.0";
int port = 8001;
// Audio settings
std::string audio_input_device = "default";
std::string audio_output_device = "default";
int sample_rate = 48000;
// Modem settings
int center_freq = 1500;
std::string callsign = "N0CALL";
std::string modulation = "QPSK";
std::string code_rate = "1/2";
bool short_frame = false;
// PTT settings
PTTType ptt_type = PTTType::RIGCTL;
// Rigctl settings
std::string rigctl_host = "localhost";
int rigctl_port = 4532;
// VOX settings
int vox_tone_freq = 1200; // Hz - tone frequency for VOX trigger
int vox_lead_ms = 550; // ms - tone before OFDM data
int vox_tail_ms = 500; // ms - tone after OFDM data
// COM/Serial PTT settings
std::string com_port = "/dev/ttyUSB0";
int com_ptt_line = 1; // 0=DTR, 1=RTS, 2=BOTH
bool com_invert_dtr = false;
bool com_invert_rts = false;
// PTT timing
int ptt_delay_ms = 50; // Delay after PTT before TX
int ptt_tail_ms = 50; // Delay after TX before PTT release
// Operational settings
int tx_delay_ms = 500; // TXDelay
bool full_duplex = false;
int slot_time_ms = 500; // CSMA slot time
int p_persistence = 128; // 0-255 (128 defualt 50%)
// CSMA settings
bool csma_enabled = true;
float carrier_threshold_db = -30.0f; // Carrier sense threshold
int carrier_sense_ms = 100; // How long to listen
int max_backoff_slots = 10; // Maximum backoff
// Settings file path
std::string config_file = "";
};
class KISSParser {
public:
using FrameCallback = std::function<void(uint8_t port, uint8_t cmd, const std::vector<uint8_t>&)>;
KISSParser(FrameCallback callback) : callback_(callback) {}
void process(const uint8_t* data, size_t len) {
for (size_t i = 0; i < len; ++i) {
process_byte(data[i]);
}
}
static std::vector<uint8_t> wrap(const std::vector<uint8_t>& data, uint8_t port = 0) {
std::vector<uint8_t> frame;
frame.push_back(KISS::FEND);
frame.push_back((port << 4) | KISS::CMD_DATA);
for (uint8_t byte : data) {
if (byte == KISS::FEND) {
frame.push_back(KISS::FESC);
frame.push_back(KISS::TFEND);
} else if (byte == KISS::FESC) {
frame.push_back(KISS::FESC);
frame.push_back(KISS::TFESC);
} else {
frame.push_back(byte);
}
}
frame.push_back(KISS::FEND);
return frame;
}
private:
void process_byte(uint8_t byte) {
if (byte == KISS::FEND) {
if (in_frame_ && buffer_.size() > 0) {
// frame complete
uint8_t cmd_byte = buffer_[0];
uint8_t port = (cmd_byte >> 4) & 0x0F;
uint8_t cmd = cmd_byte & 0x0F;
std::vector<uint8_t> payload(buffer_.begin() + 1, buffer_.end());
callback_(port, cmd, payload);
}
in_frame_ = true;
buffer_.clear();
escape_ = false;
} else if (in_frame_) {
if (escape_) {
if (byte == KISS::TFEND) {
buffer_.push_back(KISS::FEND);
} else if (byte == KISS::TFESC) {
buffer_.push_back(KISS::FESC);
} else {
buffer_.push_back(byte);
}
escape_ = false;
} else if (byte == KISS::FESC) {
escape_ = true;
} else {
buffer_.push_back(byte);
}
}
}
FrameCallback callback_;
std::vector<uint8_t> buffer_;
bool in_frame_ = false;
bool escape_ = false;
};
template<typename T>
class PacketQueue {
public:
void push(T item) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(std::move(item));
}
bool pop(T& item) {
std::lock_guard<std::mutex> lock(mutex_);
if (queue_.empty()) return false;
item = std::move(queue_.front());
queue_.pop();
return true;
}
bool empty() const {
std::lock_guard<std::mutex> lock(mutex_);
return queue_.empty();
}
size_t size() const {
std::lock_guard<std::mutex> lock(mutex_);
return queue_.size();
}
void clear() {
std::lock_guard<std::mutex> lock(mutex_);
while (!queue_.empty()) queue_.pop();
}
private:
mutable std::mutex mutex_;
std::queue<T> queue_;
};
inline void hex_dump(const char* prefix, const uint8_t* data, size_t len) {
std::cerr << prefix << " (" << len << " bytes):" << std::endl;
for (size_t i = 0; i < len; i += 16) {
std::cerr << " " << std::hex << std::setfill('0') << std::setw(4) << i << ": ";
// Hex
for (size_t j = 0; j < 16 && i + j < len; ++j) {
std::cerr << std::setw(2) << (int)data[i + j] << " ";
}
for (size_t j = len - i; j < 16 && i + j >= len; ++j) {
std::cerr << " ";
}
// ASCII
std::cerr << " |";
for (size_t j = 0; j < 16 && i + j < len; ++j) {
char c = data[i + j];
std::cerr << (c >= 32 && c < 127 ? c : '.');
}
std::cerr << "|" << std::endl;
}
std::cerr << std::dec;
}
// Length-prefix framing
// This handles OFDM frame padding where the payload is encoded within the 2 byte prefix
inline std::vector<uint8_t> frame_with_length(const std::vector<uint8_t>& data) {
std::vector<uint8_t> framed;
uint16_t len = data.size();
framed.push_back((len >> 8) & 0xFF); // high byte
framed.push_back(len & 0xFF); // low byte
framed.insert(framed.end(), data.begin(), data.end());
return framed;
}
inline std::vector<uint8_t> unframe_length(const uint8_t* data, size_t total_len) {
if (total_len < 2) return {};
uint16_t payload_len = (data[0] << 8) | data[1];
if (payload_len > total_len - 2) {
std::cerr << "Warning: length prefix " << payload_len
<< " exceeds available data " << (total_len - 2) << std::endl;
payload_len = total_len - 2;
}
return std::vector<uint8_t>(data + 2, data + 2 + payload_len);
}

7
miniaudio.c Normal file
View file

@ -0,0 +1,7 @@
#define MA_NO_DECODING
#define MA_NO_ENCODING
#define MA_NO_GENERATION
#define MA_NO_ENGINE
#define MA_NO_NODE_GRAPH
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"

95649
miniaudio.h Normal file

File diff suppressed because it is too large Load diff

444
miniaudio_audio.hh Normal file
View file

@ -0,0 +1,444 @@
#pragma once
#define MA_NO_DECODING
#define MA_NO_ENCODING
#define MA_NO_GENERATION
#define MA_NO_ENGINE
#define MA_NO_NODE_GRAPH
#include "miniaudio.h"
#include <vector>
#include <string>
#include <iostream>
#include <cmath>
#include <atomic>
#include <thread>
#include <chrono>
class MiniAudio {
public:
static std::vector<std::pair<std::string, std::string>> list_capture_devices() {
std::vector<std::pair<std::string, std::string>> result;
ma_context context;
if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) {
result.push_back({"default", "default - System Default"});
return result;
}
ma_device_info* playback_devices;
ma_uint32 playback_count;
ma_device_info* capture_devices;
ma_uint32 capture_count;
if (ma_context_get_devices(&context, &playback_devices, &playback_count,
&capture_devices, &capture_count) != MA_SUCCESS) {
ma_context_uninit(&context);
result.push_back({"default", "default - System Default"});
return result;
}
result.push_back({"default", "default - System Default"});
for (ma_uint32 i = 0; i < capture_count; i++) {
std::string id = std::to_string(i);
std::string name = capture_devices[i].name;
std::string desc = id + " - " + name;
result.push_back({id, desc});
}
ma_context_uninit(&context);
return result;
}
static std::vector<std::pair<std::string, std::string>> list_playback_devices() {
std::vector<std::pair<std::string, std::string>> result;
ma_context context;
if (ma_context_init(NULL, 0, NULL, &context) != MA_SUCCESS) {
result.push_back({"default", "default - System Default"});
return result;
}
ma_device_info* playback_devices;
ma_uint32 playback_count;
ma_device_info* capture_devices;
ma_uint32 capture_count;
if (ma_context_get_devices(&context, &playback_devices, &playback_count,
&capture_devices, &capture_count) != MA_SUCCESS) {
ma_context_uninit(&context);
result.push_back({"default", "default - System Default"}); // ====!====
return result;
}
result.push_back({"default", "default - System Default"});
for (ma_uint32 i = 0; i < playback_count; i++) {
std::string id = std::to_string(i);
std::string name = playback_devices[i].name;
std::string desc = id + " - " + name;
result.push_back({id, desc});
}
ma_context_uninit(&context);
return result;
}
// temp
static std::vector<std::pair<std::string, std::string>> list_devices() {
return list_capture_devices();
}
MiniAudio(const std::string& capture_dev = "default",
const std::string& playback_dev = "default",
int sample_rate = 48000)
: capture_device_id_(capture_dev), playback_device_id_(playback_dev), sample_rate_(sample_rate) {
capture_buffer_.resize(RING_BUFFER_SIZE, 0.0f);
playback_buffer_.resize(RING_BUFFER_SIZE, 0.0f);
}
~MiniAudio() {
close_capture();
close_playback();
if (context_initialized_) {
ma_context_uninit(&context_);
context_initialized_ = false;
}
}
const std::string& capture_device() const { return capture_device_id_; }
const std::string& playback_device() const { return playback_device_id_; }
void set_capture_device(const std::string& device) {
close_capture();
capture_device_id_ = device;
}
void set_playback_device(const std::string& device) {
close_playback();
playback_device_id_ = device;
}
bool open_playback() {
if (!ensure_context()) return false;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_f32;
config.playback.channels = 1;
config.sampleRate = sample_rate_;
config.dataCallback = playback_callback;
config.pUserData = this;
config.periodSizeInFrames = 480;
config.periods = 4;
if (playback_device_id_ != "default" && !playback_device_id_.empty()) {
int dev_index = std::atoi(playback_device_id_.c_str());
ma_device_info* playback_devices;
ma_uint32 playback_count;
ma_device_info* capture_devices;
ma_uint32 capture_count;
if (ma_context_get_devices(&context_, &playback_devices, &playback_count,
&capture_devices, &capture_count) == MA_SUCCESS) {
if (dev_index >= 0 && dev_index < (int)playback_count) {
stored_playback_id_ = playback_devices[dev_index].id;
config.playback.pDeviceID = &stored_playback_id_;
}
}
}
if (ma_device_init(&context_, &config, &playback_device_) != MA_SUCCESS) {
std::cerr << "Failed to init playback device" << std::endl;
return false;
}
if (ma_device_start(&playback_device_) != MA_SUCCESS) {
std::cerr << "Failed to start playback device" << std::endl;
ma_device_uninit(&playback_device_);
return false;
}
playback_open_ = true;
std::cerr << "Playback: " << playback_device_.playback.name << std::endl;
return true;
}
bool open_capture() {
if (!ensure_context()) return false;
ma_device_config config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_f32;
config.capture.channels = 1;
config.sampleRate = sample_rate_;
config.dataCallback = capture_callback;
config.pUserData = this;
config.periodSizeInFrames = 480;
config.periods = 4;
if (capture_device_id_ != "default" && !capture_device_id_.empty()) {
int dev_index = std::atoi(capture_device_id_.c_str());
ma_device_info* playback_devices;
ma_uint32 playback_count;
ma_device_info* capture_devices;
ma_uint32 capture_count;
if (ma_context_get_devices(&context_, &playback_devices, &playback_count,
&capture_devices, &capture_count) == MA_SUCCESS) {
if (dev_index >= 0 && dev_index < (int)capture_count) {
stored_capture_id_ = capture_devices[dev_index].id;
config.capture.pDeviceID = &stored_capture_id_;
}
}
}
if (ma_device_init(&context_, &config, &capture_device_) != MA_SUCCESS) {
std::cerr << "Failed to initialize capture device" << std::endl;
return false;
}
if (ma_device_start(&capture_device_) != MA_SUCCESS) {
std::cerr << "Failed to start capture device" << std::endl;
ma_device_uninit(&capture_device_);
return false;
}
capture_open_ = true;
std::cerr << "Capture: " << capture_device_.capture.name << std::endl;
return true;
}
void close_playback() {
if (playback_open_) {
ma_device_uninit(&playback_device_);
playback_open_ = false;
}
playback_read_pos_ = 0;
playback_write_pos_ = 0;
}
void close_capture() {
if (capture_open_) {
ma_device_uninit(&capture_device_);
capture_open_ = false;
}
capture_read_pos_ = 0;
capture_write_pos_ = 0;
}
int read(float* buffer, int frames) {
if (!capture_open_) return -1;
int frames_read = 0;
int timeout_ms = 1000;
auto start = std::chrono::steady_clock::now();
while (frames_read < frames) {
size_t read_pos = capture_read_pos_.load();
size_t write_pos = capture_write_pos_.load();
size_t available = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
if (available > 0) {
int to_read = std::min((int)available, frames - frames_read);
for (int i = 0; i < to_read; i++) {
buffer[frames_read + i] = capture_buffer_[(read_pos + i) % RING_BUFFER_SIZE];
}
capture_read_pos_ = (read_pos + to_read) % RING_BUFFER_SIZE;
frames_read += to_read;
consecutive_read_failures_ = 0;
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > timeout_ms) {
consecutive_read_failures_++;
break;
}
}
}
return frames_read;
}
int write(const float* buffer, int frames) {
if (!playback_open_) return -1;
int frames_written = 0;
int timeout_ms = 1000;
auto start = std::chrono::steady_clock::now();
while (frames_written < frames) {
size_t read_pos = playback_read_pos_.load();
size_t write_pos = playback_write_pos_.load();
size_t used = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
size_t available = RING_BUFFER_SIZE - 1 - used;
if (available > 0) {
int to_write = std::min((int)available, frames - frames_written);
for (int i = 0; i < to_write; i++) {
playback_buffer_[(write_pos + i) % RING_BUFFER_SIZE] = buffer[frames_written + i];
}
playback_write_pos_ = (write_pos + to_write) % RING_BUFFER_SIZE;
frames_written += to_write;
consecutive_write_failures_ = 0;
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > timeout_ms) {
consecutive_write_failures_++;
break;
}
}
}
return frames_written;
}
// check audio status
bool is_healthy() const {
return capture_open_ && playback_open_ &&
consecutive_read_failures_ < 3 &&
consecutive_write_failures_ < 3;
}
// attempt to reconnect audio devices
bool reconnect() {
close_capture();
close_playback();
consecutive_read_failures_ = 0;
consecutive_write_failures_ = 0;
bool ok = true;
if (!open_playback()) ok = false;
if (!open_capture()) ok = false;
return ok;
}
void write_silence(int frames) {
std::vector<float> silence(frames, 0.0f);
write(silence.data(), frames);
}
void drain_playback() {
if (!playback_open_) return;
int timeout_ms = 2000;
auto start = std::chrono::steady_clock::now();
while (true) {
size_t read_pos = playback_read_pos_.load();
size_t write_pos = playback_write_pos_.load();
if (read_pos == write_pos) break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() > timeout_ms) {
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
float measure_level(int duration_ms = 100) {
if (!capture_open_) return -100.0f;
int frames = (sample_rate_ * duration_ms) / 1000;
std::vector<float> buffer(frames);
int total_read = read(buffer.data(), frames);
if (total_read <= 0) return -100.0f;
float sum_sq = 0.0f;
for (int i = 0; i < total_read; i++) {
sum_sq += buffer[i] * buffer[i];
}
float rms = std::sqrt(sum_sq / total_read);
if (rms < 1e-10f) return -100.0f;
return 20.0f * std::log10(rms);
}
int sample_rate() const { return sample_rate_; }
private:
static constexpr size_t RING_BUFFER_SIZE = 48000;
bool ensure_context() {
if (context_initialized_) return true;
if (ma_context_init(NULL, 0, NULL, &context_) != MA_SUCCESS) {
std::cerr << "Failed to initialize audio context" << std::endl;
return false;
}
context_initialized_ = true;
return true;
}
static void playback_callback(ma_device* device, void* output, const void* input, ma_uint32 frame_count) {
(void)input;
MiniAudio* self = static_cast<MiniAudio*>(device->pUserData);
float* out = static_cast<float*>(output);
size_t read_pos = self->playback_read_pos_.load();
size_t write_pos = self->playback_write_pos_.load();
size_t available = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
ma_uint32 to_read = std::min((ma_uint32)available, frame_count);
for (ma_uint32 i = 0; i < to_read; i++) {
out[i] = self->playback_buffer_[(read_pos + i) % RING_BUFFER_SIZE];
}
for (ma_uint32 i = to_read; i < frame_count; i++) {
out[i] = 0.0f;
}
self->playback_read_pos_ = (read_pos + to_read) % RING_BUFFER_SIZE;
}
static void capture_callback(ma_device* device, void* output, const void* input, ma_uint32 frame_count) {
(void)output;
MiniAudio* self = static_cast<MiniAudio*>(device->pUserData);
const float* in = static_cast<const float*>(input);
size_t read_pos = self->capture_read_pos_.load();
size_t write_pos = self->capture_write_pos_.load();
size_t used = (write_pos - read_pos + RING_BUFFER_SIZE) % RING_BUFFER_SIZE;
size_t available = RING_BUFFER_SIZE - 1 - used;
ma_uint32 to_write = std::min((ma_uint32)available, frame_count);
for (ma_uint32 i = 0; i < to_write; i++) {
self->capture_buffer_[(write_pos + i) % RING_BUFFER_SIZE] = in[i];
}
self->capture_write_pos_ = (write_pos + to_write) % RING_BUFFER_SIZE;
}
std::string capture_device_id_;
std::string playback_device_id_;
int sample_rate_;
ma_context context_;
bool context_initialized_ = false;
ma_device playback_device_;
ma_device capture_device_;
ma_device_id stored_capture_id_;
ma_device_id stored_playback_id_;
bool playback_open_ = false;
bool capture_open_ = false;
std::vector<float> capture_buffer_;
std::vector<float> playback_buffer_;
std::atomic<size_t> capture_read_pos_{0};
std::atomic<size_t> capture_write_pos_{0};
std::atomic<size_t> playback_read_pos_{0};
std::atomic<size_t> playback_write_pos_{0};
int consecutive_read_failures_ = 0;
int consecutive_write_failures_ = 0;
};

979
modem.hh Normal file
View file

@ -0,0 +1,979 @@
#pragma once
#include <vector>
#include <cstring>
#include <cmath>
#include <functional>
#include <atomic>
#include "common.hh"
#include "schmidl_cox.hh"
#include "bip_buffer.hh"
#include "theil_sen.hh"
#include "blockdc.hh"
#include "hilbert.hh"
#include "phasor.hh"
#include "delay.hh"
#include "polar_encoder.hh"
#include "polar_list_decoder.hh"
#include "hadamard_decoder.hh"
template<typename T>
class BufferWritePCM {
public:
BufferWritePCM(int rate, int bits, int channels)
: rate_(rate), bits_(bits), channels_(channels) {}
void write(const T* buffer, int count, int ch = 1) {
for (int i = 0; i < count; ++i) {
// 2 channels, only take real part for mono output
if (ch == 2) {
samples_.push_back(buffer[i * 2]); // real
} else {
samples_.push_back(buffer[i]);
}
}
}
void silence(int count) {
for (int i = 0; i < count; ++i) {
samples_.push_back(T(0));
}
}
const std::vector<T>& samples() const { return samples_; }
std::vector<T>& samples() { return samples_; }
void clear() { samples_.clear(); }
int rate() const { return rate_; }
int bits() const { return bits_; }
int channels() const { return channels_; }
private:
std::vector<T> samples_;
int rate_, bits_, channels_;
};
// Modem configuration
struct ModemConfig {
int sample_rate = 48000;
int center_freq = 1500;
int64_t call_sign = 0;
int oper_mode = 0;
static int64_t encode_callsign(const char* str) {
int64_t acc = 0;
for (char c = *str++; c; c = *str++) {
acc *= 40;
if (c == '/')
acc += 3;
else if (c >= '0' && c <= '9')
acc += c - '0' + 4;
else if (c >= 'a' && c <= 'z')
acc += c - 'a' + 14;
else if (c >= 'A' && c <= 'Z')
acc += c - 'A' + 14;
else if (c != ' ')
return -1;
}
return acc;
}
static int encode_mode(const char* modulation, const char* code_rate, bool short_frame) {
int mode = 0;
if (!strcmp(modulation, "BPSK"))
mode |= 0 << 4;
else if (!strcmp(modulation, "QPSK"))
mode |= 1 << 4;
else if (!strcmp(modulation, "8PSK"))
mode |= 2 << 4;
else if (!strcmp(modulation, "QAM16"))
mode |= 3 << 4;
else if (!strcmp(modulation, "QAM64"))
mode |= 4 << 4;
else if (!strcmp(modulation, "QAM256"))
mode |= 5 << 4;
else if (!strcmp(modulation, "QAM1024"))
mode |= 6 << 4;
else if (!strcmp(modulation, "QAM4096"))
mode |= 7 << 4;
else
return -1;
if (!strcmp(code_rate, "1/2"))
mode |= 0 << 1;
else if (!strcmp(code_rate, "2/3"))
mode |= 1 << 1;
else if (!strcmp(code_rate, "3/4"))
mode |= 2 << 1;
else if (!strcmp(code_rate, "5/6"))
mode |= 3 << 1;
else
return -1;
if (!short_frame)
mode |= 1;
return mode;
}
};
// Encoder
template<typename value, typename cmplx, int rate>
class ModemEncoder : public Common {
public:
typedef int8_t code_type;
static const int guard_len = rate / 300;
static const int symbol_len = guard_len * 40;
ModemEncoder() {}
// encode our data to audio samples
std::vector<value> encode(const uint8_t* input_data, size_t input_len,
int freq_off, int64_t call_sign, int oper_mode) {
BufferWritePCM<value> pcm(rate, 32, 1);
if (!setup(oper_mode)) {
std::cerr << "Encoder: invalid mode" << std::endl;
return {};
}
int offset = (freq_off * symbol_len) / rate;
tone_off = offset - tone_count / 2;
guard_interval_weights();
meta_data((call_sign << 8) | oper_mode);
// leading noise
CODE::MLS noise(mls2_poly);
for (int j = 0; j < 1; ++j) {
for (int i = 0; i < tone_count; ++i)
tone[i] = nrz(noise());
symbol(&pcm, -3);
}
// Copy input data (pad if necessary)
std::memset(data, 0, data_max);
std::memcpy(data, input_data, std::min(input_len, (size_t)data_bytes));
// Scramble
CODE::Xorshift32 scrambler;
for (int i = 0; i < data_bytes; ++i)
data[i] ^= scrambler();
// Schmidl-Cox preamble
CODE::MLS seq0(mls0_poly, mls0_seed);
for (int i = 0; i < tone_count; ++i)
tone[i] = nrz(seq0());
symbol(&pcm, -2);
symbol(&pcm, -1);
// Encode payload
for (int i = 0; i < data_bits; ++i)
mesg[i] = nrz(CODE::get_le_bit(data, i));
crc1.reset();
for (int i = 0; i < data_bytes; ++i)
crc1(data[i]);
for (int i = 0; i < 32; ++i)
mesg[i + data_bits] = nrz((crc1() >> i) & 1);
polar_encoder(code, mesg, frozen_bits, code_order);
shuffle(perm, code, code_order);
// Generate symbols
CODE::MLS seq1(mls1_poly);
for (int j = 0, k = 0, m = 0; j < symbol_count + 1; ++j) {
seed_off = (block_skew * j + first_seed) % block_length;
for (int i = 0; i < tone_count; ++i) {
if (i % block_length == seed_off) {
tone[i] = nrz(seq1());
} else if (j) {
int bits = mod_bits;
if (mod_bits == 3 && k % 32 == 30) bits = 2;
if (mod_bits == 6 && k % 64 == 60) bits = 4;
if (mod_bits == 10 && k % 128 == 120) bits = 8;
if (mod_bits == 12 && k % 128 == 120) bits = 8;
tone[i] = map_bits(perm + k, bits);
k += bits;
} else {
tone[i] = map_bits(meta + m++, 1);
}
}
symbol(&pcm, j);
}
for (int i = 0; i < guard_len; ++i)
guard[i] *= 1 - weight[i];
pcm.write(reinterpret_cast<value*>(guard), guard_len, 2);
return std::move(pcm.samples());
}
int get_payload_size(int oper_mode) {
if (!setup(oper_mode)) return 0;
return data_bytes;
}
private:
DSP::FastFourierTransform<symbol_len, cmplx, -1> fwd;
DSP::FastFourierTransform<symbol_len, cmplx, 1> bwd;
CODE::PolarEncoder<code_type> polar_encoder;
code_type code[bits_max], perm[bits_max], mesg[bits_max], meta[data_tones];
cmplx fdom[symbol_len];
cmplx tdom[symbol_len];
cmplx test[symbol_len];
cmplx kern[symbol_len];
cmplx guard[guard_len];
cmplx tone[tone_count];
cmplx temp[tone_count];
value weight[guard_len];
value papr[symbols_max];
static int bin(int carrier) {
return (carrier + symbol_len) % symbol_len;
}
static int nrz(bool bit) {
return 1 - 2 * bit;
}
cmplx map_bits(code_type* b, int bits) {
switch (bits) {
case 1: return PhaseShiftKeying<2, cmplx, code_type>::map(b);
case 2: return PhaseShiftKeying<4, cmplx, code_type>::map(b);
case 3: return PhaseShiftKeying<8, cmplx, code_type>::map(b);
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::map(b);
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::map(b);
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::map(b);
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::map(b);
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::map(b);
}
return 0;
}
void shuffle(code_type* dest, const code_type* src, int order) {
if (order == 8) {
CODE::XorShiftMask<int, 8, 1, 1, 2, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 256; ++i) dest[i] = src[seq()];
} else if (order == 11) {
CODE::XorShiftMask<int, 11, 1, 3, 4, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 2048; ++i) dest[i] = src[seq()];
} else if (order == 12) {
CODE::XorShiftMask<int, 12, 1, 1, 4, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 4096; ++i) dest[i] = src[seq()];
} else if (order == 13) {
CODE::XorShiftMask<int, 13, 1, 1, 9, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 8192; ++i) dest[i] = src[seq()];
} else if (order == 14) {
CODE::XorShiftMask<int, 14, 1, 5, 10, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 16384; ++i) dest[i] = src[seq()];
} else if (order == 15) {
CODE::XorShiftMask<int, 15, 1, 1, 3, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 32768; ++i) dest[i] = src[seq()];
} else if (order == 16) {
CODE::XorShiftMask<int, 16, 1, 1, 14, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 65536; ++i) dest[i] = src[seq()];
}
}
void guard_interval_weights() {
for (int i = 0; i < guard_len / 4; ++i)
weight[i] = 0;
for (int i = guard_len / 4; i < guard_len / 4 + guard_len / 2; ++i) {
value x = value(i - guard_len / 4) / value(guard_len / 2 - 1);
weight[i] = value(0.5) * (value(1) - std::cos(DSP::Const<value>::Pi() * x));
}
for (int i = guard_len / 4 + guard_len / 2; i < guard_len; ++i)
weight[i] = 1;
}
void clipping_and_filtering(value scale) {
for (int i = 0; i < symbol_len; ++i) {
value pwr = norm(tdom[i]);
if (pwr > value(1))
tdom[i] /= sqrt(pwr);
}
fwd(fdom, tdom);
for (int i = 0; i < symbol_len; ++i) {
int j = bin(i + tone_off);
if (i >= tone_count)
fdom[j] = 0;
else
fdom[j] *= 1 / (scale * symbol_len);
}
bwd(tdom, fdom);
for (int i = 0; i < symbol_len; ++i)
tdom[i] *= scale;
auto clamp = [](value v) { return v < value(-1) ? value(-1) : v > value(1) ? value(1) : v; };
for (int i = 0; i < symbol_len; ++i)
tdom[i] = cmplx(clamp(tdom[i].real()), clamp(tdom[i].imag()));
}
void symbol(BufferWritePCM<value>* pcm, int symbol_number) {
value scale = value(0.5) / std::sqrt(value(tone_count));
if (symbol_number < 0) {
for (int i = 0; i < symbol_len; ++i)
fdom[i] = 0;
for (int i = 0; i < tone_count; ++i)
fdom[bin(i + tone_off)] = tone[i];
bwd(tdom, fdom);
for (int i = 0; i < symbol_len; ++i)
tdom[i] *= scale;
} else {
value best_papr = 1000;
for (int seed_value = 0; seed_value < 128; ++seed_value) {
for (int i = 0; i < tone_count; ++i)
temp[i] = tone[i];
hadamard_encoder(seed, seed_value);
for (int i = 0; i < seed_tones; ++i)
temp[i * block_length + seed_off] *= seed[i];
if (seed_value) {
CODE::MLS seq(mls2_poly, seed_value);
for (int i = 0; i < tone_count; ++i)
if (i % block_length != seed_off)
temp[i] *= nrz(seq());
}
for (int i = 0; i < symbol_len; ++i)
fdom[i] = 0;
for (int i = 0; i < tone_count; ++i)
fdom[bin(i + tone_off)] = temp[i];
bwd(test, fdom);
for (int i = 0; i < symbol_len; ++i)
test[i] *= scale;
value peak = 0, mean = 0;
for (int i = 0; i < symbol_len; ++i) {
value power(norm(test[i]));
peak = std::max(peak, power);
mean += power;
}
mean /= symbol_len;
value test_papr(peak / mean);
if (test_papr < best_papr) {
best_papr = test_papr;
papr[symbol_number] = test_papr;
for (int i = 0; i < symbol_len; ++i)
tdom[i] = test[i];
if (test_papr < 5)
break;
}
}
}
clipping_and_filtering(scale);
if (symbol_number != -1) {
for (int i = 0; i < guard_len; ++i)
guard[i] = DSP::lerp(guard[i], tdom[i + symbol_len - guard_len], weight[i]);
pcm->write(reinterpret_cast<value*>(guard), guard_len, 2);
}
for (int i = 0; i < guard_len; ++i)
guard[i] = tdom[i];
pcm->write(reinterpret_cast<value*>(tdom), symbol_len, 2);
}
void meta_data(uint64_t md) {
for (int i = 0; i < 56; ++i)
mesg[i] = nrz((md >> i) & 1);
crc0.reset();
crc0(md << 8);
for (int i = 0; i < 16; ++i)
mesg[i + 56] = nrz((crc0() >> i) & 1);
polar_encoder(code, mesg, frozen_256_72, 8);
shuffle(meta, code, 8);
}
};
// Decoder
template<typename value, typename cmplx, int rate>
class ModemDecoder : public Common {
public:
typedef int16_t code_type;
typedef SIMD<code_type, 32> mesg_type;
typedef DSP::Const<value> Const;
static const int guard_len = rate / 300;
static const int symbol_len = guard_len * 40;
static const int filter_len = 129;
static const int extended_len = symbol_len + guard_len;
static const int buffer_len = 5 * extended_len;
static const int search_pos = extended_len;
static const int tone_off_const = -tone_count / 2;
using FrameCallback = std::function<void(const uint8_t*, size_t)>;
ModemDecoder() {
// init fdom_mls before correlator uses it
init_mls0_seq();
correlator_ptr = new SchmidlCox<value, cmplx, search_pos, symbol_len, guard_len>(fdom_mls);
blockdc.samples(filter_len);
}
~ModemDecoder() {
delete correlator_ptr;
delete seq1_ptr;
}
void process(const value* samples, size_t count, FrameCallback callback) {
for (size_t i = 0; i < count; ++i) {
process_sample(samples[i], callback);
}
}
// Reset decoder state
void reset() {
state_ = State::SEARCHING;
sample_count_ = 0;
symbol_index_ = 0;
samples_needed_ = 0;
k_ = 0;
}
// Get average SNR from last successful decode
value get_last_snr() const { return last_avg_snr_; }
private:
enum class State {
SEARCHING, // looking for preamble
COLLECTING_SYMBOLS, // Collecting data symbols
};
// Arrays used by correlator
cmplx fdom_mls[symbol_len];
cmplx fdom[symbol_len], tdom[symbol_len];
DSP::FastFourierTransform<symbol_len, cmplx, -1> fwd;
DSP::BlockDC<value, value> blockdc;
DSP::Hilbert<cmplx, filter_len> hilbert;
DSP::BipBuffer<cmplx, buffer_len> input_hist;
DSP::TheilSenEstimator<value, tone_count> tse;
SchmidlCox<value, cmplx, search_pos, symbol_len, guard_len>* correlator_ptr = nullptr;
CODE::HadamardDecoder<7> hadamard_decoder;
CODE::PolarListDecoder<mesg_type, code_max> polar_decoder;
DSP::Phasor<cmplx> osc;
mesg_type mesg[bits_max];
code_type code[bits_max], perm[bits_max];
cmplx demod[tone_count], chan[tone_count], tone[tone_count];
value index[tone_count], phase[tone_count];
value snr[symbols_max];
value cfo_rad;
int symbol_pos;
value last_avg_snr_ = 0;
State state_ = State::SEARCHING;
size_t sample_count_ = 0;
int symbol_index_ = 0;
int samples_needed_ = 0;
int k_ = 0;
const cmplx* buf_ = nullptr;
CODE::MLS* seq1_ptr = nullptr;
static int bin(int carrier) {
return (carrier + symbol_len) % symbol_len;
}
static value nrz(bool bit) {
return 1 - 2 * bit;
}
static cmplx demod_or_erase(cmplx curr, cmplx prev) {
if (norm(prev) > 0) {
cmplx d = curr / prev;
if (norm(d) < 4)
return d;
}
return 0;
}
void init_mls0_seq() {
CODE::MLS seq0(mls0_poly, mls0_seed);
value cur = 0, prv = 0;
for (int i = 0; i < tone_count; ++i, prv = cur)
fdom_mls[bin(i + tone_off_const)] = prv * (cur = nrz(seq0()));
}
cmplx map_bits(code_type* b, int bits) {
switch (bits) {
case 1: return PhaseShiftKeying<2, cmplx, code_type>::map(b);
case 2: return PhaseShiftKeying<4, cmplx, code_type>::map(b);
case 3: return PhaseShiftKeying<8, cmplx, code_type>::map(b);
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::map(b);
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::map(b);
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::map(b);
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::map(b);
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::map(b);
}
return 0;
}
void demap_soft(code_type* b, cmplx c, value precision, int bits) {
switch (bits) {
case 1: return PhaseShiftKeying<2, cmplx, code_type>::soft(b, c, precision);
case 2: return PhaseShiftKeying<4, cmplx, code_type>::soft(b, c, precision);
case 3: return PhaseShiftKeying<8, cmplx, code_type>::soft(b, c, precision);
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::soft(b, c, precision);
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::soft(b, c, precision);
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::soft(b, c, precision);
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::soft(b, c, precision);
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::soft(b, c, precision);
}
}
void demap_hard(code_type* b, cmplx c, int bits) {
switch (bits) {
case 1: return PhaseShiftKeying<2, cmplx, code_type>::hard(b, c);
case 2: return PhaseShiftKeying<4, cmplx, code_type>::hard(b, c);
case 3: return PhaseShiftKeying<8, cmplx, code_type>::hard(b, c);
case 4: return QuadratureAmplitudeModulation<16, cmplx, code_type>::hard(b, c);
case 6: return QuadratureAmplitudeModulation<64, cmplx, code_type>::hard(b, c);
case 8: return QuadratureAmplitudeModulation<256, cmplx, code_type>::hard(b, c);
case 10: return QuadratureAmplitudeModulation<1024, cmplx, code_type>::hard(b, c);
case 12: return QuadratureAmplitudeModulation<4096, cmplx, code_type>::hard(b, c);
}
}
void shuffle(code_type* dest, const code_type* src, int order) {
if (order == 8) {
CODE::XorShiftMask<int, 8, 1, 1, 2, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 256; ++i) dest[seq()] = src[i];
} else if (order == 11) {
CODE::XorShiftMask<int, 11, 1, 3, 4, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 2048; ++i) dest[seq()] = src[i];
} else if (order == 12) {
CODE::XorShiftMask<int, 12, 1, 1, 4, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 4096; ++i) dest[seq()] = src[i];
} else if (order == 13) {
CODE::XorShiftMask<int, 13, 1, 1, 9, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 8192; ++i) dest[seq()] = src[i];
} else if (order == 14) {
CODE::XorShiftMask<int, 14, 1, 5, 10, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 16384; ++i) dest[seq()] = src[i];
} else if (order == 15) {
CODE::XorShiftMask<int, 15, 1, 1, 3, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 32768; ++i) dest[seq()] = src[i];
} else if (order == 16) {
CODE::XorShiftMask<int, 16, 1, 1, 14, 1> seq;
dest[0] = src[0];
for (int i = 1; i < 65536; ++i) dest[seq()] = src[i];
}
}
static void base40_decoder(char* str, int64_t val, int len) {
for (int i = len - 1; i >= 0; --i, val /= 40)
str[i] = " /0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[val % 40];
}
int64_t meta_data() {
shuffle(code, perm, 8);
polar_decoder(nullptr, mesg, code, frozen_256_72, 8);
int best = -1;
for (int k = 0; k < mesg_type::SIZE; ++k) {
crc0.reset();
for (int i = 0; i < 72; ++i)
crc0(mesg[i].v[k] < 0);
if (crc0() == 0) {
best = k;
break;
}
}
if (best < 0)
return -1;
uint64_t md = 0;
for (int i = 0; i < 56; ++i)
md |= uint64_t(mesg[i].v[best] < 0) << i;
return md;
}
void process_sample(value sample, FrameCallback callback) {
// Convert to complex via Hilbert transform
cmplx tmp = hilbert(blockdc(sample));
buf_ = input_hist(tmp);
++sample_count_;
switch (state_) {
case State::SEARCHING:
if ((*correlator_ptr)(buf_)) {
// Sync found
symbol_pos = correlator_ptr->symbol_pos;
cfo_rad = correlator_ptr->cfo_rad;
std::cerr << "Decoder: Sync found at sample " << sample_count_ << std::endl;
std::cerr << "Decoder: CFO = " << cfo_rad * (rate / Const::TwoPi()) << " Hz" << std::endl;
// Initialize seq1 for the whole frame
delete seq1_ptr;
seq1_ptr = new CODE::MLS(mls1_poly);
// Process preamble and start collecting symbols
if (process_preamble()) {
state_ = State::COLLECTING_SYMBOLS;
symbol_index_ = 1; // Symbol 0 (meta) already processed
// Need to advance past preamble: symbol_pos + symbol_len + extended_len
// Plus extended_len for the first data symbol
samples_needed_ = symbol_pos + symbol_len + 2 * extended_len;
}
}
break;
case State::COLLECTING_SYMBOLS:
// Keep feeding correlator to maintain buffer
(*correlator_ptr)(buf_);
samples_needed_--;
if (samples_needed_ <= 0) {
// Process this symbol
if (!process_symbol(symbol_index_)) {
// Error, go back to searching
state_ = State::SEARCHING;
break;
}
symbol_index_++;
if (symbol_index_ > symbol_count) {
// All symbols collected
decode_frame(callback);
state_ = State::SEARCHING;
} else {
samples_needed_ = extended_len;
}
}
break;
}
}
bool process_preamble() {
// Process Schmidl-Cox preamble symbols
osc.omega(-cfo_rad);
// First preamble symbol
for (int i = 0; i < symbol_len; ++i)
tdom[i] = buf_[i + symbol_pos] * osc();
fwd(fdom, tdom);
for (int i = 0; i < tone_count; ++i)
tone[i] = fdom[bin(i + tone_off_const)];
// Second preamble symbol
for (int i = 0; i < symbol_len; ++i)
tdom[i] = buf_[i + symbol_pos + symbol_len] * osc();
for (int i = 0; i < guard_len; ++i)
osc();
fwd(fdom, tdom);
for (int i = 0; i < tone_count; ++i)
chan[i] = fdom[bin(i + tone_off_const)];
// Estimate SFO
for (int i = 0; i < tone_count; ++i) {
index[i] = tone_off_const + i;
phase[i] = arg(demod_or_erase(chan[i], tone[i]));
}
tse.compute(index, phase, tone_count);
std::cerr << "Decoder: SFO = " << -1000000 * tse.slope() / Const::TwoPi() << " ppm" << std::endl;
// Correct channel estimate
for (int i = 0; i < tone_count; ++i)
tone[i] *= DSP::polar<value>(1, tse(i + tone_off_const));
for (int i = 0; i < tone_count; ++i)
chan[i] = DSP::lerp(chan[i], tone[i], value(0.5));
// Remove preamble sequence
CODE::MLS seq0(mls0_poly, mls0_seed);
for (int i = 0; i < tone_count; ++i)
chan[i] *= nrz(seq0());
// Process meta symbol (symbol 0)
for (int i = 0; i < symbol_len; ++i)
tdom[i] = buf_[i + symbol_pos + symbol_len + extended_len] * osc();
for (int i = 0; i < guard_len; ++i)
osc();
fwd(fdom, tdom);
// Decode meta symbol
seed_off = first_seed;
auto clamp = [](int v) { return v < -127 ? -127 : v > 127 ? 127 : v; };
for (int i = 0; i < tone_count; ++i)
tone[i] = fdom[bin(i + tone_off_const)];
for (int i = seed_off; i < tone_count; i += block_length)
tone[i] *= nrz((*seq1_ptr)());
for (int i = 0; i < tone_count; ++i)
demod[i] = demod_or_erase(tone[i], chan[i]);
// Decode seed for meta symbol
for (int i = 0; i < seed_tones; ++i)
seed[i] = clamp(std::nearbyint(127 * demod[i * block_length + seed_off].real()));
int seed_value = hadamard_decoder(seed);
if (seed_value < 0) {
std::cerr << "Decoder: Seed value damaged in meta" << std::endl;
return false;
}
hadamard_encoder(seed, seed_value);
for (int i = 0; i < seed_tones; ++i) {
tone[block_length * i + seed_off] *= seed[i];
demod[block_length * i + seed_off] *= seed[i];
}
// Phase correction
for (int i = 0; i < seed_tones; ++i) {
index[i] = tone_off_const + block_length * i + seed_off;
phase[i] = arg(demod[block_length * i + seed_off]);
}
tse.compute(index, phase, seed_tones);
for (int i = 0; i < tone_count; ++i)
demod[i] *= DSP::polar<value>(1, -tse(i + tone_off_const));
for (int i = 0; i < tone_count; ++i)
chan[i] *= DSP::polar<value>(1, tse(i + tone_off_const));
if (seed_value) {
CODE::MLS seq(mls2_poly, seed_value);
for (int i = 0; i < tone_count; ++i)
if (i % block_length != seed_off)
demod[i] *= nrz(seq());
}
// SNR estimation and demapping for meta symbol (mod_bits = 1 for meta)
value sp = 0, np = 0;
for (int i = 0, l = 0; i < tone_count; ++i) {
cmplx hard(1, 0);
if (i % block_length != seed_off) {
demap_hard(perm + l, demod[i], 1);
hard = map_bits(perm + l, 1);
l += 1;
}
cmplx error = demod[i] - hard;
sp += norm(hard);
np += norm(error);
}
value precision = sp / np;
precision = std::min(precision, value(1023));
// std::cerr << "Decoder: Meta symbol SNR = " << 10 * std::log10(precision) << " dB" << std::endl;
// Soft demap meta symbol
int k = 0;
for (int i = 0; i < tone_count; ++i) {
if (i % block_length != seed_off) {
demap_soft(perm + k, demod[i], precision, 1);
k += 1;
}
}
// Update channel for meta symbol pilots
for (int i = seed_off; i < tone_count; i += block_length)
chan[i] = DSP::lerp(chan[i], tone[i], value(0.5));
// Decode meta data
int64_t meta_info = meta_data();
if (meta_info < 0) {
std::cerr << "Decoder: Preamble decoding error" << std::endl;
return false;
}
int64_t call = meta_info >> 8;
if (call == 0 || call >= 262144000000000L) {
std::cerr << "Decoder: Invalid call sign" << std::endl;
return false;
}
char call_sign[10];
base40_decoder(call_sign, call, 9);
call_sign[9] = 0;
std::cerr << "Decoder: Call sign: " << call_sign << std::endl;
int mode = meta_info & 255;
if (!setup(mode)) {
std::cerr << "Decoder: Invalid mode" << std::endl;
return false;
}
std::cerr << "Decoder: Mode " << oper_mode << ", " << symbol_count << " data symbols, mod_bits=" << mod_bits << ", code_order=" << code_order << ", data_bytes=" << data_bytes << std::endl;
// Reset for data collection
k_ = 0;
snr[0] = 100;
return true;
}
bool process_symbol(int j) {
seed_off = (block_skew * j + first_seed) % block_length;
auto clamp = [](int v) { return v < -127 ? -127 : v > 127 ? 127 : v; };
// FFT the current symbol
for (int i = 0; i < symbol_len; ++i)
tdom[i] = buf_[i] * osc();
for (int i = 0; i < guard_len; ++i)
osc();
fwd(fdom, tdom);
for (int i = 0; i < tone_count; ++i)
tone[i] = fdom[bin(i + tone_off_const)];
// Remove pilot sequence
for (int i = seed_off; i < tone_count; i += block_length)
tone[i] *= nrz((*seq1_ptr)());
for (int i = 0; i < tone_count; ++i)
demod[i] = demod_or_erase(tone[i], chan[i]);
// Decode seed
for (int i = 0; i < seed_tones; ++i)
seed[i] = clamp(std::nearbyint(127 * demod[i * block_length + seed_off].real()));
int seed_value = hadamard_decoder(seed);
if (seed_value < 0) {
std::cerr << "Decoder: Seed damaged at symbol " << j << std::endl;
return false;
}
hadamard_encoder(seed, seed_value);
for (int i = 0; i < seed_tones; ++i) {
tone[block_length * i + seed_off] *= seed[i];
demod[block_length * i + seed_off] *= seed[i];
}
// Phase correction
for (int i = 0; i < seed_tones; ++i) {
index[i] = tone_off_const + block_length * i + seed_off;
phase[i] = arg(demod[block_length * i + seed_off]);
}
tse.compute(index, phase, seed_tones);
for (int i = 0; i < tone_count; ++i)
demod[i] *= DSP::polar<value>(1, -tse(i + tone_off_const));
for (int i = 0; i < tone_count; ++i)
chan[i] *= DSP::polar<value>(1, tse(i + tone_off_const));
if (seed_value) {
CODE::MLS seq(mls2_poly, seed_value);
for (int i = 0; i < tone_count; ++i)
if (i % block_length != seed_off)
demod[i] *= nrz(seq());
}
// SNR estimation and soft demapping
value sp = 0, np = 0;
for (int i = 0, l = k_; i < tone_count; ++i) {
cmplx hard(1, 0);
if (i % block_length != seed_off) {
int bits = mod_bits;
if (mod_bits == 3 && l % 32 == 30) bits = 2;
if (mod_bits == 6 && l % 64 == 60) bits = 4;
if (mod_bits == 10 && l % 128 == 120) bits = 8;
if (mod_bits == 12 && l % 128 == 120) bits = 8;
demap_hard(perm + l, demod[i], bits);
hard = map_bits(perm + l, bits);
l += bits;
}
cmplx error = demod[i] - hard;
sp += norm(hard);
np += norm(error);
}
value precision = sp / np;
snr[j] = precision;
precision = std::min(precision, value(1023));
std::cerr << "Decoder: Symbol " << j << " SNR = " << 10 * std::log10(snr[j]) << " dB, k=" << k_ << std::endl;
for (int i = 0; i < tone_count; ++i) {
if (i % block_length != seed_off) {
int bits = mod_bits;
if (mod_bits == 3 && k_ % 32 == 30) bits = 2;
if (mod_bits == 6 && k_ % 64 == 60) bits = 4;
if (mod_bits == 10 && k_ % 128 == 120) bits = 8;
if (mod_bits == 12 && k_ % 128 == 120) bits = 8;
demap_soft(perm + k_, demod[i], precision, bits);
k_ += bits;
}
}
for (int i = seed_off; i < tone_count; i += block_length)
chan[i] = DSP::lerp(chan[i], tone[i], value(0.5));
return true;
}
void decode_frame(FrameCallback callback) {
std::cerr << "Decoder: Decoding frame, k_=" << k_ << " bits collected" << std::endl;
std::cerr << "Decoder: Expected code_order=" << code_order << " (code length=" << (1 << code_order) << ")" << std::endl;
int crc_bits = data_bits + 32;
shuffle(code, perm, code_order);
polar_decoder(nullptr, mesg, code, frozen_bits, code_order);
int best = -1;
for (int k = 0; k < mesg_type::SIZE; ++k) {
crc1.reset();
for (int i = 0; i < crc_bits; ++i)
crc1(mesg[i].v[k] < 0);
if (crc1() == 0) {
best = k;
break;
}
}
if (best < 0) {
std::cerr << "Decoder: CRC failed" << std::endl;
return;
}
// calculate average SNR from data symbols
value total_snr = 0;
int snr_count = 0;
for (int i = 1; i < symbol_index_; ++i) { // skip symbol 0
if (snr[i] > 0) {
total_snr += snr[i];
snr_count++;
}
}
if (snr_count > 0) {
last_avg_snr_ = 10 * std::log10(total_snr / snr_count);
}
// Extract data
for (int i = 0; i < data_bits; ++i)
CODE::set_le_bit(data, i, mesg[i].v[best] < 0);
// Descramble
CODE::Xorshift32 scrambler;
for (int i = 0; i < data_bytes; ++i)
data[i] ^= scrambler();
std::cerr << "Decoder: Frame decoded " << data_bytes << " bytes, SNR=" << last_avg_snr_ << " dB" << std::endl;
callback(data, data_bytes);
}
};
using Encoder48k = ModemEncoder<float, DSP::Complex<float>, 48000>;
using Decoder48k = ModemDecoder<float, DSP::Complex<float>, 48000>;

137
rigctl_ptt.hh Normal file
View file

@ -0,0 +1,137 @@
#pragma once
#include <string>
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
class RigctlPTT {
public:
RigctlPTT(const std::string& host = "localhost", int port = 4532)
: host_(host), port_(port) {}
~RigctlPTT() {
disconnect();
}
bool connect() {
if (connected_) return true;
sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_ < 0) {
std::cerr << "rigctl: Failed to create socket" << std::endl;
return false;
}
struct hostent* server = gethostbyname(host_.c_str());
if (!server) {
std::cerr << "rigctl PTT: Can't connect to host " << host_ << std::endl;
close(sock_);
sock_ = -1;
return false;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
memcpy(&addr.sin_addr.s_addr, server->h_addr, server->h_length);
addr.sin_port = htons(port_);
if (::connect(sock_, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
std::cerr << "rigctl: Can't connect to " << host_ << ":" << port_ << std::endl;
close(sock_);
sock_ = -1;
return false;
}
connected_ = true;
std::cerr << "rigctl: Connected to " << host_ << ":" << port_ << std::endl;
return true;
}
void disconnect() {
if (sock_ >= 0) {
if (ptt_on_) {
set_ptt(false);
}
close(sock_);
sock_ = -1;
}
connected_ = false;
}
bool set_ptt(bool on) {
if (!connected_) {
if (!connect()) return false;
}
// T 1 (PTT on) or T 0 (PTT off)
std::string cmd = on ? "T 1\n" : "T 0\n";
if (send(sock_, cmd.c_str(), cmd.length(), 0) < 0) {
std::cerr << "rigctl: Failed to send PTT command" << std::endl;
disconnect();
return false;
}
// read response
char response[256];
int n = recv(sock_, response, sizeof(response) - 1, 0);
if (n > 0) {
response[n] = '\0';
// rigctld returns RPRT 0 on success
if (strstr(response, "RPRT 0") || n == 0) {
ptt_on_ = on;
std::cerr << "rigctl: PTT " << (on ? "ON" : "OFF") << std::endl;
return true;
} else {
std::cerr << "rigctl: PTT command failed: " << response << std::endl;
return false;
}
}
// temp fallback
ptt_on_ = on;
return true;
}
bool ptt_on() const { return ptt_on_; }
bool is_connected() const { return connected_; }
private:
std::string host_;
int port_;
int sock_ = -1;
bool connected_ = false;
bool ptt_on_ = false;
};
class DummyPTT {
public:
bool connect() {
std::cerr << "PTT: Using dummy PTT (no rigctld)" << std::endl;
return true;
}
void disconnect() {}
bool set_ptt(bool on) {
ptt_on_ = on;
std::cerr << "PTT: " << (on ? "ON" : "OFF") << " (dummy)" << std::endl;
return true;
}
bool ptt_on() const { return ptt_on_; }
bool is_connected() const { return true; }
private:
bool ptt_on_ = false;
};

167
serial_ptt.hh Normal file
View file

@ -0,0 +1,167 @@
#pragma once
#include <string>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <cerrno>
#include <cstring>
enum class PTTLine {
DTR = 0,
RTS = 1,
BOTH = 2
};
class SerialPTT {
public:
SerialPTT() = default;
~SerialPTT() {
close();
}
bool open(const std::string& port, PTTLine line = PTTLine::RTS,
bool invert_dtr = false, bool invert_rts = false) {
close();
port_ = port;
line_ = line;
invert_dtr_ = invert_dtr;
invert_rts_ = invert_rts;
fd_ = ::open(port.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd_ < 0) {
last_error_ = std::string("Failed to open ") + port + ": " + strerror(errno);
return false;
}
struct termios tty;
if (tcgetattr(fd_, &tty) != 0) {
last_error_ = "Failed to get terminal attributes";
close();
return false;
}
cfmakeraw(&tty);
tcsetattr(fd_, TCSANOW, &tty);
ptt_off();
open_ = true;
return true;
}
void close() {
if (fd_ >= 0) {
ptt_off();
::close(fd_);
fd_ = -1;
}
open_ = false;
}
bool is_open() const { return open_; }
const std::string& last_error() const { return last_error_; }
void ptt_on() {
if (fd_ < 0) return;
int flags;
if (ioctl(fd_, TIOCMGET, &flags) < 0) return;
if (line_ == PTTLine::DTR || line_ == PTTLine::BOTH) {
if (invert_dtr_) {
flags &= ~TIOCM_DTR; // clear DTR
} else {
flags |= TIOCM_DTR; // set DTR
}
}
if (line_ == PTTLine::RTS || line_ == PTTLine::BOTH) {
if (invert_rts_) {
flags &= ~TIOCM_RTS; // clear RTS
} else {
flags |= TIOCM_RTS; // set RTS
}
}
ioctl(fd_, TIOCMSET, &flags);
}
void ptt_off() {
if (fd_ < 0) return;
int flags;
if (ioctl(fd_, TIOCMGET, &flags) < 0) return;
if (line_ == PTTLine::DTR || line_ == PTTLine::BOTH) {
if (invert_dtr_) {
flags |= TIOCM_DTR; // set DTR
} else {
flags &= ~TIOCM_DTR; // clear DTR
}
}
if (line_ == PTTLine::RTS || line_ == PTTLine::BOTH) {
if (invert_rts_) {
flags |= TIOCM_RTS; // set RTS
} else {
flags &= ~TIOCM_RTS; // clear RTS
}
}
ioctl(fd_, TIOCMSET, &flags);
}
bool reconnect() {
std::string saved_port = port_;
PTTLine saved_line = line_;
bool saved_invert_dtr = invert_dtr_;
bool saved_invert_rts = invert_rts_;
close();
return open(saved_port, saved_line, saved_invert_dtr, saved_invert_rts);
}
private:
int fd_ = -1;
bool open_ = false;
std::string port_;
PTTLine line_ = PTTLine::RTS;
bool invert_dtr_ = false;
bool invert_rts_ = false;
std::string last_error_;
};

2851
tnc_ui.hh Normal file

File diff suppressed because it is too large Load diff