diff --git a/DMRBindings.pas b/DMRBindings.pas index 809c96c..54a0778 100644 --- a/DMRBindings.pas +++ b/DMRBindings.pas @@ -71,7 +71,7 @@ const {$ENDIF} var i: Integer; - LocalName: string; + LocalName, BuildName: string; begin if GLib <> dynlibs.NilHandle then Exit(True); GLastError := ''; @@ -79,6 +79,15 @@ begin begin LocalName := ExtractFilePath(ParamStr(0)) + Names[i]; GLib := LoadLibrary(LocalName); + // Lazarus debug binaries live in bin// while the documented + // optional adapter build lives in build/dmr. Make an in-tree build + // immediately usable without a system-wide cmake --install. + if GLib = dynlibs.NilHandle then + begin + BuildName := ExpandFileName(ExtractFilePath(ParamStr(0)) + + '../../build/dmr/' + Names[i]); + GLib := LoadLibrary(BuildName); + end; if GLib = dynlibs.NilHandle then GLib := LoadLibrary(Names[i]); if GLib <> dynlibs.NilHandle then Break; end; diff --git a/DMRDecoder.pas b/DMRDecoder.pas index 7bc6fdd..3adc847 100644 --- a/DMRDecoder.pas +++ b/DMRDecoder.pas @@ -139,6 +139,8 @@ type FInverted: Boolean; FSyncCount: QWord; FLastSyncTick: QWord; + FLastSyncSample: QWord; + FMobileActiveBurst: Boolean; function PopSamples(var Buf: array of Single): Integer; procedure ProcessSample(S: Single); @@ -170,6 +172,8 @@ implementation const DMR_SYNC_MASK = LongWord($00FFFFFF); DMR_SYNC_HOLD_MS = 500; + DMR_SYNC_MAX_ERRORS = 2; + DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000; // One bit per DSD-FME sync character: '1'=0, '3'=1. These are the eight // ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign. @@ -332,6 +336,8 @@ begin FInverted := False; FSyncCount := 0; FLastSyncTick := 0; + FLastSyncSample := 0; + FMobileActiveBurst := True; finally FStatusLock.Leave; end; @@ -426,6 +432,8 @@ var Pat: LongWord; Dibit: Byte; V: Double; + DeltaSamples, CycleOffset: QWord; + CandidateAllowed: Boolean; begin V := Symbol; if FInputInverted then V := -V; @@ -466,11 +474,29 @@ begin FBestSyncPhase := Phase; FBestCandidateKind := SYNC_KIND[i]; end; - end; - if FSyncShift[Phase] = Pat then - begin - NoteSync(Phase, SYNC_KIND[i], FInputInverted); - Exit; + // Real on-air sync commonly carries one or two sign errors. DSD-FME + // likewise uses a bounded sync tolerance; requiring an exact 24-symbol + // match lost almost every superframe in recorded handheld-radio IQ. + DeltaSamples := FSampleNo - FLastSyncSample; + CandidateAllowed := (FLastSyncSample = 0) or + (DeltaSamples > DMR_INPUT_RATE * 2) or + (SYNC_KIND[i] = FSyncKind); + // MS voice/data sync repeats every 360 ms. Cadence gating rejects + // chance <=2-bit matches inside AMBE payload while still accepting a + // superframe after one or more missed sync words. + if CandidateAllowed and (FLastSyncSample <> 0) and + (FSyncKind in [dskMSData, dskMSVoice]) and + (SYNC_KIND[i] = FSyncKind) and (DeltaSamples > 20) then + begin + CycleOffset := DeltaSamples mod DMR_SUPERFRAME_SAMPLES; + CandidateAllowed := (CycleOffset <= 20) or + (CycleOffset >= DMR_SUPERFRAME_SAMPLES - 20); + end; + if (Distance <= DMR_SYNC_MAX_ERRORS) and CandidateAllowed then + begin + NoteSync(Phase, SYNC_KIND[i], FInputInverted); + Exit; + end; end; end; end; @@ -542,6 +568,8 @@ begin end; end; DecodeVoiceBurst; + if FSyncKind in [dskMSData, dskMSVoice] then + FMobileActiveBurst := not FMobileActiveBurst; Inc(FBurstCount); FCurrentBurstPos := 0; end; @@ -558,6 +586,10 @@ begin (GetTickCount64 - FLastSyncTick > DMR_SYNC_HOLD_MS) then Exit; if not (FSyncKind in [dskBSVoice, dskMSVoice, dskDirectTS1Voice, dskDirectTS2Voice]) then Exit; + // Mobile simplex occupies one 30 ms half-slot and leaves the alternating + // half-slot idle. A 144-dibit continuous assembler sees both; only every + // other record contains the three AMBE frames. + if (FSyncKind = dskMSVoice) and not FMobileActiveBurst then Exit; if FSyncKind in [dskBSVoice] then begin @@ -566,6 +598,7 @@ begin end else if FSyncKind = dskDirectTS2Voice then Slot := 1 else Slot := 0; + if FSyncKind = dskMSVoice then FSelectedSlot := 0; NowTick := GetTickCount64; if (FSelectedSlot < 0) or ((Slot <> FSelectedSlot) and (FLastSelectedVoiceTick <> 0) and @@ -608,7 +641,8 @@ begin // Adjacent timing hypotheses recognize the same physical burst within one // sample period. Keep the phase with the largest 24-symbol eye opening, // not merely the first phase that happened to match the sign pattern. - if (FLastSyncTick <> 0) and (NowTick - FLastSyncTick < 5) then + if (FLastSyncSample <> 0) and + (FSampleNo - FLastSyncSample <= DMR_SAMPLES_PER_SYM * 2) then begin if FMagSum[Phase] > FBestSyncQuality then begin @@ -623,7 +657,9 @@ begin FInverted := Inverted; Inc(FSyncCount); FLastSyncTick := NowTick; + FLastSyncSample := FSampleNo; FBestSyncQuality := FMagSum[Phase]; + if Kind in [dskMSData, dskMSVoice] then FMobileActiveBurst := True; StartBurstTracking(Phase); finally FStatusLock.Leave; diff --git a/RadioController.pas b/RadioController.pas index 41d7fd4..9b386a7 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -859,6 +859,10 @@ begin // DMR front-end owns a worker thread. It stays idle until MODE_DMR is active. FDMRDiag := TDMRDiagnosticCapture.Create; FDMRDec := TDMRDecoder.Create; + // WDSP's FM discriminator polarity on the EWSDR RX path is opposite to the + // DMR dibit convention: captured handheld MS voice sync appears as MS data + // without this inversion (the two sync words are complements). + FDMRDec.SetInverted(True); FDMRDec.OnAudio := OnDMRAudioReady; FDMRDec.OnBurst := OnDMRBurstReady; diff --git a/tools/dmr_replay.pas b/tools/dmr_replay.pas index 78f69ed..33027d1 100644 --- a/tools/dmr_replay.pas +++ b/tools/dmr_replay.pas @@ -72,6 +72,10 @@ begin WriteLn('last_sync=', TDMRDecoder.SyncKindName(Status.SyncKind)); WriteLn('bursts=', Counter.Count); WriteLn('dropped_samples=', Status.DroppedSamples); + WriteLn('selected_slot=', Status.SelectedSlot + 1); + WriteLn('voice_frames=', Status.VoiceFrameCount); + WriteLn('voice_errors=', Status.VoiceErrorCount); + WriteLn('vocoder_available=', Status.VocoderAvailable); if Status.CACHValid then WriteLn('timeslot=', Status.Slot + 1); if Status.SlotTypeValid then WriteLn('color_code=', Status.ColorCode, ' data_type=', Status.DataType);