unit wavfileunit;

{$mode objfpc}{$H+}

{
    Audio recorder. WAV file unit.
    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/>.
}

interface

uses
  Classes, SysUtils,ctypes,BaseUnix;

type
  // Тип для хранения массива амплитуд
  TAmplitudeArray = array of Double;

  // Заголовок WAV-файла
  TWAVHeader = packed record
    RIFF: array[0..3] of Char;
    FileSize: Cardinal;
    WAVE: array[0..3] of Char;
  end;

  // Структура для хранения информации о формате WAV-файла
  TFmtChunk = packed record
    ChunkSize: Cardinal;
    AudioFormat: Word;
    NumChannels: Word;
    SampleRate: Cardinal;
    ByteRate: Cardinal;
    BlockAlign: Word;
    BitsPerSample: Word;
    ExtensionSize: Word; // Размер расширения (для WAVE_FORMAT_EXTENSIBLE)
  end;

  // Основной класс для работы с WAV-файлами
  TWAVFile = class
  private
    FFileName: string;
    FHeader: TWAVHeader;
    FFmtChunk: TFmtChunk;
    FDataChunkSize: Cardinal;
    FDataOffset: Integer;
    FFileHandle: cint;
  public
    constructor Create(const FileName: string);
    destructor Destroy; override;

    // Загрузка WAV-файла
    procedure LoadFromFile(const FileName: string);
    // Сохранение WAV-файла
    procedure SaveToFile(const FileName: string; const Amplitudes: array of TAmplitudeArray; SampleRate: Cardinal; NumChannels: Word; BitsPerSample: Word);

    // Чтение данных из WAV-файла в массив амплитуд
    function ReadAmplitudes(out Amplitudes: array of TAmplitudeArray): Integer;
    // Запись массива амплитуд в WAV-файл
    procedure WriteAmplitudes(const Amplitudes: array of TAmplitudeArray);

    // Получение информации о WAV-файле
    function GetSampleRate: Cardinal;
    function GetNumChannels: Word;
    function GetBitsPerSample: Word;
    function GetDataChunkSize: Cardinal;
  end;

// Функция для записи заголовка WAV-файла
procedure WriteWAVHeader(Stream: TStream; DataSize: Cardinal; SampleRate: Cardinal; NumChannels: Word; BitsPerSample: Word);

implementation

constructor TWAVFile.Create(const FileName: string);
begin
  FFileName := FileName;
  FFileHandle := -1;
end;

destructor TWAVFile.Destroy;
begin
  if FFileHandle <> -1 then
    FpClose(FFileHandle);
  inherited Destroy;
end;

procedure TWAVFile.LoadFromFile(const FileName: string);
var
  chunkID: array[0..3] of Char;
  chunkSize: Cardinal;
begin
  FFileHandle := FpOpen(FileName, O_RDONLY);
  if FFileHandle = -1 then
    raise Exception.Create('Ошибка открытия файла');

  // Чтение заголовка WAV-файла
  if FpRead(FFileHandle, FHeader, SizeOf(TWAVHeader)) <> SizeOf(TWAVHeader) then
    raise Exception.Create('Ошибка чтения заголовка WAV');

  if (String(FHeader.RIFF) <> 'RIFF') or (String(FHeader.WAVE) <> 'WAVE') then
    raise Exception.Create('Неверный формат WAV файла');

  // Поиск и чтение fmt-секции
  while FpRead(FFileHandle, chunkID, 4) = 4 do
  begin
    if FpRead(FFileHandle, chunkSize, 4) <> 4 then
      Break;

    if String(chunkID) = 'fmt ' then
    begin
      if chunkSize < 16 then
        raise Exception.Create('Ошибка: размер fmt-секции слишком мал');

      FFmtChunk.ChunkSize := chunkSize;
      if FpRead(FFileHandle, FFmtChunk.AudioFormat, 16) <> 16 then
        raise Exception.Create('Ошибка чтения fmt-секции');

      // Пропускаем оставшиеся байты, если это расширенный формат
      if FFmtChunk.ChunkSize > 16 then
        FpLseek(FFileHandle, FFmtChunk.ChunkSize - 16, SEEK_CUR);
    end
    else if String(chunkID) = 'data' then
    begin
      FDataChunkSize := chunkSize;
      FDataOffset := FpLseek(FFileHandle, 0, SEEK_CUR);
      Break;
    end
    else
    begin
      FpLseek(FFileHandle, chunkSize, SEEK_CUR);
    end;
  end;
end;

procedure TWAVFile.SaveToFile(const FileName: string; const Amplitudes: array of TAmplitudeArray; SampleRate: Cardinal; NumChannels: Word; BitsPerSample: Word);
var
  FileHandle: cint;
  i, j: Integer;
  Sample: Int16;
  ChunkSize: Cardinal;
  AudioFormat: Word;
begin
  FileHandle := FpOpen(FileName, O_WRONLY or O_CREAT or O_TRUNC, &666);
  if FileHandle = -1 then
    raise Exception.Create('Ошибка создания файла');

  try
    // Записываем заголовок RIFF
    FpWrite(FileHandle, 'RIFF', 4);
    ChunkSize := 36 + (Length(Amplitudes[0]) * NumChannels * (BitsPerSample div 8));
    FpWrite(FileHandle, @ChunkSize, SizeOf(ChunkSize));
    FpWrite(FileHandle, 'WAVE', 4);

    // Записываем fmt-секцию
    FpWrite(FileHandle, 'fmt ', 4);
    ChunkSize := 16; // Размер fmt-секции
    FpWrite(FileHandle, @ChunkSize, SizeOf(ChunkSize));

    AudioFormat := 1; // PCM
    FpWrite(FileHandle, @AudioFormat, SizeOf(AudioFormat));
    FpWrite(FileHandle, @NumChannels, SizeOf(NumChannels));
    FpWrite(FileHandle, @SampleRate, SizeOf(SampleRate));

    ChunkSize := SampleRate * NumChannels * (BitsPerSample div 8);
    FpWrite(FileHandle, @ChunkSize, SizeOf(Cardinal));

    ChunkSize := NumChannels * (BitsPerSample div 8);
    FpWrite(FileHandle, @ChunkSize, SizeOf(Word));

    FpWrite(FileHandle, @BitsPerSample, SizeOf(Word));

    // Записываем data-секцию
    FpWrite(FileHandle, 'data', 4);
    ChunkSize := Length(Amplitudes[0]) * NumChannels * (BitsPerSample div 8);
    FpWrite(FileHandle, @ChunkSize, SizeOf(ChunkSize));

    // Записываем амплитуды
    for i := 0 to High(Amplitudes[0]) do
    begin
      for j := 0 to NumChannels - 1 do
      begin
        Sample := Round(Amplitudes[j][i] * 32767);
        if FpWrite(FileHandle, @Sample, SizeOf(Sample)) <> SizeOf(Sample) then
          raise Exception.Create('Ошибка записи данных');
      end;
    end;
  finally
    FpClose(FileHandle);
  end;
end;

function TWAVFile.ReadAmplitudes(out Amplitudes: array of TAmplitudeArray): Integer;
var
  i, j: Integer;
  Sample: Int16;
begin
  FpLseek(FFileHandle, FDataOffset, SEEK_SET);
  Result := 0;

  // Проверка на наличие данных
  if FDataChunkSize = 0 then
    raise Exception.Create('Файл не содержит данных');

  // Вычисляем количество сэмплов
  Result := FDataChunkSize div (FFmtChunk.NumChannels * (FFmtChunk.BitsPerSample div 8));

  // Инициализация массива Amplitudes
  for j := 0 to High(Amplitudes) do
    SetLength(Amplitudes[j], Result);

  // Чтение данных
  for i := 0 to Result - 1 do
  begin
    for j := 0 to FFmtChunk.NumChannels - 1 do
    begin
      if FpRead(FFileHandle, @Sample, SizeOf(Sample)) <> SizeOf(Sample) then
        Break;
      Amplitudes[j][i] := Sample / 32768.0; // Нормализация амплитуды
    end;
  end;
end;

procedure TWAVFile.WriteAmplitudes(const Amplitudes: array of TAmplitudeArray);
begin
  // Реализация метода (если требуется)
end;

function TWAVFile.GetSampleRate: Cardinal;
begin
  Result := FFmtChunk.SampleRate;
end;

function TWAVFile.GetNumChannels: Word;
begin
  Result := FFmtChunk.NumChannels;
end;

function TWAVFile.GetBitsPerSample: Word;
begin
  Result := FFmtChunk.BitsPerSample;
end;

function TWAVFile.GetDataChunkSize: Cardinal;
begin
  Result := FDataChunkSize;
end;

// Функция для записи заголовка WAV-файла
procedure WriteWAVHeader(Stream: TStream; DataSize: Cardinal; SampleRate: Cardinal; NumChannels: Word; BitsPerSample: Word);
var
  ByteRate, BlockAlign: Cardinal;
begin
  // Записываем RIFF-заголовок
  Stream.WriteBuffer('RIFF', 4); // Идентификатор RIFF
  Stream.WriteDWord(DataSize + 36); // Размер файла минус 8 байт
  Stream.WriteBuffer('WAVE', 4); // Идентификатор WAVE

  // Записываем fmt-чанк
  Stream.WriteBuffer('fmt ', 4); // Идентификатор fmt
  Stream.WriteDWord(16); // Размер fmt-чанка (16 байт)
  Stream.WriteWord(1); // Формат аудио (1 = PCM)
  Stream.WriteWord(NumChannels); // Количество каналов
  Stream.WriteDWord(SampleRate); // Частота дискретизации
  ByteRate := SampleRate * NumChannels * (BitsPerSample div 8);
  Stream.WriteDWord(ByteRate); // Байтовая скорость
  BlockAlign := NumChannels * (BitsPerSample div 8);
  Stream.WriteWord(BlockAlign); // Выравнивание блока
  Stream.WriteWord(BitsPerSample); // Битовая глубина

  // Записываем data-чанк
  Stream.WriteBuffer('data', 4); // Идентификатор data
  Stream.WriteDWord(DataSize); // Размер данных
end;

end.