Skip to content

Commit

Permalink
Merge pull request #2 from membraneframework/serializer
Browse files Browse the repository at this point in the history
Add Serializer
  • Loading branch information
sheldak committed Aug 5, 2021
2 parents 896bd79 + c9450a8 commit 2c59437
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 3 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_wav_plugin)
[![CircleCI](https://circleci.com/gh/membraneframework/membrane_wav_plugin.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_wav_plugin)

This repository provides WAV Parser.
Plugin providing elements handling audio in WAV file format.

It is part of [Membrane Multimedia Framework](https://membraneframework.org).

Expand Down Expand Up @@ -32,6 +32,10 @@ Parsing steps:

It can parse only uncompressed audio.

## Serializer

The Serializer adds WAV header to the raw audio in uncompressed, PCM format.

## Sample usage

```elixir
Expand All @@ -47,14 +51,16 @@ defmodule Mixing.Pipeline do
input_caps: %Membrane.Caps.Audio.Raw{channels: 1, sample_rate: 16_000, format: :s16le},
output_caps: %Membrane.Caps.Audio.Raw{channels: 2, sample_rate: 48_000, format: :s16le}
},
player: Membrane.PortAudio.Sink
serializer: Membrane.WAV.Serializer,
file_sink: %Membrane.File.Sink{location: "/tmp/output.wav"},
]

links = [
link(:file_src)
|> to(:parser)
|> to(:converter)
|> to(:player)
|> to(:serializer)
|> to(:file_sink)
]

{{:ok, spec: %ParentSpec{children: children, links: links}}, %{}}
Expand Down
119 changes: 119 additions & 0 deletions lib/membrane_wav/serializer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
defmodule Membrane.WAV.Serializer do
@moduledoc """
Element responsible for raw audio serialization to WAV format.
Creates WAV header (its description can be found with `Membrane.WAV.Parser`) from received caps
and puts it before audio samples. The element assumes that audio is in PCM format. `File length`
and `data length` can be calculated only after processing all samples, so these values are
invalid (always set to 0).
The element has one option - `frames_per_buffer`. User can specify number of frames sent in one
buffer when demand unit on the output is `:buffers`. One frame contains `bits per sample` x
`number of channels` bits.
"""

use Membrane.Filter

alias Membrane.Buffer
alias Membrane.Caps.Audio.Raw, as: Caps
alias Membrane.Caps.Audio.Raw.Format

@file_length 0
@data_length 0

@audio_format 1
@format_chunk_length 16

def_options frames_per_buffer: [
type: :integer,
spec: pos_integer(),
description: """
Assumed number of raw audio frames in each buffer.
Used when converting demand from buffers into bytes.
""",
default: 2048
]

def_output_pad :output,
mode: :pull,
availability: :always,
caps: :any

def_input_pad :input,
mode: :pull,
availability: :always,
demand_unit: :bytes,
caps: Caps

@impl true
def handle_init(options) do
state =
options
|> Map.from_struct()
|> Map.put(:header_created, false)

{:ok, state}
end

@impl true
def handle_caps(:input, caps, _context, state) do
buffer = %Buffer{payload: create_header(caps)}
state = %{state | header_created: true}

{{:ok, caps: {:output, caps}, buffer: {:output, buffer}, redemand: :output}, state}
end

@impl true
def handle_demand(:output, _size, _unit, _context, %{header_created: false} = state) do
{:ok, state}
end

def handle_demand(:output, size, :bytes, _context, %{header_created: true} = state) do
{{:ok, demand: {:input, size}}, state}
end

def handle_demand(
:output,
buffers_count,
:buffers,
context,
%{header_created: true, frames_per_buffer: frames} = state
) do
caps = context.pads.output.caps
size = buffers_count * Caps.frames_to_bytes(frames, caps)

{{:ok, demand: {:input, size}}, state}
end

@impl true
def handle_process(:input, buffer, _context, %{header_created: true} = state) do
{{:ok, buffer: {:output, buffer}}, state}
end

def handle_process(:input, _buffer, _context, %{header_created: false}) do
raise(RuntimeError, "buffer received before caps, so the header is not created yet")
end

defp create_header(%Caps{channels: channels, sample_rate: sample_rate, format: format}) do
{_signedness, bits_per_sample, _endianness} = Format.to_tuple(format)

data_transmission_rate = ceil(channels * sample_rate * bits_per_sample / 8)
block_alignment_unit = ceil(channels * bits_per_sample / 8)

<<
"RIFF",
@file_length::32-little,
"WAVE",
"fmt ",
@format_chunk_length::32-little,
@audio_format::16-little,
channels::16-little,
sample_rate::32-little,
data_transmission_rate::32-little,
block_alignment_unit::16-little,
bits_per_sample::16-little,
"data",
@data_length::32-little
>>
end
end
Binary file added test/fixtures/reference.wav
Binary file not shown.
105 changes: 105 additions & 0 deletions test/serializer/serializer_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
defmodule Membrane.WAV.SerializerTest do
use ExUnit.Case
use Membrane.Pipeline

import Membrane.Testing.Assertions

alias Membrane.Buffer
alias Membrane.Caps.Audio.Raw, as: Caps
alias Membrane.Testing.Pipeline

@module Membrane.WAV.Serializer

@input_path Path.expand("../fixtures/input.wav", __DIR__)
@reference_path Path.expand("../fixtures/reference.wav", __DIR__)

describe "Serializer should" do
test "create header properly for one channel" do
caps = %Caps{
channels: 1,
sample_rate: 16_000,
format: :s16le
}

reference_header = <<
"RIFF",
0::32,
"WAVE",
"fmt ",
16::32-little,
1::16-little,
1::16-little,
16_000::32-little,
32_000::32-little,
2::16-little,
16::16-little,
"data",
0::32-little
>>

{actions, _state} = @module.handle_caps(:input, caps, %{}, %{header_created: false})

assert {:ok,
caps: _caps,
buffer: {:output, %Buffer{payload: ^reference_header}},
redemand: :output} = actions
end

test "create header properly for two channels" do
caps = %Caps{
channels: 2,
sample_rate: 44_100,
format: :s24le
}

reference_header = <<
"RIFF",
0::32,
"WAVE",
"fmt ",
16::32-little,
1::16-little,
2::16-little,
44_100::32-little,
264_600::32-little,
6::16-little,
24::16-little,
"data",
0::32-little
>>

{actions, _state} = @module.handle_caps(:input, caps, %{}, %{header_created: false})

assert {:ok,
caps: _caps,
buffer: {:output, %Buffer{payload: ^reference_header}},
redemand: :output} = actions
end

test "work with Parser" do
elements = [
file_src: %Membrane.File.Source{location: @input_path},
parser: Membrane.WAV.Parser,
serializer: Membrane.WAV.Serializer,
sink: Membrane.Testing.Sink
]

links = [
link(:file_src)
|> to(:parser)
|> to(:serializer)
|> to(:sink)
]

pipeline_options = %Pipeline.Options{elements: elements, links: links}
assert {:ok, pid} = Pipeline.start_link(pipeline_options)

{:ok, <<header::44-bytes, payload::8-bytes>>} = File.read(@reference_path)

assert Pipeline.play(pid) == :ok
assert_sink_buffer(pid, :sink, %Buffer{payload: ^header})
assert_sink_buffer(pid, :sink, %Buffer{payload: ^payload})
Pipeline.stop_and_terminate(pid, blocking?: true)
end
end
end

0 comments on commit 2c59437

Please sign in to comment.