Skip to content

Commit c48cf05

Browse files
authored
Add mulaw encoder and decoder (#8)
1 parent 34ea384 commit c48cf05

File tree

11 files changed

+230
-51
lines changed

11 files changed

+230
-51
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
This package provides G.711 audio decoder, encoder and parser. The decoder and encoder are based on [ffmpeg](https://www.ffmpeg.org).
88

9-
At the moment, only G.711 A-law is supported.
9+
Both G.711 A-law (PCMA) and μ-law (PCMU) formats are supported.
1010

1111
It is part of [Membrane Multimedia Framework](https://membrane.stream).
1212

c_src/membrane_g711_ffmpeg_plugin/decoder.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ void handle_destroy_state(UnifexEnv *env, State *state) {
88
}
99
}
1010

11-
UNIFEX_TERM create(UnifexEnv *env) {
11+
UNIFEX_TERM create(UnifexEnv *env, char *encoding) {
1212
UNIFEX_TERM res;
1313
State *state = unifex_alloc_state(env);
1414
state->codec_ctx = NULL;
@@ -18,7 +18,18 @@ UNIFEX_TERM create(UnifexEnv *env) {
1818
#if (LIBAVCODEC_VERSION_MAJOR < 58)
1919
avcodec_register_all();
2020
#endif
21-
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_PCM_ALAW);
21+
22+
enum AVCodecID codec_id;
23+
if (strcmp(encoding, "PCMA") == 0) {
24+
codec_id = AV_CODEC_ID_PCM_ALAW;
25+
} else if (strcmp(encoding, "PCMU") == 0) {
26+
codec_id = AV_CODEC_ID_PCM_MULAW;
27+
} else {
28+
res = create_result_error(env, "encoding");
29+
goto exit_create;
30+
}
31+
32+
const AVCodec *codec = avcodec_find_decoder(codec_id);
2233
if (!codec) {
2334
res = create_result_error(env, "nocodec");
2435
goto exit_create;

c_src/membrane_g711_ffmpeg_plugin/decoder.spec.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module Membrane.G711.FFmpeg.Decoder.Native
22

33
state_type "State"
44

5-
spec create() :: {:ok :: label, state} | {:error :: label, reason :: atom}
5+
spec create(encoding :: atom) :: {:ok :: label, state} | {:error :: label, reason :: atom}
66

77
spec decode(payload, state) ::
88
{:ok :: label, frames :: [payload]}

c_src/membrane_g711_ffmpeg_plugin/encoder.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ void handle_destroy_state(UnifexEnv *env, State *state) {
88
}
99
}
1010

11-
UNIFEX_TERM create(UnifexEnv *env, char *sample_fmt) {
11+
UNIFEX_TERM create(UnifexEnv *env, char *sample_fmt, char *encoding) {
1212
UNIFEX_TERM res;
1313
State *state = unifex_alloc_state(env);
1414
state->codec_ctx = NULL;
@@ -18,7 +18,18 @@ UNIFEX_TERM create(UnifexEnv *env, char *sample_fmt) {
1818
#if (LIBAVCODEC_VERSION_MAJOR < 58)
1919
avcodec_register_all();
2020
#endif
21-
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_PCM_ALAW);
21+
22+
enum AVCodecID codec_id;
23+
if (strcmp(encoding, "PCMA") == 0) {
24+
codec_id = AV_CODEC_ID_PCM_ALAW;
25+
} else if (strcmp(encoding, "PCMU") == 0) {
26+
codec_id = AV_CODEC_ID_PCM_MULAW;
27+
} else {
28+
res = create_result_error(env, "encoding");
29+
goto exit_create;
30+
}
31+
32+
const AVCodec *codec = avcodec_find_encoder(codec_id);
2233
if (!codec) {
2334
res = create_result_error(env, "nocodec");
2435
goto exit_create;

c_src/membrane_g711_ffmpeg_plugin/encoder.spec.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module Membrane.G711.FFmpeg.Encoder.Native
22

33
state_type "State"
44

5-
spec create(sample_fmt :: atom) ::
5+
spec create(sample_fmt :: atom, encoding :: atom) ::
66
{:ok :: label, state} | {:error :: label, reason :: atom}
77

88
spec encode(payload, state) ::

examples/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
# Membrane G.711 FFmpeg plugin examples
22

33
To run, execute `elixir <example>`, e.g. `elixir decode_example.exs`.
4+
5+
## A-law examples
6+
- `encode_example.exs` - Encodes a raw audio file to G.711 A-law format
7+
- `decode_example.exs` - Decodes a G.711 A-law file to raw audio
8+
9+
## μ-law examples
10+
- `encode_mulaw_example.exs` - Encodes a raw audio file to G.711 μ-law format
11+
- `decode_mulaw_example.exs` - Decodes a G.711 μ-law file to raw audio

examples/decode_mulaw_example.exs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Decoding example (μ-law)
2+
#
3+
# The following pipeline takes a G.711 μ-law file and decodes it to the raw audio.
4+
5+
Logger.configure(level: :info)
6+
7+
Mix.install([
8+
{:membrane_g711_ffmpeg_plugin,
9+
path: __DIR__ |> Path.join("..") |> Path.expand(), override: true},
10+
:membrane_file_plugin,
11+
:req
12+
])
13+
14+
# For this example, we'll use a local file created by encode_mulaw_example.exs
15+
# Alternatively, you could download a μ-law sample file from a URL
16+
17+
defmodule DecodingMulaw.Pipeline do
18+
use Membrane.Pipeline
19+
20+
@impl true
21+
def handle_init(_ctx, _opts) do
22+
structure =
23+
child(:source, %Membrane.File.Source{chunk_size: 40_960, location: "output.ul"})
24+
|> child(:decoder, %Membrane.G711.FFmpeg.Decoder{encoding: :PCMU})
25+
|> child(:sink, %Membrane.File.Sink{location: "output_from_mulaw.raw"})
26+
27+
{[spec: structure], %{}}
28+
end
29+
30+
@impl true
31+
def handle_element_end_of_stream(:sink, _pad, _ctx, state) do
32+
{[terminate: :shutdown], state}
33+
end
34+
35+
@impl true
36+
def handle_element_end_of_stream(_child, _pad, _ctx, state) do
37+
{[], state}
38+
end
39+
end
40+
41+
# Start and monitor the pipeline
42+
{:ok, _supervisor_pid, pipeline_pid} = Membrane.Pipeline.start_link(DecodingMulaw.Pipeline)
43+
ref = Process.monitor(pipeline_pid)
44+
45+
# Wait for the pipeline to finish
46+
receive do
47+
{:DOWN, ^ref, :process, _pipeline_pid, _reason} ->
48+
System.stop()
49+
end

examples/encode_mulaw_example.exs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Encoding example (μ-law)
2+
#
3+
# The following pipeline takes a raw audio file and encodes it as G.711 μ-law.
4+
5+
Logger.configure(level: :info)
6+
7+
Mix.install([
8+
{:membrane_g711_ffmpeg_plugin,
9+
path: __DIR__ |> Path.join("..") |> Path.expand(), override: true},
10+
:membrane_raw_audio_parser_plugin,
11+
:membrane_raw_audio_format,
12+
:membrane_file_plugin,
13+
:req
14+
])
15+
16+
raw_audio =
17+
Req.get!(
18+
"https://raw.githubusercontent.com/membraneframework/static/gh-pages/samples/beep-s16le-8kHz-mono.raw"
19+
).body
20+
21+
File.write!("input.raw", raw_audio)
22+
23+
defmodule EncodingMulaw.Pipeline do
24+
use Membrane.Pipeline
25+
26+
@impl true
27+
def handle_init(_ctx, _opts) do
28+
structure =
29+
child(:source, %Membrane.File.Source{chunk_size: 40_960, location: "input.raw"})
30+
|> child(:parser, %Membrane.RawAudioParser{
31+
stream_format: %Membrane.RawAudio{
32+
sample_format: :s16le,
33+
sample_rate: 8000,
34+
channels: 1
35+
}
36+
})
37+
|> child(:encoder, %Membrane.G711.FFmpeg.Encoder{encoding: :PCMU})
38+
|> child(:sink, %Membrane.File.Sink{location: "output.ul"})
39+
40+
{[spec: structure], %{}}
41+
end
42+
43+
@impl true
44+
def handle_element_end_of_stream(:sink, _pad, _ctx, state) do
45+
{[terminate: :shutdown], state}
46+
end
47+
48+
@impl true
49+
def handle_element_end_of_stream(_child, _pad, _ctx, state) do
50+
{[], state}
51+
end
52+
end
53+
54+
# Start and monitor the pipeline
55+
{:ok, _supervisor_pid, pipeline_pid} = Membrane.Pipeline.start_link(EncodingMulaw.Pipeline)
56+
ref = Process.monitor(pipeline_pid)
57+
58+
# Wait for the pipeline to finish
59+
receive do
60+
{:DOWN, ^ref, :process, _pipeline_pid, _reason} ->
61+
System.stop()
62+
end

lib/membrane_g711_ffmpeg/decoder.ex

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Membrane.G711.FFmpeg.Decoder do
22
@moduledoc """
33
Membrane element that decodes audio in G711 format. It is backed by decoder from FFmpeg.
44
5-
At the moment, only A-law is supported.
5+
A-law and μ-law encoding formats are supported.
66
"""
77

88
use Membrane.Filter
@@ -14,20 +14,34 @@ defmodule Membrane.G711.FFmpeg.Decoder do
1414
alias Membrane.G711.FFmpeg.Common
1515
alias Membrane.{G711, RawAudio, RemoteStream}
1616

17-
def_input_pad :input,
17+
def_options(
18+
encoding: [
19+
spec: :PCMA | :PCMU,
20+
description: "G.711 encoding to decode (A-law or μ-law)",
21+
default: :PCMA
22+
]
23+
)
24+
25+
def_input_pad(:input,
1826
flow_control: :auto,
19-
accepted_format: any_of(%RemoteStream{}, %G711{encoding: :PCMA})
27+
accepted_format:
28+
any_of(%RemoteStream{}, %G711{encoding: encoding} when encoding in [:PCMA, :PCMU])
29+
)
2030

21-
def_output_pad :output,
31+
def_output_pad(:output,
2232
flow_control: :auto,
2333
accepted_format: %RawAudio{
2434
channels: G711.num_channels(),
2535
sample_rate: G711.sample_rate()
2636
}
37+
)
2738

2839
@impl true
29-
def handle_init(_ctx, _opts) do
30-
state = %{decoder_ref: nil}
40+
def handle_init(_ctx, opts) do
41+
state = %{
42+
decoder_ref: nil,
43+
encoding: opts.encoding
44+
}
3145

3246
{[], state}
3347
end
@@ -44,9 +58,15 @@ defmodule Membrane.G711.FFmpeg.Decoder do
4458
end
4559

4660
@impl true
47-
def handle_stream_format(:input, _stream_format, _ctx, state) do
61+
def handle_stream_format(:input, stream_format, _ctx, state) do
62+
encoding =
63+
case stream_format do
64+
%G711{encoding: encoding} -> encoding
65+
%RemoteStream{} -> state.encoding
66+
end
67+
4868
with buffers <- flush_decoder_if_exists(state),
49-
{:ok, new_decoder_ref} <- Native.create() do
69+
{:ok, new_decoder_ref} <- Native.create(encoding) do
5070
stream_format = generate_stream_format(new_decoder_ref)
5171
actions = buffers ++ [stream_format: {:output, stream_format}]
5272
{actions, %{state | decoder_ref: new_decoder_ref}}

lib/membrane_g711_ffmpeg/encoder.ex

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule Membrane.G711.FFmpeg.Encoder do
22
@moduledoc """
3-
Membrane element that encodes raw audio frames to G711 format (only A-law is supported).
3+
Membrane element that encodes raw audio frames to G711 format (A-law and μ-law are supported).
44
It is backed by encoder from FFmpeg.
55
66
The element expects that each received buffer has whole samples, so the parser
@@ -19,21 +19,34 @@ defmodule Membrane.G711.FFmpeg.Encoder do
1919
alias Membrane.G711.FFmpeg.Common
2020
alias Membrane.{G711, RawAudio}
2121

22-
def_input_pad :input,
22+
def_options(
23+
encoding: [
24+
spec: :PCMA | :PCMU,
25+
description: "G.711 encoding to use (A-law or μ-law)",
26+
default: :PCMA
27+
]
28+
)
29+
30+
def_input_pad(:input,
2331
flow_control: :auto,
2432
accepted_format: %RawAudio{
2533
channels: G711.num_channels(),
2634
sample_rate: G711.sample_rate(),
2735
sample_format: :s16le
2836
}
37+
)
2938

30-
def_output_pad :output,
39+
def_output_pad(:output,
3140
flow_control: :auto,
32-
accepted_format: %G711{encoding: :PCMA}
41+
accepted_format: %G711{encoding: encoding} when encoding in [:PCMA, :PCMU]
42+
)
3343

3444
@impl true
35-
def handle_init(_ctx, _opts) do
36-
state = %{encoder_ref: nil}
45+
def handle_init(_ctx, opts) do
46+
state = %{
47+
encoder_ref: nil,
48+
encoding: opts.encoding
49+
}
3750

3851
{[], state}
3952
end
@@ -52,7 +65,8 @@ defmodule Membrane.G711.FFmpeg.Encoder do
5265
@impl true
5366
def handle_stream_format(:input, stream_format, _ctx, state) do
5467
with buffers <- flush_encoder_if_exists(state),
55-
{:ok, new_encoder_ref} <- Native.create(stream_format.sample_format) do
68+
{:ok, new_encoder_ref} <-
69+
Native.create(stream_format.sample_format, state.encoding) do
5670
stream_format = generate_stream_format(state)
5771
actions = buffers ++ [stream_format: {:output, stream_format}]
5872
{actions, %{state | encoder_ref: new_encoder_ref}}
@@ -78,7 +92,7 @@ defmodule Membrane.G711.FFmpeg.Encoder do
7892
end
7993
end
8094

81-
defp generate_stream_format(_state) do
82-
%G711{encoding: :PCMA}
95+
defp generate_stream_format(%{encoding: encoding}) do
96+
%G711{encoding: encoding}
8397
end
8498
end

0 commit comments

Comments
 (0)