mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Push-to-talk voice messaging using the Codec2 ultra-low-bitrate speech codec, transmitted as V: prefixed packets over the existing MeshCore LoRa mesh pipeline. Voice recording UI (long-press send button or + menu) is gated behind Platform.isIOS || Platform.isMacOS since the `record` package only supports microphone capture on those platforms in this build. Key changes: - VoiceRecorderService: streams 8kHz mono PCM chunks via `record` package - VoicePlayerService: decodes Codec2 bytes to WAV and plays via audioplayers - VoiceCodecService: async Codec2 encode/decode in background isolates - VoiceProvider: reassembles multi-packet sessions, drives playback - VoiceMessageBubble: shows packet progress, play/stop controls - MessagesProvider: detects V: prefix, routes to VoiceProvider - MessagesTab: PTT long-press gesture + recording indicator (iOS/macOS) - Message model: isVoice + voiceId fields for session tracking - Auto-selects codec mode from radio bandwidth (700C/1200/1300 bps) - codec2_flutter + meshcore_client switched from path to git deps
73 lines
2.7 KiB
Dart
73 lines
2.7 KiB
Dart
import 'dart:typed_data';
|
|
import 'package:codec2_flutter/codec2_flutter.dart';
|
|
import '../utils/voice_message_parser.dart';
|
|
|
|
export 'package:codec2_flutter/codec2_flutter.dart' show Codec2Mode;
|
|
|
|
/// Maps [VoicePacketMode] to the [Codec2Mode] enum from the FFI plugin.
|
|
Codec2Mode codec2ModeFor(VoicePacketMode pktMode) {
|
|
switch (pktMode) {
|
|
case VoicePacketMode.mode700c: return Codec2Mode.mode700c;
|
|
case VoicePacketMode.mode1200: return Codec2Mode.mode1200;
|
|
case VoicePacketMode.mode1300: return Codec2Mode.mode1300;
|
|
case VoicePacketMode.mode2400: return Codec2Mode.mode2400;
|
|
}
|
|
}
|
|
|
|
/// Selects the [VoicePacketMode] best suited for a given LoRa radio bandwidth.
|
|
///
|
|
/// Call with [radioBandwidthHz] from the device's radio params
|
|
/// (e.g. 125000 for 125 kHz).
|
|
VoicePacketMode voiceModeForBandwidth(int radioBandwidthHz) {
|
|
if (radioBandwidthHz <= 62500) return VoicePacketMode.mode700c;
|
|
if (radioBandwidthHz <= 125000) return VoicePacketMode.mode1200;
|
|
return VoicePacketMode.mode1300;
|
|
}
|
|
|
|
/// High-level codec service that provides async Codec2 encode/decode
|
|
/// executed in a background isolate so the UI thread is never blocked.
|
|
class VoiceCodecService {
|
|
/// Encode [pcm] (Int16 samples, 8000 Hz mono) with [mode].
|
|
/// Returns the raw Codec2-encoded bytes.
|
|
Future<Uint8List> encode(Int16List pcm, VoicePacketMode mode) =>
|
|
Codec2.encodeInIsolate(pcm, codec2ModeFor(mode));
|
|
|
|
/// Decode [codec2Bytes] back to Int16 PCM (8000 Hz mono) with [mode].
|
|
Future<Int16List> decode(Uint8List codec2Bytes, VoicePacketMode mode) =>
|
|
Codec2.decodeInIsolate(codec2Bytes, codec2ModeFor(mode));
|
|
|
|
/// Decode and concatenate multiple [packets] into a single PCM Int16List.
|
|
/// Packets with null/missing entries are substituted with silence.
|
|
Future<Int16List> decodePackets(
|
|
List<VoicePacket?> packets,
|
|
VoicePacketMode mode,
|
|
) async {
|
|
final c2Mode = codec2ModeFor(mode);
|
|
final c2 = Codec2.create(c2Mode);
|
|
final spf = c2.samplesPerFrame;
|
|
c2.destroy();
|
|
|
|
// Estimate total samples (use actual data or silence per missing packet)
|
|
final all = <Int16List>[];
|
|
for (final pkt in packets) {
|
|
if (pkt == null || pkt.codec2Data.isEmpty) {
|
|
// Silence for missing packet — duration approximated by mode
|
|
final silenceSamples = (codec2ModeFor(mode).framesPerSecond) * spf;
|
|
all.add(Int16List(silenceSamples));
|
|
} else {
|
|
final decoded = await Codec2.decodeInIsolate(pkt.codec2Data, c2Mode);
|
|
all.add(decoded);
|
|
}
|
|
}
|
|
|
|
final total = all.fold<int>(0, (sum, l) => sum + l.length);
|
|
final result = Int16List(total);
|
|
var offset = 0;
|
|
for (final chunk in all) {
|
|
result.setRange(offset, offset + chunk.length, chunk);
|
|
offset += chunk.length;
|
|
}
|
|
return result;
|
|
}
|
|
}
|