lib/membrane_element_audiometer/peakmeter.ex

defmodule Membrane.Audiometer.Peakmeter do
  @moduledoc """
  This element computes peaks in each channel of the given signal at
  regular time intervals, regardless if it receives data or not.

  It uses erlang's `:timer.send_interval/2` which might not provide
  perfect accuracy.

  It accepts audio samples in any format supported by `Membrane.RawAudio`
  module.

  It will periodically emit notifications, of the following format:

  * `{:audiometer, :underrun}` - if there were not enough data to
    compute audio level within given interval,
  * `{:audiometer, {:measurement, measurement}}` - where `measurement`
    is a `Membrane.Audiometer.Peakmeter.Notification.Measurement`
    struct containing computed audio levels. See its documentation for
    more details about the actual value format.

  See `options/0` for available options.
  """
  use Membrane.Filter
  alias __MODULE__.Amplitude
  alias Membrane.Element.PadData
  alias Membrane.RawAudio

  @type amplitude_t :: [number | :infinity | :clip]

  def_input_pad :input,
    availability: :always,
    mode: :pull,
    caps: RawAudio,
    demand_unit: :buffers,
    demand_mode: :auto

  def_output_pad :output,
    availability: :always,
    mode: :pull,
    demand_mode: :auto,
    caps: RawAudio

  def_options interval: [
                type: :integer,
                description: """
                How often peakmeter should emit messages containing sound level (in Membrane.Time units).
                """,
                default: 50 |> Membrane.Time.milliseconds()
              ]

  # Private API

  @impl true
  def handle_init(%__MODULE__{interval: interval}) do
    state = %{
      interval: interval,
      queue: <<>>
    }

    {:ok, state}
  end

  @impl true
  def handle_prepared_to_playing(_ctx, state) do
    {{:ok, start_timer: {:timer, state.interval}}, state}
  end

  @impl true
  def handle_prepared_to_stopped(_ctx, state) do
    {{:ok, stop_timer: :timer}, state}
  end

  @impl true
  def handle_process(
        :input,
        %Membrane.Buffer{payload: payload} = buffer,
        _context,
        state
      ) do
    new_state = %{state | queue: state.queue <> payload}
    {{:ok, buffer: {:output, buffer}}, new_state}
  end

  @impl true
  def handle_tick(:timer, %{pads: %{input: %PadData{caps: nil}}}, state) do
    {{:ok, notify: :underrun}, state}
  end

  def handle_tick(:timer, %{pads: %{input: %PadData{caps: caps}}}, state) do
    frame_size = RawAudio.frame_size(caps)

    if byte_size(state.queue) < frame_size do
      {{:ok, notify: {:audiometer, :underrun}}, state}
    else
      {:ok, {amplitudes, rest}} = Amplitude.find_amplitudes(state.queue, caps)
      {{:ok, notify: {:amplitudes, amplitudes}}, %{state | queue: rest}}
    end
  end
end