Add live mesh traffic view

This commit is contained in:
Janez T
2026-03-12 20:12:25 +01:00
parent d2e6b692f3
commit 69ec2dfdb5
12 changed files with 952 additions and 836 deletions

View File

@@ -1,6 +1,5 @@
import 'dart:typed_data';
import 'package:codec2_flutter/codec2_flutter.dart';
import 'package:lpcnet_flutter/lpcnet_flutter.dart';
import 'package:flutter/foundation.dart';
import '../utils/voice_message_parser.dart';
@@ -20,8 +19,6 @@ Codec2Mode codec2ModeFor(VoicePacketMode pktMode) {
return Codec2Mode.mode1300;
case VoicePacketMode.mode2400:
return Codec2Mode.mode2400;
case VoicePacketMode.lpcnet1600:
throw ArgumentError('LPCNet mode does not map to Codec2');
}
}
@@ -48,22 +45,12 @@ class VoiceCodecService {
Future<Uint8List> encode(Int16List pcm, VoicePacketMode mode) {
_ensureCodec2Supported();
switch (mode.codec) {
case VoiceCodecKind.codec2:
return Codec2.encodeInIsolate(pcm, codec2ModeFor(mode));
case VoiceCodecKind.lpcnet:
return LpcNet.encodeInIsolate(pcm);
}
return Codec2.encodeInIsolate(pcm, codec2ModeFor(mode));
}
Future<Int16List> decode(Uint8List codec2Bytes, VoicePacketMode mode) {
_ensureCodec2Supported();
switch (mode.codec) {
case VoiceCodecKind.codec2:
return Codec2.decodeInIsolate(codec2Bytes, codec2ModeFor(mode));
case VoiceCodecKind.lpcnet:
return LpcNet.decodeInIsolate(codec2Bytes);
}
return Codec2.decodeInIsolate(codec2Bytes, codec2ModeFor(mode));
}
/// Decode and concatenate multiple [packets] into a single PCM Int16List.
@@ -73,9 +60,6 @@ class VoiceCodecService {
VoicePacketMode mode,
) async {
_ensureCodec2Supported();
if (mode.codec == VoiceCodecKind.lpcnet) {
return _decodeLpcNetPackets(packets, mode);
}
final all = <Int16List>[];
for (final pkt in packets) {
if (pkt == null || pkt.codec2Data.isEmpty) {
@@ -93,38 +77,4 @@ class VoiceCodecService {
}
return result;
}
Future<Int16List> _decodeLpcNetPackets(
List<VoicePacket?> packets,
VoicePacketMode mode,
) async {
final segments = <Int16List>[];
final run = <int>[];
Future<void> flushRun() async {
if (run.isEmpty) return;
final decoded = await LpcNet.decodeInIsolate(Uint8List.fromList(run));
segments.add(decoded);
run.clear();
}
for (final pkt in packets) {
if (pkt == null || pkt.codec2Data.isEmpty) {
await flushRun();
segments.add(Int16List(mode.samplesPerPacket));
continue;
}
run.addAll(pkt.codec2Data);
}
await flushRun();
final total = segments.fold<int>(0, (sum, chunk) => sum + chunk.length);
final result = Int16List(total);
var offset = 0;
for (final chunk in segments) {
result.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
return result;
}
}

View File

@@ -60,8 +60,6 @@ Codec2Mode codec2ModeFor(VoicePacketMode pktMode) {
return Codec2Mode.mode1300;
case VoicePacketMode.mode2400:
return Codec2Mode.mode2400;
case VoicePacketMode.lpcnet1600:
throw ArgumentError('LPCNet mode does not map to Codec2');
}
}

View File

@@ -3,7 +3,6 @@ import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:record/record.dart';
import '../utils/voice_message_parser.dart';
/// Captures raw PCM audio at a codec-selected sample rate, 16-bit mono.
///
@@ -32,7 +31,6 @@ class VoiceRecorderService {
Stream<Int16List> startCapture({
Duration chunkDuration = const Duration(seconds: 1),
int sampleRateHz = 8000,
VoiceCodecKind codecKind = VoiceCodecKind.codec2,
bool enableBandPassFilter = true,
bool enableCompressor = true,
bool enableLimiter = true,
@@ -50,7 +48,6 @@ class VoiceRecorderService {
_startRecording(
chunkDuration,
sampleRateHz: sampleRateHz,
codecKind: codecKind,
enableBandPassFilter: enableBandPassFilter,
enableCompressor: enableCompressor,
enableLimiter: enableLimiter,
@@ -64,7 +61,6 @@ class VoiceRecorderService {
Future<void> _startRecording(
Duration chunkDuration, {
required int sampleRateHz,
required VoiceCodecKind codecKind,
required bool enableBandPassFilter,
required bool enableCompressor,
required bool enableLimiter,
@@ -72,10 +68,9 @@ class VoiceRecorderService {
required bool enableEchoCancellation,
required bool enableNoiseSuppression,
}) async {
final bypassProcessing = codecKind == VoiceCodecKind.lpcnet;
final useBandPassFilter = !bypassProcessing && enableBandPassFilter;
final useCompressor = !bypassProcessing && enableCompressor;
final useLimiter = !bypassProcessing && enableLimiter;
final useBandPassFilter = enableBandPassFilter;
final useCompressor = enableCompressor;
final useLimiter = enableLimiter;
final config = RecordConfig(
encoder: AudioEncoder.pcm16bits,
sampleRate: sampleRateHz,
@@ -88,25 +83,21 @@ class VoiceRecorderService {
try {
final stream = await _recorder.startStream(config);
final voiceFilter = bypassProcessing
? null
: _VoiceBandPassFilter(
sampleRate: sampleRateHz,
lowCutHz: 250.0,
highCutHz: 3400.0,
);
final dynamics = bypassProcessing
? null
: _VoiceDynamicsProcessor(
sampleRate: sampleRateHz,
thresholdDb: -18.0,
ratio: 2.5,
attackMs: 8.0,
releaseMs: 120.0,
makeupGainDb: 4.0,
enableCompressor: useCompressor,
enableLimiter: useLimiter,
);
final voiceFilter = _VoiceBandPassFilter(
sampleRate: sampleRateHz,
lowCutHz: 250.0,
highCutHz: 3400.0,
);
final dynamics = _VoiceDynamicsProcessor(
sampleRate: sampleRateHz,
thresholdDb: -18.0,
ratio: 2.5,
attackMs: 8.0,
releaseMs: 120.0,
makeupGainDb: 4.0,
enableCompressor: useCompressor,
enableLimiter: useLimiter,
);
final chunkBytes =
sampleRateHz * 2 * chunkDuration.inMilliseconds ~/ 1000;
final buffer = <int>[];
@@ -118,28 +109,16 @@ class VoiceRecorderService {
final chunk = buffer.sublist(0, chunkBytes);
buffer.removeRange(0, chunkBytes);
final pcm = _bytesToInt16(Uint8List.fromList(chunk));
if (bypassProcessing) {
_controller?.add(pcm);
} else {
final filtered = useBandPassFilter
? voiceFilter!.process(pcm)
: pcm;
_controller?.add(dynamics!.process(filtered));
}
final filtered = useBandPassFilter ? voiceFilter.process(pcm) : pcm;
_controller?.add(dynamics.process(filtered));
}
},
onDone: () {
if (buffer.isNotEmpty) {
final padded = _padToEven(buffer);
final pcm = _bytesToInt16(Uint8List.fromList(padded));
if (bypassProcessing) {
_controller?.add(pcm);
} else {
final filtered = useBandPassFilter
? voiceFilter!.process(pcm)
: pcm;
_controller?.add(dynamics!.process(filtered));
}
final filtered = useBandPassFilter ? voiceFilter.process(pcm) : pcm;
_controller?.add(dynamics.process(filtered));
}
_controller?.close();
},