Skip to content

Commit

Permalink
Add resample filter
Browse files Browse the repository at this point in the history
Also implement esp_adf_audio_element:getinfo/1 and change data type to be a
map.

Signed-off-by: Paul Guyot <pguyot@kallisys.net>
  • Loading branch information
pguyot committed May 11, 2024
1 parent 91153aa commit c8d3afc
Show file tree
Hide file tree
Showing 14 changed files with 498 additions and 65 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ set(ATOMVM_ESP_AUDIO_COMPONENT_SRCS
"nifs/atomvm_esp_adf_aac_decoder.c"
"nifs/atomvm_esp_adf_i2s_output.c"
"nifs/atomvm_esp_adf_mp3_decoder.c"
"nifs/atomvm_esp_adf_rsp_filter.c"
)

# WHOLE_ARCHIVE option is supported only with esp-idf 5.x
Expand Down
5 changes: 5 additions & 0 deletions Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ config AVM_ESP_ADF_MP3_DECODER_ENABLE
default y
depends on AVM_ESP_ADF_AUDIO_ELEMENT_ENABLE

config AVM_ESP_ADF_RSP_FILTER_ENABLE
bool "Enable AtomVM esp adf rsp filter"
default y
depends on AVM_ESP_ADF_AUDIO_ELEMENT_ENABLE

endmenu
20 changes: 20 additions & 0 deletions examples/play_mp3_mono_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
*~
22 changes: 22 additions & 0 deletions examples/play_mp3_mono_i2s/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
play_mp3_mono_i2s
=====

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

The sound is upsampled to stereo before data is sent to I2S.

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 not shown.
15 changes: 15 additions & 0 deletions examples/play_mp3_mono_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_mono_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_mono_i2s/src/play_mp3_mono_i2s.app.src
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{application, play_mp3_mono_i2s, [
{description, "AtomVM MP3 mono player"},
{vsn, "0.1.0"},
{registered, []},
{applications, [
kernel,
stdlib
]},
{env, []},
{modules, []},

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

-module(play_mp3_mono_i2s).

-export([start/0]).

%% GPIO configuration

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

flush_messages() ->
receive
{audio_element, _AudioElement, _Message} = Msg ->
io:format("~p\n", [Msg]),
flush_messages()
after 0 -> ok
end.

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

% Create the pipeline
AudioPipeline = esp_adf_audio_pipeline:init([]),
MP3File = atomvm:read_priv(?MODULE, "adf_music_mono.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">>),

Filter = esp_adf_rsp_filter:init([
{src_rate, 44100}, {src_ch, 1}, {dest_rate, 44100}, {dest_ch, 2}
]),
ok = esp_adf_audio_pipeline:register(AudioPipeline, Filter, <<"filter">>),

% We know the sample sound is 44.1 kHz, stereo
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">>, <<"filter">>, <<"i2s">>]),

% Start playing the sound
ok = esp_adf_audio_pipeline:run(AudioPipeline),

% Music info means we can fetch info and data will be meaningful
ok =
receive
{audio_element, MP3Decoder, music_info} -> ok
after 500 -> timeout
end,
MP3Info = esp_adf_audio_element:getinfo(MP3Decoder),

% We can verify that the provided MP3 is mono
1 = maps:get(channels, MP3Info),
44100 = maps:get(sample_rates, MP3Info),

% However, duration is not meaningful at this point.
0 = maps:get(duration, MP3Info),

% We know the sound is about 7 seconds long
ok =
receive
{audio_element, I2SOutput, {status, state_finished}} -> ok
after 7000 -> timeout
end,

% Once we're done, we can stop everything
ok = esp_adf_audio_pipeline:stop(AudioPipeline),

% Ensure every element has stopped so wait_for_stop will not block long.
ok =
receive
{audio_element, MP3Decoder, {status, state_stopped}} -> ok
after 500 -> timeout
end,
ok =
receive
{audio_element, Filter, {status, state_stopped}} -> ok
after 500 -> timeout
end,
ok =
receive
{audio_element, I2SOutput, {status, state_stopped}} -> ok
after 500 -> timeout
end,

% This is the proper cleanup
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, Filter),
ok = esp_adf_audio_pipeline:unregister(AudioPipeline, I2SOutput),

% Additional messages were not processed, ensure they do not stay in the queue
ok = flush_messages(),
ok.
2 changes: 1 addition & 1 deletion idf_component.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
dependencies:
## Required IDF version
idf:
version: ">=4.1.0"
version: ">=5.1.0"
audio_pipeline:
path: components/esp-adf/components/audio_pipeline
audio_sal:
Expand Down
85 changes: 41 additions & 44 deletions nifs/atomvm_esp_adf_audio_element.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,51 +152,22 @@ static term esp_codec_to_term(esp_codec_type_t esp_codec, GlobalContext *glb)
return result;
}

#define INFO_TERM_SIZE (TERM_MAP_SIZE(8))

static term info_to_term(audio_element_info_t *info, Heap *heap, GlobalContext *glb)
{
term info_list = term_nil();

term tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\xC", "sample_rates")));
term_put_tuple_element(tuple, 1, term_from_int(info->sample_rates));
info_list = term_list_prepend(tuple, info_list, heap);

tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\x8", "channels")));
term_put_tuple_element(tuple, 1, term_from_int(info->channels));
info_list = term_list_prepend(tuple, info_list, heap);

tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\x4", "bits")));
term_put_tuple_element(tuple, 1, term_from_int(info->bits));
info_list = term_list_prepend(tuple, info_list, heap);

tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\x3", "bps")));
term_put_tuple_element(tuple, 1, term_from_int(info->bps));
info_list = term_list_prepend(tuple, info_list, heap);

tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\x8", "byte_pos")));
term_put_tuple_element(tuple, 1, term_from_int(info->byte_pos));
info_list = term_list_prepend(tuple, info_list, heap);

tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\xB", "total_bytes")));
term_put_tuple_element(tuple, 1, term_from_int(info->total_bytes));
info_list = term_list_prepend(tuple, info_list, heap);

tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\x8", "duration")));
term_put_tuple_element(tuple, 1, term_from_int(info->duration));
info_list = term_list_prepend(tuple, info_list, heap);

tuple = term_alloc_tuple(2, heap);
term_put_tuple_element(tuple, 0, globalcontext_make_atom(glb, ATOM_STR("\x9", "codec_fmt")));
term_put_tuple_element(tuple, 1, esp_codec_to_term(info->codec_fmt, glb));
info_list = term_list_prepend(tuple, info_list, heap);

return info_list;
term info_map = term_alloc_map(8, heap);

term_set_map_assoc(info_map, 0, globalcontext_make_atom(glb, ATOM_STR("\xC", "sample_rates")), term_from_int(info->sample_rates));
term_set_map_assoc(info_map, 1, globalcontext_make_atom(glb, ATOM_STR("\x8", "channels")), term_from_int(info->channels));
term_set_map_assoc(info_map, 2, globalcontext_make_atom(glb, ATOM_STR("\x4", "bits")), term_from_int(info->bits));
term_set_map_assoc(info_map, 3, globalcontext_make_atom(glb, ATOM_STR("\x3", "bps")), term_from_int(info->bps));
term_set_map_assoc(info_map, 4, globalcontext_make_atom(glb, ATOM_STR("\x8", "byte_pos")), term_from_int(info->byte_pos));
term_set_map_assoc(info_map, 5, globalcontext_make_atom(glb, ATOM_STR("\xB", "total_bytes")), term_from_int(info->total_bytes));
term_set_map_assoc(info_map, 6, globalcontext_make_atom(glb, ATOM_STR("\x8", "duration")), term_from_int(info->duration));
term_set_map_assoc(info_map, 7, globalcontext_make_atom(glb, ATOM_STR("\x9", "codec_fmt")), esp_codec_to_term(info->codec_fmt, glb));

return info_map;
}

static term status_to_term(audio_element_status_t status, GlobalContext *glb)
Expand Down Expand Up @@ -255,7 +226,7 @@ static size_t event_heap_size_in_terms(audio_event_iface_msg_t *msg)
break;

case AEL_MSG_CMD_REPORT_POSITION:
return TUPLE_SIZE(2) + LIST_SIZE(8, TUPLE_SIZE(2));
return TUPLE_SIZE(2) + INFO_TERM_SIZE;
break;

default:
Expand Down Expand Up @@ -415,6 +386,25 @@ void atomvm_esp_adf_audio_element_init_resource(struct AudioElementResource *res
// Nifs
//

static term nif_getinfo(Context *ctx, int argc, term argv[])
{
TRACE("%s:%s\n", __FILE__, __func__);
UNUSED(argc);

struct AudioElementResource *rsrc_obj = atomvm_esp_adf_audio_element_opaque_to_resource(argv[0], ctx);
if (IS_NULL_PTR(rsrc_obj)) {
RAISE_ERROR(BADARG_ATOM);
}

audio_element_info_t info;
audio_element_getinfo(rsrc_obj->audio_element, &info);

if (UNLIKELY(memory_ensure_free(ctx, INFO_TERM_SIZE) != MEMORY_GC_OK)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
return info_to_term(&info, &ctx->heap, ctx->global);
}

struct BinaryCursor
{
GlobalContext *global;
Expand Down Expand Up @@ -637,6 +627,10 @@ static term nif_get_event(Context *ctx, int argc, term argv[])
return term_invalid_term();
}

static const struct Nif getinfo_nif = {
.base.type = NIFFunctionType,
.nif_ptr = nif_getinfo
};
static const struct Nif set_read_binary_nif = {
.base.type = NIFFunctionType,
.nif_ptr = nif_set_read_binary
Expand All @@ -663,6 +657,9 @@ static const struct Nif *get_nif(const char *nifname)
if (memcmp(nifname, MODULE_PREFIX, strlen(MODULE_PREFIX))) {
return NULL;
}
if (strcmp(nifname + strlen(MODULE_PREFIX), "getinfo/1") == 0) {
return &getinfo_nif;
}
if (strcmp(nifname + strlen(MODULE_PREFIX), "set_read_binary/2") == 0) {
return &set_read_binary_nif;
}
Expand Down
Loading

0 comments on commit c8d3afc

Please sign in to comment.