mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add live mesh traffic view
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user