Compare commits

...

7 Commits
master ... v0.2

Author SHA1 Message Date
Rob Watson 417f5d0545 Re-add doctest 2018-04-19 20:32:16 +02:00
Rob Watson d7d9b2e079 Experimental refactor to a more functional style 2018-04-19 20:29:11 +02:00
Rob Watson 395a6881ef Prefer exposing `length` key to `complete` boolean 2018-04-19 12:12:23 +02:00
Rob Watson 8c99f55145 fixup! Cleanup 2018-04-19 12:03:50 +02:00
Rob Watson 797a87ded8 Cleanup Frame.parse_sample_rate/1 2018-04-19 10:46:09 +02:00
Rob Watson 491b7462ca Add TODO 2018-04-18 21:50:43 +02:00
Rob Watson 510420d42d Cleanup 2018-04-18 21:48:09 +02:00
5 changed files with 98 additions and 83 deletions

View File

@ -8,6 +8,13 @@ In theory supports all of MPEG layer 1, 2 and 3 and versions 1, 2 and 2.5, but i
Currently WIP. See the [moduledoc](https://github.com/rfwatson/mpeg-audio-frame-parser/blob/master/lib/mpeg_audio_frame_parser.ex) for usage examples.
## TODO
* Validate CRC signature if present
* Extract additional metadata (channel mode, copyright, etc)
* Further tighten frame validation
* Investigate improvements to public API/interface
## Contributing
Contributions welcome! Please ensure that appropriate automated tests are included with any pull requests.

View File

@ -7,8 +7,8 @@ defmodule MPEGAudioFrameParser.Frame do
padding: 0,
channel_mode: nil,
data: <<>>,
valid: false,
complete: false
length: 0,
valid: false
alias MPEGAudioFrameParser.Frame
require Logger
@ -20,48 +20,33 @@ defmodule MPEGAudioFrameParser.Frame do
when is_binary(header)
and bit_size(header) == @header_length
do
frame = %Frame{data: header}
|> Map.put(:version_id, parse_version(header))
|> Map.put(:layer, parse_layer(header))
|> Map.put(:crc_protection, parse_crc_protection(header))
|> Map.put(:bitrate, parse_bitrate(header))
|> Map.put(:sample_rate, parse_sample_rate(header))
|> Map.put(:padding, parse_padding(header))
%{frame | valid: header_valid?(frame)}
with version_id = parse_version(header),
layer = parse_layer(header),
crc_protection = parse_crc_protection(header),
bitrate = parse_bitrate(header),
sample_rate = parse_sample_rate(header),
padding = parse_padding(header),
valid = valid?(version_id, layer, bitrate, sample_rate),
frame_length = frame_length(version_id, layer, bitrate, sample_rate, padding)
do
%Frame{
data: header,
version_id: version_id,
layer: layer,
crc_protection: crc_protection,
bitrate: bitrate,
sample_rate: sample_rate,
padding: padding,
valid: valid,
length: frame_length,
}
end
end
def header_valid?(%Frame{version_id: version_id, layer: layer, bitrate: bitrate, sample_rate: sample_rate})
when version_id != :reserved
and layer != :reserved
and bitrate != :bad
and sample_rate != :bad
do
true
end
def header_valid?(%Frame{}), do: false
def frame_length(%Frame{bitrate: bitrate, sample_rate: sample_rate} = frame)
when is_integer(bitrate)
and is_integer(sample_rate)
do
bits_per_frame = samples_per_frame(frame) / 8
(bits_per_frame * (frame.bitrate * 1000) / frame.sample_rate + frame.padding)
|> trunc
end
def frame_length(%Frame{}), do: 0
def add_bytes(frame, packet) do
limit = bytes_missing(frame)
{:ok, bytes, rest, complete} = split_packet(packet, limit)
{:ok, %{frame | data: frame.data <> bytes, complete: complete}, rest}
end
def bytes_missing(frame) do
(frame_length(frame) - byte_size(frame.data))
|> max(0)
limit = frame.length - byte_size(frame.data) |> max(0)
{:ok, bytes, rest} = split_packet(packet, limit)
{:ok, %{frame | data: frame.data <> bytes}, rest}
end
# Private Functions
@ -74,7 +59,7 @@ defmodule MPEGAudioFrameParser.Frame do
part1 = :binary.part(packet, {0, bytes_to_take})
part2 = :binary.part(packet, {bytes_available, -bytes_to_leave})
{:ok, part1, part2, bytes_to_take == limit}
{:ok, part1, part2}
end
defp parse_version(<<@sync_word::size(11), bits::size(2), _::bits>>), do: version_atom(bits)
@ -184,26 +169,49 @@ defmodule MPEGAudioFrameParser.Frame do
end
defp parse_sample_rate(<<@sync_word::size(11), version_bits::size(2), _::size(7), sample_rate_bits::size(2), _::bits>>) do
case {version_bits, sample_rate_bits} do
{0b11, 0b00} -> 44100
{0b11, 0b01} -> 48000
{0b11, 0b10} -> 32000
{0b10, 0b00} -> 22050
{0b10, 0b01} -> 24000
{0b10, 0b10} -> 16000
{0b00, 0b00} -> 11025
{0b00, 0b01} -> 12000
{0b00, 0b10} -> 8000
version_atom = version_atom(version_bits)
case {version_atom, sample_rate_bits} do
{:version1, 0b00} -> 44100
{:version1, 0b01} -> 48000
{:version1, 0b10} -> 32000
{:version2, 0b00} -> 22050
{:version2, 0b01} -> 24000
{:version2, 0b10} -> 16000
{:"version2.5", 0b00} -> 11025
{:"version2.5", 0b01} -> 12000
{:"version2.5", 0b10} -> 8000
_ -> :bad
end
end
defp parse_padding(<<@sync_word::size(11), _::size(11), 0b0::size(1), _::bits>>), do: 0
defp parse_padding(<<@sync_word::size(11), _::size(11), 0b1::size(1), _::bits>>), do: 1
defp parse_padding(<<@sync_word::size(11), _::size(11), padding_bit::size(1), _::bits>>), do: padding_bit
defp samples_per_frame(%Frame{layer: :layer1}), do: 384
defp samples_per_frame(%Frame{layer: :layer2}), do: 1152
defp samples_per_frame(%Frame{layer: :layer3, version_id: :version1}), do: 1152
defp samples_per_frame(%Frame{layer: :layer3, version_id: _}), do: 576
defp samples_per_frame(%Frame{}), do: 0
defp samples_per_frame(_, :layer1), do: 384
defp samples_per_frame(_, :layer2), do: 1152
defp samples_per_frame(:version1, :layer3), do: 1152
defp samples_per_frame(_, :layer3), do: 576
defp samples_per_frame(_, _), do: 0
defp valid?(version_id, layer, bitrate, sample_rate)
when version_id != :reserved
and layer != :reserved
and bitrate != :bad
and sample_rate != :bad
do
true
end
defp valid?(_version_id, _layer, _bitrate, _sample_rate), do: false
defp frame_length(version_id, layer, bitrate, sample_rate, padding)
when is_integer(bitrate)
and is_integer(sample_rate)
do
bits_per_frame = samples_per_frame(version_id, layer) / 8
(bits_per_frame * (bitrate * 1000) / sample_rate + padding)
|> trunc
end
defp frame_length(_version_id, _layer, _bitrate, _sample_rate, _padding), do: 0
end

View File

@ -29,7 +29,9 @@ defmodule MPEGAudioFrameParser.Impl do
# Private Functions
# Synced, and the current frame is complete:
defp process_bytes(%{current_frame: %Frame{complete: true}} = state, packet) do
defp process_bytes(%{current_frame: %Frame{length: frame_length, data: data}} = state, packet)
when frame_length == byte_size(data)
do
frames = [state.current_frame | state.frames]
process_bytes(%{state | current_frame: nil, frames: frames}, packet)
end
@ -50,8 +52,7 @@ defmodule MPEGAudioFrameParser.Impl do
# Not synced, found a sync word. Create a new frame struct:
defp process_bytes(%{current_frame: nil} = state, <<@sync_word::size(11), header::size(21), rest::bits>>) do
header = <<@sync_word::size(11), header::size(21)>>
frame = Frame.from_header(header)
frame = <<@sync_word::size(11), header::size(21)>> |> Frame.from_header
process_bytes(%{state | current_frame: frame}, rest)
end
@ -63,13 +64,12 @@ defmodule MPEGAudioFrameParser.Impl do
# Synced, but with an invalid header. Discard a byte:
defp process_bytes(%{current_frame: %Frame{valid: false}} = state, packet) do
data = <<state.current_frame.data, packet::bits>>
<<_byte, rest::bits>> = data
<<_byte, rest::bits>> = <<state.current_frame.data, packet::bits>>
process_bytes(%{state | current_frame: nil}, rest)
end
# Synced, current frame not complete and we have bytes available. Add bytes to frame:
defp process_bytes(%{current_frame: %Frame{complete: false}} = state, packet) do
defp process_bytes(state, packet) do
{:ok, frame, rest} = Frame.add_bytes(state.current_frame, packet)
process_bytes(%{state | current_frame: frame}, rest)
end

View File

@ -1,7 +1,7 @@
defmodule MPEGAudioFrameParser.FrameTest do
use ExUnit.Case
alias MPEGAudioFrameParser.Frame
import Frame, only: [from_header: 1, header_valid?: 1, frame_length: 1, bytes_missing: 1]
import Frame, only: [from_header: 1]
# MP3, 128kbps, no CRC protection, 44100hz, no padding, stereo
@header1 <<0b11111111111_11_01_0_1001_00_0_0_00_00_0_0_00::size(32)>>
@ -80,25 +80,21 @@ defmodule MPEGAudioFrameParser.FrameTest do
end
test "header validity" do
assert from_header(@header1) |> header_valid?
assert from_header(@header2) |> header_valid?
assert from_header(@header3) |> header_valid?
assert from_header(@header4) |> header_valid?
assert from_header(@header5) |> header_valid?
assert from_header(@header6) |> header_valid?
refute from_header(@header7) |> header_valid?
assert from_header(@header1).valid
assert from_header(@header2).valid
assert from_header(@header3).valid
assert from_header(@header4).valid
assert from_header(@header5).valid
assert from_header(@header6).valid
refute from_header(@header7).valid
end
test "frame_length" do
assert from_header(@header1) |> frame_length == 417
assert from_header(@header2) |> frame_length == 768
assert from_header(@header3) |> frame_length == 105
assert from_header(@header4) |> frame_length == 835
assert from_header(@header5) |> frame_length == 208
assert from_header(@header6) |> frame_length == 360
end
test "bytes missing" do
assert from_header(@header1) |> bytes_missing == 413
assert from_header(@header1).length == 417
assert from_header(@header2).length == 768
assert from_header(@header3).length == 105
assert from_header(@header4).length == 835
assert from_header(@header5).length == 208
assert from_header(@header6).length == 360
end
end

View File

@ -0,0 +1,4 @@
defmodule MPEGAudioFrameParserTest do
use ExUnit.Case
doctest MPEGAudioFrameParser
end