lib/membrane_wav/postprocessing.ex

defmodule Membrane.WAV.Postprocessing do
  @moduledoc """
  Module responsible for post-processing serialized WAV files.

  Due to the fact that `Membrane.WAV.Serializer` with seeking disabled creates WAV file with
  incorrect `file length` and `data length` blocks in the header, post-processing is needed.
  `fix_wav_header/1` fixes that problem.

  Header description can be found in `Membrane.WAV.Parser`.
  """

  @type fix_error :: {:error, File.posix() | :invalid_file | :badarg | :terminated} | IO.nodata()

  @doc """
  Fixes header of the WAV file located in `path`.
  """
  @spec fix_wav_header(String.t()) :: :ok | fix_error()
  def fix_wav_header(path) do
    with {:ok, file_length} <- get_file_length(path),
         {:ok, file} <- File.open(path, [:binary, :read, :write]),
         {:ok, header_length} <- get_header_length(file),
         data_length = file_length - header_length,
         :ok <- update_file(file, file_length, header_length, data_length) do
      File.close(file)
    end
  end

  defp get_file_length(path) do
    with {:ok, info} <- File.stat(path) do
      {:ok, info.size}
    end
  end

  defp get_header_length(file) do
    with "RIFF" <- IO.binread(file, 4),
         {:ok, _new_position} <- :file.position(file, 8),
         <<"WAVE", "fmt ", format_chunk_size::32-little>> <- IO.binread(file, 12),
         next_chunk_position = 20 + format_chunk_size,
         {:ok, current_position} <- :file.position(file, next_chunk_position),
         binary when is_binary(binary) <- IO.binread(file, 8),
         {:ok, header_length} <- check_fact_and_data(file, binary, current_position) do
      {:ok, header_length}
    else
      binary when is_binary(binary) -> {:error, :invalid_file}
      error -> error
    end
  end

  defp check_fact_and_data(file, binary, current_position) do
    case binary do
      <<"fact", fact_chunk_length::32-little>> ->
        data_position = current_position + 8 + fact_chunk_length

        with {:ok, _new_position} <- :file.position(file, data_position),
             <<"data", _rest::32>> <- IO.binread(file, 8) do
          {:ok, data_position + 8}
        end

      <<"data", _rest::32>> ->
        {:ok, current_position + 8}

      _unexpected_data ->
        {:error, :invalid_file}
    end
  end

  defp update_file(file, file_length, header_length, data_length) do
    with {:ok, _new_position} <- :file.position(file, 4),
         # subtracting 8 bytes as `file_length` doesn't include "RIFF" header and the field itself
         :ok <- IO.binwrite(file, <<file_length - 8::32-little>>),
         {:ok, _new_position} <- :file.position(file, header_length - 4) do
      IO.binwrite(file, <<data_length::32-little>>)
    end
  end
end