defmodule Membrane.Audiometer.Peakmeter.Amplitude do @moduledoc false alias Membrane.RawAudio @doc """ Finds frame within given payload that has the highest amplitude for any of its channels. On success, it returns `{:ok, {values, rest}}`, where: * `values` is a list of amplitudes per channel expressed in decibels, or one of `:infinity` or `:clip`, * `rest` is a binary containing payload remaining after processing if given payload contained incomplete frames. On error it returns, `{:error, reason}`, where `reason` is one of the following: * `:empty` - the payload was empty. """ @spec find_amplitudes(binary, RawAudio.t()) :: {:ok, {[number | :infinity | :clip], binary}} | {:error, any} def find_amplitudes(<<>>, _stream_format) do {:error, :empty} end def find_amplitudes(payload, stream_format) do # Get silence as a point of reference silence_payload = RawAudio.silence(stream_format) silence_value = RawAudio.sample_to_value(silence_payload, stream_format) # Find max sample values within given payload {:ok, {frame_values, rest}} = do_find_frame_with_max_values( payload, stream_format, RawAudio.frame_size(stream_format), RawAudio.sample_size(stream_format), silence_value, nil ) # Convert values into decibels max_amplitude_value = if RawAudio.sample_type_float?(stream_format) do 1.0 else # +1 is needed so max int value for frame does not cause clipping RawAudio.sample_max(stream_format) - silence_value + 1 end {:ok, frame_values_in_dbs} = do_convert_values_to_dbs(frame_values, max_amplitude_value, []) # Return amplitudes in dBs and remaining payload {:ok, {frame_values_in_dbs, rest}} end # If we have at least one frame in the payload, get values of its samples. defp do_find_frame_with_max_values( payload, stream_format, frame_size, sample_size, silence_value, acc ) when byte_size(payload) >= frame_size do <> = payload # Get list of sample values per channel, normalized to 0..n scale {:ok, sample_values} = do_sample_to_channel_values(frame, stream_format, sample_size, silence_value, []) # Check if the new frame is louder than the last known loudest {:ok, max_sample_values} = do_find_max_sample_values(acc, sample_values) do_find_frame_with_max_values( rest, stream_format, frame_size, sample_size, silence_value, max_sample_values ) end # If we have not less than one frame in the payload, return the amplitudes # and the remaning payload. defp do_find_frame_with_max_values( payload, _stream_format, _frame_size, _sample_size, _silence_value, acc ), do: {:ok, {acc, payload}} # If there's any payload, convert sample data to actual values normalized on 0..n scale defp do_sample_to_channel_values(payload, stream_format, sample_size, silence_value, acc) when byte_size(payload) >= sample_size do <> = payload value = (RawAudio.sample_to_value(sample, stream_format) - silence_value) |> abs do_sample_to_channel_values(rest, stream_format, sample_size, silence_value, [value | acc]) end # If there's no more payload in the frame, return a list with sample values per channel. defp do_sample_to_channel_values(<<>>, _stream_format, _sample_size, _silence_value, acc), do: {:ok, Enum.reverse(acc)} # If there's no previous sample known, current one has to be louder. defp do_find_max_sample_values(nil, current), do: {:ok, current} # Use pattern matching for the most common cases for a performance reasons: # Mono, previous frame is louder defp do_find_max_sample_values([previous_value] = previous, [current_value]) when previous_value >= current_value, do: {:ok, previous} # Mono, current frame is louder defp do_find_max_sample_values([previous_value], [current_value] = current) when previous_value < current_value, do: {:ok, current} # Stereo, any of the channels in the previous frame is louder than any of the channels in the current frame defp do_find_max_sample_values([previous_value1, previous_value2] = previous, [ current_value1, current_value2 ]) when (previous_value1 >= current_value1 and previous_value1 >= current_value2) or (previous_value2 >= current_value1 and previous_value2 >= current_value2), do: {:ok, previous} # Stereo, any of the channels in the current frame is louder than any of the channels in the previous frame defp do_find_max_sample_values( [previous_value1, previous_value2], [current_value1, current_value2] = current ) when (current_value1 >= previous_value1 and current_value1 >= previous_value2) or (current_value2 >= previous_value1 and current_value2 >= previous_value2), do: {:ok, current} # Generic handler for multi-channel audio defp do_find_max_sample_values(previous, current) do if Enum.max(previous) >= Enum.max(current) do {:ok, previous} else {:ok, current} end end defp do_convert_values_to_dbs([head | tail], max_amplitude_value, acc) do value = cond do head > max_amplitude_value -> :clip head > 0 -> 20 * :math.log10(head / max_amplitude_value) true -> :infinity end do_convert_values_to_dbs(tail, max_amplitude_value, [value | acc]) end defp do_convert_values_to_dbs([], _max_amplitude_value, acc), do: {:ok, Enum.reverse(acc)} end