Files
ewsdr/AudioInput.pas
T

593 lines
19 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
Copyright (C)
2026 - Uladzimir Karpenka, EW8BAK
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
}
unit AudioInput;
{
PortAudio input — TX microphone capture, parallel to AudioOutput.pas.
Key features:
- Mono input stream, paFloat32, fixed at 48000 Hz
- 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)
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)
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 — читает mono float32 из inputBuffer и пишет в кольцевой буфер.
// ---------------------------------------------------------------------------
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 — открывает входной аудиопоток.
// ---------------------------------------------------------------------------
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 input
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 — останавливает и закрывает входной аудиопоток.
// ---------------------------------------------------------------------------
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 — возвращает следующий микрофонный сэмпл из буфера.
// ---------------------------------------------------------------------------
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.