mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(dmr): decode and route AMBE voice
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user