From bac0b7612ef53be27b5eab26af36653e71f52d5d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 14 Jul 2026 20:08:49 +0300 Subject: [PATCH] feat(dmr): decode independent receiver slices --- DMRBindings.pas | 14 +++- DMRDecoder.pas | 10 ++- RadioController.pas | 138 ++++++++++++++++++++++++++++++++++-- WDSPEngine.pas | 58 ++++++++++++--- tests/dmr_bindings_test.pas | 5 ++ 5 files changed, 208 insertions(+), 17 deletions(-) diff --git a/DMRBindings.pas b/DMRBindings.pas index 54a0778..443153b 100644 --- a/DMRBindings.pas +++ b/DMRBindings.pas @@ -50,6 +50,7 @@ var GMbeDestroy: TMbeDestroy = nil; GMbeReset: TMbeReset = nil; GMbeDecode: TMbeDecode = nil; + GLoadRefs: Integer = 0; function Symbol(const Name: PChar): Pointer; begin @@ -73,7 +74,11 @@ var i: Integer; LocalName, BuildName: string; begin - if GLib <> dynlibs.NilHandle then Exit(True); + if GLib <> dynlibs.NilHandle then + begin + Inc(GLoadRefs); + Exit(True); + end; GLastError := ''; for i := Low(Names) to High(Names) do begin @@ -115,11 +120,18 @@ begin DMRUnload; Exit(False); end; + GLoadRefs := 1; Result := True; end; procedure DMRUnload; begin + if GLoadRefs > 1 then + begin + Dec(GLoadRefs); + Exit; + end; + GLoadRefs := 0; GAbiVersion := nil; GMbeCreate := nil; GMbeDestroy := nil; diff --git a/DMRDecoder.pas b/DMRDecoder.pas index 3d8dc19..010e81a 100644 --- a/DMRDecoder.pas +++ b/DMRDecoder.pas @@ -97,6 +97,7 @@ type FWake: PRTLEvent; FThread: TDMRDecoderThread; FMbe: Pointer; + FBindingsLoaded: Boolean; FRing: array[0..DMR_RING_SIZE - 1] of Single; FReadPos, FWritePos: Integer; FDropped: QWord; @@ -272,7 +273,8 @@ begin FStatusLock := TCriticalSection.Create; FWake := RTLEventCreate; FThread := TDMRDecoderThread.Create(Self); - if DMRLoad then FMbe := DMRMbeCreate(3); + FBindingsLoaded := DMRLoad; + if FBindingsLoaded then FMbe := DMRMbeCreate(3); ResetDSP; FThread.Start; end; @@ -288,7 +290,11 @@ begin end; DMRMbeDestroy(FMbe); FMbe := nil; - DMRUnload; + if FBindingsLoaded then + begin + DMRUnload; + FBindingsLoaded := False; + end; RTLEventDestroy(FWake); FStatusLock.Free; FDSPLock.Free; diff --git a/RadioController.pas b/RadioController.pas index 9b386a7..aa60f32 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -128,6 +128,22 @@ type PLLLock: Boolean; end; + TRadioController = class; + + // Event adapter: every DMR slice owns a decoder, while its asynchronous + // audio callback still has to retain the originating logical slice id. + TDMRSliceDecoder = class + private + FOwner: TRadioController; + FSliceId: Integer; + FDecoder: TDMRDecoder; + procedure AudioReady(const PCM: array of Single; Count: Integer); + public + constructor Create(AOwner: TRadioController; ASliceId: Integer); + destructor Destroy; override; + procedure FeedAudio(const Left, Right: array of Single; Count: Integer); + end; + // Слайс уровня контроллера: логическая обёртка над WDSP-слайс-каналом // (в TWDSPEngine) + собственный аудио-выход со своей звуковухой. TCtrlSlice = record @@ -147,6 +163,7 @@ type DeviceIndex: Integer; // PortAudio-индекс (-1 = default) DevName: string; Audio: TAudioOutput; // владеет; свой поток вывода + DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса end; { TRadioController } @@ -166,6 +183,10 @@ type function FindSliceIndex(Id: Integer): Integer; // -1 если нет procedure OnSliceAudioReady(SliceId: Integer; const Left, Right: array of Single; Count: Integer); + procedure OnSliceDemodAudioReady(SliceId: Integer; + const Left, Right: array of Single; Count: Integer); + procedure OnSliceDMRAudioReady(SliceId: Integer; + const PCM: array of Single; Count: Integer); procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра // --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) --- procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст @@ -794,6 +815,43 @@ const // (TUNLevel), падение уровня IQ ~1.6дБ компенсируется калибровкой TUNLevel. TUN_TONE_MAG = 0.83; +constructor TDMRSliceDecoder.Create(AOwner: TRadioController; + ASliceId: Integer); +begin + inherited Create; + FOwner := AOwner; + FSliceId := ASliceId; + FDecoder := TDMRDecoder.Create; + FDecoder.SetInverted(True); + FDecoder.OnAudio := AudioReady; + FDecoder.SetEnabled(True); +end; + +destructor TDMRSliceDecoder.Destroy; +begin + if Assigned(FDecoder) then + begin + FDecoder.SetEnabled(False); + FDecoder.OnAudio := nil; + FDecoder.OnBurst := nil; + FreeAndNil(FDecoder); + end; + FOwner := nil; + inherited Destroy; +end; + +procedure TDMRSliceDecoder.FeedAudio(const Left, Right: array of Single; + Count: Integer); +begin + if Assigned(FDecoder) then FDecoder.FeedAudio(Left, Right, Count); +end; + +procedure TDMRSliceDecoder.AudioReady(const PCM: array of Single; + Count: Integer); +begin + if Assigned(FOwner) then FOwner.OnSliceDMRAudioReady(FSliceId, PCM, Count); +end; + constructor TRadioController.Create; begin inherited Create; @@ -839,6 +897,7 @@ begin // Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не // зависит от фронтенда, в отличие от OnAudio/OnSpectrum). FDSPEngine.OnSliceAudio := OnSliceAudioReady; + FDSPEngine.OnSliceDemodAudio := OnSliceDemodAudioReady; FDSPEngine.OnDemodAudio := OnDemodAudioReady; FDSPEngine.SetDisplayResLimit(FDisplayPixels); @@ -876,6 +935,12 @@ end; procedure TRadioController.FreeEngines; var i: Integer; begin + // Stop new slice callbacks before their decoder/audio destinations vanish. + if Assigned(FDSPEngine) then + begin + FDSPEngine.OnSliceAudio := nil; + FDSPEngine.OnSliceDemodAudio := nil; + end; // Stop the DMR worker before destroying any of its audio destinations. if Assigned(FDMRDec) then begin @@ -890,12 +955,21 @@ begin if FNetwork.Connected then FNetwork.Disconnect; FreeAndNil(FNetwork); end; - // Слайсы: закрываем их аудио-выходы (WDSP-каналы закроет FDSPEngine.Close ниже). + // RemoveSlice takes the DSP slice lock. Besides closing the WDSP channel it + // waits out a callback that might already have entered before we detached it. + if Assigned(FDSPEngine) then + for i := 0 to MAX_SLICES - 1 do + if FSlices[i].Active then FDSPEngine.RemoveSlice(FSlices[i].Id); + // Слайсы: каналы уже закрыты, останавливаем декодеры и аудио. for i := 0 to MAX_SLICES - 1 do - if FSlices[i].Active and (FSlices[i].Audio <> nil) then + if FSlices[i].Active then begin - FSlices[i].Audio.Close; - FreeAndNil(FSlices[i].Audio); + FreeAndNil(FSlices[i].DMR); + if FSlices[i].Audio <> nil then + begin + FSlices[i].Audio.Close; + FreeAndNil(FSlices[i].Audio); + end; FSlices[i].Active := False; end; if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end; @@ -1274,10 +1348,55 @@ begin if not FLocalAudio then Exit; idx := FindSliceIndex(SliceId); if idx < 0 then Exit; + if FSlices[idx].Mode = MODE_DMR then Exit; if FSlices[idx].Audio <> nil then FSlices[idx].Audio.Write(Left, Right, Count); end; +// DSP thread: flat, volume-independent discriminator of the corresponding +// WDSP slice channel. FeedAudio only copies it into that decoder's ring. +procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer; + const Left, Right: array of Single; Count: Integer); +var idx: Integer; +begin + idx := FindSliceIndex(SliceId); + if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) then Exit; + if Assigned(FSlices[idx].DMR) then + FSlices[idx].DMR.FeedAudio(Left, Right, Count); +end; + +// DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output. +procedure TRadioController.OnSliceDMRAudioReady(SliceId: Integer; + const PCM: array of Single; Count: Integer); +var + Left, Right: array of Single; + idx, i, Phase, OutPos, N: Integer; + A, B, V, Gain: Single; +begin + if not FLocalAudio or (Count <= 0) then Exit; + idx := FindSliceIndex(SliceId); + if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) or + FSlices[idx].Muted or not Assigned(FSlices[idx].Audio) then Exit; + N := Min(Count, Length(PCM)); + SetLength(Left, N * 6); + SetLength(Right, N * 6); + Gain := FSlices[idx].Volume; + 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; + FSlices[idx].Audio.Write(Left, Right, OutPos); +end; + procedure TRadioController.RefreshSliceShifts; // Смена центра ГЛАВНОГО захвата: пересчитываем только слайсы пана 0 — // слайсы доп. панов привязаны к центрам своих DDC (SetPanDDCFreq). @@ -1684,6 +1803,9 @@ begin FSlices[slot].DeviceIndex := DevIndex; FSlices[slot].DevName := DevName; FSlices[slot].Audio := AO; + FSlices[slot].DMR := nil; + if Mode = MODE_DMR then + FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id); Result := Id; Changed(rfDevice); @@ -1697,6 +1819,7 @@ begin // Удаляемый слайс был источником TX — вернуть передачу на главный VFO. if FTxSliceId = Id then SetTxSlice(0); if Assigned(FDSPEngine) then FDSPEngine.RemoveSlice(Id); + FreeAndNil(FSlices[idx].DMR); if FSlices[idx].Audio <> nil then begin FSlices[idx].Audio.Close; @@ -1717,12 +1840,17 @@ begin end; procedure TRadioController.SetSliceMode(Id, Mode: Integer); -var idx: Integer; +var idx, OldMode: Integer; begin idx := FindSliceIndex(Id); if idx < 0 then Exit; + OldMode := FSlices[idx].Mode; + if (Mode = MODE_DMR) and not Assigned(FSlices[idx].DMR) then + FSlices[idx].DMR := TDMRSliceDecoder.Create(Self, Id); FSlices[idx].Mode := Mode; if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode); + if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then + FreeAndNil(FSlices[idx].DMR); end; procedure TRadioController.SetSliceFilter(Id, Low, High: Integer); diff --git a/WDSPEngine.pas b/WDSPEngine.pas index c8144ee..6a20289 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -337,6 +337,7 @@ type FSliceOutL: array of Single; // деинтерливленный L для callback FSliceOutR: array of Single; // R FOnSliceAudio: TOnSliceAudio; + FOnSliceDemodAudio: TOnSliceAudio; // pre-volume/mute tap for digital slices // --- Панадаптеры на аппаратных DDC (этап 3.2) --- FPans: array[1..MAX_PANS-1] of TPanChan; @@ -452,6 +453,7 @@ type function EffectiveTXNC: Integer; procedure ApplyDSPRate(Mode: Integer); procedure ApplyRXWFMToChan(Chan: Integer); + procedure ApplyRXDMRToChan(Chan: Integer); procedure ApplyWFMSettings; // --- Мультислайсы --- function FindSliceIdx(Id: Integer): Integer; // -1 если нет @@ -687,6 +689,8 @@ type property OnAudio: TOnAudioReady read FOnAudio write FOnAudio; property OnDemodAudio: TOnAudioReady read FOnDemodAudio write FOnDemodAudio; property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio; + property OnSliceDemodAudio: TOnSliceAudio + read FOnSliceDemodAudio write FOnSliceDemodAudio; property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum; property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall; // Пиксели доп. панов (display-поток). UI вешает при появлении панов (3.3). @@ -1061,6 +1065,14 @@ begin SetRXAWFMDeemphTau (Chan, WFM_TAU_DE_US); end; +procedure TWDSPEngine.ApplyRXDMRToChan(Chan: Integer); +begin + // DMR needs the flat 4FSK discriminator, not broadcast-FM deemphasis. + SetRXAWFMDeviation (Chan, DMR_DEVIATION); + SetRXAWFMAFFilter (Chan, 20.0, 7000.0); + SetRXAWFMDeemphRun (Chan, 0); +end; + procedure TWDSPEngine.ApplyWFMSettings; begin ApplyRXWFMToChan(RXA_CHAN); @@ -1177,6 +1189,7 @@ begin FSliceLock := TCriticalSection.Create; FillChar(FSlices, SizeOf(FSlices), 0); FOnSliceAudio := nil; + FOnSliceDemodAudio := nil; AllocSliceBuffers; // TX инициализация @@ -1945,7 +1958,7 @@ begin if Pan^.AccPos >= Pan^.BufSize then begin Pan^.AccPos := 0; - if Assigned(FOnSliceAudio) then + if Assigned(FOnSliceAudio) or Assigned(FOnSliceDemodAudio) then try ProcessSlicesFor(PanId, Pan^.AccI, Pan^.AccQ, Pan^.BufSize); except @@ -2232,6 +2245,7 @@ begin SetRXABandpassWindow(Chan, 1); SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan); + if FSlices[Idx].Mode = MODE_DMR then ApplyRXDMRToChan(Chan); SetRXAShiftRun(Chan, 1); SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate), @@ -2243,7 +2257,11 @@ begin ApplySNBToChan(Chan, FSlices[Idx].SNBOn); ApplyANFToChan(Chan, FSlices[Idx].ANFOn); ApplyFMSQToChan(Chan, FSlices[Idx].FMSQOn, FSlices[Idx].FMSQLevel); - SetRXAPanelGain1(Chan, FSlices[Idx].Volume); + // Decoder input must not disappear or change scale with speaker volume. + if FSlices[Idx].Mode = MODE_DMR then + SetRXAPanelGain1(Chan, 1.0) + else + SetRXAPanelGain1(Chan, FSlices[Idx].Volume); SetRXAPanelSelect(Chan, 3); SetRXAPanelRun(Chan, 1); SetChannelState(Chan, 1, 0); @@ -2268,7 +2286,7 @@ end; // после основного канала. procedure TWDSPEngine.ProcessSlices; begin - if not Assigned(FOnSliceAudio) then Exit; + if not Assigned(FOnSliceAudio) and not Assigned(FOnSliceDemodAudio) then Exit; FSliceLock.Enter; try ProcessSlicesFor(0, FRXAccI, FRXAccQ, FBufSize); @@ -2289,7 +2307,8 @@ begin begin if not (FSlices[s].Active and FSlices[s].Opened) then Continue; if FSlices[s].PanId <> PanId then Continue; - if FSlices[s].Muted then Continue; + // Muting a DMR speaker must not make its decoder lose synchronization. + if FSlices[s].Muted and (FSlices[s].Mode <> MODE_DMR) then Continue; // Свежая копия входного IQ — fexchange0 портит вход in-place. for i := 0 to N - 1 do begin @@ -2300,13 +2319,28 @@ begin fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err); // S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор. FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV); - V := FSlices[s].Volume; for k := 0 to FAudioBufSize - 1 do begin - FSliceOutL[k] := FSliceOut[k * 2] * V; - FSliceOutR[k] := FSliceOut[k * 2 + 1] * V; + FSliceOutL[k] := FSliceOut[k * 2]; + FSliceOutR[k] := FSliceOut[k * 2 + 1]; + end; + if FSlices[s].Mode = MODE_DMR then + begin + if Assigned(FOnSliceDemodAudio) then + FOnSliceDemodAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, + FAudioBufSize); + Continue; // never route the raw 4FSK discriminator to the speaker + end; + if Assigned(FOnSliceAudio) then + begin + V := FSlices[s].Volume; + for k := 0 to FAudioBufSize - 1 do + begin + FSliceOutL[k] := FSliceOutL[k] * V; + FSliceOutR[k] := FSliceOutR[k] * V; + end; + FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize); end; - FOnSliceAudio(FSlices[s].Id, FSliceOutL, FSliceOutR, FAudioBufSize); end; end; @@ -2515,6 +2549,11 @@ begin begin SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode)); if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan); + if Mode = MODE_DMR then ApplyRXDMRToChan(FSlices[idx].Chan); + if Mode = MODE_DMR then + SetRXAPanelGain1(FSlices[idx].Chan, 1.0) + else + SetRXAPanelGain1(FSlices[idx].Chan, FSlices[idx].Volume); end; finally FSliceLock.Leave; @@ -2560,7 +2599,8 @@ begin try idx := FindSliceIdx(Id); if idx < 0 then Exit; FSlices[idx].Volume := Vol; - if FSlices[idx].Opened then SetRXAPanelGain1(FSlices[idx].Chan, Vol); + if FSlices[idx].Opened and (FSlices[idx].Mode <> MODE_DMR) then + SetRXAPanelGain1(FSlices[idx].Chan, Vol); finally FSliceLock.Leave; end; diff --git a/tests/dmr_bindings_test.pas b/tests/dmr_bindings_test.pas index 5603736..abd5941 100644 --- a/tests/dmr_bindings_test.pas +++ b/tests/dmr_bindings_test.pas @@ -16,6 +16,10 @@ begin WriteLn(DMRLastError); Halt(1); end; + // Several simultaneous slice decoders share one dynamically loaded adapter. + if not DMRLoad then Halt(4); + DMRUnload; + if not DMRLoaded then Halt(5); Decoder := DMRMbeCreate(3); if Decoder = nil then Halt(2); try @@ -27,5 +31,6 @@ begin DMRMbeDestroy(Decoder); DMRUnload; end; + if DMRLoaded then Halt(6); WriteLn('DMR native bindings tests passed'); end.