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,
    accepted_format: RawAudio

  def_output_pad :output,
    availability: :always,
    accepted_format: RawAudio

  def_options interval: [
                spec: Membrane.Time.t(),
                description: """
                How often peakmeter should emit messages containing sound level (in Membrane.Time units).
                """,
                default: Membrane.Time.milliseconds(50),
                inspector: &Membrane.Time.inspect/1
              ]

  # Private API

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

    {[], state}
  end

  @impl true
  def handle_playing(_ctx, state) do
    {[start_timer: {:timer, state.interval}], state}
  end

  @impl true
  def handle_buffer(
        :input,
        %Membrane.Buffer{payload: payload} = buffer,
        _ctx,
        state
      ) do
    new_state = %{state | queue: state.queue <> payload}
    {[buffer: {:output, buffer}], new_state}
  end

  @impl true
  def handle_tick(:timer, %{pads: %{input: %PadData{stream_format: nil}}}, state) do
    {[notify_parent: :underrun], state}
  end

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

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