Compare commits

...

No commits in common. "master" and "dev" have entirely different histories.
master ... dev

8 changed files with 171 additions and 27 deletions

View File

@ -6,7 +6,7 @@ In theory supports all of MPEG layer 1, 2 and 3 and versions 1, 2 and 2.5, but i
## Usage
Currently WIP. See the [moduledoc](https://github.com/rfwatson/mpeg-audio-frame-parser/blob/master/lib/mpeg_audio_frame_parser.ex) for usage examples.
Currently WIP. See the moduledoc for usage examples.
## Contributing

View File

@ -36,7 +36,9 @@ defmodule MPEGAudioFrameParser do
## Example
Using a faked 128kbps 44.1k stereo MP3 frame:
Using a faked 128kbps 44.1k stereo MP3 frame. Note at least two headers must
be processed to return a single MP3 frame - otherwise frame completeness
couldn't be assured:
iex> packet = <<0b11111111111_11_01_0_1001_00_0_0_00_00_0_0_00::size(32), 1::size(3304)>>
...> {:ok, _pid} = MPEGAudioFrameParser.start_link()
@ -72,7 +74,9 @@ defmodule MPEGAudioFrameParser do
## Example
Using a faked 128kbps 44.1k stereo MP3 frame:
Using a faked 128kbps 44.1k stereo MP3 frame. Note at least two headers must
be processed to return a single MP3 frame - otherwise frame completeness
couldn't be assured:
iex> packet = <<0b11111111111_11_01_0_1001_00_0_0_00_00_0_0_00::size(32), 1::size(3304)>>
...> {:ok, _pid} = MPEGAudioFrameParser.start_link()

View File

@ -0,0 +1,70 @@
defmodule MPEGAudioFrameParser.Example do
require Logger
def run(path_to_mp3, block_size \\ 2048) do
# start our test process:
MPEGAudioFrameParser.start_link()
# open the provided file:
{:ok, file} = File.open(path_to_mp3, [:read, :binary])
# and start to read blocks of data:
read_bytes(file, block_size, 0, 0, 0)
end
defp read_bytes(file, block_size, total_packets, total_frames, total_bytes_processed) do
IO.binread(file, block_size)
|> handle_read(file, block_size, total_packets + 1, total_frames, total_bytes_processed)
end
defp handle_read(:eof, _file, _block_size, total_packets, total_frames, total_bytes_processed) do
Logger.info(
"End of file detected. Parsed #{total_frames} MP3 frames from #{total_packets} packets of data, total bytes processed #{
total_bytes_processed
}"
)
end
defp handle_read(
{:error, reason},
_file,
_block_size,
_total_packets,
_total_frames,
_total_bytes_processed
) do
Logger.error("Error reading file: #{reason}")
exit(:shutdown)
end
defp handle_read(data, file, block_size, total_packets, total_frames, total_bytes_processed) do
frames = MPEGAudioFrameParser.add_packet(data)
check_frames(frames)
read_bytes(
file,
block_size,
total_packets,
total_frames + length(frames),
total_byte_size(frames, total_bytes_processed)
)
end
defp check_frames([]), do: nil
defp check_frames(frames) do
[head | tail] = frames
print_frame(head)
check_frames(tail)
end
defp print_frame(_frame) do
end
defp total_byte_size([], total), do: total
defp total_byte_size(list, total) do
[head | tail] = list
total_byte_size(tail, total + byte_size(head.data))
end
end

View File

@ -17,8 +17,7 @@ defmodule MPEGAudioFrameParser.Frame do
@header_length 32
def from_header(header)
when is_binary(header)
and bit_size(header) == @header_length
when bit_size(header) == @header_length
do
frame = %Frame{data: header}
|> Map.put(:version_id, parse_version(header))

View File

@ -28,12 +28,6 @@ defmodule MPEGAudioFrameParser.Impl do
# Private Functions
# Synced, and the current frame is complete:
defp process_bytes(%{current_frame: %Frame{complete: true}} = state, packet) do
frames = [state.current_frame | state.frames]
process_bytes(%{state | current_frame: nil, frames: frames}, packet)
end
# No data left, or not enough to be able to validate next frame. Return:
defp process_bytes(state, packet)
when bit_size(packet) < 32
@ -68,6 +62,21 @@ defmodule MPEGAudioFrameParser.Impl do
process_bytes(%{state | current_frame: nil}, rest)
end
# Synced, frame complete, and looks like another sync word. Create a new frame struct:
defp process_bytes(%{current_frame: %Frame{complete: true}} = state, <<@sync_word::size(11), header::size(21), rest::bits>>) do
header = <<@sync_word::size(11), header::size(21)>>
frames = [state.current_frame | state.frames]
new_state = %{state | current_frame: Frame.from_header(header), frames: frames}
process_bytes(new_state, rest)
end
# Synced, frame complete, but next frame looks bad. Prepend, discard a byte and unsync:
defp process_bytes(%{current_frame: %Frame{complete: true}} = state, packet) do
Logger.warn "Lost sync. Discarding a byte and searching again for a sync word."
<<_byte, rest>> = state.current_frame.data
process_bytes(%{state | current_frame: nil}, <<rest, packet::bits>>)
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
{:ok, frame, rest} = Frame.add_bytes(state.current_frame, packet)

View File

@ -3,27 +3,27 @@ defmodule MPEGAudioFrameParserIntegrationTest do
test "128kbps 44100hz MP3" do
MPEGAudioFrameParser.start_link()
assert count_frames("test/fixtures/test_128_44100.mp3") == 254
assert count_frames("test/fixtures/test_128_44100.mp3") == 253
end
test "64kbps 12000hz MP3" do
MPEGAudioFrameParser.start_link()
assert count_frames("test/fixtures/test_64_12000.mp3") == 140
assert count_frames("test/fixtures/test_64_12000.mp3") == 139
end
test "160kbps 24000hz MP3" do
MPEGAudioFrameParser.start_link()
assert count_frames("test/fixtures/test_160_24000.mp3") == 277
assert count_frames("test/fixtures/test_160_24000.mp3") == 276
end
test "128kbps 44100hz MP3 with CRC protection" do
MPEGAudioFrameParser.start_link()
assert count_frames("test/fixtures/test_128_44100_crc_protection.mp3") == 254
assert count_frames("test/fixtures/test_128_44100_crc_protection.mp3") == 253
end
test "64kbps 22050hz MP2" do
MPEGAudioFrameParser.start_link()
assert count_frames("test/fixtures/test_64_22050.mp2") == 126
assert count_frames("test/fixtures/test_64_22050.mp2") == 125
end
defp count_frames(path) do

View File

@ -13,9 +13,12 @@ defmodule MPEGAudioFrameParser.ImplTest do
test "handles a single frame at the start of a packet" do
{:ok, state} = init()
{:ok, state} = add_packet(state, @frame1)
assert %{current_frame: nil, frames: [%{data: @frame1}]} = state
assert state.current_frame.data == @frame1
assert state.current_frame.complete
assert state.frames == []
end
test "handles a single frame in the middle of a packet" do
@ -24,7 +27,9 @@ defmodule MPEGAudioFrameParser.ImplTest do
packet = <<0, 1, 2, 3, @frame1::binary>>
{:ok, state} = add_packet(state, packet)
assert %{current_frame: nil, frames: [%{data: @frame1}]} = state
assert state.current_frame.data == @frame1
assert state.current_frame.complete
assert state.frames == []
end
test "ignores a packet that includes no valid frames at all" do
@ -32,7 +37,8 @@ defmodule MPEGAudioFrameParser.ImplTest do
{:ok, state} = add_packet(state, <<1::size(10240)>>)
assert %{current_frame: nil, frames: []} = state
assert state.frames == []
assert state.current_frame == nil
end
test "handles two frames in consecutive packets" do
@ -41,7 +47,9 @@ defmodule MPEGAudioFrameParser.ImplTest do
{:ok, state} = add_packet(state, @frame1)
{:ok, state} = add_packet(state, @frame3)
assert %{current_frame: nil, frames: [%{data: @frame3}, %{data: @frame1}]} = state
assert length(state.frames) == 1
assert List.first(state.frames).data == @frame1
assert state.current_frame.data == @frame3
end
test "handles a frame split unevenly across consecutive packets" do
@ -49,9 +57,13 @@ defmodule MPEGAudioFrameParser.ImplTest do
part1 = :binary.part(@frame1, {0, 256})
part2 = :binary.part(@frame1, {byte_size(@frame1), -(byte_size(@frame1) - 256)})
part3 = :binary.part(@frame3, {0, 256})
{:ok, state} = add_packet(state, <<0, 1, 2, 3, part1::binary>>)
{:ok, state} = add_packet(state, part2)
packet = <<8, 0, 1, 0, 0, 0, 7, 90, 93, part1::binary>>
{:ok, state} = add_packet(state, packet)
packet = <<part2::binary, part3::binary>>
{:ok, state} = add_packet(state, packet)
assert length(state.frames) == 1
assert List.first(state.frames).data == @frame1
@ -59,21 +71,23 @@ defmodule MPEGAudioFrameParser.ImplTest do
test "handles three frames in a single packet" do
{:ok, state} = init()
{:ok, state} = add_packet(state, <<@frame1::binary, @frame1::binary, @frame1::binary>>)
assert length(state.frames) == 3
assert Enum.map(state.frames, & &1.data) == [@frame1, @frame1, @frame1]
assert length(state.frames) == 2
assert Enum.map(state.frames, & &1.data) == [@frame1, @frame1]
end
test "handles three frames in consecutive packets" do
{:ok, state} = init()
{:ok, state} = add_packet(state, @frame3)
{:ok, state} = add_packet(state, @frame3)
{:ok, state} = add_packet(state, @frame3)
assert length(state.frames) == 3
assert Enum.map(state.frames, & &1.data) == [@frame3, @frame3, @frame3]
assert is_nil(state.current_frame)
assert length(state.frames) == 2
assert Enum.map(state.frames, & &1.data) == [@frame3, @frame3]
assert state.current_frame.data == @frame3
end
end
@ -90,6 +104,8 @@ defmodule MPEGAudioFrameParser.ImplTest do
{:ok, state} = init()
{:ok, state} = add_packet(state, @frame1)
{:ok, state} = add_packet(state, @frame1)
{:ok, frame, state} = pop_frame(state)
assert frame.data == @frame1
@ -101,6 +117,7 @@ defmodule MPEGAudioFrameParser.ImplTest do
{:ok, state} = add_packet(state, @frame1)
{:ok, state} = add_packet(state, @frame2)
{:ok, state} = add_packet(state, @frame1)
{:ok, frame, state} = pop_frame(state)
assert frame.data == @frame1

View File

@ -0,0 +1,45 @@
defmodule MPEGAudioFrameParserTest do
use ExUnit.Case
alias MPEGAudioFrameParser.Frame
doctest MPEGAudioFrameParser
# MP3, 128kbps, no CRC protection, 44100hz, no padding, stereo
@frame1 <<0b11111111111_11_01_0_1001_00_0_0_00_00_0_0_00::size(32), 1::size(3304)>>
@frame2 <<0b11111111111_11_01_0_1001_00_0_0_00_00_0_0_00::size(32), 0::size(3304)>>
test "start_link" do
MPEGAudioFrameParser.start_link()
end
test "add_packet" do
MPEGAudioFrameParser.start_link()
MPEGAudioFrameParser.add_packet(@frame1)
result = MPEGAudioFrameParser.add_packet(@frame2)
assert [%Frame{data: @frame1}] = result
end
test "cast_packet" do
MPEGAudioFrameParser.start_link()
MPEGAudioFrameParser.cast_packet(@frame1)
MPEGAudioFrameParser.cast_packet(@frame2)
end
test "pop_frame" do
MPEGAudioFrameParser.start_link()
MPEGAudioFrameParser.cast_packet(@frame1)
MPEGAudioFrameParser.cast_packet(@frame2)
assert %Frame{data: @frame1} = MPEGAudioFrameParser.pop_frame()
assert nil == MPEGAudioFrameParser.pop_frame()
end
test "flush" do
MPEGAudioFrameParser.start_link()
MPEGAudioFrameParser.cast_packet(@frame1)
MPEGAudioFrameParser.cast_packet(@frame2)
MPEGAudioFrameParser.flush()
assert nil == MPEGAudioFrameParser.pop_frame()
end
end