Skip to content

Instantly share code, notes, and snippets.

@c4710n
Last active July 8, 2022 09:12
Show Gist options
  • Save c4710n/86db60622fe10bba5bd537d4ae03b56e to your computer and use it in GitHub Desktop.
Save c4710n/86db60622fe10bba5bd537d4ae03b56e to your computer and use it in GitHub Desktop.
A working example of calculating the duration of MP3 files in Elixir.
defmodule MP3 do
def get_duration(data) when is_binary(data) do
{_, rest} = parse_tag(data)
parse_frame(rest, 0, 0, 0)
end
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
{:not_implemented, rest}
end
defp parse_tag(data) do
{:not_implemented, data}
end
def 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
def parse_frame(<<_::size(8), rest::binary>>, acc, frame_count, offset) do
parse_frame(rest, acc, frame_count, offset + 1)
end
def 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
@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
]
defp lookup_bitrate(_version, _layer, 0), do: :invalid
defp lookup_bitrate(_version, _layer, 0xF), do: :invalid
defp lookup_bitrate(:version1, :layer1, index), do: Enum.at(@v1_l1_bitrates, index)
defp lookup_bitrate(:version1, :layer2, index), do: Enum.at(@v1_l2_bitrates, index)
defp lookup_bitrate(:version1, :layer3, index), do: Enum.at(@v1_l3_bitrates, index)
defp lookup_bitrate(v, :layer1, index) when v in [:version2, :version25],
do: Enum.at(@v2_l1_bitrates, index)
defp lookup_bitrate(v, l, index) when v in [:version2, :version25] and l in [:layer2, :layer3],
do: Enum.at(@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
"./test.mp3"
|> File.read!()
|> MP3.get_duration()
|> IO.inspect()
# From https://shadowfacts.net/2021/mp3-duration/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment