mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-25 01:10:59 +00:00
Replace ffmpeg with elixir mp3 decoder 🔥🔥🔥
This commit is contained in:
parent
2b4f29034b
commit
1fc60aae2a
1 changed files with 350 additions and 26 deletions
|
@ -1,12 +1,32 @@
|
|||
defmodule LiveBeats.MP3Stat do
|
||||
@moduledoc """
|
||||
Decodes MP3s and parses out information.
|
||||
|
||||
MP3 decoding and duration calculation credit to:
|
||||
https://shadowfacts.net/2021/mp3-duration/
|
||||
"""
|
||||
use Bitwise
|
||||
alias LiveBeats.MP3Stat
|
||||
|
||||
defstruct duration: 0, path: nil
|
||||
|
||||
@declared_frame_ids ~w(AENC APIC ASPI COMM COMR ENCR EQU2 ETCO GEOB GRID LINK MCDI MLLT OWNE PRIV PCNT POPM POSS RBUF RVA2 RVRB SEEK SIGN SYLT SYTC TALB TBPM TCOM TCON TCOP TDEN TDLY TDOR TDRC TDRL TDTG TENC TEXT TFLT TIPL TIT1 TIT2 TIT3 TKEY TLAN TLEN TMCL TMED TMOO TOAL TOFN TOLY TOPE TOWN TPE1 TPE2 TPE3 TPE4 TPOS TPRO TPUB TRCK TRSN TRSO TSOA TSOP TSOT TSRC TSSE TSST TXXX UFID USER USLT WCOM WCOP WOAF WOAR WOAS WORS WPAY WPUB WXXX)
|
||||
|
||||
@v1_l1_bitrates {:invalid, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448,
|
||||
:invalid}
|
||||
@v1_l2_bitrates {:invalid, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,
|
||||
:invalid}
|
||||
@v1_l3_bitrates {:invalid, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
|
||||
:invalid}
|
||||
@v2_l1_bitrates {:invalid, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256,
|
||||
:invalid}
|
||||
@v2_l2_l3_bitrates {:invalid, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160,
|
||||
:invalid}
|
||||
|
||||
def to_mmss(duration) when is_integer(duration) do
|
||||
hours = div(duration, 60 * 60)
|
||||
minutes = div(duration - (hours * 60 * 60), 60)
|
||||
seconds = rem(duration - (hours * 60 * 60) - (minutes * 60), 60)
|
||||
minutes = div(duration - hours * 60 * 60, 60)
|
||||
seconds = rem(duration - hours * 60 * 60 - minutes * 60, 60)
|
||||
|
||||
[minutes, seconds]
|
||||
|> Enum.map(fn count -> String.pad_leading("#{count}", 2, ["0"]) end)
|
||||
|
@ -14,33 +34,337 @@ defmodule LiveBeats.MP3Stat do
|
|||
end
|
||||
|
||||
def parse(path) do
|
||||
args = ["-v", "quiet", "-stats", "-i", path, "-f", "null", "-"]
|
||||
|
||||
# "size=N/A time=00:03:00.00 bitrate=N/A speed= 674x"
|
||||
case System.cmd("ffmpeg", args, stderr_to_stdout: true) do
|
||||
{output, 0} -> parse_output(output, path)
|
||||
{_, 1} -> {:error, :bad_file}
|
||||
other -> {:error, other}
|
||||
end
|
||||
{%{} = _tag_info, rest} = parse_tag(File.read!(path))
|
||||
duration = parse_frame(rest, 0, 0, 0)
|
||||
{:ok, %MP3Stat{duration: round(duration), path: path}}
|
||||
rescue
|
||||
_ -> {:error, :bad_file}
|
||||
end
|
||||
|
||||
defp parse_output(output, path) do
|
||||
with %{"time" => time} <- Regex.named_captures(~r/.*time=(?<time>[^\s]+).*/, output),
|
||||
[hours, minutes, seconds, _milliseconds] <- ints(String.split(time, [":", "."])) do
|
||||
duration = hours * 60 * 60 + minutes * 60 + seconds
|
||||
{:ok, %MP3Stat{duration: duration, path: path}}
|
||||
defp parse_tag(<<
|
||||
"ID3",
|
||||
major_version::integer,
|
||||
_revision::integer,
|
||||
_unsynchronized::size(1),
|
||||
extended_header::size(1),
|
||||
_experimental::size(1),
|
||||
_footer::size(1),
|
||||
0::size(4),
|
||||
tag_size_synchsafe::binary-size(4),
|
||||
rest::binary
|
||||
>>) do
|
||||
tag_size = decode_synchsafe_integer(tag_size_synchsafe)
|
||||
|
||||
{rest, _ext_header_size} =
|
||||
if extended_header == 1 do
|
||||
skip_extended_header(major_version, rest)
|
||||
else
|
||||
_ -> {:error, :bad_duration}
|
||||
{rest, 0}
|
||||
end
|
||||
|
||||
parse_frames(major_version, rest, tag_size - extended_header)
|
||||
end
|
||||
|
||||
defp parse_tag(_), do: %{}
|
||||
|
||||
defp decode_synchsafe_integer(<<bin>>), do: bin
|
||||
|
||||
defp decode_synchsafe_integer(binary) do
|
||||
binary
|
||||
|> :binary.bin_to_list()
|
||||
|> Enum.reverse()
|
||||
|> Enum.with_index()
|
||||
|> Enum.reduce(0, fn {el, index}, acc -> acc ||| el <<< (index * 7) end)
|
||||
end
|
||||
|
||||
defp skip_extended_header(3, <<
|
||||
ext_header_size::size(32),
|
||||
_flags::size(16),
|
||||
_padding_size::size(32),
|
||||
rest::binary
|
||||
>>) do
|
||||
remaining_ext_header_size = ext_header_size - 6
|
||||
<<_::binary-size(remaining_ext_header_size), rest::binary>> = rest
|
||||
{rest, ext_header_size}
|
||||
end
|
||||
|
||||
defp skip_extended_header(4, <<
|
||||
ext_header_size_synchsafe::size(32),
|
||||
1::size(8),
|
||||
_flags::size(8),
|
||||
rest::binary
|
||||
>>) do
|
||||
ext_header_size = decode_synchsafe_integer(ext_header_size_synchsafe)
|
||||
remaining_ext_header_size = ext_header_size - 6
|
||||
<<_::binary-size(remaining_ext_header_size), rest::binary>> = rest
|
||||
{rest, ext_header_size}
|
||||
end
|
||||
|
||||
defp parse_frames(major_version, data, tag_length_remaining, frames \\ [])
|
||||
|
||||
defp parse_frames(
|
||||
major_version,
|
||||
<<
|
||||
frame_id::binary-size(4),
|
||||
frame_size_maybe_synchsafe::binary-size(4),
|
||||
0::size(1),
|
||||
_tag_alter_preservation::size(1),
|
||||
_file_alter_preservation::size(1),
|
||||
_read_only::size(1),
|
||||
0::size(4),
|
||||
_grouping_identity::size(1),
|
||||
0::size(2),
|
||||
_compression::size(1),
|
||||
_encryption::size(1),
|
||||
_unsynchronized::size(1),
|
||||
_has_data_length_indicator::size(1),
|
||||
rest::binary
|
||||
>>,
|
||||
tag_length_remaining,
|
||||
frames
|
||||
) do
|
||||
frame_size =
|
||||
case major_version do
|
||||
4 ->
|
||||
decode_synchsafe_integer(frame_size_maybe_synchsafe)
|
||||
|
||||
3 ->
|
||||
<<size::size(32)>> = frame_size_maybe_synchsafe
|
||||
size
|
||||
end
|
||||
|
||||
total_frame_size = frame_size + 10
|
||||
next_tag_length_remaining = tag_length_remaining - total_frame_size
|
||||
|
||||
result = decode_frame(frame_id, frame_size, rest)
|
||||
|
||||
case result do
|
||||
{nil, rest, :halt} ->
|
||||
{Map.new(frames), rest}
|
||||
|
||||
{nil, rest, :cont} ->
|
||||
parse_frames(major_version, rest, next_tag_length_remaining, frames)
|
||||
|
||||
{new_frame, rest} ->
|
||||
parse_frames(major_version, rest, next_tag_length_remaining, [new_frame | frames])
|
||||
end
|
||||
end
|
||||
|
||||
defp ints(strings) when is_list(strings) do
|
||||
Enum.flat_map(strings, fn str ->
|
||||
case Integer.parse(str) do
|
||||
{int, ""} -> [int]
|
||||
{_, _} -> []
|
||||
:error -> []
|
||||
defp parse_frames(_, data, tag_length_remaining, frames)
|
||||
when tag_length_remaining <= 0 do
|
||||
{Map.new(frames), data}
|
||||
end
|
||||
end)
|
||||
|
||||
defp parse_frames(_, data, _, frames) do
|
||||
{Map.new(frames), data}
|
||||
end
|
||||
|
||||
defp decode_frame("TXXX", frame_size, <<text_encoding::size(8), rest::binary>>) do
|
||||
{description, desc_size, rest} = decode_string(text_encoding, frame_size - 1, rest)
|
||||
{value, _, rest} = decode_string(text_encoding, frame_size - 1 - desc_size, rest)
|
||||
{{"TXXX", {description, value}}, rest}
|
||||
end
|
||||
|
||||
defp decode_frame(
|
||||
"COMM",
|
||||
frame_size,
|
||||
<<text_encoding::size(8), language::binary-size(3), rest::binary>>
|
||||
) do
|
||||
{short_desc, desc_size, rest} = decode_string(text_encoding, frame_size - 4, rest)
|
||||
{value, _, rest} = decode_string(text_encoding, frame_size - 4 - desc_size, rest)
|
||||
{{"COMM", {language, short_desc, value}}, rest}
|
||||
end
|
||||
|
||||
defp decode_frame("APIC", frame_size, <<text_encoding::size(8), rest::binary>>) do
|
||||
{mime_type, mime_len, rest} = decode_string(0, frame_size - 1, rest)
|
||||
|
||||
<<picture_type::size(8), rest::binary>> = rest
|
||||
|
||||
{description, desc_len, rest} =
|
||||
decode_string(text_encoding, frame_size - 1 - mime_len - 1, rest)
|
||||
|
||||
image_data_size = frame_size - 1 - mime_len - 1 - desc_len
|
||||
{image_data, rest} = :erlang.split_binary(rest, image_data_size)
|
||||
|
||||
{{"APIC", {mime_type, picture_type, description, image_data}}, rest}
|
||||
end
|
||||
|
||||
defp decode_frame(id, frame_size, rest) do
|
||||
cond do
|
||||
Regex.match?(~r/^T[0-9A-Z]$/, id) ->
|
||||
decode_text_frame(id, frame_size, rest)
|
||||
|
||||
id in @declared_frame_ids ->
|
||||
<<_frame_data::binary-size(frame_size), rest::binary>> = rest
|
||||
{nil, rest, :cont}
|
||||
|
||||
true ->
|
||||
{nil, rest, :halt}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_text_frame(id, frame_size, <<text_encoding::size(8), rest::binary>>) do
|
||||
{strs, rest} = decode_string_sequence(text_encoding, frame_size - 1, rest)
|
||||
{{id, strs}, rest}
|
||||
end
|
||||
|
||||
defp decode_string_sequence(encoding, max_byte_size, data, acc \\ [])
|
||||
|
||||
defp decode_string_sequence(_, max_byte_size, data, acc) when max_byte_size <= 0 do
|
||||
{Enum.reverse(acc), data}
|
||||
end
|
||||
|
||||
defp decode_string_sequence(encoding, max_byte_size, data, acc) do
|
||||
{str, str_size, rest} = decode_string(encoding, max_byte_size, data)
|
||||
decode_string_sequence(encoding, max_byte_size - str_size, rest, [str | acc])
|
||||
end
|
||||
|
||||
defp convert_string(encoding, str) when encoding in [0, 3] do
|
||||
str
|
||||
end
|
||||
|
||||
defp convert_string(1, data) do
|
||||
{encoding, bom_length} = :unicode.bom_to_encoding(data)
|
||||
{_, string_data} = String.split_at(data, bom_length)
|
||||
:unicode.characters_to_binary(string_data, encoding)
|
||||
end
|
||||
|
||||
defp convert_string(2, data) do
|
||||
:unicode.characters_to_binary(data, {:utf16, :big})
|
||||
end
|
||||
|
||||
defp decode_string(encoding, max_byte_size, data) when encoding in [1, 2] do
|
||||
{str, rest} = get_double_null_terminated(data, max_byte_size)
|
||||
|
||||
{convert_string(encoding, str), byte_size(str) + 2, rest}
|
||||
end
|
||||
|
||||
defp decode_string(encoding, max_byte_size, data) when encoding in [0, 3] do
|
||||
case :binary.split(data, <<0>>) do
|
||||
[str, rest] when byte_size(str) + 1 <= max_byte_size ->
|
||||
{str, byte_size(str) + 1, rest}
|
||||
|
||||
_ ->
|
||||
{str, rest} = :erlang.split_binary(data, max_byte_size)
|
||||
{str, max_byte_size, rest}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_double_null_terminated(data, max_byte_size, acc \\ [])
|
||||
|
||||
defp get_double_null_terminated(rest, 0, acc) do
|
||||
{acc |> Enum.reverse() |> :binary.list_to_bin(), rest}
|
||||
end
|
||||
|
||||
defp get_double_null_terminated(<<0, 0, rest::binary>>, _, acc) do
|
||||
{acc |> Enum.reverse() |> :binary.list_to_bin(), rest}
|
||||
end
|
||||
|
||||
defp get_double_null_terminated(<<a::size(8), b::size(8), rest::binary>>, max_byte_size, acc) do
|
||||
next_max_byte_size = max_byte_size - 2
|
||||
get_double_null_terminated(rest, next_max_byte_size, [b, a | acc])
|
||||
end
|
||||
|
||||
defp parse_frame(
|
||||
<<
|
||||
0xFF::size(8),
|
||||
0b111::size(3),
|
||||
version_bits::size(2),
|
||||
layer_bits::size(2),
|
||||
_protected::size(1),
|
||||
bitrate_index::size(4),
|
||||
sampling_rate_index::size(2),
|
||||
padding::size(1),
|
||||
_private::size(1),
|
||||
_channel_mode_index::size(2),
|
||||
_mode_extension::size(2),
|
||||
_copyright::size(1),
|
||||
_original::size(1),
|
||||
_emphasis::size(2),
|
||||
_rest::binary
|
||||
>> = data,
|
||||
acc,
|
||||
frame_count,
|
||||
offset
|
||||
) do
|
||||
with version when version != :invalid <- lookup_version(version_bits),
|
||||
layer when layer != :invalid <- lookup_layer(layer_bits),
|
||||
sampling_rate when sampling_rate != :invalid <-
|
||||
lookup_sampling_rate(version, sampling_rate_index),
|
||||
bitrate when bitrate != :invalid <- lookup_bitrate(version, layer, bitrate_index) do
|
||||
samples = lookup_samples_per_frame(version, layer)
|
||||
frame_size = get_frame_size(samples, layer, bitrate, sampling_rate, padding)
|
||||
frame_duration = samples / sampling_rate
|
||||
<<_skipped::binary-size(frame_size), rest::binary>> = data
|
||||
parse_frame(rest, acc + frame_duration, frame_count + 1, offset + frame_size)
|
||||
else
|
||||
_ ->
|
||||
<<_::size(8), rest::binary>> = data
|
||||
parse_frame(rest, acc, frame_count, offset + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_frame(<<_::size(8), rest::binary>>, acc, frame_count, offset) do
|
||||
parse_frame(rest, acc, frame_count, offset + 1)
|
||||
end
|
||||
|
||||
defp parse_frame(<<>>, acc, _frame_count, _offset) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp lookup_version(0b00), do: :version25
|
||||
defp lookup_version(0b01), do: :invalid
|
||||
defp lookup_version(0b10), do: :version2
|
||||
defp lookup_version(0b11), do: :version1
|
||||
|
||||
defp lookup_layer(0b00), do: :invalid
|
||||
defp lookup_layer(0b01), do: :layer3
|
||||
defp lookup_layer(0b10), do: :layer2
|
||||
defp lookup_layer(0b11), do: :layer1
|
||||
|
||||
defp lookup_sampling_rate(_version, 0b11), do: :invalid
|
||||
defp lookup_sampling_rate(:version1, 0b00), do: 44100
|
||||
defp lookup_sampling_rate(:version1, 0b01), do: 48000
|
||||
defp lookup_sampling_rate(:version1, 0b10), do: 32000
|
||||
defp lookup_sampling_rate(:version2, 0b00), do: 22050
|
||||
defp lookup_sampling_rate(:version2, 0b01), do: 24000
|
||||
defp lookup_sampling_rate(:version2, 0b10), do: 16000
|
||||
defp lookup_sampling_rate(:version25, 0b00), do: 11025
|
||||
defp lookup_sampling_rate(:version25, 0b01), do: 12000
|
||||
defp lookup_sampling_rate(:version25, 0b10), do: 8000
|
||||
|
||||
defp lookup_bitrate(_version, _layer, 0), do: :invalid
|
||||
defp lookup_bitrate(_version, _layer, 0xF), do: :invalid
|
||||
defp lookup_bitrate(:version1, :layer1, index), do: elem(@v1_l1_bitrates, index)
|
||||
defp lookup_bitrate(:version1, :layer2, index), do: elem(@v1_l2_bitrates, index)
|
||||
defp lookup_bitrate(:version1, :layer3, index), do: elem(@v1_l3_bitrates, index)
|
||||
|
||||
defp lookup_bitrate(v, :layer1, index) when v in [:version2, :version25],
|
||||
do: elem(@v2_l1_bitrates, index)
|
||||
|
||||
defp lookup_bitrate(v, l, index) when v in [:version2, :version25] and l in [:layer2, :layer3],
|
||||
do: elem(@v2_l2_l3_bitrates, index)
|
||||
|
||||
defp lookup_samples_per_frame(:version1, :layer1), do: 384
|
||||
defp lookup_samples_per_frame(:version1, :layer2), do: 1152
|
||||
defp lookup_samples_per_frame(:version1, :layer3), do: 1152
|
||||
defp lookup_samples_per_frame(v, :layer1) when v in [:version2, :version25], do: 384
|
||||
defp lookup_samples_per_frame(v, :layer2) when v in [:version2, :version25], do: 1152
|
||||
defp lookup_samples_per_frame(v, :layer3) when v in [:version2, :version25], do: 576
|
||||
|
||||
defp get_frame_size(samples, layer, kbps, sampling_rate, padding) do
|
||||
sample_duration = 1 / sampling_rate
|
||||
frame_duration = samples * sample_duration
|
||||
bytes_per_second = kbps * 1000 / 8
|
||||
size = floor(frame_duration * bytes_per_second)
|
||||
|
||||
if padding == 1 do
|
||||
size + lookup_slot_size(layer)
|
||||
else
|
||||
size
|
||||
end
|
||||
end
|
||||
|
||||
defp lookup_slot_size(:layer1), do: 4
|
||||
defp lookup_slot_size(l) when l in [:layer2, :layer3], do: 1
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue