mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(dmr): add diagnostic capture probes
This commit is contained in:
@@ -109,6 +109,7 @@ PROJECT_NOTES_FOR_CLAUDE.md
|
|||||||
*.app/
|
*.app/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
|
/build/
|
||||||
wdspWisdom00
|
wdspWisdom00
|
||||||
*.log
|
*.log
|
||||||
ewsdr
|
ewsdr
|
||||||
|
|||||||
@@ -14,3 +14,13 @@ 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
|
||||||
LD_LIBRARY_PATH=build/dmr /tmp/dmr_frontend_test
|
LD_LIBRARY_PATH=build/dmr /tmp/dmr_frontend_test
|
||||||
|
fpc -Fu. -FE/tmp tests/dmr_diagnostics_test.pas
|
||||||
|
/tmp/dmr_diagnostics_test
|
||||||
|
|
||||||
|
# Replay a captured 48 kHz discriminator stream through the DMR decoder
|
||||||
|
fpc -Fu. -FE/tmp tools/dmr_replay.pas
|
||||||
|
/tmp/dmr_replay /tmp/ewsdr-dmr-*.demod_f32le
|
||||||
|
|
||||||
|
# Convert exact 24-bit network IQ to GNU Radio/inspectrum complex float32
|
||||||
|
fpc -FE/tmp tools/iq24be_to_cf32.pas
|
||||||
|
/tmp/iq24be_to_cf32 /tmp/ewsdr-dmr-*.iq24be
|
||||||
|
|||||||
+45
-1
@@ -31,6 +31,7 @@ const
|
|||||||
|
|
||||||
type
|
type
|
||||||
TDMRAudioEvent = procedure(const PCM: array of Single; Count: Integer) of object;
|
TDMRAudioEvent = procedure(const PCM: array of Single; Count: Integer) of object;
|
||||||
|
TDMRBurstEvent = procedure(const Dibits: array of Byte; Count: Integer) of object;
|
||||||
|
|
||||||
TDMRSyncKind = (
|
TDMRSyncKind = (
|
||||||
dskNone,
|
dskNone,
|
||||||
@@ -68,6 +69,11 @@ type
|
|||||||
SelectedSlot: Integer;
|
SelectedSlot: Integer;
|
||||||
VoiceFrameCount: QWord;
|
VoiceFrameCount: QWord;
|
||||||
VoiceErrorCount: QWord;
|
VoiceErrorCount: QWord;
|
||||||
|
BestSyncDistance: Integer;
|
||||||
|
BestSyncPhase: Integer;
|
||||||
|
BestCandidateKind: TDMRSyncKind;
|
||||||
|
SymbolOuterLevel: Single;
|
||||||
|
SignalMean: Single;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TDMRDecoder = class;
|
TDMRDecoder = class;
|
||||||
@@ -121,8 +127,12 @@ type
|
|||||||
FLastSelectedVoiceTick: QWord;
|
FLastSelectedVoiceTick: QWord;
|
||||||
FVoiceFrameCount, FVoiceErrorCount: QWord;
|
FVoiceFrameCount, FVoiceErrorCount: QWord;
|
||||||
FOnAudio: TDMRAudioEvent;
|
FOnAudio: TDMRAudioEvent;
|
||||||
|
FOnBurst: TDMRBurstEvent;
|
||||||
FBurstCount: QWord;
|
FBurstCount: QWord;
|
||||||
FRMSSq: Double;
|
FRMSSq: Double;
|
||||||
|
FSignalMean: Double;
|
||||||
|
FBestSyncDistance, FBestSyncPhase: Integer;
|
||||||
|
FBestCandidateKind: TDMRSyncKind;
|
||||||
|
|
||||||
FSyncKind: TDMRSyncKind;
|
FSyncKind: TDMRSyncKind;
|
||||||
FInputInverted: Boolean;
|
FInputInverted: Boolean;
|
||||||
@@ -152,6 +162,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;
|
property OnAudio: TDMRAudioEvent read FOnAudio write FOnAudio;
|
||||||
|
property OnBurst: TDMRBurstEvent read FOnBurst write FOnBurst;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -191,6 +202,17 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function PopCount24(V: LongWord): Integer;
|
||||||
|
begin
|
||||||
|
V := V and DMR_SYNC_MASK;
|
||||||
|
Result := 0;
|
||||||
|
while V <> 0 do
|
||||||
|
begin
|
||||||
|
V := V and (V - 1);
|
||||||
|
Inc(Result);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
constructor TDMRDecoderThread.Create(AOwner: TDMRDecoder);
|
constructor TDMRDecoderThread.Create(AOwner: TDMRDecoder);
|
||||||
begin
|
begin
|
||||||
inherited Create(True);
|
inherited Create(True);
|
||||||
@@ -300,6 +322,10 @@ begin
|
|||||||
FWindowSum := 0;
|
FWindowSum := 0;
|
||||||
FSampleNo := 0;
|
FSampleNo := 0;
|
||||||
FRMSSq := 0;
|
FRMSSq := 0;
|
||||||
|
FSignalMean := 0;
|
||||||
|
FBestSyncDistance := 25;
|
||||||
|
FBestSyncPhase := -1;
|
||||||
|
FBestCandidateKind := dskNone;
|
||||||
FStatusLock.Enter;
|
FStatusLock.Enter;
|
||||||
try
|
try
|
||||||
FSyncKind := dskNone;
|
FSyncKind := dskNone;
|
||||||
@@ -379,6 +405,7 @@ var
|
|||||||
Sym: Double;
|
Sym: Double;
|
||||||
begin
|
begin
|
||||||
FRMSSq := FRMSSq + 0.001 * (S * S - FRMSSq);
|
FRMSSq := FRMSSq + 0.001 * (S * S - FRMSSq);
|
||||||
|
FSignalMean := FSignalMean + 0.001 * (S - FSignalMean);
|
||||||
FWindowSum := FWindowSum - FSampleWindow[FWindowPos] + S;
|
FWindowSum := FWindowSum - FSampleWindow[FWindowPos] + S;
|
||||||
FSampleWindow[FWindowPos] := S;
|
FSampleWindow[FWindowPos] := S;
|
||||||
FWindowPos := (FWindowPos + 1) mod DMR_SAMPLES_PER_SYM;
|
FWindowPos := (FWindowPos + 1) mod DMR_SAMPLES_PER_SYM;
|
||||||
@@ -395,7 +422,7 @@ end;
|
|||||||
|
|
||||||
procedure TDMRDecoder.ProcessSymbol(Phase: Integer; Symbol: Double);
|
procedure TDMRDecoder.ProcessSymbol(Phase: Integer; Symbol: Double);
|
||||||
var
|
var
|
||||||
i: Integer;
|
i, Distance: Integer;
|
||||||
Pat: LongWord;
|
Pat: LongWord;
|
||||||
Dibit: Byte;
|
Dibit: Byte;
|
||||||
V: Double;
|
V: Double;
|
||||||
@@ -430,6 +457,16 @@ begin
|
|||||||
for i := 0 to High(SYNC_TEXT) do
|
for i := 0 to High(SYNC_TEXT) do
|
||||||
begin
|
begin
|
||||||
Pat := SyncBits(SYNC_TEXT[i]);
|
Pat := SyncBits(SYNC_TEXT[i]);
|
||||||
|
if FHistoryCount[Phase] >= 24 then
|
||||||
|
begin
|
||||||
|
Distance := PopCount24(FSyncShift[Phase] xor Pat);
|
||||||
|
if Distance < FBestSyncDistance then
|
||||||
|
begin
|
||||||
|
FBestSyncDistance := Distance;
|
||||||
|
FBestSyncPhase := Phase;
|
||||||
|
FBestCandidateKind := SYNC_KIND[i];
|
||||||
|
end;
|
||||||
|
end;
|
||||||
if FSyncShift[Phase] = Pat then
|
if FSyncShift[Phase] = Pat then
|
||||||
begin
|
begin
|
||||||
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
|
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
|
||||||
@@ -478,6 +515,7 @@ procedure TDMRDecoder.CompleteBurst;
|
|||||||
begin
|
begin
|
||||||
Move(FCurrentBurst[0], FLastBurst[0], SizeOf(FLastBurst));
|
Move(FCurrentBurst[0], FLastBurst[0], SizeOf(FLastBurst));
|
||||||
DMRParseBurst(FLastBurst, FLastBurstInfo);
|
DMRParseBurst(FLastBurst, FLastBurstInfo);
|
||||||
|
if Assigned(FOnBurst) then FOnBurst(FLastBurst, Length(FLastBurst));
|
||||||
if FLastBurstInfo.SlotType.Valid and
|
if FLastBurstInfo.SlotType.Valid and
|
||||||
(FSyncKind in [dskBSData, dskMSData, dskDirectTS1Data,
|
(FSyncKind in [dskBSData, dskMSData, dskDirectTS1Data,
|
||||||
dskDirectTS2Data]) then
|
dskDirectTS2Data]) then
|
||||||
@@ -644,6 +682,12 @@ begin
|
|||||||
S.SelectedSlot := FSelectedSlot;
|
S.SelectedSlot := FSelectedSlot;
|
||||||
S.VoiceFrameCount := FVoiceFrameCount;
|
S.VoiceFrameCount := FVoiceFrameCount;
|
||||||
S.VoiceErrorCount := FVoiceErrorCount;
|
S.VoiceErrorCount := FVoiceErrorCount;
|
||||||
|
S.BestSyncDistance := FBestSyncDistance;
|
||||||
|
S.BestSyncPhase := FBestSyncPhase;
|
||||||
|
S.BestCandidateKind := FBestCandidateKind;
|
||||||
|
if FBestSyncPhase >= 0 then
|
||||||
|
S.SymbolOuterLevel := FOuterLevel[FBestSyncPhase];
|
||||||
|
S.SignalMean := FSignalMean;
|
||||||
finally
|
finally
|
||||||
FStatusLock.Leave;
|
FStatusLock.Leave;
|
||||||
end;
|
end;
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
unit DMRDiagnostics;
|
||||||
|
|
||||||
|
{$IFDEF FPC}
|
||||||
|
{$MODE Delphi}
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils, SyncObjs;
|
||||||
|
|
||||||
|
type
|
||||||
|
TDMRDiagnosticCapture = class;
|
||||||
|
|
||||||
|
TDMRDumpWriter = class(TThread)
|
||||||
|
private
|
||||||
|
FOwner: TDMRDiagnosticCapture;
|
||||||
|
protected
|
||||||
|
procedure Execute; override;
|
||||||
|
public
|
||||||
|
constructor Create(AOwner: TDMRDiagnosticCapture);
|
||||||
|
end;
|
||||||
|
|
||||||
|
TDMRDiagnosticCapture = class
|
||||||
|
private
|
||||||
|
FLock: TCriticalSection;
|
||||||
|
FWake: PRTLEvent;
|
||||||
|
FThread: TDMRDumpWriter;
|
||||||
|
FAccepting: Boolean;
|
||||||
|
FDeadline: QWord;
|
||||||
|
FBasePath: string;
|
||||||
|
FIQ, FDemod, FBursts: TBytes;
|
||||||
|
FIQRead, FIQWrite, FDemodRead, FDemodWrite: Integer;
|
||||||
|
FBurstRead, FBurstWrite: Integer;
|
||||||
|
FIQDropped, FDemodDropped, FBurstDropped: QWord;
|
||||||
|
FIQStream, FDemodStream, FBurstStream: TFileStream;
|
||||||
|
function RingWrite(var Ring: TBytes; var ReadPos, WritePos: Integer;
|
||||||
|
Source: Pointer; Count: Integer; var Dropped: QWord): Boolean;
|
||||||
|
function RingRead(var Ring: TBytes; var ReadPos, WritePos: Integer;
|
||||||
|
Dest: Pointer; MaxCount: Integer): Integer;
|
||||||
|
function RingsEmpty: Boolean;
|
||||||
|
procedure WriterExecute;
|
||||||
|
procedure CloseStreams;
|
||||||
|
procedure WriteStats;
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
destructor Destroy; override;
|
||||||
|
function Start(DurationSeconds: Integer; SampleRate: Integer;
|
||||||
|
CenterHz, VfoHz: Double; const Directory: string = ''): string;
|
||||||
|
procedure Stop;
|
||||||
|
procedure FeedIQ24BE(const Data: array of Byte; Count: Integer);
|
||||||
|
procedure FeedDemod(const Left, Right: array of Single; Count: Integer);
|
||||||
|
procedure FeedBurst(const Dibits: array of Byte; Count: Integer);
|
||||||
|
function Active: Boolean;
|
||||||
|
property BasePath: string read FBasePath;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
const
|
||||||
|
IQ_RING_BYTES = 32 * 1024 * 1024;
|
||||||
|
DEMOD_RING_BYTES = 4 * 1024 * 1024;
|
||||||
|
BURST_RING_BYTES = 1024 * 1024;
|
||||||
|
WRITER_BLOCK_BYTES = 256 * 1024;
|
||||||
|
|
||||||
|
constructor TDMRDumpWriter.Create(AOwner: TDMRDiagnosticCapture);
|
||||||
|
begin
|
||||||
|
inherited Create(True);
|
||||||
|
FreeOnTerminate := False;
|
||||||
|
FOwner := AOwner;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDumpWriter.Execute;
|
||||||
|
begin
|
||||||
|
FOwner.WriterExecute;
|
||||||
|
end;
|
||||||
|
|
||||||
|
constructor TDMRDiagnosticCapture.Create;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FLock := TCriticalSection.Create;
|
||||||
|
FWake := RTLEventCreate;
|
||||||
|
SetLength(FIQ, IQ_RING_BYTES);
|
||||||
|
SetLength(FDemod, DEMOD_RING_BYTES);
|
||||||
|
SetLength(FBursts, BURST_RING_BYTES);
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TDMRDiagnosticCapture.Destroy;
|
||||||
|
begin
|
||||||
|
Stop;
|
||||||
|
SetLength(FIQ, 0);
|
||||||
|
SetLength(FDemod, 0);
|
||||||
|
SetLength(FBursts, 0);
|
||||||
|
RTLEventDestroy(FWake);
|
||||||
|
FLock.Free;
|
||||||
|
inherited Destroy;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDMRDiagnosticCapture.RingWrite(var Ring: TBytes;
|
||||||
|
var ReadPos, WritePos: Integer; Source: Pointer; Count: Integer;
|
||||||
|
var Dropped: QWord): Boolean;
|
||||||
|
var
|
||||||
|
FreeBytes, First: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if (Source = nil) or (Count <= 0) or (Length(Ring) = 0) then Exit;
|
||||||
|
if WritePos >= ReadPos then FreeBytes := Length(Ring) - (WritePos - ReadPos) - 1
|
||||||
|
else FreeBytes := ReadPos - WritePos - 1;
|
||||||
|
if Count > FreeBytes then
|
||||||
|
begin
|
||||||
|
Inc(Dropped, Count);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
First := Count;
|
||||||
|
if First > Length(Ring) - WritePos then First := Length(Ring) - WritePos;
|
||||||
|
Move(Source^, Ring[WritePos], First);
|
||||||
|
if Count > First then
|
||||||
|
Move(PByte(Source)[First], Ring[0], Count - First);
|
||||||
|
WritePos := (WritePos + Count) mod Length(Ring);
|
||||||
|
Result := True;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDMRDiagnosticCapture.RingRead(var Ring: TBytes;
|
||||||
|
var ReadPos, WritePos: Integer; Dest: Pointer; MaxCount: Integer): Integer;
|
||||||
|
var
|
||||||
|
Available, First: Integer;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
if (Dest = nil) or (MaxCount <= 0) then Exit;
|
||||||
|
if WritePos >= ReadPos then Available := WritePos - ReadPos
|
||||||
|
else Available := Length(Ring) - ReadPos + WritePos;
|
||||||
|
Result := Available;
|
||||||
|
if Result > MaxCount then Result := MaxCount;
|
||||||
|
First := Result;
|
||||||
|
if First > Length(Ring) - ReadPos then First := Length(Ring) - ReadPos;
|
||||||
|
if First > 0 then Move(Ring[ReadPos], Dest^, First);
|
||||||
|
if Result > First then Move(Ring[0], PByte(Dest)[First], Result - First);
|
||||||
|
ReadPos := (ReadPos + Result) mod Length(Ring);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDMRDiagnosticCapture.RingsEmpty: Boolean;
|
||||||
|
begin
|
||||||
|
Result := (FIQRead = FIQWrite) and (FDemodRead = FDemodWrite) and
|
||||||
|
(FBurstRead = FBurstWrite);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDiagnosticCapture.CloseStreams;
|
||||||
|
begin
|
||||||
|
FreeAndNil(FIQStream);
|
||||||
|
FreeAndNil(FDemodStream);
|
||||||
|
FreeAndNil(FBurstStream);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDiagnosticCapture.WriteStats;
|
||||||
|
var
|
||||||
|
Stats: TStringList;
|
||||||
|
begin
|
||||||
|
if FBasePath = '' then Exit;
|
||||||
|
Stats := TStringList.Create;
|
||||||
|
try
|
||||||
|
Stats.Add(Format('iq_dropped_bytes=%d', [FIQDropped]));
|
||||||
|
Stats.Add(Format('demod_dropped_bytes=%d', [FDemodDropped]));
|
||||||
|
Stats.Add(Format('burst_dropped_bytes=%d', [FBurstDropped]));
|
||||||
|
Stats.SaveToFile(FBasePath + '.stats.txt');
|
||||||
|
finally
|
||||||
|
Stats.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDMRDiagnosticCapture.Start(DurationSeconds: Integer;
|
||||||
|
SampleRate: Integer; CenterHz, VfoHz: Double; const Directory: string): string;
|
||||||
|
var
|
||||||
|
Dir, Stamp: string;
|
||||||
|
Meta: TStringList;
|
||||||
|
begin
|
||||||
|
Stop;
|
||||||
|
if DurationSeconds < 1 then DurationSeconds := 1;
|
||||||
|
if Directory <> '' then Dir := IncludeTrailingPathDelimiter(Directory)
|
||||||
|
else Dir := IncludeTrailingPathDelimiter(GetTempDir(False));
|
||||||
|
ForceDirectories(Dir);
|
||||||
|
Stamp := FormatDateTime('yyyymmdd-hhnnss-zzz', Now);
|
||||||
|
FBasePath := Dir + 'ewsdr-dmr-' + Stamp;
|
||||||
|
FIQStream := TFileStream.Create(FBasePath + '.iq24be', fmCreate);
|
||||||
|
FDemodStream := TFileStream.Create(FBasePath + '.demod_f32le', fmCreate);
|
||||||
|
FBurstStream := TFileStream.Create(FBasePath + '.bursts_u8', fmCreate);
|
||||||
|
|
||||||
|
Meta := TStringList.Create;
|
||||||
|
try
|
||||||
|
Meta.Add('{');
|
||||||
|
Meta.Add(' "format": "EWSDR DMR diagnostic capture v1",');
|
||||||
|
Meta.Add(' "iq_format": "interleaved signed 24-bit big-endian I,Q",');
|
||||||
|
Meta.Add(' "demod_format": "mono float32 little-endian, 48000 Hz",');
|
||||||
|
Meta.Add(' "burst_format": "144 uint8 dibits per record",');
|
||||||
|
Meta.Add(Format(' "iq_sample_rate": %d,', [SampleRate]));
|
||||||
|
Meta.Add(Format(' "center_hz": %.0f,', [CenterHz]));
|
||||||
|
Meta.Add(Format(' "vfo_hz": %.0f,', [VfoHz]));
|
||||||
|
Meta.Add(Format(' "duration_seconds": %d', [DurationSeconds]));
|
||||||
|
Meta.Add('}');
|
||||||
|
Meta.SaveToFile(FBasePath + '.json');
|
||||||
|
finally
|
||||||
|
Meta.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FIQRead := 0; FIQWrite := 0;
|
||||||
|
FDemodRead := 0; FDemodWrite := 0;
|
||||||
|
FBurstRead := 0; FBurstWrite := 0;
|
||||||
|
FIQDropped := 0; FDemodDropped := 0; FBurstDropped := 0;
|
||||||
|
FDeadline := GetTickCount64 + QWord(DurationSeconds) * 1000;
|
||||||
|
FAccepting := True;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
FThread := TDMRDumpWriter.Create(Self);
|
||||||
|
FThread.Start;
|
||||||
|
Result := FBasePath;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDiagnosticCapture.Stop;
|
||||||
|
begin
|
||||||
|
if FThread = nil then
|
||||||
|
begin
|
||||||
|
CloseStreams;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
FAccepting := False;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
RTLEventSetEvent(FWake);
|
||||||
|
FThread.WaitFor;
|
||||||
|
FreeAndNil(FThread);
|
||||||
|
CloseStreams;
|
||||||
|
WriteStats;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDiagnosticCapture.WriterExecute;
|
||||||
|
var
|
||||||
|
IQBuf, DemodBuf, BurstBuf: TBytes;
|
||||||
|
NIQ, NDemod, NBurst: Integer;
|
||||||
|
AcceptingNow, EmptyNow: Boolean;
|
||||||
|
begin
|
||||||
|
SetLength(IQBuf, WRITER_BLOCK_BYTES);
|
||||||
|
SetLength(DemodBuf, WRITER_BLOCK_BYTES);
|
||||||
|
SetLength(BurstBuf, WRITER_BLOCK_BYTES);
|
||||||
|
repeat
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if FAccepting and (GetTickCount64 >= FDeadline) then FAccepting := False;
|
||||||
|
NIQ := RingRead(FIQ, FIQRead, FIQWrite, @IQBuf[0], Length(IQBuf));
|
||||||
|
NDemod := RingRead(FDemod, FDemodRead, FDemodWrite,
|
||||||
|
@DemodBuf[0], Length(DemodBuf));
|
||||||
|
NBurst := RingRead(FBursts, FBurstRead, FBurstWrite,
|
||||||
|
@BurstBuf[0], Length(BurstBuf));
|
||||||
|
AcceptingNow := FAccepting;
|
||||||
|
EmptyNow := RingsEmpty;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if NIQ > 0 then FIQStream.WriteBuffer(IQBuf[0], NIQ);
|
||||||
|
if NDemod > 0 then FDemodStream.WriteBuffer(DemodBuf[0], NDemod);
|
||||||
|
if NBurst > 0 then FBurstStream.WriteBuffer(BurstBuf[0], NBurst);
|
||||||
|
if (NIQ = 0) and (NDemod = 0) and (NBurst = 0) and AcceptingNow then
|
||||||
|
RTLEventWaitFor(FWake, 100);
|
||||||
|
until (not AcceptingNow) and EmptyNow;
|
||||||
|
CloseStreams;
|
||||||
|
WriteStats;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDiagnosticCapture.FeedIQ24BE(const Data: array of Byte;
|
||||||
|
Count: Integer);
|
||||||
|
var
|
||||||
|
Wrote: Boolean;
|
||||||
|
begin
|
||||||
|
if Count > Length(Data) then Count := Length(Data);
|
||||||
|
if Count <= 0 then Exit;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if not FAccepting then Exit;
|
||||||
|
Wrote := RingWrite(FIQ, FIQRead, FIQWrite, @Data[0], Count, FIQDropped);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if Wrote then RTLEventSetEvent(FWake);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDiagnosticCapture.FeedDemod(const Left, Right: array of Single;
|
||||||
|
Count: Integer);
|
||||||
|
var
|
||||||
|
Mono: array[0..2047] of Single;
|
||||||
|
i, N: Integer;
|
||||||
|
Wrote: Boolean;
|
||||||
|
begin
|
||||||
|
N := Count;
|
||||||
|
if N > Length(Left) then N := Length(Left);
|
||||||
|
if N > Length(Right) then N := Length(Right);
|
||||||
|
if N > Length(Mono) then N := Length(Mono);
|
||||||
|
if N <= 0 then Exit;
|
||||||
|
for i := 0 to N - 1 do Mono[i] := 0.5 * (Left[i] + Right[i]);
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if not FAccepting then Exit;
|
||||||
|
Wrote := RingWrite(FDemod, FDemodRead, FDemodWrite, @Mono[0],
|
||||||
|
N * SizeOf(Single), FDemodDropped);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if Wrote then RTLEventSetEvent(FWake);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDMRDiagnosticCapture.FeedBurst(const Dibits: array of Byte;
|
||||||
|
Count: Integer);
|
||||||
|
var
|
||||||
|
Wrote: Boolean;
|
||||||
|
begin
|
||||||
|
if Count > Length(Dibits) then Count := Length(Dibits);
|
||||||
|
if Count <= 0 then Exit;
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if not FAccepting then Exit;
|
||||||
|
Wrote := RingWrite(FBursts, FBurstRead, FBurstWrite, @Dibits[0], Count,
|
||||||
|
FBurstDropped);
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
if Wrote then RTLEventSetEvent(FWake);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDMRDiagnosticCapture.Active: Boolean;
|
||||||
|
begin
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
Result := FAccepting;
|
||||||
|
finally
|
||||||
|
FLock.Leave;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
+16
-2
@@ -3591,7 +3591,8 @@ var
|
|||||||
begin
|
begin
|
||||||
if not FController.GetDMRStatus(S) then Exit('DMR unavailable');
|
if not FController.GetDMRStatus(S) then Exit('DMR unavailable');
|
||||||
if not S.Enabled then Exit('DMR off');
|
if not S.Enabled then Exit('DMR off');
|
||||||
if not S.Synced then Exit(Format('DMR sync... %.3f', [S.SignalRMS]));
|
if not S.Synced then
|
||||||
|
Exit(Format('DMR sync d%d %.3f', [S.BestSyncDistance, S.SignalRMS]));
|
||||||
if S.Inverted then Inv := ' INV' else Inv := '';
|
if S.Inverted then Inv := ' INV' else Inv := '';
|
||||||
Details := '';
|
Details := '';
|
||||||
if S.LinkControlValid then Details := Format(' TS%d', [S.LCSlot + 1])
|
if S.LinkControlValid then Details := Format(' TS%d', [S.LCSlot + 1])
|
||||||
@@ -4930,10 +4931,23 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BtnModeClick(Sender: TObject);
|
procedure TMainForm.BtnModeClick(Sender: TObject);
|
||||||
|
var
|
||||||
|
Mode: Integer;
|
||||||
|
Path: string;
|
||||||
begin
|
begin
|
||||||
|
Mode := (Sender as TFlatButton).Tag;
|
||||||
|
if (Mode = MODE_DMR) and (FController.FMode = MODE_DMR) then
|
||||||
|
begin
|
||||||
|
Path := FController.StartDMRDiagnostics(15);
|
||||||
|
if Path <> '' then
|
||||||
|
ShowMessage('DMR diagnostic capture started for 15 seconds.' +
|
||||||
|
LineEnding + 'Transmit from the radio now.' + LineEnding + LineEnding +
|
||||||
|
'Files: ' + Path + '.*');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
// Логика (FMode, дефолтный фильтр, DSP SetMode/TUN/ApplyModeFilter, band cache)
|
// Логика (FMode, дефолтный фильтр, DSP SetMode/TUN/ApplyModeFilter, band cache)
|
||||||
// — в контроллере; рендер кнопок/фильтра/спектра — в OnControllerState(rfMode).
|
// — в контроллере; рендер кнопок/фильтра/спектра — в OnControllerState(rfMode).
|
||||||
FController.SetMode((Sender as TFlatButton).Tag);
|
FController.SetMode(Mode);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
procedure TMainForm.BtnFilterClick(Sender: TObject);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ uses
|
|||||||
Classes, SysUtils, Math,
|
Classes, SysUtils, Math,
|
||||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
||||||
|
DMRDiagnostics,
|
||||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||||
|
|
||||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||||
@@ -251,6 +252,7 @@ type
|
|||||||
|
|
||||||
// ---- DMR 4FSK receive decoder ----
|
// ---- DMR 4FSK receive decoder ----
|
||||||
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||||||
|
FDMRDiag: TDMRDiagnosticCapture; // bounded async IQ/demod/burst recorder
|
||||||
|
|
||||||
// ---- Аудио / TX ----
|
// ---- Аудио / TX ----
|
||||||
FVolume: Integer; // громкость RX
|
FVolume: Integer; // громкость RX
|
||||||
@@ -406,6 +408,7 @@ type
|
|||||||
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 OnDMRAudioReady(const PCM: array of Single; Count: Integer);
|
||||||
|
procedure OnDMRBurstReady(const Dibits: array of Byte; 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);
|
||||||
@@ -676,6 +679,9 @@ type
|
|||||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||||
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||||
|
function StartDMRDiagnostics(DurationSeconds: Integer = 15): string;
|
||||||
|
procedure StopDMRDiagnostics;
|
||||||
|
function DMRDiagnosticsActive: Boolean;
|
||||||
|
|
||||||
// Отображение / приём
|
// Отображение / приём
|
||||||
procedure SetCTun(On_: Boolean);
|
procedure SetCTun(On_: Boolean);
|
||||||
@@ -851,8 +857,10 @@ begin
|
|||||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||||
|
|
||||||
// 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.
|
||||||
|
FDMRDiag := TDMRDiagnosticCapture.Create;
|
||||||
FDMRDec := TDMRDecoder.Create;
|
FDMRDec := TDMRDecoder.Create;
|
||||||
FDMRDec.OnAudio := OnDMRAudioReady;
|
FDMRDec.OnAudio := OnDMRAudioReady;
|
||||||
|
FDMRDec.OnBurst := OnDMRBurstReady;
|
||||||
|
|
||||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||||
// формы / при подключении устройства).
|
// формы / при подключении устройства).
|
||||||
@@ -869,8 +877,10 @@ begin
|
|||||||
begin
|
begin
|
||||||
FDMRDec.SetEnabled(False);
|
FDMRDec.SetEnabled(False);
|
||||||
FDMRDec.OnAudio := nil;
|
FDMRDec.OnAudio := nil;
|
||||||
|
FDMRDec.OnBurst := nil;
|
||||||
FreeAndNil(FDMRDec);
|
FreeAndNil(FDMRDec);
|
||||||
end;
|
end;
|
||||||
|
FreeAndNil(FDMRDiag);
|
||||||
if Assigned(FNetwork) then
|
if Assigned(FNetwork) then
|
||||||
begin
|
begin
|
||||||
if FNetwork.Connected then FNetwork.Disconnect;
|
if FNetwork.Connected then FNetwork.Disconnect;
|
||||||
@@ -982,6 +992,9 @@ begin
|
|||||||
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
|
SamplesPerFrame := (Integer(Data.SamplesPerFrame[0]) shl 8) or
|
||||||
Integer(Data.SamplesPerFrame[1]);
|
Integer(Data.SamplesPerFrame[1]);
|
||||||
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
|
if SamplesPerFrame <= 0 then SamplesPerFrame := 238; // fallback
|
||||||
|
if (DDCIndex = FActiveDDC) and (FMode = MODE_DMR) and
|
||||||
|
Assigned(FDMRDiag) and FDMRDiag.Active then
|
||||||
|
FDMRDiag.FeedIQ24BE(Data.IQData, SamplesPerFrame * 6);
|
||||||
if DDCIndex = FActiveDDC then
|
if DDCIndex = FActiveDDC then
|
||||||
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
|
FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
|
||||||
else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS)
|
else if (DDCIndex >= 0) and (DDCIndex < MAX_DDCS)
|
||||||
@@ -1187,6 +1200,8 @@ begin
|
|||||||
if not Assigned(FDMRDec) then Exit;
|
if not Assigned(FDMRDec) then Exit;
|
||||||
if FMode = MODE_DMR then
|
if FMode = MODE_DMR then
|
||||||
begin
|
begin
|
||||||
|
if Assigned(FDMRDiag) and FDMRDiag.Active then
|
||||||
|
FDMRDiag.FeedDemod(Left, Right, Count);
|
||||||
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
|
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
|
||||||
FDMRDec.FeedAudio(Left, Right, Count);
|
FDMRDec.FeedAudio(Left, Right, Count);
|
||||||
end
|
end
|
||||||
@@ -1194,6 +1209,13 @@ begin
|
|||||||
FDMRDec.SetEnabled(False);
|
FDMRDec.SetEnabled(False);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.OnDMRBurstReady(const Dibits: array of Byte;
|
||||||
|
Count: Integer);
|
||||||
|
begin
|
||||||
|
if Assigned(FDMRDiag) and FDMRDiag.Active then
|
||||||
|
FDMRDiag.FeedBurst(Dibits, Count);
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TRadioController.OnDMRAudioReady(const PCM: array of Single;
|
procedure TRadioController.OnDMRAudioReady(const PCM: array of Single;
|
||||||
Count: Integer);
|
Count: Integer);
|
||||||
// DMR worker thread, 8 kHz mono from mbelib. Linear 6x interpolation brings
|
// DMR worker thread, 8 kHz mono from mbelib. Linear 6x interpolation brings
|
||||||
@@ -2233,6 +2255,23 @@ begin
|
|||||||
else FillChar(S, SizeOf(S), 0);
|
else FillChar(S, SizeOf(S), 0);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TRadioController.StartDMRDiagnostics(DurationSeconds: Integer): string;
|
||||||
|
begin
|
||||||
|
if not Assigned(FDMRDiag) then Exit('');
|
||||||
|
Result := FDMRDiag.Start(DurationSeconds, FSampleRate, FCenterFreq,
|
||||||
|
ActiveVfoHz);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.StopDMRDiagnostics;
|
||||||
|
begin
|
||||||
|
if Assigned(FDMRDiag) then FDMRDiag.Stop;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRadioController.DMRDiagnosticsActive: Boolean;
|
||||||
|
begin
|
||||||
|
Result := Assigned(FDMRDiag) and FDMRDiag.Active;
|
||||||
|
end;
|
||||||
|
|
||||||
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||||||
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||||
var Half: Integer;
|
var Half: Integer;
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
program dmr_diagnostics_test;
|
||||||
|
|
||||||
|
{$MODE Delphi}
|
||||||
|
|
||||||
|
uses
|
||||||
|
{$IFDEF UNIX}cthreads,{$ENDIF}
|
||||||
|
Classes, SysUtils, DMRDiagnostics;
|
||||||
|
|
||||||
|
procedure Check(Condition: Boolean; const Message_: string);
|
||||||
|
begin
|
||||||
|
if not Condition then
|
||||||
|
begin
|
||||||
|
WriteLn(Message_);
|
||||||
|
Halt(1);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function SizeOfFile(const Name: string): Int64;
|
||||||
|
var
|
||||||
|
S: TFileStream;
|
||||||
|
begin
|
||||||
|
S := TFileStream.Create(Name, fmOpenRead or fmShareDenyNone);
|
||||||
|
try
|
||||||
|
Result := S.Size;
|
||||||
|
finally
|
||||||
|
S.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
D: TDMRDiagnosticCapture;
|
||||||
|
IQ: array[0..11] of Byte;
|
||||||
|
L, R: array[0..1] of Single;
|
||||||
|
Burst: array[0..143] of Byte;
|
||||||
|
Base: string;
|
||||||
|
i: Integer;
|
||||||
|
begin
|
||||||
|
for i := 0 to High(IQ) do IQ[i] := i;
|
||||||
|
L[0] := 0.25; L[1] := -0.5;
|
||||||
|
R[0] := 0.75; R[1] := 0.5;
|
||||||
|
for i := 0 to High(Burst) do Burst[i] := i and 3;
|
||||||
|
D := TDMRDiagnosticCapture.Create;
|
||||||
|
try
|
||||||
|
Base := D.Start(2, 192000, 145500000, 145500000);
|
||||||
|
D.FeedIQ24BE(IQ, Length(IQ));
|
||||||
|
D.FeedDemod(L, R, Length(L));
|
||||||
|
D.FeedBurst(Burst, Length(Burst));
|
||||||
|
D.Stop;
|
||||||
|
Check(SizeOfFile(Base + '.iq24be') = Length(IQ), 'IQ dump size mismatch');
|
||||||
|
Check(SizeOfFile(Base + '.demod_f32le') =
|
||||||
|
Length(L) * SizeOf(Single), 'demod dump size mismatch');
|
||||||
|
Check(SizeOfFile(Base + '.bursts_u8') = Length(Burst),
|
||||||
|
'burst dump size mismatch');
|
||||||
|
Check(FileExists(Base + '.json'), 'metadata was not written');
|
||||||
|
Check(FileExists(Base + '.stats.txt'), 'drop statistics were not written');
|
||||||
|
DeleteFile(Base + '.iq24be');
|
||||||
|
DeleteFile(Base + '.demod_f32le');
|
||||||
|
DeleteFile(Base + '.bursts_u8');
|
||||||
|
DeleteFile(Base + '.json');
|
||||||
|
DeleteFile(Base + '.stats.txt');
|
||||||
|
WriteLn('DMR diagnostic capture test passed');
|
||||||
|
finally
|
||||||
|
D.Free;
|
||||||
|
end;
|
||||||
|
end.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
program dmr_replay;
|
||||||
|
|
||||||
|
{$MODE Delphi}
|
||||||
|
|
||||||
|
uses
|
||||||
|
{$IFDEF UNIX}cthreads,{$ENDIF}
|
||||||
|
Classes, SysUtils, DMRDecoder;
|
||||||
|
|
||||||
|
type
|
||||||
|
TBurstCounter = class
|
||||||
|
public
|
||||||
|
Count: QWord;
|
||||||
|
procedure BurstReady(const Dibits: array of Byte; DibitCount: Integer);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TBurstCounter.BurstReady(const Dibits: array of Byte;
|
||||||
|
DibitCount: Integer);
|
||||||
|
begin
|
||||||
|
if DibitCount = DMR_BURST_DIBITS then Inc(Count);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure Usage;
|
||||||
|
begin
|
||||||
|
WriteLn('Usage: dmr_replay <capture.demod_f32le> [--invert]');
|
||||||
|
Halt(2);
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
Stream: TFileStream;
|
||||||
|
Decoder: TDMRDecoder;
|
||||||
|
Counter: TBurstCounter;
|
||||||
|
Samples, Right: array[0..2047] of Single;
|
||||||
|
Status: TDMRStatus;
|
||||||
|
BytesRead, N, i: Integer;
|
||||||
|
TotalSamples: QWord;
|
||||||
|
Invert: Boolean;
|
||||||
|
begin
|
||||||
|
if ParamCount < 1 then Usage;
|
||||||
|
Invert := (ParamCount >= 2) and (ParamStr(2) = '--invert');
|
||||||
|
Stream := TFileStream.Create(ParamStr(1), fmOpenRead or fmShareDenyNone);
|
||||||
|
Counter := TBurstCounter.Create;
|
||||||
|
Decoder := TDMRDecoder.Create;
|
||||||
|
try
|
||||||
|
Decoder.OnBurst := Counter.BurstReady;
|
||||||
|
Decoder.SetInverted(Invert);
|
||||||
|
Decoder.SetEnabled(True);
|
||||||
|
TotalSamples := 0;
|
||||||
|
repeat
|
||||||
|
BytesRead := Stream.Read(Samples[0], SizeOf(Samples));
|
||||||
|
N := BytesRead div SizeOf(Single);
|
||||||
|
if N <= 0 then Break;
|
||||||
|
for i := 0 to N - 1 do Right[i] := Samples[i];
|
||||||
|
Decoder.FeedAudio(Samples, Right, N);
|
||||||
|
Inc(TotalSamples, N);
|
||||||
|
// Keep the bounded realtime ring from becoming the replay bottleneck.
|
||||||
|
Sleep(2);
|
||||||
|
until BytesRead < SizeOf(Samples);
|
||||||
|
Sleep(500);
|
||||||
|
Decoder.GetStatus(Status);
|
||||||
|
WriteLn('file=', ParamStr(1));
|
||||||
|
WriteLn('seconds=', FormatFloat('0.000', TotalSamples / DMR_INPUT_RATE));
|
||||||
|
WriteLn('invert=', Invert);
|
||||||
|
WriteLn('rms=', FormatFloat('0.000000', Status.SignalRMS));
|
||||||
|
WriteLn('sync_count=', Status.SyncCount);
|
||||||
|
WriteLn('best_sync_distance=', Status.BestSyncDistance);
|
||||||
|
WriteLn('best_sync_phase=', Status.BestSyncPhase);
|
||||||
|
WriteLn('best_candidate=',
|
||||||
|
TDMRDecoder.SyncKindName(Status.BestCandidateKind));
|
||||||
|
WriteLn('symbol_outer_level=',
|
||||||
|
FormatFloat('0.000000', Status.SymbolOuterLevel));
|
||||||
|
WriteLn('dc_mean=', FormatFloat('0.000000', Status.SignalMean));
|
||||||
|
WriteLn('last_sync=', TDMRDecoder.SyncKindName(Status.SyncKind));
|
||||||
|
WriteLn('bursts=', Counter.Count);
|
||||||
|
WriteLn('dropped_samples=', Status.DroppedSamples);
|
||||||
|
if Status.CACHValid then WriteLn('timeslot=', Status.Slot + 1);
|
||||||
|
if Status.SlotTypeValid then
|
||||||
|
WriteLn('color_code=', Status.ColorCode, ' data_type=', Status.DataType);
|
||||||
|
if Status.LinkControlValid then
|
||||||
|
WriteLn('lc_slot=', Status.LCSlot + 1, ' target=', Status.TargetID,
|
||||||
|
' source=', Status.SourceID, ' service_options=', Status.ServiceOptions);
|
||||||
|
finally
|
||||||
|
Decoder.Free;
|
||||||
|
Counter.Free;
|
||||||
|
Stream.Free;
|
||||||
|
end;
|
||||||
|
end.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
program iq24be_to_cf32;
|
||||||
|
|
||||||
|
{$MODE Delphi}
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils;
|
||||||
|
|
||||||
|
const
|
||||||
|
PAIRS_PER_BLOCK = 8192;
|
||||||
|
SCALE = 1.0 / 8388608.0;
|
||||||
|
|
||||||
|
var
|
||||||
|
Input, Output: TFileStream;
|
||||||
|
Raw: array[0..PAIRS_PER_BLOCK * 6 - 1] of Byte;
|
||||||
|
IQ: array[0..PAIRS_PER_BLOCK * 2 - 1] of Single;
|
||||||
|
InputName, OutputName: string;
|
||||||
|
BytesRead, Pairs, i, p: Integer;
|
||||||
|
I24, Q24: LongInt;
|
||||||
|
TotalPairs: QWord;
|
||||||
|
begin
|
||||||
|
if ParamCount < 1 then
|
||||||
|
begin
|
||||||
|
WriteLn('Usage: iq24be_to_cf32 <capture.iq24be> [output.cf32le]');
|
||||||
|
Halt(2);
|
||||||
|
end;
|
||||||
|
InputName := ParamStr(1);
|
||||||
|
if ParamCount >= 2 then OutputName := ParamStr(2)
|
||||||
|
else OutputName := ChangeFileExt(InputName, '.cf32le');
|
||||||
|
Input := TFileStream.Create(InputName, fmOpenRead or fmShareDenyNone);
|
||||||
|
Output := TFileStream.Create(OutputName, fmCreate);
|
||||||
|
try
|
||||||
|
TotalPairs := 0;
|
||||||
|
repeat
|
||||||
|
BytesRead := Input.Read(Raw[0], SizeOf(Raw));
|
||||||
|
Pairs := BytesRead div 6;
|
||||||
|
p := 0;
|
||||||
|
for i := 0 to Pairs - 1 do
|
||||||
|
begin
|
||||||
|
I24 := (LongInt(Raw[p]) shl 16) or (LongInt(Raw[p + 1]) shl 8) or
|
||||||
|
LongInt(Raw[p + 2]);
|
||||||
|
Q24 := (LongInt(Raw[p + 3]) shl 16) or (LongInt(Raw[p + 4]) shl 8) or
|
||||||
|
LongInt(Raw[p + 5]);
|
||||||
|
if (I24 and $800000) <> 0 then I24 := I24 or LongInt($FF000000);
|
||||||
|
if (Q24 and $800000) <> 0 then Q24 := Q24 or LongInt($FF000000);
|
||||||
|
IQ[i * 2] := I24 * SCALE;
|
||||||
|
IQ[i * 2 + 1] := Q24 * SCALE;
|
||||||
|
Inc(p, 6);
|
||||||
|
end;
|
||||||
|
if Pairs > 0 then Output.WriteBuffer(IQ[0], Pairs * 2 * SizeOf(Single));
|
||||||
|
Inc(TotalPairs, Pairs);
|
||||||
|
until BytesRead < SizeOf(Raw);
|
||||||
|
WriteLn('output=', OutputName);
|
||||||
|
WriteLn('iq_pairs=', TotalPairs);
|
||||||
|
finally
|
||||||
|
Output.Free;
|
||||||
|
Input.Free;
|
||||||
|
end;
|
||||||
|
end.
|
||||||
Reference in New Issue
Block a user