116 lines
5.8 KiB
Dart
116 lines
5.8 KiB
Dart
// ignore_for_file: constant_identifier_names
|
||
import 'dart:math' as math;
|
||
|
||
/// All system-wide numeric constants for the secure-voice pipeline.
|
||
///
|
||
/// Design target:
|
||
/// modem gross rate : 1 200 bps (4-FSK, 600 baud, 2 bits/symbol)
|
||
/// RS(15,11) FEC net : ~880 bps
|
||
/// voice codec : ~360 bps (LPC, 200 ms super-frames)
|
||
/// one-way latency : ~250 ms (frame + processing + buffer)
|
||
abstract final class C {
|
||
// ── Sample rate ────────────────────────────────────────────────────
|
||
/// Must match the cellular audio path (PCM 8 kHz narrowband).
|
||
static const int sampleRate = 8000;
|
||
|
||
// ── 4-FSK modem ────────────────────────────────────────────────────
|
||
/// Symbol rate in baud. 600 baud × 2 bits/symbol = 1 200 bps gross.
|
||
static const int baudRate = 600;
|
||
|
||
/// Samples per symbol as a float (8000 / 600 = 13.333…).
|
||
/// The modulator uses a phase accumulator; the demodulator uses an
|
||
/// early–late gate timing recovery loop to handle the fraction.
|
||
static const double samplesPerSymbol = sampleRate / baudRate;
|
||
|
||
/// 4-FSK tone frequencies (Hz). Spaced 400 Hz apart, centred in
|
||
/// the 300–3400 Hz cellular pass-band; chosen to survive AMR-NB codec.
|
||
/// symbol dibit → frequency
|
||
/// 00 → tone0 01 → tone1 10 → tone2 11 → tone3
|
||
static const List<double> fskToneHz = [1000.0, 1400.0, 1800.0, 2200.0];
|
||
|
||
/// Amplitude of the transmitted FSK signal (0–1 normalised).
|
||
/// Kept below 0.7 to avoid clipping after phone mic AGC compresses it.
|
||
static const double txAmplitude = 0.60;
|
||
|
||
/// Preamble: alternating dibit 00/11 repeated this many times before
|
||
/// each packet. Used by the demodulator for timing acquisition.
|
||
static const int preambleSymbols = 24;
|
||
|
||
/// Two-byte wire sync-word placed after preamble (0x5A, 0xA5).
|
||
static const List<int> syncWord = [0x5A, 0xA5];
|
||
|
||
// ── Packet layout (pre-FEC plaintext frame) ────────────────────────
|
||
/// Bytes: [SYNC(2)] [SEQ(2)] [TYPE(1)] [LEN(1)] [PAYLOAD(11)] [CRC16(2)]
|
||
static const int pktSyncLen = 2;
|
||
static const int pktSeqLen = 2;
|
||
static const int pktTypeLen = 1;
|
||
static const int pktLenLen = 1;
|
||
static const int pktPayloadLen = 11; // encrypted voice frame bytes
|
||
static const int pktCrcLen = 2;
|
||
static const int pktTotalBytes = pktSyncLen + pktSeqLen + pktTypeLen +
|
||
pktLenLen + pktPayloadLen + pktCrcLen; // 19 B
|
||
|
||
/// Packet type codes.
|
||
static const int pkTypeVoice = 0x01;
|
||
static const int pkTypeControl = 0x02;
|
||
static const int pkTypeHandshake = 0x03;
|
||
static const int pkTypePing = 0x04;
|
||
|
||
// ── Reed-Solomon RS(15,11) over GF(16) ────────────────────────────
|
||
/// RS codeword length n (nibbles).
|
||
static const int rsN = 15;
|
||
/// RS data symbols per codeword k (nibbles).
|
||
static const int rsK = 11;
|
||
/// RS parity symbols per codeword (n-k).
|
||
static const int rsParity = rsN - rsK; // 4
|
||
/// Correction capability: t = parity/2 = 2 nibble errors per codeword.
|
||
static const int rsT = rsParity ~/ 2;
|
||
|
||
// ── LPC voice codec ────────────────────────────────────────────────
|
||
/// Sub-frame size (samples). One LPC analysis frame = 20 ms at 8 kHz.
|
||
static const int lpcSubframeSamples = 160; // 20 ms
|
||
|
||
/// Number of sub-frames in one super-frame (transmitted as a packet).
|
||
static const int lpcSubframesPerSuper = 10; // 200 ms total
|
||
|
||
/// LPC analysis order (number of predictor coefficients).
|
||
static const int lpcOrder = 10;
|
||
|
||
/// Minimum pitch period in samples (max fundamental ~400 Hz).
|
||
static const int pitchMinSamples = 20;
|
||
|
||
/// Maximum pitch period in samples (min fundamental ~50 Hz).
|
||
static const int pitchMaxSamples = 160;
|
||
|
||
/// Pre-emphasis coefficient. Applied as s'[n] = s[n] - alpha·s[n-1].
|
||
static const double preEmphasis = 0.97;
|
||
|
||
// ── AES-256-CTR ────────────────────────────────────────────────────
|
||
/// Key length in bytes.
|
||
static const int aesKeyBytes = 32;
|
||
/// Nonce length in bytes (packed from session-ID + packet-seq).
|
||
static const int aesNonceBytes = 16; // AES block size = 128 bits
|
||
|
||
// ── PBKDF2 ─────────────────────────────────────────────────────────
|
||
static const int pbkdf2Iterations = 100000;
|
||
static const int pbkdf2SaltBytes = 16;
|
||
|
||
// ── AGC / Signal conditioning ──────────────────────────────────────
|
||
/// Target RMS level for received signal (normalised 0–1).
|
||
static const double agcTargetRms = 0.15;
|
||
/// AGC attack time constant (samples). Fast attack.
|
||
static const double agcAlpha = 0.001;
|
||
|
||
// ── Goertzel window length (must be ≥ samplesPerSymbol) ───────────
|
||
/// We snap to 14 integer samples which is the nearest integer above
|
||
/// samplesPerSymbol. Timing recovery compensates for the 0.67 sample drift.
|
||
static const int goertzelWindow = 14;
|
||
|
||
// ── Derived constants ──────────────────────────────────────────────
|
||
/// Pre-computed Goertzel coefficients for each FSK tone.
|
||
/// coeff[i] = 2 · cos(2π · f_i / sampleRate)
|
||
static final List<double> goertzelCoeff = fskToneHz
|
||
.map((f) => 2.0 * math.cos(2.0 * math.pi * f / sampleRate))
|
||
.toList(growable: false);
|
||
}
|