program wavrec;

{$mode objfpc}{$H+}

{
    Audio recorder.
    For GNU/Linux 64 bit version.
    Version: 1.
    Written on FreePascal (https://freepascal.org/).
    Copyright (C) 2025  Artyomov Alexander
    Used https://chat.deepseek.com/
    http://self-made-free.ru/
    aralni@mail.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
}


uses
  cmem, cthreads, Classes, SysUtils, BaseUnix, Unix, ALSA, wavfileunit, CTypes;

const
  SAMPLE_RATE = 44100;       // Частота дискретизации
  NUM_CHANNELS = 2;          // Количество каналов (стерео)
  BITS_PER_SAMPLE = 16;      // Битовая глубина
  SILENCE_THRESHOLD = 0.01;  // Порог тишины
  SILENCE_DURATION = 1.0;    // Длительность тишины для остановки записи
  INDICATOR_WIDTH = 20;      // Ширина индикатора уровня записи
  BUFFER_FRAMES = 4096;      // Размер буфера в кадрах

var
  ShouldStop: Boolean = False;

procedure HandleSignal(Signal: cint); cdecl;
begin
  case Signal of
    SIGINT, SIGTERM:
      begin
        WriteLn('Получен сигнал завершения. Останавливаем запись...');
        ShouldStop := True;
      end;
  end;
end;

// Функция для расчёта RMS (среднеквадратичного значения) сигнала
function CalculateRMS(const Buffer: array of Int16; NumSamples: Integer): Double;
var
  i: Integer;
  Sum: Double;
begin
  Sum := 0;
  for i := 0 to NumSamples - 1 do
  begin
    Sum := Sum + (Buffer[i] / 32768.0) * (Buffer[i] / 32768.0);
  end;
  Result := Sqrt(Sum / NumSamples);
end;

// Поток для отображения индикатора уровня записи
type
  TLevelIndicatorThread = class(TThread)
  private
    FBuffer: array of Int16;
    FBufferSize: Integer;
    FLevelLeft, FLevelRight: Double;
    FLock: TRTLCriticalSection; // Для синхронизации доступа к данным
  protected
    procedure Execute; override;
  public
    constructor Create(BufferSize: Integer);
    destructor Destroy; override;
    procedure UpdateBuffer(const NewBuffer: array of Int16);
    property LevelLeft: Double read FLevelLeft;
    property LevelRight: Double read FLevelRight;
  end;

constructor TLevelIndicatorThread.Create(BufferSize: Integer);
begin
  inherited Create(True); // Создаём поток в приостановленном состоянии
  FBufferSize := BufferSize;
  SetLength(FBuffer, FBufferSize);
  InitCriticalSection(FLock);
  FreeOnTerminate := True;
end;

destructor TLevelIndicatorThread.Destroy;
begin
  DoneCriticalSection(FLock);
  inherited Destroy;
end;

procedure TLevelIndicatorThread.UpdateBuffer(const NewBuffer: array of Int16);
begin
  EnterCriticalSection(FLock);
  try
    Move(NewBuffer[0], FBuffer[0], FBufferSize * SizeOf(Int16));
  finally
    LeaveCriticalSection(FLock);
  end;
end;

procedure TLevelIndicatorThread.Execute;
var
  i: Integer;
  LeftSum, RightSum: Double;
begin
  while not Terminated do
  begin
    EnterCriticalSection(FLock);
    try
      LeftSum := 0;
      RightSum := 0;
      for i := 0 to FBufferSize - 1 do
      begin
        if i mod 2 = 0 then
          LeftSum := LeftSum + Abs(FBuffer[i] / 32768.0)
        else
          RightSum := RightSum + Abs(FBuffer[i] / 32768.0);
      end;
      FLevelLeft := LeftSum / (FBufferSize div 2);
      FLevelRight := RightSum / (FBufferSize div 2);
    finally
      LeaveCriticalSection(FLock);
    end;

    Write(#13 + 'Уровень сигнала: L=', FormatFloat('0.00', FLevelLeft * 100), '% R=', FormatFloat('0.00', FLevelRight * 100), '%');
    Sleep(100); // Обновляем индикатор каждые 100 мс
  end;
end;

procedure RecordWAV(const OutputFile: string; SilenceThreshold: Double; DiscardSilence: Boolean; NumChannels: Integer);
var
  alsaHandle: Psnd_pcm_t;
  alsaParams: Psnd_pcm_hw_params_t;
  format: snd_pcm_format_t;
  frames: snd_pcm_uframes_t;
  err: Integer;
  dir: Integer;
  rate: Cardinal;
  bufferSize: Integer;
  Buffer: array of Int16;
  FileStream: TFileStream;
  DataSize: Cardinal;
  BytesWritten: Integer;
  LevelIndicatorThread: TLevelIndicatorThread;
  SilenceStartTime: Double;
  IsSilent: Boolean;
  Paused: Boolean;
begin
  // Открываем устройство ALSA для записи
  if snd_pcm_open(@alsaHandle, 'default', SND_PCM_STREAM_CAPTURE, 0) < 0 then
  begin
    WriteLn('Ошибка открытия ALSA устройства');
    Exit;
  end;

  // Выделяем память для параметров ALSA
  snd_pcm_hw_params_malloc(@alsaParams);
  if alsaParams = nil then
  begin
    WriteLn('Ошибка выделения памяти для параметров ALSA');
    snd_pcm_close(alsaHandle);
    Exit;
  end;

  // Инициализируем параметры ALSA
  snd_pcm_hw_params_any(alsaHandle, alsaParams);

  // Устанавливаем параметры записи
  snd_pcm_hw_params_set_access(alsaHandle, alsaParams, SND_PCM_ACCESS_RW_INTERLEAVED);
  snd_pcm_hw_params_set_format(alsaHandle, alsaParams, SND_PCM_FORMAT_S16_LE);
  snd_pcm_hw_params_set_channels(alsaHandle, alsaParams, NumChannels);

  rate := SAMPLE_RATE;
  dir := 0;
  err := snd_pcm_hw_params_set_rate_near(alsaHandle, alsaParams, @rate, @dir);
  if err < 0 then
  begin
    WriteLn('Ошибка установки частоты дискретизации: ', snd_strerror(err));
    snd_pcm_hw_params_free(alsaParams);
    snd_pcm_close(alsaHandle);
    Exit;
  end;
  WriteLn('Используемая частота дискретизации: ', rate, ' Гц');
  WriteLn('Число каналов: ', NumChannels);

  frames := BUFFER_FRAMES;
  err := snd_pcm_hw_params_set_buffer_size_near(alsaHandle, alsaParams, @frames);
  if err < 0 then
  begin
    WriteLn('Ошибка установки размера буфера: ', snd_strerror(err));
    snd_pcm_hw_params_free(alsaParams);
    snd_pcm_close(alsaHandle);
    Exit;
  end;

  // Применяем параметры
  err := snd_pcm_hw_params(alsaHandle, alsaParams);
  if err < 0 then
  begin
    WriteLn('Ошибка применения параметров: ', snd_strerror(err));
    snd_pcm_hw_params_free(alsaParams);
    snd_pcm_close(alsaHandle);
    Exit;
  end;

  // Освобождаем память, выделенную для параметров
  snd_pcm_hw_params_free(alsaParams);

  // Вычисляем размер буфера в байтах
  bufferSize := frames * NumChannels;
  SetLength(Buffer, bufferSize);

  // Создаём файл для записи
  FileStream := TFileStream.Create(OutputFile, fmCreate);
  try
    // Записываем заголовок WAV-файла с нулевым размером данных
    WriteWAVHeader(FileStream, 0, SAMPLE_RATE, NumChannels, BITS_PER_SAMPLE);

    // Записываем заголовок чанка <data>
    FileStream.WriteBuffer('data', 4); // Идентификатор чанка
    FileStream.WriteDWord(0);          // Временный размер данных (будет обновлён позже)

    DataSize := 0;
    SilenceStartTime := -1;
    IsSilent := False;
    Paused := False;

    // Запускаем индикатор уровня записи
    LevelIndicatorThread := TLevelIndicatorThread.Create(bufferSize);
    LevelIndicatorThread.Start;

    while not ShouldStop do
    begin
      // Читаем данные из ALSA
      if snd_pcm_readi(alsaHandle, @Buffer[0], frames) < 0 then
      begin
        WriteLn('Ошибка чтения данных из ALSA');
        Break;
      end;

      // Обновляем буфер в потоке индикатора
      LevelIndicatorThread.UpdateBuffer(Buffer);

      // Проверяем уровень сигнала
      if DiscardSilence then
      begin
        IsSilent := CalculateRMS(Buffer, bufferSize) < SilenceThreshold;

        if IsSilent then
        begin
          if SilenceStartTime < 0 then
            SilenceStartTime := Now
          else if (Now - SilenceStartTime) * 86400 >= SILENCE_DURATION then
          begin
            if not Paused then
            begin
              WriteLn('Тишина обнаружена, пауза...');
              Paused := True;
            end;
          end;
        end
        else
        begin
          if Paused then
          begin
            WriteLn('Звук обнаружен, продолжение записи...');
            Paused := False;
          end;
          SilenceStartTime := -1;
        end;
      end;

      if not Paused then
      begin
        // Записываем данные в файл
        BytesWritten := FileStream.Write(Buffer[0], bufferSize * SizeOf(Int16));
        Inc(DataSize, BytesWritten);
      end;
    end;

    // Останавливаем индикатор уровня
    LevelIndicatorThread.Terminate;
    LevelIndicatorThread.WaitFor;

    // Обновляем размеры в RIFF-заголовке и чанке <data>
    FileStream.Position := 4;
    FileStream.WriteDWord(DataSize + 36); // Общий размер файла минус 8 байт (RIFF и WAVE)

    FileStream.Position := 40; // Переходим к полю размера данных в чанке <data>
    FileStream.WriteDWord(DataSize); // Записываем размер данных

    // Проверяем, что размер файла соответствует ожидаемому
    if FileStream.Size <> (DataSize + 44) then
    begin
      WriteLn('Предупреждение: размер файла не соответствует ожидаемому. Возможно, есть лишние байты.');
      // Усекаем файл до ожидаемого размера
      FileStream.Size := DataSize + 44;
      WriteLn('Файл усечён до ожидаемого размера.');
    end;
  finally
    FileStream.Free;
  end;

  // Закрываем устройство ALSA
  snd_pcm_close(alsaHandle);
end;

begin
  if ParamCount < 1 then
  begin
    WriteLn('Использование: wavrec <outputfile.wav> [silence_threshold] [discard_silence] [channels]');
    WriteLn('  silence_threshold: порог тишины (по умолчанию 0.01)');
    WriteLn('  discard_silence: пропускать ли тишину (1 или 0, по умолчанию 1)');
    WriteLn('  channels: число каналов (1 для моно, 2 для стерео, по умолчанию 2)');
    Exit;
  end;

  // Обработка сигналов завершения
  fpSignal(SIGINT, @HandleSignal);
  fpSignal(SIGTERM, @HandleSignal);

  // Запускаем запись
  RecordWAV(
    ParamStr(1),
    StrToFloatDef(ParamStr(2), SILENCE_THRESHOLD),
    StrToBoolDef(ParamStr(3), True),
    StrToIntDef(ParamStr(4), 2) // По умолчанию 2 канала (стерео)
  );
end.