{
  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.
