feat(dmr): decode independent receiver slices

This commit is contained in:
2026-07-14 20:08:49 +03:00
parent 9e2f8c7743
commit bac0b7612e
5 changed files with 208 additions and 17 deletions
+133 -5
View File
@@ -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);