mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
Диагностика временным TXLog (снят) вскрыла цепочку проблем TX-тракта: - Задержка ~200мс своего сигнала в DUP: за время приёма ринг PortAudio- микрофона наполнялся до упора (9600 сэмплов) и на старте TX бёрстом оседал стоячим запасом в DUC-очереди. Фикс: TAudioInput.Flush на RX→TX (заодно убрана передача в эфир аудио, записанного до нажатия PTT). - Всплески на TUN: (1) старт — радио начинало с пустого DUC-FIFO; (2) по ходу — пейсинг со строгой каденцией держал запас FIFO у нуля, джиттер 3-6мс давал underrun; (3) клип: голос из бэклога складывался с PostGen- тоном (стоит после ALC) поверх шкалы, а сам тон при mag=1.0 давал выброс Гиббса x1.18 на старте. Фиксы: pre-roll PrimeDUCIQ (THROTTLE + 3 блока подушки в очереди — sender метрит непрерывно, FIFO радио прижат 2000..2400), анти-старв доливка нулями до REANCHOR при просадке, TUN_TONE_MAG=0.83, мьют mic-входа TXA при активном тоне. - Огрызок TUN-тона в эфир на первом блоке следующего TX: стоп канала dmode=1 ждал фейд, который некому прокрутить (продюсер уже остановлен) — канал стопился по таймауту Sleep(1)x100 с недобранным сигналом в буферах. Это же было и «остаточным фризом ~106мс» GUI на отпускании PTT: чистый таймаут, фейд никогда не исполнялся. Фикс: dmode=0 + синхронный слив/продувка канала 4 нулевыми fexchange0 на стопе и старте (<1мс, штатные down-slew и flush). - Дисплей: ResetPixelBuffers(TX_DISP_ID) на старте TX — усредняющая история TX-анализатора показывала спектр тона прошлой сессии. - StopRunning при активном TUN не чистил DUC-очередь и не сбрасывал FTuning/тон — закрыто. Латентность тракта mic→радио теперь ~43мс (WDSP 11 + подушка 21 + FIFO 10.4). Проверено на эфире: q=0..30, fill/drops/underrun=0, clip=0, клоки радио/ПК без дрейфа (micRx 48000/с). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
576 lines
18 KiB
ObjectPascal
576 lines
18 KiB
ObjectPascal
unit AudioInput;
|
||
|
||
{
|
||
PortAudio input — TX microphone capture.
|
||
Modelled after piHPSDR/portaudio.c (DL1YCF), parallel to AudioOutput.pas.
|
||
|
||
Key features:
|
||
- Mono input stream, paFloat32, 48000 Hz (fixed, как в piHPSDR)
|
||
- Pa_Initialize called once in Initialize
|
||
- Ring buffer of MY_MIC_RING_SIZE single-channel Double samples
|
||
- Callback (PaInCallback) writes mic samples into the ring buffer
|
||
- ReadSample() returns next sample, 0.0 if buffer empty
|
||
- EnumInputDevices enumerates devices with maxInputChannels > 0
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$PACKRECORDS C}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
Classes, SysUtils, SyncObjs, DynLibs;
|
||
|
||
const
|
||
MY_MIC_BUFFER_SIZE = 128; // PA frames per callback
|
||
MY_MIC_RING_SIZE = 9600; // ring buffer size (mono), как в piHPSDR
|
||
|
||
type
|
||
TPaError = LongInt;
|
||
TPaDeviceIndex = LongInt;
|
||
TPaTime = Double;
|
||
TPaStream = Pointer;
|
||
PPaStream = ^TPaStream;
|
||
|
||
TPaMicStreamParameters = record
|
||
device: TPaDeviceIndex;
|
||
channelCount: LongInt;
|
||
sampleFormat: LongWord;
|
||
suggestedLatency: TPaTime;
|
||
hostApiSpecificStreamInfo: Pointer;
|
||
end;
|
||
PPaMicStreamParameters = ^TPaMicStreamParameters;
|
||
|
||
TPaMicDeviceInfo = record
|
||
structVersion: Integer;
|
||
name: PAnsiChar;
|
||
hostApi: Integer;
|
||
maxInputChannels: Integer;
|
||
maxOutputChannels: Integer;
|
||
defaultLowInputLatency: TPaTime;
|
||
defaultLowOutputLatency: TPaTime;
|
||
defaultHighInputLatency: TPaTime;
|
||
defaultHighOutputLatency: TPaTime;
|
||
defaultSampleRate: Double;
|
||
end;
|
||
PPaMicDeviceInfo = ^TPaMicDeviceInfo;
|
||
|
||
TPaMicStreamCallback = function(inputBuffer, outputBuffer: Pointer;
|
||
framesPerBuffer: LongWord;
|
||
timeInfo: Pointer;
|
||
statusFlags: LongWord;
|
||
userData: Pointer): LongInt; cdecl;
|
||
|
||
TPaMic_Initialize = function: TPaError; cdecl;
|
||
TPaMic_Terminate = function: TPaError; cdecl;
|
||
TPaMic_GetDeviceCount = function: TPaDeviceIndex; cdecl;
|
||
TPaMic_GetDeviceInfo = function(device: TPaDeviceIndex): PPaMicDeviceInfo; cdecl;
|
||
TPaMic_OpenStream = function(stream: PPaStream;
|
||
inputParam: PPaMicStreamParameters;
|
||
outputParam: PPaMicStreamParameters;
|
||
sampleRate: Double;
|
||
framesPerBuffer: LongWord;
|
||
streamFlags: LongWord;
|
||
callback: TPaMicStreamCallback;
|
||
userData: Pointer): TPaError; cdecl;
|
||
TPaMic_StartStream = function(stream: TPaStream): TPaError; cdecl;
|
||
TPaMic_StopStream = function(stream: TPaStream): TPaError; cdecl;
|
||
TPaMic_CloseStream = function(stream: TPaStream): TPaError; cdecl;
|
||
TPaMic_GetErrorText = function(err: TPaError): PAnsiChar; cdecl;
|
||
|
||
{ TAudioInput }
|
||
TAudioInput = class
|
||
private
|
||
FLibHandle: TLibHandle;
|
||
FStream: TPaStream;
|
||
FSampleRate: Integer;
|
||
FOpen: Boolean;
|
||
FPAInited: Boolean;
|
||
FLastError: string;
|
||
FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора)
|
||
|
||
// Ring buffer (mono double), как в piHPSDR
|
||
FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double;
|
||
FInPt: Integer; // write pointer (callback writes)
|
||
FOutPt: Integer; // read pointer (main thread reads via ReadSample)
|
||
FMutex: TCriticalSection;
|
||
|
||
// PortAudio functions
|
||
FPa_Initialize: TPaMic_Initialize;
|
||
FPa_Terminate: TPaMic_Terminate;
|
||
FPa_GetDeviceCount: TPaMic_GetDeviceCount;
|
||
FPa_GetDeviceInfo: TPaMic_GetDeviceInfo;
|
||
FPa_OpenStream: TPaMic_OpenStream;
|
||
FPa_StartStream: TPaMic_StartStream;
|
||
FPa_StopStream: TPaMic_StopStream;
|
||
FPa_CloseStream: TPaMic_CloseStream;
|
||
FPa_GetErrorText: TPaMic_GetErrorText;
|
||
|
||
function LoadLib: Boolean;
|
||
function GetLastError: string;
|
||
|
||
public
|
||
constructor Create(SampleRate: Integer = 48000);
|
||
destructor Destroy; override;
|
||
|
||
// Загружает PA и вызывает Pa_Initialize (без открытия стрима)
|
||
function Initialize: Boolean;
|
||
// Открывает input stream для выбранного устройства
|
||
function Open: Boolean;
|
||
procedure Close;
|
||
|
||
// Перечисляет устройства ввода (maxInputChannels > 0).
|
||
// Names[i] — имя, Indices[i] — PA device index, Count — реальное кол-во.
|
||
function EnumInputDevices(Names: TStrings; out Indices: array of Integer;
|
||
out Count: Integer): Boolean;
|
||
|
||
// Найти PA device index по имени (-1 если не найдено)
|
||
function FindDeviceByName(const AName: string): Integer;
|
||
|
||
// Читает следующий моно-сэмпл из кольцевого буфера.
|
||
// Возвращает 0.0 если буфер пуст.
|
||
function ReadSample: Double;
|
||
// Сколько сэмплов реально лежит в ring буфере (0..MY_MIC_RING_SIZE-1).
|
||
// Использовать для чтения только доступных данных без пуш-тишины.
|
||
function Available: Integer;
|
||
// Сбросить накопленный бэклог (читатель догоняет писателя). Зовётся на
|
||
// старте TX: пока идёт приём, ринг наполняется до упора (~200мс), и без
|
||
// сброса весь этот «старый» звук бёрстом уходит в TX-тракт — стоячая
|
||
// задержка в DUC-очереди + передача аудио, записанного до нажатия PTT.
|
||
function Flush: Integer;
|
||
|
||
property IsOpen: Boolean read FOpen;
|
||
property LastError: string read GetLastError;
|
||
property DeviceIndex: Integer read FDeviceIndex write FDeviceIndex;
|
||
property SampleRate: Integer read FSampleRate write FSampleRate;
|
||
end;
|
||
|
||
// Глобальный PA callback для input (cdecl, не метод)
|
||
function PaInCallback(inputBuffer, outputBuffer: Pointer;
|
||
framesPerBuffer: LongWord;
|
||
timeInfo: Pointer;
|
||
statusFlags: LongWord;
|
||
userData: Pointer): LongInt; cdecl;
|
||
|
||
implementation
|
||
|
||
const
|
||
{$IFDEF DARWIN}
|
||
PA_IN_LIBS: array[0..3] of AnsiString = (
|
||
'libportaudio.dylib',
|
||
'libportaudio.2.dylib',
|
||
'/usr/local/lib/libportaudio.dylib',
|
||
'/opt/homebrew/lib/libportaudio.dylib'
|
||
);
|
||
{$ELSE}
|
||
{$IFDEF UNIX}
|
||
PA_IN_LIBS: array[0..3] of AnsiString = (
|
||
'libportaudio.so.2',
|
||
'libportaudio.so',
|
||
'libportaudio.so.2.0.0',
|
||
'libportaudio.so.0'
|
||
);
|
||
{$ELSE}
|
||
PA_IN_LIBS: array[0..5] of AnsiString = (
|
||
'portaudio_x64.dll',
|
||
'portaudio_x86.dll',
|
||
'portaudio.dll',
|
||
'libportaudio-2.dll',
|
||
'libportaudio.dll',
|
||
'libportaudio64.dll'
|
||
);
|
||
{$ENDIF}
|
||
{$ENDIF}
|
||
PA_NO_ERROR = 0;
|
||
PA_FLOAT32 = LongWord(1);
|
||
PA_NO_FLAG = LongWord(0);
|
||
PA_CONTINUE = LongInt(0);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Callback — читает моно float32 из inputBuffer, пишет в кольцевой буфер.
|
||
// Точно по образцу pa_in_cb из piHPSDR/portaudio.c
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function PaInCallback(inputBuffer, outputBuffer: Pointer;
|
||
framesPerBuffer: LongWord;
|
||
timeInfo: Pointer;
|
||
statusFlags: LongWord;
|
||
userData: Pointer): LongInt; cdecl;
|
||
var
|
||
Audio: TAudioInput;
|
||
In_: PSingle;
|
||
i: LongWord;
|
||
newpt: Integer;
|
||
begin
|
||
Audio := TAudioInput(userData);
|
||
In_ := PSingle(inputBuffer);
|
||
|
||
if In_ = nil then
|
||
begin
|
||
Result := PA_CONTINUE;
|
||
Exit;
|
||
end;
|
||
|
||
// Lock-free: callback только пишет FInPt, читает FOutPt.
|
||
// ReadSample только читает FInPt, пишет FOutPt — каждый указатель пишет один поток.
|
||
newpt := Audio.FInPt;
|
||
for i := 0 to framesPerBuffer - 1 do
|
||
begin
|
||
// Проверяем переполнение: если буфер полон — новый сэмпл отбрасывается
|
||
if (newpt + 1) mod MY_MIC_RING_SIZE <> Audio.FOutPt then
|
||
begin
|
||
Audio.FBuf[newpt] := In_^;
|
||
Inc(newpt);
|
||
if newpt >= MY_MIC_RING_SIZE then newpt := 0;
|
||
end;
|
||
Inc(In_);
|
||
end;
|
||
Audio.FInPt := newpt;
|
||
|
||
Result := PA_CONTINUE;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
|
||
constructor TAudioInput.Create(SampleRate: Integer);
|
||
begin
|
||
inherited Create;
|
||
FSampleRate := SampleRate;
|
||
FDeviceIndex := -1;
|
||
FOpen := False;
|
||
FPAInited := False;
|
||
FStream := nil;
|
||
FLibHandle := NilHandle;
|
||
FLastError := '';
|
||
FInPt := 0;
|
||
FOutPt := 0;
|
||
FillChar(FBuf, SizeOf(FBuf), 0);
|
||
FMutex := TCriticalSection.Create;
|
||
end;
|
||
|
||
destructor TAudioInput.Destroy;
|
||
begin
|
||
Close;
|
||
if FPAInited and Assigned(FPa_Terminate) then
|
||
begin
|
||
FPa_Terminate();
|
||
FPAInited := False;
|
||
end;
|
||
if FLibHandle <> NilHandle then
|
||
begin
|
||
FreeLibrary(FLibHandle);
|
||
FLibHandle := NilHandle;
|
||
end;
|
||
FMutex.Free;
|
||
inherited;
|
||
end;
|
||
|
||
function TAudioInput.GetLastError: string;
|
||
begin
|
||
Result := FLastError;
|
||
end;
|
||
|
||
function TAudioInput.LoadLib: Boolean;
|
||
var
|
||
i: Integer;
|
||
begin
|
||
Result := False;
|
||
if FLibHandle <> NilHandle then begin Result := True; Exit; end;
|
||
|
||
for i := 0 to High(PA_IN_LIBS) do
|
||
begin
|
||
FLibHandle := LoadLibrary(PA_IN_LIBS[i]);
|
||
if FLibHandle <> NilHandle then Break;
|
||
end;
|
||
|
||
if FLibHandle = NilHandle then
|
||
begin
|
||
{$IFDEF WINDOWS}
|
||
FLastError := 'PortAudio DLL not found. Put portaudio_x64.dll, portaudio.dll, or libportaudio-2.dll next to ewsdr.exe';
|
||
{$ELSE}
|
||
{$IFDEF DARWIN}
|
||
FLastError := 'libportaudio not found. Install via: brew install portaudio';
|
||
{$ELSE}
|
||
FLastError := 'libportaudio not found. sudo apt install libportaudio2';
|
||
{$ENDIF}
|
||
{$ENDIF}
|
||
Exit;
|
||
end;
|
||
|
||
FPa_Initialize := TPaMic_Initialize (GetProcAddress(FLibHandle, 'Pa_Initialize'));
|
||
FPa_Terminate := TPaMic_Terminate (GetProcAddress(FLibHandle, 'Pa_Terminate'));
|
||
FPa_GetDeviceCount := TPaMic_GetDeviceCount(GetProcAddress(FLibHandle, 'Pa_GetDeviceCount'));
|
||
FPa_GetDeviceInfo := TPaMic_GetDeviceInfo (GetProcAddress(FLibHandle, 'Pa_GetDeviceInfo'));
|
||
FPa_OpenStream := TPaMic_OpenStream (GetProcAddress(FLibHandle, 'Pa_OpenStream'));
|
||
FPa_StartStream := TPaMic_StartStream (GetProcAddress(FLibHandle, 'Pa_StartStream'));
|
||
FPa_StopStream := TPaMic_StopStream (GetProcAddress(FLibHandle, 'Pa_StopStream'));
|
||
FPa_CloseStream := TPaMic_CloseStream (GetProcAddress(FLibHandle, 'Pa_CloseStream'));
|
||
FPa_GetErrorText := TPaMic_GetErrorText(GetProcAddress(FLibHandle, 'Pa_GetErrorText'));
|
||
|
||
if not Assigned(FPa_Initialize) or
|
||
not Assigned(FPa_Terminate) or
|
||
not Assigned(FPa_GetDeviceCount) or
|
||
not Assigned(FPa_GetDeviceInfo) or
|
||
not Assigned(FPa_OpenStream) or
|
||
not Assigned(FPa_StartStream) or
|
||
not Assigned(FPa_StopStream) or
|
||
not Assigned(FPa_CloseStream) or
|
||
not Assigned(FPa_GetErrorText) then
|
||
begin
|
||
FLastError := 'libportaudio: symbols not found';
|
||
FreeLibrary(FLibHandle);
|
||
FLibHandle := NilHandle;
|
||
Exit;
|
||
end;
|
||
|
||
Result := True;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Initialize
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TAudioInput.Initialize: Boolean;
|
||
var
|
||
Err: TPaError;
|
||
begin
|
||
Result := False;
|
||
if not LoadLib then Exit;
|
||
if FPAInited then begin Result := True; Exit; end;
|
||
Err := FPa_Initialize();
|
||
if Err <> PA_NO_ERROR then
|
||
begin
|
||
FLastError := 'Pa_Initialize: ';
|
||
if Assigned(FPa_GetErrorText) then
|
||
FLastError := FLastError + string(FPa_GetErrorText(Err))
|
||
else
|
||
FLastError := FLastError + IntToStr(Err);
|
||
Exit;
|
||
end;
|
||
FPAInited := True;
|
||
Result := True;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// EnumInputDevices
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TAudioInput.EnumInputDevices(Names: TStrings;
|
||
out Indices: array of Integer; out Count: Integer): Boolean;
|
||
var
|
||
I, N: Integer;
|
||
Info: PPaMicDeviceInfo;
|
||
DevName: string;
|
||
begin
|
||
Count := 0;
|
||
Result := False;
|
||
if not Initialize then Exit;
|
||
if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then
|
||
begin
|
||
FLastError := 'PortAudio device enumeration is not available';
|
||
Exit;
|
||
end;
|
||
|
||
N := FPa_GetDeviceCount();
|
||
if N < 0 then
|
||
begin
|
||
FLastError := 'Pa_GetDeviceCount: ';
|
||
if Assigned(FPa_GetErrorText) then
|
||
FLastError := FLastError + string(FPa_GetErrorText(N))
|
||
else
|
||
FLastError := FLastError + IntToStr(N);
|
||
Exit;
|
||
end;
|
||
if N = 0 then
|
||
begin
|
||
FLastError := 'No PortAudio devices found';
|
||
Exit;
|
||
end;
|
||
|
||
for I := 0 to N - 1 do
|
||
begin
|
||
Info := FPa_GetDeviceInfo(I);
|
||
if Info = nil then Continue;
|
||
if Info^.maxInputChannels <= 0 then Continue;
|
||
if Count >= Length(Indices) then Break;
|
||
|
||
if Info^.name <> nil then
|
||
DevName := string(AnsiString(Info^.name))
|
||
else
|
||
DevName := Format('Device %d', [I]);
|
||
|
||
Names.Add(DevName);
|
||
Indices[Count] := I;
|
||
Inc(Count);
|
||
end;
|
||
Result := Count > 0;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// FindDeviceByName
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TAudioInput.FindDeviceByName(const AName: string): Integer;
|
||
var
|
||
I, N: Integer;
|
||
Info: PPaMicDeviceInfo;
|
||
DevName: string;
|
||
begin
|
||
Result := -1;
|
||
if AName = '' then Exit;
|
||
if not Initialize then Exit;
|
||
if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit;
|
||
|
||
N := FPa_GetDeviceCount();
|
||
for I := 0 to N - 1 do
|
||
begin
|
||
Info := FPa_GetDeviceInfo(I);
|
||
if Info = nil then Continue;
|
||
if Info^.maxInputChannels <= 0 then Continue;
|
||
if Info^.name <> nil then
|
||
DevName := string(AnsiString(Info^.name))
|
||
else
|
||
DevName := Format('Device %d', [I]);
|
||
if SameText(DevName, AName) then
|
||
begin
|
||
Result := I;
|
||
Exit;
|
||
end;
|
||
end;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Open — открывает input stream, как audio_open_input в piHPSDR
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TAudioInput.Open: Boolean;
|
||
var
|
||
InParam: TPaMicStreamParameters;
|
||
Err: TPaError;
|
||
Info: PPaMicDeviceInfo;
|
||
begin
|
||
Result := False;
|
||
if FOpen then begin Result := True; Exit; end;
|
||
if FDeviceIndex < 0 then
|
||
begin
|
||
FLastError := 'No input device selected';
|
||
Exit;
|
||
end;
|
||
if not Initialize then Exit;
|
||
|
||
FillChar(InParam, SizeOf(InParam), 0);
|
||
InParam.channelCount := 1; // Mono, как в piHPSDR
|
||
InParam.device := FDeviceIndex;
|
||
InParam.sampleFormat := PA_FLOAT32;
|
||
|
||
Info := FPa_GetDeviceInfo(FDeviceIndex);
|
||
{$IFDEF WINDOWS}
|
||
// На Windows latency=0 часто ломает WASAPI/MME; берём значение от устройства.
|
||
{$ENDIF}
|
||
if Info <> nil then
|
||
InParam.suggestedLatency := Info^.defaultLowInputLatency
|
||
else
|
||
InParam.suggestedLatency := 0.0;
|
||
|
||
InParam.hostApiSpecificStreamInfo := nil;
|
||
|
||
Err := FPa_OpenStream(@FStream, @InParam, nil,
|
||
FSampleRate, MY_MIC_BUFFER_SIZE,
|
||
PA_NO_FLAG, PaInCallback, Self);
|
||
if Err <> PA_NO_ERROR then
|
||
begin
|
||
FLastError := 'Pa_OpenStream (in): ';
|
||
if Assigned(FPa_GetErrorText) then
|
||
FLastError := FLastError + string(FPa_GetErrorText(Err))
|
||
else
|
||
FLastError := FLastError + IntToStr(Err);
|
||
FStream := nil;
|
||
Exit;
|
||
end;
|
||
|
||
// Сбрасываем буфер перед стартом
|
||
FInPt := 0;
|
||
FOutPt := 0;
|
||
FillChar(FBuf, SizeOf(FBuf), 0);
|
||
|
||
Err := FPa_StartStream(FStream);
|
||
if Err <> PA_NO_ERROR then
|
||
begin
|
||
FLastError := 'Pa_StartStream (in): ';
|
||
if Assigned(FPa_GetErrorText) then
|
||
FLastError := FLastError + string(FPa_GetErrorText(Err))
|
||
else
|
||
FLastError := FLastError + IntToStr(Err);
|
||
FPa_CloseStream(FStream);
|
||
FStream := nil;
|
||
Exit;
|
||
end;
|
||
|
||
FOpen := True;
|
||
FLastError := '';
|
||
Result := True;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Close — как audio_close_input в piHPSDR
|
||
// ---------------------------------------------------------------------------
|
||
|
||
procedure TAudioInput.Close;
|
||
var Err: TPaError;
|
||
begin
|
||
if not FOpen then Exit;
|
||
if FStream <> nil then
|
||
begin
|
||
Err := FPa_StopStream(FStream);
|
||
if (Err <> PA_NO_ERROR) and Assigned(FPa_GetErrorText) then
|
||
FLastError := 'Pa_StopStream (in): ' + string(FPa_GetErrorText(Err));
|
||
|
||
Err := FPa_CloseStream(FStream);
|
||
if (Err <> PA_NO_ERROR) and Assigned(FPa_GetErrorText) then
|
||
FLastError := 'Pa_CloseStream (in): ' + string(FPa_GetErrorText(Err));
|
||
|
||
FStream := nil;
|
||
end;
|
||
FOpen := False;
|
||
end;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ReadSample — как audio_get_next_mic_sample в piHPSDR
|
||
// ---------------------------------------------------------------------------
|
||
|
||
function TAudioInput.ReadSample: Double;
|
||
var newpt: Integer;
|
||
begin
|
||
// Lock-free: читает FOutPt, читает FInPt
|
||
if FOutPt = FInPt then
|
||
begin
|
||
Result := 0.0; // буфер пуст — тишина
|
||
Exit;
|
||
end;
|
||
Result := FBuf[FOutPt];
|
||
newpt := FOutPt + 1;
|
||
if newpt >= MY_MIC_RING_SIZE then newpt := 0;
|
||
FOutPt := newpt;
|
||
end;
|
||
|
||
function TAudioInput.Available: Integer;
|
||
var d: Integer;
|
||
begin
|
||
// Считаем сколько непрочитанных сэмплов между OutPt (читатель) и InPt (писатель).
|
||
d := FInPt - FOutPt;
|
||
if d < 0 then d := d + MY_MIC_RING_SIZE;
|
||
Result := d;
|
||
end;
|
||
|
||
function TAudioInput.Flush: Integer;
|
||
begin
|
||
// Lock-free, как ReadSample: одно присваивание Integer. Гонка с callback
|
||
// безобидна — в худшем случае останется/пропадёт несколько свежих сэмплов.
|
||
Result := Available;
|
||
FOutPt := FInPt;
|
||
end;
|
||
|
||
end.
|