mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 08:20:36 +00:00
Add live mesh traffic view
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,6 @@ import '../widgets/messages/messages_content.dart';
|
||||
import '../widgets/common/contact_avatar.dart';
|
||||
import '../services/message_destination_preferences.dart';
|
||||
import '../services/voice_bitrate_preferences.dart';
|
||||
import '../services/voice_codec_preferences.dart';
|
||||
import '../services/voice_recorder_service.dart';
|
||||
import '../services/voice_codec_service.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
@@ -72,7 +71,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final VoiceRecorderService _voiceRecorder = VoiceRecorderService();
|
||||
bool _isRecording = false;
|
||||
bool _isSendingVoice = false;
|
||||
static const Duration _maxVoiceRecordingDuration = Duration(seconds: 4);
|
||||
static const Duration _maxVoiceRecordingDuration = Duration(seconds: 30);
|
||||
static const double _silenceRmsThreshold = 500.0;
|
||||
static const double _silencePeakThreshold = 1400.0;
|
||||
static const int _maxInteriorSilentChunks = 2;
|
||||
@@ -82,7 +81,6 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final List<Int16List> _recordedChunks = [];
|
||||
VoicePacketMode? _activeVoiceMode;
|
||||
int _selectedVoiceBitrate = VoiceBitratePreferences.defaultBitrate;
|
||||
VoiceCodecKind _selectedVoiceCodec = VoiceCodecPreferences.defaultCodec;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -98,11 +96,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
|
||||
Future<void> _loadVoiceSettings() async {
|
||||
final bitrate = await VoiceBitratePreferences.getBitrate();
|
||||
final codec = await VoiceCodecPreferences.getCodec();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_selectedVoiceBitrate = bitrate;
|
||||
_selectedVoiceCodec = codec;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -823,15 +819,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
debugPrint('🎙️ [Voice] _startVoiceRecording called');
|
||||
// Read fresh voice preferences so settings changes apply immediately.
|
||||
final selectedBitrate = await VoiceBitratePreferences.getBitrate();
|
||||
final selectedCodec = await VoiceCodecPreferences.getCodec();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_selectedVoiceBitrate = selectedBitrate;
|
||||
_selectedVoiceCodec = selectedCodec;
|
||||
});
|
||||
} else {
|
||||
_selectedVoiceBitrate = selectedBitrate;
|
||||
_selectedVoiceCodec = selectedCodec;
|
||||
}
|
||||
final hasPermission = await _voiceRecorder.requestPermission();
|
||||
debugPrint('🎙️ [Voice] hasPermission=$hasPermission');
|
||||
@@ -859,9 +852,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
(_) => rng.nextInt(256),
|
||||
).map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
|
||||
_activeVoiceMode = _selectedVoiceCodec == VoiceCodecKind.lpcnet
|
||||
? VoicePacketMode.lpcnet1600
|
||||
: VoiceBitratePreferences.toVoiceMode(_selectedVoiceBitrate);
|
||||
_activeVoiceMode = VoiceBitratePreferences.toVoiceMode(
|
||||
_selectedVoiceBitrate,
|
||||
);
|
||||
final packetDuration = Duration(
|
||||
milliseconds: _activeVoiceMode!.packetDurationMs,
|
||||
);
|
||||
@@ -878,7 +871,6 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final stream = _voiceRecorder.startCapture(
|
||||
chunkDuration: packetDuration,
|
||||
sampleRateHz: _activeVoiceMode!.sampleRateHz,
|
||||
codecKind: _activeVoiceMode!.codec,
|
||||
enableBandPassFilter: appProvider.isVoiceBandPassFilterEnabled,
|
||||
enableCompressor: appProvider.isVoiceCompressorEnabled,
|
||||
enableLimiter: appProvider.isVoiceLimiterEnabled,
|
||||
@@ -1021,15 +1013,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
debugPrint(
|
||||
'🎙️ [Voice] encoding $total packets for deferred voice fetch, mode=${mode.label}, session=$sessionId',
|
||||
);
|
||||
final encodedChunks = mode.codec == VoiceCodecKind.lpcnet
|
||||
? await _encodeLpcNetChunks(codec, chunks, mode)
|
||||
: <Uint8List>[];
|
||||
for (var i = 0; i < total; i++) {
|
||||
if (!mounted) return;
|
||||
try {
|
||||
final codec2Data = mode.codec == VoiceCodecKind.lpcnet
|
||||
? encodedChunks[i]
|
||||
: await codec.encode(chunks[i], mode);
|
||||
final codec2Data = await codec.encode(chunks[i], mode);
|
||||
debugPrint(
|
||||
'🎙️ [Voice] packet $i/$total encoded: ${codec2Data.length} bytes',
|
||||
);
|
||||
@@ -1133,36 +1120,6 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
messagesProvider.markMessageSent(msgId, 0, 0);
|
||||
}
|
||||
|
||||
Future<List<Uint8List>> _encodeLpcNetChunks(
|
||||
VoiceCodecService codec,
|
||||
List<Int16List> chunks,
|
||||
VoicePacketMode mode,
|
||||
) async {
|
||||
final totalSamples = chunks.fold<int>(
|
||||
0,
|
||||
(sum, chunk) => sum + chunk.length,
|
||||
);
|
||||
final merged = Int16List(totalSamples);
|
||||
final encodedByteLengths = <int>[];
|
||||
var sampleOffset = 0;
|
||||
for (final chunk in chunks) {
|
||||
merged.setRange(sampleOffset, sampleOffset + chunk.length, chunk);
|
||||
sampleOffset += chunk.length;
|
||||
encodedByteLengths.add((chunk.length ~/ 640) * 8);
|
||||
}
|
||||
|
||||
final encoded = await codec.encode(merged, mode);
|
||||
final encodedChunks = <Uint8List>[];
|
||||
var byteOffset = 0;
|
||||
for (final length in encodedByteLengths) {
|
||||
encodedChunks.add(
|
||||
Uint8List.sublistView(encoded, byteOffset, byteOffset + length),
|
||||
);
|
||||
byteOffset += length;
|
||||
}
|
||||
return encodedChunks;
|
||||
}
|
||||
|
||||
List<Int16List> _prepareChunksForSending(
|
||||
List<Int16List> chunks,
|
||||
VoicePacketMode mode,
|
||||
|
||||
@@ -19,7 +19,6 @@ import '../services/locale_preferences.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../services/update_checker_service.dart';
|
||||
import '../services/voice_bitrate_preferences.dart';
|
||||
import '../services/voice_codec_preferences.dart';
|
||||
import '../services/image_preferences.dart';
|
||||
import '../services/route_hash_preferences.dart';
|
||||
import '../services/image_codec_service.dart';
|
||||
@@ -60,7 +59,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
bool _showRxTxIndicators = true;
|
||||
bool _isCheckingForUpdates = false;
|
||||
int _voiceBitrate = VoiceBitratePreferences.defaultBitrate;
|
||||
VoiceCodecKind _voiceCodec = VoiceCodecPreferences.defaultCodec;
|
||||
int _routeHashSize = RouteHashPreferences.defaultHashSize;
|
||||
int _imageMaxSize = ImagePreferences.defaultMaxSize;
|
||||
int _imageCompression = ImagePreferences.defaultQuality;
|
||||
@@ -181,11 +179,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
Future<void> _loadVoicePreferences() async {
|
||||
final value = await VoiceBitratePreferences.getBitrate();
|
||||
final codec = await VoiceCodecPreferences.getCodec();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_voiceBitrate = value;
|
||||
_voiceCodec = codec;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,27 +193,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveVoiceCodecPreference(VoiceCodecKind value) async {
|
||||
await VoiceCodecPreferences.setCodec(value);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_voiceCodec = value;
|
||||
});
|
||||
}
|
||||
|
||||
String _voiceBitrateSubtitle(int bitrate) {
|
||||
if (_voiceCodec == VoiceCodecKind.lpcnet) {
|
||||
return 'Fixed 1600 bps';
|
||||
}
|
||||
return '$bitrate bps';
|
||||
}
|
||||
|
||||
String _voiceCodecSubtitle() {
|
||||
return _voiceCodec == VoiceCodecKind.codec2
|
||||
? 'Selectable low-bitrate modes'
|
||||
: 'Fixed 16 kHz / 1.6 kbps mode';
|
||||
}
|
||||
|
||||
Future<void> _loadRouteHashSizePreference() async {
|
||||
final value = await RouteHashPreferences.getHashSize();
|
||||
if (!mounted) return;
|
||||
@@ -1179,7 +1158,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Consumer2<AppProvider, ConnectionProvider>(
|
||||
builder: (context, appProvider, connectionProvider, child) =>
|
||||
_buildVoiceStatsCard(
|
||||
codec: _voiceCodec,
|
||||
bitrate: _voiceBitrate,
|
||||
connectionProvider: connectionProvider,
|
||||
bandPassEnabled: appProvider.isVoiceBandPassFilterEnabled,
|
||||
@@ -1194,22 +1172,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
),
|
||||
_buildSettingsCard([
|
||||
ListTile(
|
||||
leading: const Icon(Icons.record_voice_over),
|
||||
title: const Text('Voice codec'),
|
||||
subtitle: Text(_voiceCodecSubtitle()),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _showVoiceCodecDialog,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.graphic_eq),
|
||||
title: const Text('Voice bitrate'),
|
||||
subtitle: Text(_voiceBitrateSubtitle(_voiceBitrate)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
enabled: _voiceCodec == VoiceCodecKind.codec2,
|
||||
onTap: _voiceCodec == VoiceCodecKind.codec2
|
||||
? _showVoiceBitrateDialog
|
||||
: null,
|
||||
onTap: _showVoiceBitrateDialog,
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
@@ -1797,7 +1765,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
Widget _buildVoiceStatsCard({
|
||||
required VoiceCodecKind codec,
|
||||
required int bitrate,
|
||||
required ConnectionProvider connectionProvider,
|
||||
required bool bandPassEnabled,
|
||||
@@ -1826,9 +1793,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final radioSf = connectionProvider.deviceInfo.radioSf;
|
||||
final radioCr = connectionProvider.deviceInfo.radioCr;
|
||||
final bwHz = _resolveBandwidthHz(radioBw);
|
||||
final voiceMode = codec == VoiceCodecKind.lpcnet
|
||||
? VoicePacketMode.lpcnet1600
|
||||
: VoiceBitratePreferences.toVoiceMode(bitrate);
|
||||
final voiceMode = VoiceBitratePreferences.toVoiceMode(bitrate);
|
||||
const voicePreviewMs = 10000; // 10-second reference clip
|
||||
final packetDurationMs = voiceMode.packetDurationMs;
|
||||
final voicePacketCount =
|
||||
@@ -1865,7 +1830,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Codec: ${voiceMode.label}${codec == VoiceCodecKind.codec2 ? ' · $bitrate bps' : ' · 1600 bps'}',
|
||||
'Codec: ${voiceMode.label} · $bitrate bps',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -2219,47 +2184,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showVoiceCodecDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Voice codec'),
|
||||
content: SingleChildScrollView(
|
||||
child: RadioGroup<VoiceCodecKind>(
|
||||
groupValue: _voiceCodec,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
_saveVoiceCodecPreference(value);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<VoiceCodecKind>(
|
||||
value: VoiceCodecKind.codec2,
|
||||
title: const Text('Codec2'),
|
||||
subtitle: const Text('Selectable ultra-low bitrate modes'),
|
||||
),
|
||||
RadioListTile<VoiceCodecKind>(
|
||||
value: VoiceCodecKind.lpcnet,
|
||||
title: const Text('LPCNet'),
|
||||
subtitle: const Text('Fixed 16 kHz / 1.6 kbps neural mode'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAboutDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
||||
Reference in New Issue
Block a user