mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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
|
fpc -Fu. -FE/tmp tests/dmr_protocol_test.pas
|
||||||
/tmp/dmr_protocol_test
|
/tmp/dmr_protocol_test
|
||||||
fpc -Fu. -FE/tmp tests/dmr_frontend_test.pas
|
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_RING_SIZE = 65536; // >1.3 s at 48 kHz, power of two
|
||||||
DMR_BURST_DIBITS = 144;
|
DMR_BURST_DIBITS = 144;
|
||||||
DMR_SYNC_END_DIBIT = 89; // sync occupies burst dibits 66..89
|
DMR_SYNC_END_DIBIT = 89; // sync occupies burst dibits 66..89
|
||||||
|
DMR_VOICE_FRAME_SAMPLES = DMR_PCM_SAMPLES; // 20 ms at 8 kHz
|
||||||
|
|
||||||
type
|
type
|
||||||
|
TDMRAudioEvent = procedure(const PCM: array of Single; Count: Integer) of object;
|
||||||
|
|
||||||
TDMRSyncKind = (
|
TDMRSyncKind = (
|
||||||
dskNone,
|
dskNone,
|
||||||
dskBSData, dskBSVoice,
|
dskBSData, dskBSVoice,
|
||||||
@@ -62,6 +65,9 @@ type
|
|||||||
ServiceOptions: Integer;
|
ServiceOptions: Integer;
|
||||||
TargetID: LongWord;
|
TargetID: LongWord;
|
||||||
SourceID: LongWord;
|
SourceID: LongWord;
|
||||||
|
SelectedSlot: Integer;
|
||||||
|
VoiceFrameCount: QWord;
|
||||||
|
VoiceErrorCount: QWord;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TDMRDecoder = class;
|
TDMRDecoder = class;
|
||||||
@@ -111,6 +117,9 @@ type
|
|||||||
FLastLCSlot: Integer;
|
FLastLCSlot: Integer;
|
||||||
FColorCodeValid: Boolean;
|
FColorCodeValid: Boolean;
|
||||||
FColorCode, FLastDataType, FSlotTypeCorrectedBits: Integer;
|
FColorCode, FLastDataType, FSlotTypeCorrectedBits: Integer;
|
||||||
|
FSelectedSlot: Integer;
|
||||||
|
FVoiceFrameCount, FVoiceErrorCount: QWord;
|
||||||
|
FOnAudio: TDMRAudioEvent;
|
||||||
FBurstCount: QWord;
|
FBurstCount: QWord;
|
||||||
FRMSSq: Double;
|
FRMSSq: Double;
|
||||||
|
|
||||||
@@ -127,6 +136,7 @@ type
|
|||||||
function Digitize(Phase: Integer; Symbol: Double): Byte;
|
function Digitize(Phase: Integer; Symbol: Double): Byte;
|
||||||
procedure StartBurstTracking(Phase: Integer);
|
procedure StartBurstTracking(Phase: Integer);
|
||||||
procedure CompleteBurst;
|
procedure CompleteBurst;
|
||||||
|
procedure DecodeVoiceBurst;
|
||||||
procedure ResetDSP;
|
procedure ResetDSP;
|
||||||
public
|
public
|
||||||
constructor Create;
|
constructor Create;
|
||||||
@@ -140,6 +150,7 @@ type
|
|||||||
|
|
||||||
class function SyncKindName(Kind: TDMRSyncKind): string; static;
|
class function SyncKindName(Kind: TDMRSyncKind): string; static;
|
||||||
property Enabled: Boolean read FEnabled;
|
property Enabled: Boolean read FEnabled;
|
||||||
|
property OnAudio: TDMRAudioEvent read FOnAudio write FOnAudio;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -275,6 +286,10 @@ begin
|
|||||||
FColorCode := 0;
|
FColorCode := 0;
|
||||||
FLastDataType := 0;
|
FLastDataType := 0;
|
||||||
FSlotTypeCorrectedBits := 0;
|
FSlotTypeCorrectedBits := 0;
|
||||||
|
FSelectedSlot := -1;
|
||||||
|
FVoiceFrameCount := 0;
|
||||||
|
FVoiceErrorCount := 0;
|
||||||
|
DMRMbeReset(FMbe);
|
||||||
FTrackPhase := -1;
|
FTrackPhase := -1;
|
||||||
FCurrentBurstPos := 0;
|
FCurrentBurstPos := 0;
|
||||||
FBurstCount := 0;
|
FBurstCount := 0;
|
||||||
@@ -480,11 +495,51 @@ begin
|
|||||||
FLastLCSlot := 1
|
FLastLCSlot := 1
|
||||||
else
|
else
|
||||||
FLastLCSlot := 0;
|
FLastLCSlot := 0;
|
||||||
|
if FSelectedSlot < 0 then FSelectedSlot := FLastLCSlot;
|
||||||
end;
|
end;
|
||||||
|
DecodeVoiceBurst;
|
||||||
Inc(FBurstCount);
|
Inc(FBurstCount);
|
||||||
FCurrentBurstPos := 0;
|
FCurrentBurstPos := 0;
|
||||||
end;
|
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;
|
procedure TDMRDecoder.NoteSync(Phase: Integer; Kind: TDMRSyncKind;
|
||||||
Inverted: Boolean);
|
Inverted: Boolean);
|
||||||
var
|
var
|
||||||
@@ -559,6 +614,9 @@ begin
|
|||||||
S.ServiceOptions := FLastLinkControl.ServiceOptions;
|
S.ServiceOptions := FLastLinkControl.ServiceOptions;
|
||||||
S.TargetID := FLastLinkControl.TargetID;
|
S.TargetID := FLastLinkControl.TargetID;
|
||||||
S.SourceID := FLastLinkControl.SourceID;
|
S.SourceID := FLastLinkControl.SourceID;
|
||||||
|
S.SelectedSlot := FSelectedSlot;
|
||||||
|
S.VoiceFrameCount := FVoiceFrameCount;
|
||||||
|
S.VoiceErrorCount := FVoiceErrorCount;
|
||||||
finally
|
finally
|
||||||
FStatusLock.Leave;
|
FStatusLock.Leave;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+77
-3
@@ -3,9 +3,21 @@ unit DMRProtocol;
|
|||||||
{
|
{
|
||||||
Small, allocation-free DMR burst protocol layer.
|
Small, allocation-free DMR burst protocol layer.
|
||||||
|
|
||||||
The CACH interleave and Hamming/Golay generator matrices are derived from
|
The CACH/AMBE interleavers and Hamming/Golay matrices are derived from
|
||||||
dsd-fme (src/dmr_bs.c and src/fec.c). The original code is distributed
|
dsd-fme (src/dmr_bs.c, src/fec.c and include/dmr_const.h).
|
||||||
under the ISC licence; see the dsd-fme source tree for its full notice.
|
|
||||||
|
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}
|
{$IFDEF FPC}
|
||||||
@@ -25,6 +37,8 @@ type
|
|||||||
TDMRBit7 = array[0..6] of Byte;
|
TDMRBit7 = array[0..6] of Byte;
|
||||||
TDMRBit20 = array[0..19] of Byte;
|
TDMRBit20 = array[0..19] of Byte;
|
||||||
TDMRBit96 = array[0..95] of Byte;
|
TDMRBit96 = array[0..95] of Byte;
|
||||||
|
TDMRAMBEFrame = array[0..95] of Byte;
|
||||||
|
TDMRAMBEFrames = array[0..2] of TDMRAMBEFrame;
|
||||||
|
|
||||||
TDMRCACH = record
|
TDMRCACH = record
|
||||||
Valid: Boolean;
|
Valid: Boolean;
|
||||||
@@ -68,6 +82,10 @@ function DMRGolay208Encode(Value: Byte): TDMRBit20;
|
|||||||
procedure DMRBPTC19696Encode(const Payload: TDMRBit96;
|
procedure DMRBPTC19696Encode(const Payload: TDMRBit96;
|
||||||
out Burst: TDMRDibitBurst);
|
out Burst: TDMRDibitBurst);
|
||||||
procedure DMRFullLCSetParity(var Payload: TDMRBit96; DataType: Integer);
|
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 DMRDecodeCACH(const Burst: TDMRDibitBurst; out CACH: TDMRCACH): Boolean;
|
||||||
function DMRDecodeSlotType(const Burst: TDMRDibitBurst;
|
function DMRDecodeSlotType(const Burst: TDMRDibitBurst;
|
||||||
out SlotType: TDMRSlotType): Boolean;
|
out SlotType: TDMRSlotType): Boolean;
|
||||||
@@ -110,6 +128,19 @@ const
|
|||||||
HAMMING_15_11_SYNDROME: array[0..14] of Byte =
|
HAMMING_15_11_SYNDROME: array[0..14] of Byte =
|
||||||
(9,13,15,14,7,10,5,11,12,6,3, 8,4,2,1);
|
(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;
|
function DMRHamming74Encode(Value: Byte): TDMRBit7;
|
||||||
var
|
var
|
||||||
i, j: Integer;
|
i, j: Integer;
|
||||||
@@ -203,6 +234,49 @@ begin
|
|||||||
end;
|
end;
|
||||||
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;
|
function DecodeHamming74(const Received: TDMRBit7; out Value: Byte;
|
||||||
out Corrected: Integer): Boolean;
|
out Corrected: Integer): Boolean;
|
||||||
var
|
var
|
||||||
|
|||||||
+42
-1
@@ -405,6 +405,7 @@ type
|
|||||||
// вызывающим (GUI или демон) после CreateEngines.
|
// вызывающим (GUI или демон) после CreateEngines.
|
||||||
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
|
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
|
||||||
procedure OnDemodAudioReady(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 OnSpectrumReady(const Pixels: array of Single; Count: Integer);
|
||||||
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
||||||
procedure OnMicPacket(const Data: TMicDataPacket);
|
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.
|
// DMR front-end owns a worker thread. It stays idle until MODE_DMR is active.
|
||||||
FDMRDec := TDMRDecoder.Create;
|
FDMRDec := TDMRDecoder.Create;
|
||||||
|
FDMRDec.OnAudio := OnDMRAudioReady;
|
||||||
|
|
||||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||||
// формы / при подключении устройства).
|
// формы / при подключении устройства).
|
||||||
@@ -862,6 +864,13 @@ end;
|
|||||||
procedure TRadioController.FreeEngines;
|
procedure TRadioController.FreeEngines;
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
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
|
if Assigned(FNetwork) then
|
||||||
begin
|
begin
|
||||||
if FNetwork.Connected then FNetwork.Disconnect;
|
if FNetwork.Connected then FNetwork.Disconnect;
|
||||||
@@ -883,7 +892,6 @@ begin
|
|||||||
FDSPEngine.Close;
|
FDSPEngine.Close;
|
||||||
FreeAndNil(FDSPEngine);
|
FreeAndNil(FDSPEngine);
|
||||||
end;
|
end;
|
||||||
if Assigned(FDMRDec) then FreeAndNil(FDMRDec);
|
|
||||||
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1186,6 +1194,39 @@ begin
|
|||||||
FDMRDec.SetEnabled(False);
|
FDMRDec.SetEnabled(False);
|
||||||
end;
|
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)
|
// Мультислайсы (софт-fan-out)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ cmake -S native/dmr -B build/dmr -DCMAKE_BUILD_TYPE=Release
|
|||||||
cmake --build build/dmr
|
cmake --build build/dmr
|
||||||
```
|
```
|
||||||
|
|
||||||
The current ABI implements the stateful AMBE+2 vocoder boundary. The burst/FEC
|
The Pascal burst/FEC layer extracts three row-major `char[4][24]` AMBE+2
|
||||||
layer will feed its row-major `char[4][24]` output to this library. Keep the ABI
|
frames from each selected voice-slot burst and feeds them through this ABI.
|
||||||
versioned: the GUI and daemon must remain usable when the optional library is
|
Decoded 8 kHz mono PCM is resampled to EWSDR's 48 kHz speaker/web route.
|
||||||
not installed.
|
|
||||||
|
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
|
const
|
||||||
BS_VOICE_SYNC = '131111333113313313113313';
|
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);
|
procedure FeedSync(D: TDMRDecoder; const Pattern: string; Invert: Boolean);
|
||||||
var
|
var
|
||||||
L, R: array of Single;
|
L, R: array of Single;
|
||||||
@@ -95,9 +109,12 @@ end;
|
|||||||
var
|
var
|
||||||
D: TDMRDecoder;
|
D: TDMRDecoder;
|
||||||
S: TDMRStatus;
|
S: TDMRStatus;
|
||||||
|
Sink: TAudioSink;
|
||||||
begin
|
begin
|
||||||
|
Sink := TAudioSink.Create;
|
||||||
D := TDMRDecoder.Create;
|
D := TDMRDecoder.Create;
|
||||||
try
|
try
|
||||||
|
D.OnAudio := Sink.AudioReady;
|
||||||
D.SetEnabled(True);
|
D.SetEnabled(True);
|
||||||
FeedSync(D, BS_VOICE_SYNC, False);
|
FeedSync(D, BS_VOICE_SYNC, False);
|
||||||
Sleep(50);
|
Sleep(50);
|
||||||
@@ -124,8 +141,18 @@ begin
|
|||||||
D.SetInverted(False);
|
D.SetInverted(False);
|
||||||
D.SetEnabled(True);
|
D.SetEnabled(True);
|
||||||
TestBurst(D);
|
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');
|
WriteLn('DMR front-end sync tests passed');
|
||||||
finally
|
finally
|
||||||
D.Free;
|
D.Free;
|
||||||
|
Sink.Free;
|
||||||
end;
|
end;
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ var
|
|||||||
Burst: TDMRDibitBurst;
|
Burst: TDMRDibitBurst;
|
||||||
Info: TDMRBurstInfo;
|
Info: TDMRBurstInfo;
|
||||||
Payload: TDMRBit96;
|
Payload: TDMRBit96;
|
||||||
|
Frames: TDMRAMBEFrames;
|
||||||
|
VoiceDibits: array[0..107] of Byte;
|
||||||
i: Integer;
|
i: Integer;
|
||||||
begin
|
begin
|
||||||
FillChar(Burst, SizeOf(Burst), 0);
|
FillChar(Burst, SizeOf(Burst), 0);
|
||||||
@@ -155,5 +157,20 @@ begin
|
|||||||
DMRParseBurst(Burst, Info);
|
DMRParseBurst(Burst, Info);
|
||||||
Check(not Info.LinkControl.Decoded, 'invalid Full LC RS checksum accepted');
|
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.
|
end.
|
||||||
|
|||||||
Reference in New Issue
Block a user