Neda/call/lib/core/framing/deframer.dart

126 lines
4.2 KiB
Dart

import 'dart:typed_data';
import '../crypto/aes_cipher.dart';
import '../crypto/key_manager.dart';
import '../fec/reed_solomon.dart';
import '../modem/fsk_demodulator.dart';
import 'packet.dart';
import '../../utils/audio_math.dart';
import '../../utils/constants.dart';
/// Receive pipeline: FSK audio → demodulate → RS FEC → decrypt → packet.
///
/// Feed audio chunks from the microphone via [pushAudio]. Decoded voice
/// super-frames (9-byte LPC payloads) are queued in [voiceQueue].
class Deframer {
final KeyManager _keys;
final Uint8List _sessionId;
final FskDemodulator _demod = FskDemodulator();
// Decoded LPC payloads waiting to be synthesised.
final List<Uint8List> voiceQueue = [];
// Stats for UI display.
int rxPackets = 0;
int rxErrors = 0;
int rxCorrected = 0;
Deframer(this._keys, this._sessionId);
// ── Public API ─────────────────────────────────────────────────────
/// Push a chunk of Float64 audio samples from the microphone.
///
/// Internally demodulates FSK, assembles wire frames, applies FEC,
/// decrypts, and enqueues decoded voice payloads into [voiceQueue].
void pushAudio(Float64List samples) {
final rawFrames = _demod.pushSamples(samples);
for (final raw in rawFrames) {
_processWireFrame(raw);
}
}
/// Push raw Int16 LE PCM bytes (as received from AudioRecord).
void pushPcmBytes(Uint8List pcmBytes) {
pushAudio(AudioMath.int16BytesToFloat(pcmBytes));
}
void reset() {
_demod.reset();
voiceQueue.clear();
rxPackets = 0;
rxErrors = 0;
rxCorrected = 0;
}
// ── Private pipeline ───────────────────────────────────────────────
/// Process one wire frame (22 bytes) through FEC → CRC → decrypt → queue.
void _processWireFrame(Uint8List wire) {
if (wire.length < 22) {
rxErrors++;
return;
}
// ── Wire layout: [SYNC:2][SEQ:2][TYPE:1][RS_PAYLOAD:15][CRC:2] ──
final seq = (wire[2] << 8) | wire[3];
final type = wire[4];
// Extract the 15-byte RS-encoded block.
final rsBlock = Uint8List.fromList(wire.sublist(5, 20));
// CRC check over bytes [2..19].
final expectedCrc = AudioMath.crc16(wire.sublist(2, 20));
final wireCrc = (wire[20] << 8) | wire[21];
if (expectedCrc != wireCrc) {
rxErrors++;
return; // frame corrupted beyond CRC recovery
}
// ── RS(15,11) FEC decode (corrects up to 2 nibble errors) ────────
final rsOk = ReedSolomon.decodeBlock(rsBlock);
if (!rsOk) {
rxErrors++;
return;
}
// Detect whether RS actually corrected any bytes (compare before/after).
bool wasCorrected = false;
for (int i = 0; i < rsBlock.length; i++) {
if (rsBlock[i] != wire[5 + i]) { wasCorrected = true; break; }
}
if (wasCorrected) rxCorrected++;
// Extract the 11-byte data payload.
final encrypted = ReedSolomon.extractData(rsBlock);
// ── AES-256-CTR decrypt ───────────────────────────────────────────
final nonce = _keys.buildNonce(_sessionId, seq);
final plaintext = AesCipher.decrypt(encrypted, _keys.key, nonce);
// ── Parse inner packet structure ──────────────────────────────────
// plaintext layout: [lpc(9)][seq_low(1)][type_tag(1)]
// Validate inner type tag matches outer type.
final innerType = plaintext[10];
if (innerType != type && type == C.pkTypeVoice) {
rxErrors++;
return;
}
rxPackets++;
// Build a Packet for higher-level handlers.
final pkt = Packet(
seq: seq,
type: type,
payload: plaintext,
valid: true,
);
if (pkt.isVoice) {
// First 9 bytes are the LPC super-frame.
voiceQueue.add(Uint8List.fromList(plaintext.sublist(0, 9)));
}
// Control packets (handshake, ping) are ignored here;
// higher layers can subscribe via [SecureChannel].
}
}