mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
Merge feature/slice-audio-input: устройство ВВОДА на каждом слайсе
Вкладки OUT/IN в fly-out AUD флага, mic-пул с refcount (переключение TX-слайса = смена указателя, без Close/Open — не даёт щелчков), слайс в FM RAW можно назначать источником передачи, персист in_dev_name. Плюс удобства AUD-панели: открывается на выбранном устройстве и прокручивается колесом мыши. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+43
-13
@@ -599,6 +599,7 @@ type
|
|||||||
procedure OnVfoOverlayVolume(Vol: Integer);
|
procedure OnVfoOverlayVolume(Vol: Integer);
|
||||||
procedure OnVfoOverlayMute(Sender: TObject);
|
procedure OnVfoOverlayMute(Sender: TObject);
|
||||||
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
procedure OnVfoOverlayAudioDevice(DevIndex: Integer; const DevName: string);
|
||||||
|
procedure OnVfoOverlayAudioInDevice(DevIndex: Integer; const DevName: string);
|
||||||
procedure OnVfoOverlaySplit(Sender: TObject);
|
procedure OnVfoOverlaySplit(Sender: TObject);
|
||||||
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
procedure OnVfoOverlayTxSelect(Sender: TObject);
|
||||||
procedure OnVfoOverlayRxMuteTx(Sender: TObject); // RXM — приём слайса на TX
|
procedure OnVfoOverlayRxMuteTx(Sender: TObject); // RXM — приём слайса на TX
|
||||||
@@ -3838,8 +3839,10 @@ function TMainForm.DefaultMicSource: TTXMicSource;
|
|||||||
// иначе используем HW mic-стрим от трансивера.
|
// иначе используем HW mic-стрим от трансивера.
|
||||||
// HW PTT всегда переключает на txmsRadio непосредственно в ApplyMOX.
|
// HW PTT всегда переключает на txmsRadio непосредственно в ApplyMOX.
|
||||||
begin
|
begin
|
||||||
if FController.FAudioInDevName <> '' then Result := txmsSoundCard
|
// Учитываем вход TX-слайса: у него может быть свой (виртуальный кабель),
|
||||||
else Result := txmsRadio;
|
// даже когда общего входа нет — иначе взяли бы микрофон радио.
|
||||||
|
if FController.ActiveMicInDevName <> '' then Result := txmsSoundCard
|
||||||
|
else Result := txmsRadio;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// ApplyTXSettingsToDSP переехал в TRadioController (вся TX-цепь над FTXSettings).
|
// ApplyTXSettingsToDSP переехал в TRadioController (вся TX-цепь над FTXSettings).
|
||||||
@@ -5739,6 +5742,20 @@ begin
|
|||||||
FPan.PushMainFlagAudioDevices; // обновить подсветку текущего устройства
|
FPan.PushMainFlagAudioDevices; // обновить подсветку текущего устройства
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnVfoOverlayAudioInDevice(DevIndex: Integer; const DevName: string);
|
||||||
|
// Вкладка IN во флаге: откуда слайс берёт модуляцию на передаче. У флага
|
||||||
|
// главного VFO это общий вход приложения (то же поле, что в настройках).
|
||||||
|
begin
|
||||||
|
if CurSliceId <> 0 then
|
||||||
|
begin
|
||||||
|
FController.SetSliceInDevice(CurSliceId, DevIndex, DevName);
|
||||||
|
PushFlagStateAllPans(CurSliceId);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
ApplyAudioInputDevice(DevIndex, DevName);
|
||||||
|
FPan.PushMainFlagAudioDevices;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
procedure TMainForm.OnVfoOverlayInvalidate(Sender: TObject);
|
||||||
var P: TPanafallPanel;
|
var P: TPanafallPanel;
|
||||||
begin
|
begin
|
||||||
@@ -5797,6 +5814,7 @@ begin
|
|||||||
O.OnVolume := OnVfoOverlayVolume;
|
O.OnVolume := OnVfoOverlayVolume;
|
||||||
O.OnMute := OnVfoOverlayMute;
|
O.OnMute := OnVfoOverlayMute;
|
||||||
O.OnAudioDevice := OnVfoOverlayAudioDevice;
|
O.OnAudioDevice := OnVfoOverlayAudioDevice;
|
||||||
|
O.OnAudioInDevice := OnVfoOverlayAudioInDevice;
|
||||||
O.OnSplit := OnVfoOverlaySplit;
|
O.OnSplit := OnVfoOverlaySplit;
|
||||||
O.OnTxSelect := OnVfoOverlayTxSelect;
|
O.OnTxSelect := OnVfoOverlayTxSelect;
|
||||||
O.OnRxMuteTx := OnVfoOverlayRxMuteTx;
|
O.OnRxMuteTx := OnVfoOverlayRxMuteTx;
|
||||||
@@ -6742,6 +6760,7 @@ begin
|
|||||||
Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn;
|
Cfg.Pans[i].Slices[n].FMSQOn := FController.FSlices[s].FMSQOn;
|
||||||
Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel;
|
Cfg.Pans[i].Slices[n].FMSQLevel := FController.FSlices[s].FMSQLevel;
|
||||||
Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName;
|
Cfg.Pans[i].Slices[n].DevName := FController.FSlices[s].DevName;
|
||||||
|
Cfg.Pans[i].Slices[n].InDevName := FController.FSlices[s].InDevName;
|
||||||
Inc(n);
|
Inc(n);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -6762,6 +6781,10 @@ begin
|
|||||||
TWDSPAGCMode(EnsureRange(SC.AGC, 0, Ord(High(TWDSPAGCMode)))),
|
TWDSPAGCMode(EnsureRange(SC.AGC, 0, Ord(High(TWDSPAGCMode)))),
|
||||||
EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId);
|
EnsureRange(SC.Volume, 0.0, 1.0), DevIdx, SC.DevName, PanId);
|
||||||
if Id < 0 then Exit;
|
if Id < 0 then Exit;
|
||||||
|
// Свой вход слайса (имя устройства → PA-индекс текущей сессии).
|
||||||
|
if SC.InDevName <> '' then
|
||||||
|
FController.SetSliceInDevice(Id,
|
||||||
|
FController.FAudioIn.FindDeviceByName(SC.InDevName), SC.InDevName);
|
||||||
if SC.Muted then FController.SetSliceMute(Id, True);
|
if SC.Muted then FController.SetSliceMute(Id, True);
|
||||||
FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx);
|
FController.SetSliceRxMuteOnTx(Id, SC.RxMuteOnTx);
|
||||||
if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel);
|
if SC.FMSQOn then FController.SetSliceFMSquelch(Id, True, SC.FMSQLevel);
|
||||||
@@ -7862,12 +7885,27 @@ var
|
|||||||
SLo, SHi, SNew: Double;
|
SLo, SHi, SNew: Double;
|
||||||
Sl: TCtrlSlice;
|
Sl: TCtrlSlice;
|
||||||
SlicePan: TPanafallPanel;
|
SlicePan: TPanafallPanel;
|
||||||
|
P: TPoint;
|
||||||
begin
|
begin
|
||||||
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
// Над дисплеем VFO (A или B) колесо обрабатывает сам TFreqDisplay поразрядно
|
||||||
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
// (включая неактивный VFO). Не дублируем здесь, иначе менялись бы обе частоты:
|
||||||
// виджет крутил бы B, а этот хендлер — активный A.
|
// виджет крутил бы B, а этот хендлер — активный A.
|
||||||
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
if OverFreqDisp(FreqDispA) or OverFreqDisp(FreqDispB) then Exit;
|
||||||
|
|
||||||
|
// Колесо над раскрытым списком устройств во флаге — прокрутка этого списка.
|
||||||
|
for i := 0 to MAX_PANS - 1 do
|
||||||
|
if (FPans[i] <> nil) and Assigned(FPans[i].PbSpectrum) and
|
||||||
|
FPans[i].PbSpectrum.Visible then
|
||||||
|
begin
|
||||||
|
P := FPans[i].PbSpectrum.ScreenToClient(Mouse.CursorPos);
|
||||||
|
if FPans[i].DispatchFlagsMouseWheel(P.X, P.Y, WheelDelta) then
|
||||||
|
begin
|
||||||
|
MarkPanDirty(FPans[i]);
|
||||||
|
Handled := True;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
// Колесо над маркером слайса (любого пана) — двигаем частоту ЭТОГО слайса.
|
// Колесо над маркером слайса (любого пана) — двигаем частоту ЭТОГО слайса.
|
||||||
SliceId := 0;
|
SliceId := 0;
|
||||||
SlicePan := nil;
|
SlicePan := nil;
|
||||||
@@ -8176,18 +8214,10 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
procedure TMainForm.ApplyAudioInputDevice(DevIndex: Integer; const DevName: string);
|
||||||
|
// Общий вход: контроллер сам переоткрывает поток и перепривязывает к нему
|
||||||
|
// слайсы, которые ссылались на общее устройство (mic-пул).
|
||||||
begin
|
begin
|
||||||
if FController.FAudioIn.IsOpen then
|
FController.SetMainMicDevice(DevIndex, DevName);
|
||||||
FController.FAudioIn.Close;
|
|
||||||
FController.FAudioIn.DeviceIndex := DevIndex;
|
|
||||||
FController.FAudioInDevName := DevName;
|
|
||||||
if DevIndex >= 0 then
|
|
||||||
begin
|
|
||||||
try
|
|
||||||
FController.FAudioIn.Open;
|
|
||||||
except
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.ApplyAudioBufferSize(BufferSize: Integer);
|
procedure TMainForm.ApplyAudioBufferSize(BufferSize: Integer);
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ type
|
|||||||
procedure DispatchFlagsMouseMove(X, Y: Integer);
|
procedure DispatchFlagsMouseMove(X, Y: Integer);
|
||||||
function DispatchFlagsMouseUp(X, Y: Integer): Boolean;
|
function DispatchFlagsMouseUp(X, Y: Integer): Boolean;
|
||||||
procedure DispatchFlagsMouseLeave;
|
procedure DispatchFlagsMouseLeave;
|
||||||
|
function DispatchFlagsMouseWheel(X, Y, WheelDelta: Integer): Boolean;
|
||||||
|
|
||||||
property Controller: TRadioController read FController write FController;
|
property Controller: TRadioController read FController write FController;
|
||||||
property MainFlag: TVfoOverlay read FMainFlag;
|
property MainFlag: TVfoOverlay read FMainFlag;
|
||||||
@@ -1353,6 +1354,20 @@ begin
|
|||||||
Names.Free;
|
Names.Free;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
// Устройства ВВОДА + свой вход слайса ('' = общий вход приложения).
|
||||||
|
if Assigned(FController.FAudioIn) then
|
||||||
|
begin
|
||||||
|
Names := TStringList.Create;
|
||||||
|
try
|
||||||
|
Cnt := 0;
|
||||||
|
FController.FAudioIn.EnumInputDevices(Names, Indices, Cnt);
|
||||||
|
SetLength(Idx, Cnt);
|
||||||
|
for I := 0 to Cnt - 1 do Idx[I] := Indices[I];
|
||||||
|
O.SetAudioInDevices(Names, Idx, S.InDevName);
|
||||||
|
finally
|
||||||
|
Names.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TPanafallPanel.PushAllSliceFlagStates;
|
procedure TPanafallPanel.PushAllSliceFlagStates;
|
||||||
@@ -1390,6 +1405,16 @@ begin
|
|||||||
SetLength(Idx, Cnt);
|
SetLength(Idx, Cnt);
|
||||||
for I := 0 to Cnt - 1 do Idx[I] := Indices[I];
|
for I := 0 to Cnt - 1 do Idx[I] := Indices[I];
|
||||||
FMainFlag.SetAudioDevices(Names, Idx, FController.FAudioOutDevName);
|
FMainFlag.SetAudioDevices(Names, Idx, FController.FAudioOutDevName);
|
||||||
|
// Вход главного VFO = общее устройство ввода (то же, что в Settings).
|
||||||
|
if Assigned(FController.FAudioIn) then
|
||||||
|
begin
|
||||||
|
Cnt := 0;
|
||||||
|
Names.Clear;
|
||||||
|
FController.FAudioIn.EnumInputDevices(Names, Indices, Cnt);
|
||||||
|
SetLength(Idx, Cnt);
|
||||||
|
for I := 0 to Cnt - 1 do Idx[I] := Indices[I];
|
||||||
|
FMainFlag.SetAudioInDevices(Names, Idx, FController.FAudioInDevName);
|
||||||
|
end;
|
||||||
finally
|
finally
|
||||||
Names.Free;
|
Names.Free;
|
||||||
end;
|
end;
|
||||||
@@ -1533,4 +1558,24 @@ begin
|
|||||||
if FSliceFlags[i] <> nil then FSliceFlags[i].HandleMouseLeave;
|
if FSliceFlags[i] <> nil then FSliceFlags[i].HandleMouseLeave;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Колесо над раскрытым списком устройств во флаге: прокрутка списка вместо
|
||||||
|
// перестройки частоты. Координаты — в системе спектра (как у остальных Handle*).
|
||||||
|
function TPanafallPanel.DispatchFlagsMouseWheel(X, Y, WheelDelta: Integer): Boolean;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if Assigned(FMainFlag) and FMainFlag.Visible then
|
||||||
|
begin
|
||||||
|
FActiveFlag := FMainFlag;
|
||||||
|
if FMainFlag.HandleMouseWheel(X, Y, WheelDelta) then Exit(True);
|
||||||
|
end;
|
||||||
|
for i := 0 to High(FSliceFlags) do
|
||||||
|
if (FSliceFlags[i] <> nil) and FSliceFlags[i].Visible then
|
||||||
|
begin
|
||||||
|
FActiveFlag := FSliceFlags[i];
|
||||||
|
if FSliceFlags[i].HandleMouseWheel(X, Y, WheelDelta) then Exit(True);
|
||||||
|
end;
|
||||||
|
FActiveFlag := nil;
|
||||||
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
+221
-19
@@ -162,9 +162,14 @@ type
|
|||||||
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
|
RxMuteOnTx: Boolean; // True (умолч.) = динамик слайса молчит на передаче
|
||||||
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
|
FMSQOn: Boolean; // FM-squelch включён (слайс в режиме FM)
|
||||||
FMSQLevel: Integer; // порог squelch 0..100
|
FMSQLevel: Integer; // порог squelch 0..100
|
||||||
DeviceIndex: Integer; // PortAudio-индекс (-1 = default)
|
DeviceIndex: Integer; // PortAudio-индекс вывода (-1 = default)
|
||||||
DevName: string;
|
DevName: string;
|
||||||
Audio: TAudioOutput; // владеет; свой поток вывода
|
Audio: TAudioOutput; // владеет; свой поток вывода
|
||||||
|
// Вход слайса (mic/виртуальный кабель) — источник модуляции, когда именно
|
||||||
|
// этот слайс выбран TX-источником. -1/'' = брать общий вход (FAudioIn).
|
||||||
|
InDeviceIndex: Integer; // PortAudio-индекс ввода (-1 = общий)
|
||||||
|
InDevName: string;
|
||||||
|
AudioIn: TAudioInput; // НЕ владеет: ссылка на поток из mic-пула
|
||||||
DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса
|
DMR: TDMRSliceDecoder; // владеет; nil для не-DMR слайса
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -190,6 +195,11 @@ type
|
|||||||
procedure OnSliceDMRAudioReady(SliceId: Integer;
|
procedure OnSliceDMRAudioReady(SliceId: Integer;
|
||||||
const PCM: array of Single; Count: Integer);
|
const PCM: array of Single; Count: Integer);
|
||||||
function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче?
|
function SliceMutedByTx(Idx: Integer): Boolean; // слайс молчит на передаче?
|
||||||
|
// Пул входных потоков: захват/освобождение ссылки на устройство ввода.
|
||||||
|
function AcquireMicInput(DevIndex: Integer): TAudioInput;
|
||||||
|
procedure ReleaseMicInput(var AI: TAudioInput);
|
||||||
|
procedure SetSliceInDeviceIdx(Idx, DevIndex: Integer; const DevName: string);
|
||||||
|
procedure FreeMicPool; // финальная зачистка (FreeEngines)
|
||||||
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
|
procedure RefreshSliceShifts; // пересчёт ShiftHz слайсов пана 0 при смене центра
|
||||||
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
|
// --- Панадаптеры на аппаратных DDC (этап 3.1/3.2) ---
|
||||||
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
|
procedure ResetPanDDCMaps; // все пан-слоты свободны, DDC→Pan пуст
|
||||||
@@ -230,6 +240,18 @@ type
|
|||||||
// TargetHz абсолютна; ShiftHz в движок = TargetHz - FCenterFreq.
|
// TargetHz абсолютна; ShiftHz в движок = TargetHz - FCenterFreq.
|
||||||
FSlices: array[0..MAX_SLICES-1] of TCtrlSlice;
|
FSlices: array[0..MAX_SLICES-1] of TCtrlSlice;
|
||||||
FNextSliceId: Integer; // монотонный генератор id слайсов
|
FNextSliceId: Integer; // монотонный генератор id слайсов
|
||||||
|
|
||||||
|
// ---- Пул входных потоков (mic) ----
|
||||||
|
// Каждому УНИКАЛЬНОМУ устройству ввода — один открытый TAudioInput на всё
|
||||||
|
// приложение; слайсы держат на него ссылку по refcount. Потоки живут, пока
|
||||||
|
// на них кто-то ссылается: переключение TX-источника = смена указателя,
|
||||||
|
// без Close/Open (иначе на старте передачи терялись бы первые сэмплы).
|
||||||
|
// Читатель у кольца всегда один — TX-источник в данный момент только один.
|
||||||
|
FMicPool: array of record
|
||||||
|
DevIdx: Integer;
|
||||||
|
Refs: Integer;
|
||||||
|
AI: TAudioInput;
|
||||||
|
end;
|
||||||
FSettings: TSettingsManager;
|
FSettings: TSettingsManager;
|
||||||
FChannelStore: TChannelStore;
|
FChannelStore: TChannelStore;
|
||||||
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
|
FDeviceStore: TDeviceStore; // общий список устройств (saved+discovered)
|
||||||
@@ -642,6 +664,14 @@ type
|
|||||||
// hardwaregain у AD936x) — входит в оба офсета выше.
|
// hardwaregain у AD936x) — входит в оба офсета выше.
|
||||||
function FrontEndCalDB: Double;
|
function FrontEndCalDB: Double;
|
||||||
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
|
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
|
||||||
|
// Устройство ВВОДА слайса (источник модуляции, когда слайс = TX-источник).
|
||||||
|
// DevIndex=-1 / DevName='' — вернуться на общий вход (FAudioIn).
|
||||||
|
procedure SetSliceInDevice(Id, DevIndex: Integer; const DevName: string);
|
||||||
|
// Общее устройство ввода (главный VFO = слайс A + фолбэк для слайсов).
|
||||||
|
procedure SetMainMicDevice(DevIndex: Integer; const DevName: string);
|
||||||
|
// Поток/имя устройства ввода активного TX-источника (слайс или общий).
|
||||||
|
function ActiveMicInput: TAudioInput;
|
||||||
|
function ActiveMicInDevName: string;
|
||||||
function SliceCount: Integer;
|
function SliceCount: Integer;
|
||||||
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
|
||||||
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
// Диапазон захваченной полосы пана (0 = главный), куда можно ставить слайс.
|
||||||
@@ -1065,8 +1095,10 @@ begin
|
|||||||
FSlices[i].Audio.Close;
|
FSlices[i].Audio.Close;
|
||||||
FreeAndNil(FSlices[i].Audio);
|
FreeAndNil(FSlices[i].Audio);
|
||||||
end;
|
end;
|
||||||
|
FSlices[i].AudioIn := nil; // владелец — mic-пул (FreeMicPool ниже)
|
||||||
FSlices[i].Active := False;
|
FSlices[i].Active := False;
|
||||||
end;
|
end;
|
||||||
|
FreeMicPool;
|
||||||
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||||||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
||||||
if Assigned(FDSPEngine) then
|
if Assigned(FDSPEngine) then
|
||||||
@@ -1928,6 +1960,10 @@ begin
|
|||||||
FSlices[slot].DeviceIndex := DevIndex;
|
FSlices[slot].DeviceIndex := DevIndex;
|
||||||
FSlices[slot].DevName := DevName;
|
FSlices[slot].DevName := DevName;
|
||||||
FSlices[slot].Audio := AO;
|
FSlices[slot].Audio := AO;
|
||||||
|
// Вход слайса по умолчанию общий (FAudioIn); свой ставится SetSliceInDevice.
|
||||||
|
FSlices[slot].InDeviceIndex := -1;
|
||||||
|
FSlices[slot].InDevName := '';
|
||||||
|
FSlices[slot].AudioIn := nil;
|
||||||
FSlices[slot].DMR := nil;
|
FSlices[slot].DMR := nil;
|
||||||
if Mode = MODE_DMR then
|
if Mode = MODE_DMR then
|
||||||
FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id);
|
FSlices[slot].DMR := TDMRSliceDecoder.Create(Self, Id);
|
||||||
@@ -1950,6 +1986,9 @@ begin
|
|||||||
FSlices[idx].Audio.Close;
|
FSlices[idx].Audio.Close;
|
||||||
FreeAndNil(FSlices[idx].Audio);
|
FreeAndNil(FSlices[idx].Audio);
|
||||||
end;
|
end;
|
||||||
|
ReleaseMicInput(FSlices[idx].AudioIn); // поток закроется, когда уйдёт последняя ссылка
|
||||||
|
FSlices[idx].InDeviceIndex := -1;
|
||||||
|
FSlices[idx].InDevName := '';
|
||||||
FSlices[idx].Active := False;
|
FSlices[idx].Active := False;
|
||||||
Changed(rfDevice);
|
Changed(rfDevice);
|
||||||
end;
|
end;
|
||||||
@@ -1980,6 +2019,21 @@ begin
|
|||||||
if (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0);
|
if (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0);
|
||||||
if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then
|
if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then
|
||||||
FreeAndNil(FSlices[idx].DMR);
|
FreeAndNil(FSlices[idx].DMR);
|
||||||
|
// Этот слайс — источник передачи: передающий тракт идёт за его режимом
|
||||||
|
// (иначе после смены режима в эфир ушёл бы прежний вид модуляции). Вход FM
|
||||||
|
// RAW — внешний PCM, поэтому заодно пересматриваем источник модуляции.
|
||||||
|
if (FTxSliceId = Id) and (Mode <> OldMode) and
|
||||||
|
FWDSPReady and Assigned(FDSPEngine) then
|
||||||
|
begin
|
||||||
|
FDSPEngine.SetTXMode(ActiveTXMode);
|
||||||
|
if (Mode = MODE_FMRAW) or (OldMode = MODE_FMRAW) then
|
||||||
|
begin
|
||||||
|
if Mode = MODE_FMRAW then FWebMicActive := False;
|
||||||
|
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||||||
|
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
|
||||||
|
ActiveMicInput.Flush;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetSliceFilter(Id, Low, High: Integer);
|
procedure TRadioController.SetSliceFilter(Id, Low, High: Integer);
|
||||||
@@ -2156,6 +2210,147 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// --- Пул входных потоков -----------------------------------------------------
|
||||||
|
|
||||||
|
function TRadioController.AcquireMicInput(DevIndex: Integer): TAudioInput;
|
||||||
|
// Ссылка на открытый поток устройства DevIndex (создаёт при первом обращении).
|
||||||
|
// DevIndex<0 = «своего входа нет» → nil (читатель упадёт на общий FAudioIn).
|
||||||
|
var
|
||||||
|
i, n: Integer;
|
||||||
|
AI: TAudioInput;
|
||||||
|
begin
|
||||||
|
Result := nil;
|
||||||
|
if DevIndex < 0 then Exit;
|
||||||
|
// Общий вход уже открыт на этом же устройстве — переиспользуем его: второй
|
||||||
|
// Open того же PA-устройства на эксклюзивном ALSA-hw просто не прошёл бы.
|
||||||
|
if Assigned(FAudioIn) and (FAudioIn.DeviceIndex = DevIndex) then
|
||||||
|
Exit(FAudioIn);
|
||||||
|
for i := 0 to High(FMicPool) do
|
||||||
|
if FMicPool[i].DevIdx = DevIndex then
|
||||||
|
begin
|
||||||
|
// Запись с Refs=0 — поток закрыт, но объект жив (см. ReleaseMicInput).
|
||||||
|
if (FMicPool[i].Refs <= 0) and FLocalAudio and
|
||||||
|
(not FMicPool[i].AI.IsOpen) then
|
||||||
|
try FMicPool[i].AI.Open; except end;
|
||||||
|
Inc(FMicPool[i].Refs);
|
||||||
|
Exit(FMicPool[i].AI);
|
||||||
|
end;
|
||||||
|
AI := TAudioInput.Create(48000);
|
||||||
|
AI.DeviceIndex := DevIndex;
|
||||||
|
if FLocalAudio then
|
||||||
|
try AI.Open; except end;
|
||||||
|
n := Length(FMicPool);
|
||||||
|
SetLength(FMicPool, n + 1);
|
||||||
|
FMicPool[n].DevIdx := DevIndex;
|
||||||
|
FMicPool[n].Refs := 1;
|
||||||
|
FMicPool[n].AI := AI;
|
||||||
|
Result := AI;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ReleaseMicInput(var AI: TAudioInput);
|
||||||
|
// Последняя ссылка ушла — поток ЗАКРЫВАЕМ, но объект оставляем в пуле до
|
||||||
|
// выхода: TX-поток (PullSoundCardMic) мог взять указатель прямо сейчас, а
|
||||||
|
// освобождение памяти под ним = падение. Повторный выбор устройства просто
|
||||||
|
// переоткроет ту же запись.
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
if AI = nil then Exit;
|
||||||
|
if AI = FAudioIn then begin AI := nil; Exit; end; // общий вход не из пула
|
||||||
|
for i := 0 to High(FMicPool) do
|
||||||
|
if FMicPool[i].AI = AI then
|
||||||
|
begin
|
||||||
|
Dec(FMicPool[i].Refs);
|
||||||
|
if FMicPool[i].Refs <= 0 then
|
||||||
|
begin
|
||||||
|
FMicPool[i].Refs := 0;
|
||||||
|
FMicPool[i].AI.Close;
|
||||||
|
end;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
AI := nil;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.FreeMicPool;
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
for i := 0 to High(FMicPool) do
|
||||||
|
if FMicPool[i].AI <> nil then
|
||||||
|
begin
|
||||||
|
FMicPool[i].AI.Close;
|
||||||
|
FMicPool[i].AI.Free;
|
||||||
|
end;
|
||||||
|
SetLength(FMicPool, 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.SetSliceInDevice(Id, DevIndex: Integer;
|
||||||
|
const DevName: string);
|
||||||
|
begin
|
||||||
|
SetSliceInDeviceIdx(FindSliceIndex(Id), DevIndex, DevName);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.SetSliceInDeviceIdx(Idx, DevIndex: Integer;
|
||||||
|
const DevName: string);
|
||||||
|
begin
|
||||||
|
if (Idx < 0) or (Idx >= MAX_SLICES) or (not FSlices[Idx].Active) then Exit;
|
||||||
|
if DevName = '' then DevIndex := -1;
|
||||||
|
ReleaseMicInput(FSlices[Idx].AudioIn);
|
||||||
|
FSlices[Idx].InDeviceIndex := DevIndex;
|
||||||
|
FSlices[Idx].InDevName := DevName;
|
||||||
|
FSlices[Idx].AudioIn := AcquireMicInput(DevIndex);
|
||||||
|
// Слайс сейчас источник TX — источник модуляции мог сменить тип
|
||||||
|
// (звуковуха ↔ микрофон радио).
|
||||||
|
if (FTxSliceId = FSlices[Idx].Id) and FWDSPReady and Assigned(FDSPEngine) then
|
||||||
|
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.SetMainMicDevice(DevIndex: Integer;
|
||||||
|
const DevName: string);
|
||||||
|
var i: Integer;
|
||||||
|
begin
|
||||||
|
if Assigned(FAudioIn) then
|
||||||
|
begin
|
||||||
|
if FAudioIn.IsOpen then FAudioIn.Close;
|
||||||
|
FAudioIn.DeviceIndex := DevIndex;
|
||||||
|
if (DevIndex >= 0) and FLocalAudio then
|
||||||
|
try FAudioIn.Open; except end;
|
||||||
|
end;
|
||||||
|
FAudioInDevName := DevName;
|
||||||
|
// Слайсы могли ссылаться на общий вход (или держать свой поток на устройстве,
|
||||||
|
// которое теперь стало общим) — перепривязать все ссылки к новой картине.
|
||||||
|
for i := 0 to MAX_SLICES - 1 do
|
||||||
|
if FSlices[i].Active then
|
||||||
|
begin
|
||||||
|
ReleaseMicInput(FSlices[i].AudioIn);
|
||||||
|
FSlices[i].AudioIn := AcquireMicInput(FSlices[i].InDeviceIndex);
|
||||||
|
end;
|
||||||
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
|
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRadioController.ActiveMicInput: TAudioInput;
|
||||||
|
// Вход TX-источника: свой поток слайса, иначе общий (слайс без выбора).
|
||||||
|
var idx: Integer;
|
||||||
|
begin
|
||||||
|
Result := FAudioIn;
|
||||||
|
if FTxSliceId = 0 then Exit;
|
||||||
|
idx := FindSliceIndex(FTxSliceId);
|
||||||
|
// Поток слайса мог не открыться (устройство занято/исчезло) — тогда лучше
|
||||||
|
// передавать с общего входа, чем молчать в эфир.
|
||||||
|
if (idx >= 0) and (FSlices[idx].AudioIn <> nil) and
|
||||||
|
FSlices[idx].AudioIn.IsOpen then
|
||||||
|
Result := FSlices[idx].AudioIn;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRadioController.ActiveMicInDevName: string;
|
||||||
|
var idx: Integer;
|
||||||
|
begin
|
||||||
|
Result := FAudioInDevName;
|
||||||
|
if FTxSliceId = 0 then Exit;
|
||||||
|
idx := FindSliceIndex(FTxSliceId);
|
||||||
|
if (idx >= 0) and (FSlices[idx].InDevName <> '') then
|
||||||
|
Result := FSlices[idx].InDevName;
|
||||||
|
end;
|
||||||
|
|
||||||
function TRadioController.SliceCount: Integer;
|
function TRadioController.SliceCount: Integer;
|
||||||
var i: Integer;
|
var i: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -2233,15 +2428,17 @@ function TRadioController.PullSoundCardMic(MaxN: Integer): Integer;
|
|||||||
// пачкой в локальный буфер — экономим будилки семафора.
|
// пачкой в локальный буфер — экономим будилки семафора.
|
||||||
var
|
var
|
||||||
i, n: Integer;
|
i, n: Integer;
|
||||||
|
AI: TAudioInput;
|
||||||
Buf: array[0..2047] of Double;
|
Buf: array[0..2047] of Double;
|
||||||
begin
|
begin
|
||||||
Result := 0;
|
Result := 0;
|
||||||
if (FAudioIn = nil) or (not FAudioIn.IsOpen) then Exit;
|
AI := ActiveMicInput; // вход TX-слайса (свой) либо общий
|
||||||
n := FAudioIn.Available;
|
if (AI = nil) or (not AI.IsOpen) then Exit;
|
||||||
|
n := AI.Available;
|
||||||
if n > MaxN then n := MaxN;
|
if n > MaxN then n := MaxN;
|
||||||
if n > Length(Buf) then n := Length(Buf);
|
if n > Length(Buf) then n := Length(Buf);
|
||||||
for i := 0 to n - 1 do
|
for i := 0 to n - 1 do
|
||||||
Buf[i] := FAudioIn.ReadSample;
|
Buf[i] := AI.ReadSample;
|
||||||
if n > 0 then
|
if n > 0 then
|
||||||
FDSPEngine.PushTXMicSamplesD(Buf, n);
|
FDSPEngine.PushTXMicSamplesD(Buf, n);
|
||||||
Result := n;
|
Result := n;
|
||||||
@@ -2371,7 +2568,15 @@ begin
|
|||||||
FTxSliceId := SliceId;
|
FTxSliceId := SliceId;
|
||||||
// Передающий тракт под режим нового источника (idle-канал, применится к PTT).
|
// Передающий тракт под режим нового источника (idle-канал, применится к PTT).
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
|
begin
|
||||||
FDSPEngine.SetTXMode(ActiveTXMode);
|
FDSPEngine.SetTXMode(ActiveTXMode);
|
||||||
|
// Источник модуляции — вход НОВОГО слайса (у него может быть свой).
|
||||||
|
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||||||
|
end;
|
||||||
|
// Кольцо нового входа копило звук всё время, пока он не был источником:
|
||||||
|
// без сброса первые ~200мс передачи ушли бы в эфир «из прошлого».
|
||||||
|
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
|
||||||
|
ActiveMicInput.Flush;
|
||||||
FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса
|
FDriveLevel := CalcDriveByte; // калибровка драйва банда TX-слайса
|
||||||
ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса
|
ApplyAntPorts; // AD936x: TX-разъём — по диапазону TX-слайса
|
||||||
PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx
|
PushNetworkState; // TX-частота/Alex через ActiveTXFreqHz/TxBandIdx
|
||||||
@@ -3422,7 +3627,8 @@ begin
|
|||||||
begin
|
begin
|
||||||
FWebMicActive := False;
|
FWebMicActive := False;
|
||||||
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
FDSPEngine.SetTXMicSource(DefaultMicSource);
|
||||||
if Assigned(FAudioIn) and FAudioIn.IsOpen then FAudioIn.Flush;
|
if Assigned(ActiveMicInput) and ActiveMicInput.IsOpen then
|
||||||
|
ActiveMicInput.Flush;
|
||||||
end;
|
end;
|
||||||
// Если TUN активен — пересчитываем знак частоты под новый режим
|
// Если TUN активен — пересчитываем знак частоты под новый режим
|
||||||
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, TUN_TONE_MAG);
|
||||||
@@ -4236,9 +4442,11 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.DefaultMicSource: TTXMicSource;
|
function TRadioController.DefaultMicSource: TTXMicSource;
|
||||||
|
// Источник модуляции решает ТЕКУЩИЙ TX-слайс: у него может быть свой вход
|
||||||
|
// (виртуальный кабель), даже когда общего входа нет, и наоборот.
|
||||||
begin
|
begin
|
||||||
if FAudioInDevName <> '' then Result := txmsSoundCard
|
if ActiveMicInDevName <> '' then Result := txmsSoundCard
|
||||||
else Result := txmsRadio;
|
else Result := txmsRadio;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.BuildMicLineSelectByte: Byte;
|
function TRadioController.BuildMicLineSelectByte: Byte;
|
||||||
@@ -4422,9 +4630,9 @@ begin
|
|||||||
// звук бёрстом уходит в тракт: стоячая задержка ~190мс в DUC-очереди
|
// звук бёрстом уходит в тракт: стоячая задержка ~190мс в DUC-очереди
|
||||||
// + в эфир передаётся аудио, записанное ДО нажатия PTT (на TUN оно
|
// + в эфир передаётся аудио, записанное ДО нажатия PTT (на TUN оно
|
||||||
// складывается с тоном поверх полной шкалы → клип-всплеск).
|
// складывается с тоном поверх полной шкалы → клип-всплеск).
|
||||||
if FTransmitting and (not WasTransmitting) and Assigned(FAudioIn)
|
if FTransmitting and (not WasTransmitting) and Assigned(ActiveMicInput)
|
||||||
and FAudioIn.IsOpen then
|
and ActiveMicInput.IsOpen then
|
||||||
FAudioIn.Flush;
|
ActiveMicInput.Flush;
|
||||||
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
|
// В non-DUP RX-IQ пакеты дропаются на входе DSP-потока во время TX, чтобы
|
||||||
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
|
// TX leakage не накапливался в RXA pipeline / FFT-истории анализатора.
|
||||||
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
|
FDSPEngine.KeepRXDuringTX := FDisplayDuplex;
|
||||||
@@ -5325,16 +5533,10 @@ begin
|
|||||||
// Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT —
|
// Аудио устройство ввода (TX mic). MicSource определяется в SetMOX по PTT —
|
||||||
// здесь только открываем устройство.
|
// здесь только открываем устройство.
|
||||||
if FLoadedGlobal.AudioInDevice <> '' then
|
if FLoadedGlobal.AudioInDevice <> '' then
|
||||||
begin
|
SetMainMicDevice(FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice),
|
||||||
FAudioIn.DeviceIndex := FAudioIn.FindDeviceByName(FLoadedGlobal.AudioInDevice);
|
FLoadedGlobal.AudioInDevice)
|
||||||
FAudioInDevName := FLoadedGlobal.AudioInDevice;
|
|
||||||
FAudioIn.Open;
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
begin
|
SetMainMicDevice(-1, '');
|
||||||
FAudioIn.DeviceIndex := -1;
|
|
||||||
FAudioInDevName := '';
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -122,7 +122,8 @@ type
|
|||||||
RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл)
|
RxMuteOnTx: Boolean; // True = слайс молчит на передаче (self-monitor выкл)
|
||||||
FMSQOn: Boolean;
|
FMSQOn: Boolean;
|
||||||
FMSQLevel: Integer;
|
FMSQLevel: Integer;
|
||||||
DevName: string; // имя PortAudio-устройства ('' = default)
|
DevName: string; // имя PortAudio-устройства вывода ('' = default)
|
||||||
|
InDevName: string; // имя устройства ВВОДА слайса ('' = общий вход)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TPanCfg = record
|
TPanCfg = record
|
||||||
@@ -1972,6 +1973,7 @@ begin
|
|||||||
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
|
SliceObj.Add('fmsq_on', P.Pans[i].Slices[j].FMSQOn);
|
||||||
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
|
SliceObj.Add('fmsq_level', P.Pans[i].Slices[j].FMSQLevel);
|
||||||
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
|
SliceObj.Add('dev_name', P.Pans[i].Slices[j].DevName);
|
||||||
|
SliceObj.Add('in_dev_name', P.Pans[i].Slices[j].InDevName);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -2051,6 +2053,7 @@ begin
|
|||||||
P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False);
|
P.Pans[id].Slices[n].FMSQOn := JB(SliceObj, 'fmsq_on', False);
|
||||||
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
|
P.Pans[id].Slices[n].FMSQLevel := JI(SliceObj, 'fmsq_level', 30);
|
||||||
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
|
P.Pans[id].Slices[n].DevName := JS(SliceObj, 'dev_name', '');
|
||||||
|
P.Pans[id].Slices[n].InDevName := JS(SliceObj, 'in_dev_name', '');
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+164
-17
@@ -94,8 +94,14 @@ type
|
|||||||
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
|
FDevIdx: array of Integer; // соответствующие PortAudio-индексы (-1=default)
|
||||||
FCurDev: string; // имя текущего устройства ('' = default)
|
FCurDev: string; // имя текущего устройства ('' = default)
|
||||||
|
|
||||||
|
FInDevNames: TStringList; // имена устройств ВВОДА (микрофон/кабель)
|
||||||
|
FInDevIdx: array of Integer; // их PortAudio-индексы
|
||||||
|
FCurInDev: string; // текущий вход слайса ('' = общий вход)
|
||||||
|
FAudInTab: Boolean; // AUD-панель показывает вход (IN), а не выход
|
||||||
|
|
||||||
FPanel: TFlyPanel; // открытая fly-out панель
|
FPanel: TFlyPanel; // открытая fly-out панель
|
||||||
FAudScroll: Integer; // прокрутка списка аудио-устройств (верхний индекс)
|
FAudScroll: Integer; // прокрутка списка аудио-устройств (верхний индекс)
|
||||||
|
FAudListRect: TRect; // область списка AUD (лок. коорд.) — зона колёсика
|
||||||
FVolTrack: TRect; // трек громкости (локальные координаты)
|
FVolTrack: TRect; // трек громкости (локальные координаты)
|
||||||
FVolDrag: Boolean;
|
FVolDrag: Boolean;
|
||||||
FSqlTrack: TRect; // трек squelch в DSP-панели (лок. координаты)
|
FSqlTrack: TRect; // трек squelch в DSP-панели (лок. координаты)
|
||||||
@@ -108,6 +114,7 @@ type
|
|||||||
FOnVolume: TVolumeEvent;
|
FOnVolume: TVolumeEvent;
|
||||||
FOnMute: TNotifyEvent;
|
FOnMute: TNotifyEvent;
|
||||||
FOnAudioDevice: TAudioDeviceEvent;
|
FOnAudioDevice: TAudioDeviceEvent;
|
||||||
|
FOnAudioInDevice: TAudioDeviceEvent;
|
||||||
FOnSplit: TNotifyEvent;
|
FOnSplit: TNotifyEvent;
|
||||||
FOnTxSelect: TNotifyEvent;
|
FOnTxSelect: TNotifyEvent;
|
||||||
FOnRxMuteTx: TNotifyEvent;
|
FOnRxMuteTx: TNotifyEvent;
|
||||||
@@ -146,6 +153,12 @@ type
|
|||||||
procedure DrawModePanel(C: TCanvas; X, Y, RowW: Integer);
|
procedure DrawModePanel(C: TCanvas; X, Y, RowW: Integer);
|
||||||
procedure DrawDspPanel(C: TCanvas; X, Y, RowW: Integer);
|
procedure DrawDspPanel(C: TCanvas; X, Y, RowW: Integer);
|
||||||
procedure DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
|
procedure DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
|
||||||
|
// Строки активной вкладки AUD (OUT — устройства вывода, IN — ввода;
|
||||||
|
// у IN первая строка псевдо: «общий вход» / «нет»).
|
||||||
|
function AudRowCount: Integer;
|
||||||
|
function AudRowName(I: Integer): string;
|
||||||
|
function AudRowCurrent(I: Integer): Boolean;
|
||||||
|
procedure ScrollAudToCurrent; // прокрутка к выбранному устройству
|
||||||
procedure RegisterHit(const HR: TRect; HV: Integer);
|
procedure RegisterHit(const HR: TRect; HV: Integer);
|
||||||
procedure ApplyThemeColors; // раскладывает FTheme → структурные CLR_*
|
procedure ApplyThemeColors; // раскладывает FTheme → структурные CLR_*
|
||||||
function FormatFreq(Hz: Double): string;
|
function FormatFreq(Hz: Double): string;
|
||||||
@@ -184,6 +197,8 @@ type
|
|||||||
function HandleMouseMove(X, Y: Integer): Boolean;
|
function HandleMouseMove(X, Y: Integer): Boolean;
|
||||||
function HandleMouseUp(X, Y: Integer): Boolean;
|
function HandleMouseUp(X, Y: Integer): Boolean;
|
||||||
function HandleMouseLeave: Boolean;
|
function HandleMouseLeave: Boolean;
|
||||||
|
// Колесо над списком устройств AUD (координаты — как у HandleMouseDown).
|
||||||
|
function HandleMouseWheel(X, Y, WheelDelta: Integer): Boolean;
|
||||||
procedure SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
|
procedure SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
|
||||||
procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
|
procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean;
|
||||||
AAGCMode: Integer);
|
AAGCMode: Integer);
|
||||||
@@ -192,6 +207,9 @@ type
|
|||||||
procedure SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX)
|
procedure SetRxMuteTxState(AOn: Boolean); // бейдж RXM (self-monitor на TX)
|
||||||
procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
|
procedure SetAudioDevices(Names: TStrings; const Indices: array of Integer;
|
||||||
const CurName: string);
|
const CurName: string);
|
||||||
|
// Список устройств ВВОДА + текущий выбор ('' = общий вход приложения).
|
||||||
|
procedure SetAudioInDevices(Names: TStrings; const Indices: array of Integer;
|
||||||
|
const CurName: string);
|
||||||
procedure UpdateSMeter(DB: Double);
|
procedure UpdateSMeter(DB: Double);
|
||||||
procedure UpdateVfo(AVfoHz: Double);
|
procedure UpdateVfo(AVfoHz: Double);
|
||||||
procedure UpdateDMRStatus(const AText: string; Synced: Boolean);
|
procedure UpdateDMRStatus(const AText: string; Synced: Boolean);
|
||||||
@@ -223,6 +241,7 @@ type
|
|||||||
property OnVolume: TVolumeEvent read FOnVolume write FOnVolume;
|
property OnVolume: TVolumeEvent read FOnVolume write FOnVolume;
|
||||||
property OnMute: TNotifyEvent read FOnMute write FOnMute;
|
property OnMute: TNotifyEvent read FOnMute write FOnMute;
|
||||||
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
property OnAudioDevice: TAudioDeviceEvent read FOnAudioDevice write FOnAudioDevice;
|
||||||
|
property OnAudioInDevice: TAudioDeviceEvent read FOnAudioInDevice write FOnAudioInDevice;
|
||||||
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
property OnSplit: TNotifyEvent read FOnSplit write FOnSplit;
|
||||||
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
property OnTxSelect: TNotifyEvent read FOnTxSelect write FOnTxSelect;
|
||||||
property OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx;
|
property OnRxMuteTx: TNotifyEvent read FOnRxMuteTx write FOnRxMuteTx;
|
||||||
@@ -262,6 +281,8 @@ const
|
|||||||
HIT_TX = -26;
|
HIT_TX = -26;
|
||||||
HIT_AUD_UP = -27;
|
HIT_AUD_UP = -27;
|
||||||
HIT_AUD_DN = -28;
|
HIT_AUD_DN = -28;
|
||||||
|
HIT_AUD_TAB_OUT = -34; // вкладка «OUT» в AUD-панели
|
||||||
|
HIT_AUD_TAB_IN = -35; // вкладка «IN»
|
||||||
HIT_SLICE_SEL = -29; // клик по бейджу буквы (выбрать слайс активным)
|
HIT_SLICE_SEL = -29; // клик по бейджу буквы (выбрать слайс активным)
|
||||||
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
|
HIT_CLOSE = -30; // × закрытия слайса (неглавные)
|
||||||
HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели)
|
HIT_SQL = -31; // тумблер FM-squelch (в DSP-панели)
|
||||||
@@ -446,8 +467,10 @@ end;
|
|||||||
|
|
||||||
function TVfoOverlay.TxSelectionAllowed: Boolean;
|
function TVfoOverlay.TxSelectionAllowed: Boolean;
|
||||||
begin
|
begin
|
||||||
// Main A remains the normal TX source. Additional DMR slices are RX-only.
|
// Main A remains the normal TX source. DMR slices stay RX-only (no AMBE
|
||||||
Result := FIsMain or not (FMode in [OVL_MODE_DMR, OVL_MODE_FMRAW]);
|
// encoder). FM RAW transmits external PCM from its own input, so it is a
|
||||||
|
// valid TX source.
|
||||||
|
Result := FIsMain or (FMode <> OVL_MODE_DMR);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ ---- Lifecycle ---- }
|
{ ---- Lifecycle ---- }
|
||||||
@@ -481,6 +504,9 @@ begin
|
|||||||
FVolDrag := False;
|
FVolDrag := False;
|
||||||
FDevNames := TStringList.Create;
|
FDevNames := TStringList.Create;
|
||||||
FCurDev := '';
|
FCurDev := '';
|
||||||
|
FInDevNames := TStringList.Create;
|
||||||
|
FCurInDev := '';
|
||||||
|
FAudInTab := False; // AUD открывается на вкладке OUT
|
||||||
FCacheBitmap := TBitmap.Create;
|
FCacheBitmap := TBitmap.Create;
|
||||||
FCacheBitmap.PixelFormat := pf32bit;
|
FCacheBitmap.PixelFormat := pf32bit;
|
||||||
FCacheDirty := True;
|
FCacheDirty := True;
|
||||||
@@ -520,6 +546,7 @@ end;
|
|||||||
|
|
||||||
destructor TVfoOverlay.Destroy;
|
destructor TVfoOverlay.Destroy;
|
||||||
begin
|
begin
|
||||||
|
FInDevNames.Free;
|
||||||
FDevNames.Free;
|
FDevNames.Free;
|
||||||
FCacheBitmap.Free;
|
FCacheBitmap.Free;
|
||||||
inherited Destroy;
|
inherited Destroy;
|
||||||
@@ -618,7 +645,9 @@ begin
|
|||||||
Result := ROW_H + IGAP + ROW_H; // toggles + AGC
|
Result := ROW_H + IGAP + ROW_H; // toggles + AGC
|
||||||
if IsFM then Inc(Result, IGAP + ROW_H); // + ряд SQL (только FM)
|
if IsFM then Inc(Result, IGAP + ROW_H); // + ряд SQL (только FM)
|
||||||
end;
|
end;
|
||||||
fpAud: Result := Min(Max(1, FDevNames.Count), AUD_MAX_VIS) * AUD_ROW_H;
|
// ряд вкладок OUT/IN + список устройств активной вкладки
|
||||||
|
fpAud: Result := ROW_H + IGAP +
|
||||||
|
Min(Max(1, AudRowCount), AUD_MAX_VIS) * AUD_ROW_H;
|
||||||
else Result := 0;
|
else Result := 0;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -632,7 +661,7 @@ end;
|
|||||||
procedure TVfoOverlay.SetPanel(P: TFlyPanel);
|
procedure TVfoOverlay.SetPanel(P: TFlyPanel);
|
||||||
begin
|
begin
|
||||||
if FPanel = P then FPanel := fpNone else FPanel := P;
|
if FPanel = P then FPanel := fpNone else FPanel := P;
|
||||||
if FPanel = fpAud then FAudScroll := 0; // список с начала
|
if FPanel = fpAud then ScrollAudToCurrent; // сразу показать выбранное
|
||||||
Height := ComputeHeight;
|
Height := ComputeHeight;
|
||||||
RequestInvalidate;
|
RequestInvalidate;
|
||||||
end;
|
end;
|
||||||
@@ -907,20 +936,80 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TVfoOverlay.AudRowCount: Integer;
|
||||||
|
begin
|
||||||
|
// У вкладки IN первая строка — «общий вход» (слайс) / «нет» (главный флаг).
|
||||||
|
if FAudInTab then Result := FInDevNames.Count + 1
|
||||||
|
else Result := FDevNames.Count;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TVfoOverlay.AudRowName(I: Integer): string;
|
||||||
|
begin
|
||||||
|
if not FAudInTab then Exit(FDevNames[I]);
|
||||||
|
if I = 0 then
|
||||||
|
begin
|
||||||
|
if FIsMain then Result := '(нет · мик радио)'
|
||||||
|
else Result := '(общий вход)';
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
Result := FInDevNames[I - 1];
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TVfoOverlay.AudRowCurrent(I: Integer): Boolean;
|
||||||
|
begin
|
||||||
|
if FAudInTab then
|
||||||
|
begin
|
||||||
|
if I = 0 then Exit(FCurInDev = '');
|
||||||
|
Result := FInDevNames[I - 1] = FCurInDev;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
// Пустое имя вывода = устройство по умолчанию (первая строка списка).
|
||||||
|
Result := (FDevNames[I] = FCurDev) or ((FCurDev = '') and (I = 0));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TVfoOverlay.ScrollAudToCurrent;
|
||||||
|
var
|
||||||
|
I, Cnt, MaxScroll: Integer;
|
||||||
|
begin
|
||||||
|
FAudScroll := 0;
|
||||||
|
Cnt := AudRowCount;
|
||||||
|
if Cnt <= AUD_MAX_VIS then Exit;
|
||||||
|
for I := 0 to Cnt - 1 do
|
||||||
|
if AudRowCurrent(I) then
|
||||||
|
begin
|
||||||
|
// Ставим выбранную строку примерно в середину видимого окна.
|
||||||
|
MaxScroll := Cnt - AUD_MAX_VIS;
|
||||||
|
FAudScroll := Min(Max(0, I - AUD_MAX_VIS div 2), MaxScroll);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TVfoOverlay.DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
|
procedure TVfoOverlay.DrawAudPanel(C: TCanvas; X, Y, RowW: Integer);
|
||||||
const
|
const
|
||||||
ARROW_W = 15;
|
ARROW_W = 15;
|
||||||
var
|
var
|
||||||
I, Slot, VisN, RowWidth, MaxScroll, PanelH, ArrowX, MidY, CY: Integer;
|
I, Slot, VisN, RowWidth, MaxScroll, PanelH, ArrowX, MidY, CY, Cnt: Integer;
|
||||||
HR: TRect;
|
HR, TabR: TRect;
|
||||||
Nm: string;
|
Nm: string;
|
||||||
Cur, Scrollable, CanUp, CanDn: Boolean;
|
Cur, Scrollable, CanUp, CanDn: Boolean;
|
||||||
Tri: array[0..2] of TPoint;
|
Tri: array[0..2] of TPoint;
|
||||||
begin
|
begin
|
||||||
|
// Ряд вкладок: OUT (куда отдаём звук) / IN (откуда берём модуляцию).
|
||||||
|
TabR := Rect(X, Y, X + (RowW - 4) div 2, Y + ROW_H);
|
||||||
|
DrawButton(C, TabR, 'OUT', not FAudInTab,
|
||||||
|
(FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_AUD_TAB_OUT), 7);
|
||||||
|
RegisterHit(TabR, HIT_AUD_TAB_OUT);
|
||||||
|
TabR := Rect(TabR.Right + 4, Y, X + RowW, Y + ROW_H);
|
||||||
|
DrawButton(C, TabR, 'IN', FAudInTab,
|
||||||
|
(FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_AUD_TAB_IN), 7);
|
||||||
|
RegisterHit(TabR, HIT_AUD_TAB_IN);
|
||||||
|
Inc(Y, ROW_H + IGAP);
|
||||||
|
|
||||||
C.Font.Name := 'Courier New';
|
C.Font.Name := 'Courier New';
|
||||||
C.Font.Size := 7;
|
C.Font.Size := 7;
|
||||||
C.Font.Style := [];
|
C.Font.Style := [];
|
||||||
if FDevNames.Count = 0 then
|
Cnt := AudRowCount;
|
||||||
|
if Cnt = 0 then
|
||||||
begin
|
begin
|
||||||
C.Font.Color := CLR_DIM;
|
C.Font.Color := CLR_DIM;
|
||||||
C.Brush.Style := bsClear;
|
C.Brush.Style := bsClear;
|
||||||
@@ -928,9 +1017,10 @@ begin
|
|||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
VisN := Min(FDevNames.Count, AUD_MAX_VIS);
|
VisN := Min(Cnt, AUD_MAX_VIS);
|
||||||
Scrollable := FDevNames.Count > AUD_MAX_VIS;
|
FAudListRect := Rect(X, Y, X + RowW, Y + VisN*AUD_ROW_H); // зона колёсика
|
||||||
MaxScroll := FDevNames.Count - VisN;
|
Scrollable := Cnt > AUD_MAX_VIS;
|
||||||
|
MaxScroll := Cnt - VisN;
|
||||||
if FAudScroll > MaxScroll then FAudScroll := MaxScroll;
|
if FAudScroll > MaxScroll then FAudScroll := MaxScroll;
|
||||||
if FAudScroll < 0 then FAudScroll := 0;
|
if FAudScroll < 0 then FAudScroll := 0;
|
||||||
if Scrollable then RowWidth := RowW - ARROW_W - 2 else RowWidth := RowW;
|
if Scrollable then RowWidth := RowW - ARROW_W - 2 else RowWidth := RowW;
|
||||||
@@ -939,8 +1029,8 @@ begin
|
|||||||
begin
|
begin
|
||||||
I := FAudScroll + Slot;
|
I := FAudScroll + Slot;
|
||||||
HR := Rect(X, Y + Slot*AUD_ROW_H, X + RowWidth, Y + Slot*AUD_ROW_H + 16);
|
HR := Rect(X, Y + Slot*AUD_ROW_H, X + RowWidth, Y + Slot*AUD_ROW_H + 16);
|
||||||
Nm := FDevNames[I];
|
Nm := AudRowName(I);
|
||||||
Cur := (Nm = FCurDev) or ((FCurDev = '') and (I = 0));
|
Cur := AudRowCurrent(I);
|
||||||
if Cur then C.Brush.Color := CLR_ACCENT_D
|
if Cur then C.Brush.Color := CLR_ACCENT_D
|
||||||
else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_DEV_BASE + I) then
|
else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_DEV_BASE + I) then
|
||||||
C.Brush.Color := CLR_BTN_HOT
|
C.Brush.Color := CLR_BTN_HOT
|
||||||
@@ -954,7 +1044,7 @@ begin
|
|||||||
if C.TextWidth(Nm) > RowWidth - 8 then
|
if C.TextWidth(Nm) > RowWidth - 8 then
|
||||||
while (Length(Nm) > 1) and (C.TextWidth(Nm + '…') > RowWidth - 8) do
|
while (Length(Nm) > 1) and (C.TextWidth(Nm + '…') > RowWidth - 8) do
|
||||||
SetLength(Nm, Length(Nm) - 1);
|
SetLength(Nm, Length(Nm) - 1);
|
||||||
if Nm <> FDevNames[I] then Nm := Nm + '…';
|
if Nm <> AudRowName(I) then Nm := Nm + '…';
|
||||||
C.TextOut(HR.Left + 4, HR.Top + (16 - C.TextHeight('A')) div 2, Nm);
|
C.TextOut(HR.Left + 4, HR.Top + (16 - C.TextHeight('A')) div 2, Nm);
|
||||||
RegisterHit(HR, HIT_DEV_BASE + I);
|
RegisterHit(HR, HIT_DEV_BASE + I);
|
||||||
end;
|
end;
|
||||||
@@ -1007,6 +1097,7 @@ var
|
|||||||
BarL, BarW, MeterX: Integer;
|
BarL, BarW, MeterX: Integer;
|
||||||
begin
|
begin
|
||||||
FHitCount := 0;
|
FHitCount := 0;
|
||||||
|
FAudListRect := Rect(0, 0, 0, 0); // выставит DrawAudPanel, если панель открыта
|
||||||
|
|
||||||
// Фон карточки
|
// Фон карточки
|
||||||
C.Brush.Color := CLR_CARD_BG;
|
C.Brush.Color := CLR_CARD_BG;
|
||||||
@@ -1235,16 +1326,42 @@ begin
|
|||||||
HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end;
|
HIT_CLOSE: begin if Assigned(FOnClose) then FOnClose(Self); Exit; end;
|
||||||
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
|
HIT_AUD_UP: begin if FAudScroll > 0 then Dec(FAudScroll);
|
||||||
RequestInvalidate; Exit; end;
|
RequestInvalidate; Exit; end;
|
||||||
HIT_AUD_DN: begin if FAudScroll < FDevNames.Count - AUD_MAX_VIS then
|
HIT_AUD_DN: begin if FAudScroll < AudRowCount - AUD_MAX_VIS then
|
||||||
Inc(FAudScroll);
|
Inc(FAudScroll);
|
||||||
RequestInvalidate; Exit; end;
|
RequestInvalidate; Exit; end;
|
||||||
|
HIT_AUD_TAB_OUT,
|
||||||
|
HIT_AUD_TAB_IN:
|
||||||
|
begin
|
||||||
|
if FAudInTab <> (HV = HIT_AUD_TAB_IN) then
|
||||||
|
begin
|
||||||
|
FAudInTab := HV = HIT_AUD_TAB_IN;
|
||||||
|
ScrollAudToCurrent; // список другой длины
|
||||||
|
Height := ComputeHeight;
|
||||||
|
RequestInvalidate;
|
||||||
|
end;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Устройство аудио
|
// Устройство аудио (строка активной вкладки OUT/IN)
|
||||||
if HV >= HIT_DEV_BASE then
|
if HV >= HIT_DEV_BASE then
|
||||||
begin
|
begin
|
||||||
NewMode := HV - HIT_DEV_BASE; // индекс в списке
|
NewMode := HV - HIT_DEV_BASE; // индекс строки
|
||||||
if (NewMode >= 0) and (NewMode < FDevNames.Count) then
|
if (NewMode < 0) or (NewMode >= AudRowCount) then Exit;
|
||||||
|
if FAudInTab then
|
||||||
|
begin
|
||||||
|
// Строка 0 = «общий вход» (слайс) / «нет устройства» (главный флаг).
|
||||||
|
if NewMode = 0 then FCurInDev := ''
|
||||||
|
else FCurInDev := FInDevNames[NewMode - 1];
|
||||||
|
FPanel := fpNone; Height := ComputeHeight;
|
||||||
|
RequestInvalidate;
|
||||||
|
if Assigned(FOnAudioInDevice) then
|
||||||
|
begin
|
||||||
|
if NewMode = 0 then FOnAudioInDevice(-1, '')
|
||||||
|
else FOnAudioInDevice(FInDevIdx[NewMode - 1], FInDevNames[NewMode - 1]);
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
begin
|
begin
|
||||||
FCurDev := FDevNames[NewMode];
|
FCurDev := FDevNames[NewMode];
|
||||||
FPanel := fpNone; Height := ComputeHeight;
|
FPanel := fpNone; Height := ComputeHeight;
|
||||||
@@ -1394,6 +1511,22 @@ begin
|
|||||||
Result := True; // клик был по накладке — не пускаем его в спектр
|
Result := True; // клик был по накладке — не пускаем его в спектр
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TVfoOverlay.HandleMouseWheel(X, Y, WheelDelta: Integer): Boolean;
|
||||||
|
var
|
||||||
|
MaxScroll, Old: Integer;
|
||||||
|
begin
|
||||||
|
Result := False;
|
||||||
|
if (not Visible) or (FPanel <> fpAud) then Exit;
|
||||||
|
if not PtInRect(FAudListRect, Point(X - Left, Y - Top)) then Exit;
|
||||||
|
Result := True; // зона наша — колесо в спектр не пускаем
|
||||||
|
MaxScroll := AudRowCount - AUD_MAX_VIS;
|
||||||
|
if MaxScroll <= 0 then Exit; // список целиком виден — крутить нечего
|
||||||
|
Old := FAudScroll;
|
||||||
|
if WheelDelta > 0 then Dec(FAudScroll) else Inc(FAudScroll);
|
||||||
|
FAudScroll := EnsureRange(FAudScroll, 0, MaxScroll);
|
||||||
|
if FAudScroll <> Old then RequestInvalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
function TVfoOverlay.HandleMouseLeave: Boolean;
|
function TVfoOverlay.HandleMouseLeave: Boolean;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
@@ -1543,6 +1676,20 @@ begin
|
|||||||
RequestInvalidate;
|
RequestInvalidate;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TVfoOverlay.SetAudioInDevices(Names: TStrings;
|
||||||
|
const Indices: array of Integer; const CurName: string);
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
FInDevNames.Assign(Names);
|
||||||
|
SetLength(FInDevIdx, FInDevNames.Count);
|
||||||
|
for I := 0 to FInDevNames.Count - 1 do
|
||||||
|
if I <= High(Indices) then FInDevIdx[I] := Indices[I] else FInDevIdx[I] := -1;
|
||||||
|
FCurInDev := CurName;
|
||||||
|
if FPanel = fpAud then Height := ComputeHeight;
|
||||||
|
RequestInvalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TVfoOverlay.UpdateSMeter(DB: Double);
|
procedure TVfoOverlay.UpdateSMeter(DB: Double);
|
||||||
begin
|
begin
|
||||||
if Abs(DB - FSMeterDB) > FSMeterThreshold then
|
if Abs(DB - FSMeterDB) > FSMeterThreshold then
|
||||||
|
|||||||
Reference in New Issue
Block a user