Skip to content

Commit

Permalink
Add mp3 example
Browse files Browse the repository at this point in the history
Also add channels and bits to i2s output configuration.

Signed-off-by: Paul Guyot <pguyot@kallisys.net>
  • Loading branch information
pguyot committed May 8, 2024
1 parent 44e2962 commit 137ea65
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 40 deletions.
19 changes: 18 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name: Build
on:
push:
branches: ["main"]
pull_request:
workflow_dispatch:

Expand Down Expand Up @@ -79,6 +80,22 @@ jobs:
path: AtomVM/src/platforms/esp32/build/atomvm-esp-adf-${{ matrix.soc }}-${{ matrix.idf-version }}.img
if-no-files-found: error

build-examples:
runs-on: ubuntu-latest
container: erlang:26
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build examples
run: |
cd examples
for conf in */rebar.config ; do
example_dir=`dirname ${conf}`
cd ${example_dir}
rebar3 atomvm packbeam
cd ..
done
build-doc:
runs-on: ubuntu-latest
container: erlang:26
Expand Down Expand Up @@ -120,7 +137,7 @@ jobs:
- name: Checkout erlfmt
run: |
cd ..
git clone --depth 1 -b v1.1.0 https://github.com/WhatsApp/erlfmt.git
git clone --depth 1 -b v1.3.0 https://github.com/WhatsApp/erlfmt.git
cd erlfmt
rebar3 as release escriptize
- name: Check format with erlfmt
Expand Down
36 changes: 7 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# AtomVM driver for Espressif Audio Framework
# AtomVM driver for Espressif Audio Development Framework

This component ports [Espressif Audio Framework](https://github.com/espressif/esp-adf)
This component ports [Espressif Audio Development Framework](https://github.com/espressif/esp-adf)
to AtomVM, thus enabling audio processing to esp32-based systems powered by
AtomVM.

[![Build](https://github.com/pguyot/atomvm_esp_adf/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/pguyot/atomvm_esp_adf/actions/workflows/build.yml)

## Usage:
## Installation

1. Clone this repository into atomvm/src/platforms/esp32/components with
the required submodules. You can clone with recursive submodules but most
Expand All @@ -23,7 +23,7 @@ git submodule update --init components/esp-adf-libs/

2. Build and flash AtomVM

Espressif Audio Framework requires `CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY`
Espressif Audio Development Framework requires `CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY`
which you can set using the configuration menu or editing sdkconfig file.

You can use a command sequence such as: (port name depends on OS)
Expand All @@ -40,30 +40,8 @@ You will also need to flash AtomVM core libraries.

Alternatively, you can flash an image generated by the [CI of this project](https://github.com/pguyot/atomvm_esp_adf/actions/workflows/build.yml).

3. Use `esp_adf_*` modules in your code.
## Usage

You can refer to [API documentation](https://pguyot.github.io/atomvm_esp_adf/).
This project defines `esp_adf_*` modules.

```erlang
AudioPipeline = esp_adf_audio_pipeline:init([]),

MP3File = atomvm:read_priv(?MODULE, "adf_music.mp3"),
MP3Decoder = esp_adf_mp3_decoder:init([]),
ok = esp_adf_audio_element:set_read_binary(MP3Decoder, MP3File),
ok = esp_adf_audio_pipeline:register(AudioPipeline, MP3Decoder, <<"mp3">>),

I2SOutput = esp_adf_i2s_output:init([{gpio_bclk, ?MAX_BCLK_GPIO}, {gpio_lrclk, ?MAX_LRC_GPIO}, {gpio_dout, ?MAX_DIN_GPIO}]),
ok = esp_adf_audio_pipeline:register(AudioPipeline, I2SOutput, <<"i2s">>),

ok = esp_adf_audio_pipeline:link(AudioPipeline, [<<"mp3">>, <<"i2s">>]),

ok = esp_adf_audio_pipeline:run(AudioPipeline),

...

ok = esp_adf_audio_pipeline:stop(AudioPipeline),
ok = esp_adf_audio_pipeline:wait_for_stop(AudioPipeline),
ok = esp_adf_audio_pipeline:terminate(AudioPipeline),
ok = esp_adf_audio_pipeline:unregister(AudioPipeline, MP3Decoder),
ok = esp_adf_audio_pipeline:unregister(AudioPipeline, I2SOutput),
```
You can refer to [examples](examples/) or [API documentation](https://pguyot.github.io/atomvm_esp_adf/).
20 changes: 20 additions & 0 deletions examples/play_mp3_i2s/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.rebar3
_build
_checkouts
_vendor
.eunit
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
logs
.idea
*.iml
rebar3.crashdump
*~
20 changes: 20 additions & 0 deletions examples/play_mp3_i2s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
play_mp3_i2s
=====

Sample code to plan an MP3 sound on an I2S card.

It was tested with an esp32c3 card connected to a MAX98357A module.

The MAX98357A module is connected to the following esp32c3 gpios:

- LRCLK: gpio 7
- BCLK: gpio 6
- DIN: gpio 5

The MAX98357A module doesn't require any MCLK, so this example doesn't
configure a gpio for it.

Build and flash
---------------

$ rebar3 atomvm esp32_flash -p /dev/cu.usbmodem*
Binary file added examples/play_mp3_i2s/priv/adf_music.mp3
Binary file not shown.
15 changes: 15 additions & 0 deletions examples/play_mp3_i2s/rebar.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{erl_opts, [debug_info]}.
{deps, []}.

{shell, [
% {config, "config/sys.config"},
{apps, [play_mp3_i2s]}
]}.
{plugins, [
atomvm_rebar3_plugin
]}.
{profiles, [
{check, [
{plugins, [erlfmt]}
]}
]}.
1 change: 1 addition & 0 deletions examples/play_mp3_i2s/rebar.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[].
14 changes: 14 additions & 0 deletions examples/play_mp3_i2s/src/play_mp3_i2s.app.src
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{application, play_mp3_i2s, [
{description, "AtomVM MP3 player"},
{vsn, "0.1.0"},
{registered, []},
{applications, [
kernel,
stdlib
]},
{env, []},
{modules, []},

{licenses, ["MIT"]},
{links, []}
]}.
50 changes: 50 additions & 0 deletions examples/play_mp3_i2s/src/play_mp3_i2s.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
%%%-------------------------------------------------------------------
%% @doc Example code to play an MP3 file over i2s
%% @end
%%%-------------------------------------------------------------------

-module(play_mp3_i2s).

-export([start/0]).

%% GPIO configuration

-define(I2S_LRC_GPIO, 7).
-define(I2S_BCLK_GPIO, 6).
-define(I2S_DIN_GPIO, 5).

start() ->
io:format("Play MP3 using AtomVM ESP ADF\n"),

AudioPipeline = esp_adf_audio_pipeline:init([]),

MP3File = atomvm:read_priv(?MODULE, "adf_music.mp3"),

io:format("MP3File: ~B bytes\n", [byte_size(MP3File)]),

MP3Decoder = esp_adf_mp3_decoder:init([]),
ok = esp_adf_audio_element:set_read_binary(MP3Decoder, MP3File),
ok = esp_adf_audio_pipeline:register(AudioPipeline, MP3Decoder, <<"mp3">>),

I2SOutput = esp_adf_i2s_output:init([
{rate, 44100},
{bits, 16},
{gpio_bclk, ?I2S_BCLK_GPIO},
{gpio_lrclk, ?I2S_LRC_GPIO},
{gpio_dout, ?I2S_DIN_GPIO}
]),
ok = esp_adf_audio_pipeline:register(AudioPipeline, I2SOutput, <<"i2s">>),

ok = esp_adf_audio_pipeline:link(AudioPipeline, [<<"mp3">>, <<"i2s">>]),

ok = esp_adf_audio_pipeline:run(AudioPipeline),

timer:sleep(7000),

ok = esp_adf_audio_pipeline:stop(AudioPipeline),
ok = esp_adf_audio_pipeline:wait_for_stop(AudioPipeline),
ok = esp_adf_audio_pipeline:terminate(AudioPipeline),
ok = esp_adf_audio_pipeline:unregister(AudioPipeline, MP3Decoder),
ok = esp_adf_audio_pipeline:unregister(AudioPipeline, I2SOutput),

ok.
58 changes: 49 additions & 9 deletions nifs/atomvm_esp_adf_i2s_output.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ struct I2SData
i2s_std_config_t std_config;
};

struct MusicInfo
{
int rate;
i2s_slot_mode_t channels;
i2s_data_bit_width_t bits;
};

static esp_err_t i2s_open(audio_element_handle_t self)
{
TRACE("%s\n", __func__);
Expand Down Expand Up @@ -108,16 +115,16 @@ static int i2s_write(audio_element_handle_t self, char *buffer, int len, TickTyp
return bytes_written;
}

static void i2s_data_init(struct I2SData *driver_data, int rate, int gpio_mclk, int gpio_bclk, int gpio_lrclk, int gpio_dout)
static void i2s_data_init(struct I2SData *driver_data, const struct MusicInfo *music_info, int gpio_mclk, int gpio_bclk, int gpio_lrclk, int gpio_dout)
{
TRACE("%s\n", __func__);

driver_data->chan_cfg = (i2s_chan_config_t) I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
i2s_new_channel(&driver_data->chan_cfg, &driver_data->tx_handle, NULL);

driver_data->std_config = (i2s_std_config_t){
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_STEREO),
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(music_info->rate),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(music_info->bits, music_info->channels),
.gpio_cfg = {
.mclk = gpio_mclk,
.bclk = gpio_bclk,
Expand All @@ -134,7 +141,7 @@ static void i2s_data_init(struct I2SData *driver_data, int rate, int gpio_mclk,
i2s_channel_init_std_mode(driver_data->tx_handle, &driver_data->std_config);
}

static term i2s_output_new(Context *ctx, term argv[], int rate, int gpio_mclk, int gpio_bclk, int gpio_lrclk, int gpio_dout)
static term i2s_output_new(Context *ctx, term argv[], const struct MusicInfo *music_info, int gpio_mclk, int gpio_bclk, int gpio_lrclk, int gpio_dout)
{
TRACE("%s\n", __func__);

Expand Down Expand Up @@ -165,7 +172,7 @@ static term i2s_output_new(Context *ctx, term argv[], int rate, int gpio_mclk, i
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}

i2s_data_init(driver_data, rate, gpio_mclk, gpio_bclk, gpio_lrclk, gpio_dout);
i2s_data_init(driver_data, music_info, gpio_mclk, gpio_bclk, gpio_lrclk, gpio_dout);

rsrc_obj->audio_element = audio_element_init(&cfg);
if (IS_NULL_PTR(rsrc_obj->audio_element)) {
Expand All @@ -175,6 +182,7 @@ static term i2s_output_new(Context *ctx, term argv[], int rate, int gpio_mclk, i
}

audio_element_setdata(rsrc_obj->audio_element, driver_data);
audio_element_set_music_info(rsrc_obj->audio_element, music_info->rate, music_info->channels, music_info->bits);

return obj;
}
Expand All @@ -188,19 +196,16 @@ static term nif_init(Context *ctx, int argc, term argv[])

VALIDATE_VALUE(cfg, term_is_list);

term rate_term = interop_kv_get_value_default(cfg, ATOM_STR("\x4", "rate"), term_from_int(48000), ctx->global);
term gpio_mclk_term = interop_kv_get_value_default(cfg, ATOM_STR("\x9", "gpio_mclk"), term_from_int(I2S_GPIO_UNUSED), ctx->global);
term gpio_bclk_term = interop_kv_get_value(cfg, ATOM_STR("\x9", "gpio_bclk"), ctx->global);
term gpio_lrclk_term = interop_kv_get_value(cfg, ATOM_STR("\xA", "gpio_lrclk"), ctx->global);
term gpio_dout_term = interop_kv_get_value(cfg, ATOM_STR("\x9", "gpio_dout"), ctx->global);

VALIDATE_VALUE(rate_term, term_is_integer);
VALIDATE_VALUE(gpio_mclk_term, term_is_integer);
VALIDATE_VALUE(gpio_bclk_term, term_is_integer);
VALIDATE_VALUE(gpio_lrclk_term, term_is_integer);
VALIDATE_VALUE(gpio_dout_term, term_is_integer);

avm_int_t rate = term_to_int(rate_term);
avm_int_t gpio_mclk = term_to_int(gpio_mclk_term);
avm_int_t gpio_bclk = term_to_int(gpio_bclk_term);
avm_int_t gpio_lrclk = term_to_int(gpio_lrclk_term);
Expand All @@ -219,7 +224,42 @@ static term nif_init(Context *ctx, int argc, term argv[])
RAISE_ERROR(BADARG_ATOM);
}

return i2s_output_new(ctx, argv, rate, gpio_mclk, gpio_bclk, gpio_lrclk, gpio_dout);
term rate_term = interop_kv_get_value_default(cfg, ATOM_STR("\x4", "rate"), term_from_int(48000), ctx->global);
term bits_term = interop_kv_get_value_default(cfg, ATOM_STR("\x4", "bits"), term_from_int(32), ctx->global);
term channels_term = interop_kv_get_value_default(cfg, ATOM_STR("\x8", "channels"), term_from_int(2), ctx->global);

VALIDATE_VALUE(rate_term, term_is_integer);
VALIDATE_VALUE(bits_term, term_is_integer);
VALIDATE_VALUE(channels_term, term_is_integer);

avm_int_t rate = term_to_int(rate_term);
avm_int_t bits = term_to_int(bits_term);
avm_int_t channels = term_to_int(channels_term);

// In standard mode, there are always 2 channels
if (UNLIKELY(channels != 2)) {
RAISE_ERROR(BADARG_ATOM);
}
// In standard mode, bits can be 8/16/24 or 32
if (UNLIKELY(bits != 8 && bits != 16 && bits != 24 && bits != 32)) {
RAISE_ERROR(BADARG_ATOM);
}

#if __STDC_VERSION__ >= 201112L
_Static_assert(I2S_SLOT_MODE_STEREO == 2, "expected I2S_SLOT_MODE_STEREO to be equal to 2");
_Static_assert(I2S_DATA_BIT_WIDTH_8BIT == 8, "expected I2S_DATA_BIT_WIDTH_8BIT to be equal to 8");
_Static_assert(I2S_DATA_BIT_WIDTH_16BIT == 16, "expected I2S_DATA_BIT_WIDTH_16BIT to be equal to 16");
_Static_assert(I2S_DATA_BIT_WIDTH_24BIT == 24, "expected I2S_DATA_BIT_WIDTH_24BIT to be equal to 24");
_Static_assert(I2S_DATA_BIT_WIDTH_32BIT == 32, "expected I2S_DATA_BIT_WIDTH_32BIT to be equal to 32");
#endif

struct MusicInfo music_info = {
.rate = rate,
.bits = bits,
.channels = channels
};

return i2s_output_new(ctx, argv, &music_info, gpio_mclk, gpio_bclk, gpio_lrclk, gpio_dout);
}

static const struct Nif init_nif = {
Expand Down
5 changes: 5 additions & 0 deletions rebar.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{erl_opts, [debug_info]}.
{deps, []}.
{plugins, [atomvm_rebar3_plugin]}.
{profiles, [
{check, [
{plugins, [erlfmt]}
]}
]}.
2 changes: 1 addition & 1 deletion src/atomvm_esp_adf.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{vsn, "0.1.0"},
{registered, []},
{applications, [kernel, stdlib]},
{env,[]},
{env, []},
{modules, []},
{licenses, ["MIT"]},
{links, []}
Expand Down
4 changes: 4 additions & 0 deletions src/esp_adf_i2s_output.erl
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@

-type i2s_output_option() ::
{rate, pos_integer()}
| {bits, 8 | 16 | 24 | 32}
| {channels, 1 | 2}
| {gpio_mclk, -1 | non_neg_integer()}
| {gpio_bclk, non_neg_integer()}
| {gpio_lrclk, non_neg_integer()}
| {gpio_dout, non_neg_integer()}.

%% @doc Create a handle to an i2s output audio element.
%% `gpio_bclk', `gpio_lrclk' and `gpio_dout' options are required.
%% I2S is configured in standard mode ("Philips"), which means that
%% channels should be 2 (this is the default) and bits can be 8, 16, 24 or 32.
%% @param Cfg configuration
-spec init([i2s_output_option()]) -> audio_element:audio_element().
init(_Cfg) ->
Expand Down

0 comments on commit 137ea65

Please sign in to comment.