Compare commits
No commits in common. "master" and "dev" have entirely different histories.
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue