feat(dmr): decode and route AMBE voice

This commit is contained in:
2026-07-14 18:55:28 +03:00
parent e0e74b5337
commit 28c434d9a7
7 changed files with 231 additions and 10 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ cmake --build build/dmr
fpc -Fu. -FE/tmp tests/dmr_protocol_test.pas
/tmp/dmr_protocol_test
fpc -Fu. -FE/tmp tests/dmr_frontend_test.pas
/tmp/dmr_frontend_test
LD_LIBRARY_PATH=build/dmr /tmp/dmr_frontend_test
+58
View File
@@ -27,8 +27,11 @@ const
DMR_RING_SIZE = 65536; // >1.3 s at 48 kHz, power of two
DMR_BURST_DIBITS = 144;
DMR_SYNC_END_DIBIT = 89; // sync occupies burst dibits 66..89
DMR_VOICE_FRAME_SAMPLES = DMR_PCM_SAMPLES; // 20 ms at 8 kHz
type
TDMRAudioEvent = procedure(const PCM: array of Single; Count: Integer) of object;
TDMRSyncKind = (
dskNone,
dskBSData, dskBSVoice,
@@ -62,6 +65,9 @@ type
ServiceOptions: Integer;
TargetID: LongWord;
SourceID: LongWord;
SelectedSlot: Integer;
VoiceFrameCount: QWord;
VoiceErrorCount: QWord;
end;
TDMRDecoder = class;
@@ -111,6 +117,9 @@ type
FLastLCSlot: Integer;
FColorCodeValid: Boolean;
FColorCode, FLastDataType, FSlotTypeCorrectedBits: Integer;
FSelectedSlot: Integer;
FVoiceFrameCount, FVoiceErrorCount: QWord;
FOnAudio: TDMRAudioEvent;
FBurstCount: QWord;
FRMSSq: Double;
@@ -127,6 +136,7 @@ type
function Digitize(Phase: Integer; Symbol: Double): Byte;
procedure StartBurstTracking(Phase: Integer);
procedure CompleteBurst;
procedure DecodeVoiceBurst;
procedure ResetDSP;
public
constructor Create;
@@ -140,6 +150,7 @@ type
class function SyncKindName(Kind: TDMRSyncKind): string; static;
property Enabled: Boolean read FEnabled;
property OnAudio: TDMRAudioEvent read FOnAudio write FOnAudio;
end;
implementation
@@ -275,6 +286,10 @@ begin
FColorCode := 0;
FLastDataType := 0;
FSlotTypeCorrectedBits := 0;
FSelectedSlot := -1;
FVoiceFrameCount := 0;
FVoiceErrorCount := 0;
DMRMbeReset(FMbe);
FTrackPhase := -1;
FCurrentBurstPos := 0;
FBurstCount := 0;
@@ -480,11 +495,51 @@ begin
FLastLCSlot := 1
else
FLastLCSlot := 0;
if FSelectedSlot < 0 then FSelectedSlot := FLastLCSlot;
end;
DecodeVoiceBurst;
Inc(FBurstCount);
FCurrentBurstPos := 0;
end;
procedure TDMRDecoder.DecodeVoiceBurst;
var
Frames: TDMRAMBEFrames;
PCM: array[0..DMR_PCM_SAMPLES - 1] of Single;
MbeResult: TDMRMbeResult;
Slot, FrameIndex, RC: Integer;
begin
if (FMbe = nil) or (FLastSyncTick = 0) or
(GetTickCount64 - FLastSyncTick > DMR_SYNC_HOLD_MS) then Exit;
if not (FSyncKind in [dskBSVoice, dskMSVoice, dskDirectTS1Voice,
dskDirectTS2Voice]) then Exit;
if FSyncKind in [dskBSVoice] then
begin
if not FLastBurstInfo.CACH.Valid then Exit;
Slot := FLastBurstInfo.CACH.Slot;
end
else if FSyncKind = dskDirectTS2Voice then Slot := 1
else Slot := 0;
if FSelectedSlot < 0 then FSelectedSlot := Slot;
if Slot <> FSelectedSlot then Exit;
DMRExtractAMBEFrames(FLastBurst, Frames);
for FrameIndex := 0 to 2 do
begin
FillChar(MbeResult, SizeOf(MbeResult), 0);
RC := DMRMbeDecode(FMbe, @Frames[FrameIndex][0], @PCM[0], @MbeResult);
if RC <> 0 then
begin
Inc(FVoiceErrorCount);
Continue;
end;
Inc(FVoiceFrameCount);
if MbeResult.ErrorsTotal > 0 then Inc(FVoiceErrorCount, MbeResult.ErrorsTotal);
if Assigned(FOnAudio) then FOnAudio(PCM, Length(PCM));
end;
end;
procedure TDMRDecoder.NoteSync(Phase: Integer; Kind: TDMRSyncKind;
Inverted: Boolean);
var
@@ -559,6 +614,9 @@ begin
S.ServiceOptions := FLastLinkControl.ServiceOptions;
S.TargetID := FLastLinkControl.TargetID;
S.SourceID := FLastLinkControl.SourceID;
S.SelectedSlot := FSelectedSlot;
S.VoiceFrameCount := FVoiceFrameCount;
S.VoiceErrorCount := FVoiceErrorCount;
finally
FStatusLock.Leave;
end;
+77 -3
View File
@@ -3,9 +3,21 @@ unit DMRProtocol;
{
Small, allocation-free DMR burst protocol layer.
The CACH interleave and Hamming/Golay generator matrices are derived from
dsd-fme (src/dmr_bs.c and src/fec.c). The original code is distributed
under the ISC licence; see the dsd-fme source tree for its full notice.
The CACH/AMBE interleavers and Hamming/Golay matrices are derived from
dsd-fme (src/dmr_bs.c, src/fec.c and include/dmr_const.h).
Copyright (C) 2010 DSD Author
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
}
{$IFDEF FPC}
@@ -25,6 +37,8 @@ type
TDMRBit7 = array[0..6] of Byte;
TDMRBit20 = array[0..19] of Byte;
TDMRBit96 = array[0..95] of Byte;
TDMRAMBEFrame = array[0..95] of Byte;
TDMRAMBEFrames = array[0..2] of TDMRAMBEFrame;
TDMRCACH = record
Valid: Boolean;
@@ -68,6 +82,10 @@ function DMRGolay208Encode(Value: Byte): TDMRBit20;
procedure DMRBPTC19696Encode(const Payload: TDMRBit96;
out Burst: TDMRDibitBurst);
procedure DMRFullLCSetParity(var Payload: TDMRBit96; DataType: Integer);
procedure DMRExtractAMBEFrames(const Burst: TDMRDibitBurst;
out Frames: TDMRAMBEFrames);
procedure DMRInsertAMBEFrames(const Frames: TDMRAMBEFrames;
var Burst: TDMRDibitBurst);
function DMRDecodeCACH(const Burst: TDMRDibitBurst; out CACH: TDMRCACH): Boolean;
function DMRDecodeSlotType(const Burst: TDMRDibitBurst;
out SlotType: TDMRSlotType): Boolean;
@@ -110,6 +128,19 @@ const
HAMMING_15_11_SYNDROME: array[0..14] of Byte =
(9,13,15,14,7,10,5,11,12,6,3, 8,4,2,1);
AMBE_W: array[0..35] of Byte = (
0,1,0,1,0,1, 0,1,0,1,0,1, 0,1,0,1,0,1,
0,1,0,1,0,2, 0,2,0,2,0,2, 0,2,0,2,0,2);
AMBE_X: array[0..35] of Byte = (
23,10,22,9,21,8, 20,7,19,6,18,5, 17,4,16,3,15,2,
14,1,13,0,12,10, 11,9,10,8,9,7, 8,6,7,5,6,4);
AMBE_Y: array[0..35] of Byte = (
0,2,0,2,0,2, 0,2,0,3,0,3, 1,3,1,3,1,3,
1,3,1,3,1,3, 1,3,1,3,1,3, 1,3,1,3,1,3);
AMBE_Z: array[0..35] of Byte = (
5,3,4,2,3,1, 2,0,1,13,0,12, 22,11,21,10,20,9,
19,8,18,7,17,6, 16,5,15,4,14,3, 13,2,12,1,11,0);
function DMRHamming74Encode(Value: Byte): TDMRBit7;
var
i, j: Integer;
@@ -203,6 +234,49 @@ begin
end;
end;
function AMBEDibitIndex(FrameIndex, DibitIndex: Integer): Integer;
begin
case FrameIndex of
0: Result := 12 + DibitIndex;
1: if DibitIndex < 18 then Result := 48 + DibitIndex
else Result := 90 + DibitIndex - 18;
else
Result := 108 + DibitIndex;
end;
end;
procedure DMRExtractAMBEFrames(const Burst: TDMRDibitBurst;
out Frames: TDMRAMBEFrames);
var
FrameIndex, i, DibitIndex: Integer;
Dibit: Byte;
begin
FillChar(Frames, SizeOf(Frames), 0);
for FrameIndex := 0 to 2 do
for i := 0 to 35 do
begin
DibitIndex := AMBEDibitIndex(FrameIndex, i);
Dibit := Burst[DibitIndex];
Frames[FrameIndex][AMBE_W[i] * 24 + AMBE_X[i]] := (Dibit shr 1) and 1;
Frames[FrameIndex][AMBE_Y[i] * 24 + AMBE_Z[i]] := Dibit and 1;
end;
end;
procedure DMRInsertAMBEFrames(const Frames: TDMRAMBEFrames;
var Burst: TDMRDibitBurst);
var
FrameIndex, i, DibitIndex: Integer;
begin
for FrameIndex := 0 to 2 do
for i := 0 to 35 do
begin
DibitIndex := AMBEDibitIndex(FrameIndex, i);
Burst[DibitIndex] :=
((Frames[FrameIndex][AMBE_W[i] * 24 + AMBE_X[i]] and 1) shl 1) or
(Frames[FrameIndex][AMBE_Y[i] * 24 + AMBE_Z[i]] and 1);
end;
end;
function DecodeHamming74(const Received: TDMRBit7; out Value: Byte;
out Corrected: Integer): Boolean;
var
+42 -1
View File
@@ -405,6 +405,7 @@ type
// вызывающим (GUI или демон) после CreateEngines.
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnDemodAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnDMRAudioReady(const PCM: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
procedure OnMicPacket(const Data: TMicDataPacket);
@@ -851,6 +852,7 @@ begin
// DMR front-end owns a worker thread. It stays idle until MODE_DMR is active.
FDMRDec := TDMRDecoder.Create;
FDMRDec.OnAudio := OnDMRAudioReady;
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
// формы / при подключении устройства).
@@ -862,6 +864,13 @@ end;
procedure TRadioController.FreeEngines;
var i: Integer;
begin
// Stop the DMR worker before destroying any of its audio destinations.
if Assigned(FDMRDec) then
begin
FDMRDec.SetEnabled(False);
FDMRDec.OnAudio := nil;
FreeAndNil(FDMRDec);
end;
if Assigned(FNetwork) then
begin
if FNetwork.Connected then FNetwork.Disconnect;
@@ -883,7 +892,6 @@ begin
FDSPEngine.Close;
FreeAndNil(FDSPEngine);
end;
if Assigned(FDMRDec) then FreeAndNil(FDMRDec);
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
end;
@@ -1186,6 +1194,39 @@ begin
FDMRDec.SetEnabled(False);
end;
procedure TRadioController.OnDMRAudioReady(const PCM: array of Single;
Count: Integer);
// DMR worker thread, 8 kHz mono from mbelib. Linear 6x interpolation brings
// it to the common 48 kHz output route used by local, web and radio speakers.
var
Left, Right: array of Single;
i, Phase, OutPos, N: Integer;
A, B, V, Gain: Single;
begin
if (FMode <> MODE_DMR) or FMuted or (Count <= 0) then Exit;
N := Min(Count, Length(PCM));
SetLength(Left, N * 6);
SetLength(Right, N * 6);
Gain := FVolume / 100.0;
OutPos := 0;
for i := 0 to N - 1 do
begin
A := PCM[i];
if i + 1 < N then B := PCM[i + 1] else B := A;
for Phase := 0 to 5 do
begin
V := (A + (B - A) * (Phase / 6.0)) * Gain;
Left[OutPos] := V;
Right[OutPos] := V;
Inc(OutPos);
end;
end;
if FSendAudioToRadio and Assigned(FNetwork) then
FNetwork.SendSpeakerAudio(Left, Right, OutPos);
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, OutPos) then Exit;
if FLocalAudio and Assigned(FAudioOut) then FAudioOut.Write(Left, Right, OutPos);
end;
// ---------------------------------------------------------------------------
// Мультислайсы (софт-fan-out)
// ---------------------------------------------------------------------------
+8 -4
View File
@@ -11,7 +11,11 @@ cmake -S native/dmr -B build/dmr -DCMAKE_BUILD_TYPE=Release
cmake --build build/dmr
```
The current ABI implements the stateful AMBE+2 vocoder boundary. The burst/FEC
layer will feed its row-major `char[4][24]` output to this library. Keep the ABI
versioned: the GUI and daemon must remain usable when the optional library is
not installed.
The Pascal burst/FEC layer extracts three row-major `char[4][24]` AMBE+2
frames from each selected voice-slot burst and feeds them through this ABI.
Decoded 8 kHz mono PCM is resampled to EWSDR's 48 kHz speaker/web route.
For a build-tree run, either copy `libewsdr_dmr.so` beside the EWSDR executable
or add `build/dmr` to `LD_LIBRARY_PATH`. `cmake --install build/dmr` installs the
adapter system-wide. The GUI and daemon remain usable (with metadata but no
voice) when this optional library is absent.
+27
View File
@@ -9,6 +9,20 @@ uses
const
BS_VOICE_SYNC = '131111333113313313113313';
type
TAudioSink = class
public
CallCount: Integer;
SampleCount: Integer;
procedure AudioReady(const PCM: array of Single; Count: Integer);
end;
procedure TAudioSink.AudioReady(const PCM: array of Single; Count: Integer);
begin
Inc(CallCount);
Inc(SampleCount, Count);
end;
procedure FeedSync(D: TDMRDecoder; const Pattern: string; Invert: Boolean);
var
L, R: array of Single;
@@ -95,9 +109,12 @@ end;
var
D: TDMRDecoder;
S: TDMRStatus;
Sink: TAudioSink;
begin
Sink := TAudioSink.Create;
D := TDMRDecoder.Create;
try
D.OnAudio := Sink.AudioReady;
D.SetEnabled(True);
FeedSync(D, BS_VOICE_SYNC, False);
Sleep(50);
@@ -124,8 +141,18 @@ begin
D.SetInverted(False);
D.SetEnabled(True);
TestBurst(D);
D.GetStatus(S);
if S.VocoderAvailable and
((Sink.CallCount <> 3) or
(Sink.SampleCount <> 3 * DMR_VOICE_FRAME_SAMPLES)) then
begin
WriteLn('AMBE callback failed: calls=', Sink.CallCount,
' samples=', Sink.SampleCount);
Halt(5);
end;
WriteLn('DMR front-end sync tests passed');
finally
D.Free;
Sink.Free;
end;
end.
+18 -1
View File
@@ -76,6 +76,8 @@ var
Burst: TDMRDibitBurst;
Info: TDMRBurstInfo;
Payload: TDMRBit96;
Frames: TDMRAMBEFrames;
VoiceDibits: array[0..107] of Byte;
i: Integer;
begin
FillChar(Burst, SizeOf(Burst), 0);
@@ -155,5 +157,20 @@ begin
DMRParseBurst(Burst, Info);
Check(not Info.LinkControl.Decoded, 'invalid Full LC RS checksum accepted');
WriteLn('DMR CACH, Slot Type, BPTC and Full LC tests passed');
// Three AMBE frames occupy 36 + 18/18 + 36 dibits around the centre sync.
FillChar(Burst, SizeOf(Burst), 0);
for i := 12 to 65 do Burst[i] := (i * 3 + 1) and 3;
for i := 90 to 143 do Burst[i] := (i * 3 + 2) and 3;
for i := 0 to 53 do VoiceDibits[i] := Burst[12 + i];
for i := 0 to 53 do VoiceDibits[54 + i] := Burst[90 + i];
DMRExtractAMBEFrames(Burst, Frames);
for i := 12 to 65 do Burst[i] := 0;
for i := 90 to 143 do Burst[i] := 0;
DMRInsertAMBEFrames(Frames, Burst);
for i := 0 to 53 do Check(Burst[12 + i] = VoiceDibits[i],
'AMBE frame 1/2 interleave mismatch');
for i := 0 to 53 do Check(Burst[90 + i] = VoiceDibits[54 + i],
'AMBE frame 2/3 interleave mismatch');
WriteLn('DMR protocol, FEC, Full LC and AMBE extraction tests passed');
end.