今までESP-IDFで幾つかのサンプルプログラムを試して来ました。ちなみにIDFとは、IoT Development Framework の略で、IoT開発用のアプリです。WebでIDFの関連記事を検索していて、ADF:(Audio Development Framework)という音響開発用のアプリが有ることを知りました。今回はそのアプリをインストールしてサンプルプログラムを実行して見ようと思います。
ADFのインストール
ADFのインストール自体は難しく有りません。ここ Get Started に公式のインストールガイドが有ります。ガイドに従って行えば簡単にインストール出来ます。ただ、ADFインストール前にIDFが既にインストールされていることが前提条件です。そこだけは注意して下さい。
Step 5. Start a Projectからのサンプルプログラム
ガイドではStep5からサンプルプログラムとして get-started/play_mp3_control を用いて説明しています。Espressif社製の開発ボード(SDや音源ICが実装されている製品)を持っているなら問題無く、サンプルプログラムをコンパイルして実行出来るのでしょうが、持っていない場合はmenuconfigでユニークボードを選んでコンパイルするのですが上手く実行出来ませんでした。そこでサンプルプログラムの中身を見たんですがかなり難しい内容です。
サンプルのReadmeを読むと、ESP内のFlashに保存されたMP3データをデコードし、I2Sを使ってアンプに出力しスピーカーで再生するものの様ですが、聞いたことの無い関数が多くとにかく理解に苦しみました。独自のボードを選択した場合は当然そのボード用のセットアップを独自で設定する必要が有るんですが、何処をどう変更して良いのか分からないのです。四苦八苦してやっと分かったので以下にその説明をします。
今回使用した機器
今回使用したハードは、CPUとして ”ESP32”。I2Sアンプとして ”MAX98357A”。機器の配線は
- ESP32とMAX98357Aの接続
- GPIO26<ー>BCLK : GPIO25<ー>LRCLK : GPIO22<ー>DIN
- MAX98357Aとスピーカー
- MAX98357Aの+ <ー> スピーカーの+
- MAX98357Aのー <ー> スピーカーのー
の様に行っています。MAX98357Aはデフォルトで左右の音が1つのスピーカーに出力されます。
プログラムの変更
サンプルプログラムは複雑過ぎるので、このハードに合わせて電源をいれたらFlashに保存されたMP3データを再生して終了するシンプルなものに変更しました。
変更したプログラム用プロジェクトの構成
サンプルプロジェクトを基本に今回のプログラムを実行するための必要最低限のファイルで構成しています。サンプルプロジェクトには有るが今回使用しないファイルは削除しています。使用したファイルは、同じファイル名ですが内容を変更しています。
- ~esp/play_mp3_control/
- CMakeLists.txt
- components/
- my_board/
- CMakeLists.txt
- Kconfig.projbuild
- my_board_v1_0/
- board.h
- board_pins_config.c
- main/
- CMakeLists.txt
- music-16b-2c-44100hz.mp3
- play_mp3_control_example.c
プログラムの説明
サンプルプログラムの説明が API Reference にも有りました。今回のプログラムはこれを元に書いています。
- 先ず最初はAudio Pipelineを製作。ADFでは音響処理をこのパイプラインを使って行う様です。
- 2つのエレメント、MP3 Decoder(②)、I2S Stream(③) を製作。
- ①のRead MP3 Fileは、MP3 DecoderのCallback関数です。MP3 Decoderから呼び出されFlashから曲データを読み込み、MP3 Decoderにデータを渡す関数です。
- 2つのエレメント、MP3 Decoder(②)、I2S Stream(③)を Audio Pipeline に登録。
- 実際に登録するのは上記の2つ。
- MP3 DecoderがRead MP3 Fileを呼び出すので動作的には上図の様になります。
- Audio Pipelineを開始すると、①、②、③の順にデータが処理さます。
- 処理されたデータは、Codec chip(MAX98357A)に送られ増幅。
- 最後にアンプに接続されたスピーカーから曲が流れます。
実際のサンプルプログラムには、
- Codec chipの管理。
- 曲の選択。
- 音量の調整。
- 曲の開始、停止。
等の機能が有りますが、今回書き直したプログラムはそれらを全て排除して、
- 電源を入れたら、1曲演奏して終わる。
としています。
プログラムの説明(main フォルダー)
main フォルダーの構成
- play_mp3_control_example.c : メインプログラム
- music-16b-2c-44100hz.mp3 : 元々有った曲データファイル
- CMakeLists.txt : CMake用のファイル
play_mp3_control_example.cがメインのプログラムです。
play_mp3_control_example.c
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "audio_pipeline.h"
#include "audio_mem.h"
#include "i2s_stream.h"
#include "mp3_decoder.h"
static const char *TAG = "PLAY_FLASH_MP3_CONTROL";
static struct marker {
int pos;
const uint8_t *start;
const uint8_t *end;
} file_marker;
// high rate mp3 audio
extern const uint8_t hr_mp3_start[] asm("_binary_music_16b_2c_44100hz_mp3_start");
extern const uint8_t hr_mp3_end[] asm("_binary_music_16b_2c_44100hz_mp3_end");
int mp3_music_read_cb(audio_element_handle_t el, char *buf, int len, TickType_t wait_time, void *ctx)
{
int read_size = file_marker.end - file_marker.start - file_marker.pos;
if (read_size == 0) {
return AEL_IO_DONE;
} else if (len < read_size) {
read_size = len;
}
memcpy(buf, file_marker.start + file_marker.pos, read_size);
file_marker.pos += read_size;
return read_size;
}
void app_main(void)
{
audio_pipeline_handle_t pipeline;
audio_element_handle_t i2s_stream_writer, mp3_decoder;
ESP_LOGI(TAG, "[ 2 ] Create audio pipeline, add all elements to pipeline, and subscribe pipeline event");
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
pipeline = audio_pipeline_init(&pipeline_cfg);
mem_assert(pipeline);
ESP_LOGI(TAG, "[2.1] Create mp3 decoder to decode mp3 file and set custom read callback");
mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
mp3_decoder = mp3_decoder_init(&mp3_cfg);
audio_element_set_read_cb(mp3_decoder, mp3_music_read_cb, NULL);
ESP_LOGI(TAG, "[2.2] Create i2s stream to write data to codec chip");
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.type = AUDIO_STREAM_WRITER;
i2s_stream_writer = i2s_stream_init(&i2s_cfg);
ESP_LOGI(TAG, "[2.3] Register all elements to audio pipeline");
audio_pipeline_register(pipeline, mp3_decoder, "mp3");
audio_pipeline_register(pipeline, i2s_stream_writer, "i2s");
ESP_LOGI(TAG, "[2.4] Link it together [mp3_music_read_cb]-->mp3_decoder-->i2s_stream-->[codec_chip]");
const char *link_tag[2] = {"mp3", "i2s"};
audio_pipeline_link(pipeline, &link_tag[0], 2);
ESP_LOGI(TAG, "[ 5.1 ] Start audio_pipeline");
file_marker.start = hr_mp3_start;
file_marker.end = hr_mp3_end;
file_marker.pos = 0;
audio_pipeline_run(pipeline);
int flg = 1;
while(flg)
{
vTaskDelay(100 / portTICK_PERIOD_MS);
if(audio_element_get_state(i2s_stream_writer) == AEL_STATE_FINISHED)
flg = 0;
}
ESP_LOGI(TAG, "[ 6 ] Stop audio_pipeline");
audio_pipeline_terminate(pipeline);
audio_pipeline_unregister(pipeline, mp3_decoder);
audio_pipeline_unregister(pipeline, i2s_stream_writer);
/* Terminate the pipeline before removing the listener */
audio_pipeline_remove_listener(pipeline);
/* Release all resources */
audio_pipeline_deinit(pipeline);
audio_element_deinit(i2s_stream_writer);
audio_element_deinit(mp3_decoder);
}
- 20,21行: 読み込み用ファイルの先頭アドレスと終了アドレス。
- 23から34行:MP3デコーダーのコールバック関数。
- MP3デコーダーからから呼び出されます。
- Flashに有るデータを読み込みデコーダーに渡します。
- 一回の読み込み量は、file_marker.end、 file_marker.start、 file_marker.pos を使用して計算。
- 戻り値は読み込んだバイト数。0なら読み込み終了。
- 36行から: メインルーチン
- 38行:オーディオパイプラインハンドルの宣言
- 39行:オーディオエレメントハンドルの宣言
- 42,43行:オーディオパイプラインハンドルの初期化
- 47,48行:MP3デコーダーの初期化
- 49行:MP3デコーダー用コールバック関数の指定。
- 52行:I2S_Streanエレメント用コンフィグの宣言
- 53行:書き込み仕様の設定
- 54行:I2S_Streanエレメントの初期化
- 57行:オーディオパイプラインにMP3デコーダーを、”mp3″と登録
- 58行:オーディオパイプラインにI2S_Streanエレメントを、”i2s”と登録
- 61行:オーディオパイプラインでのエレメント順番指定用の配列の設定。
- 62行:オーディオパイプライン内のエレメントの順番の設定。今回は、MP3が最初でi2sがその次になります。
- 65行:読み込みファイルの先頭アドレス
- 66行:読み込みファイルの最終アドレス
- 67行:現在の読み込み位置
- 68行:オーディオパイプラインの開始。
- 71から76行:曲の終了待ちループ
- 曲の再生は別タスクで行われています。
- I2S_Streanエレメントの状態を定期的に読み込み曲の終了を判断します。
- 曲が終わるとループから抜けます。
- 移行は、オーディオパイプラインやエレメントの停止と削除。
次は、CMakeLists.txt の説明
CMakeLists.txt
set(COMPONENT_SRCS ./play_mp3_control_example.c)
set(COMPONENT_ADD_INCLUDEDIRS "")
set(COMPONENT_EMBED_FILES music-16b-2c-44100hz.mp3)
register_component()
- 3行:set(COMPONENT_EMBED_FILES music-16b-2c-44100hz.mp3)
- これで、music-16b-2c-44100hz.mp3 がFlashに書き込まれます。
- play_mp3_control_example.cの20,21行でファイルの始まりと終わりを指定していました。
- 例えば、開始アドレス:asm(“_binary_music_16b_2c_44100hz_mp3_start”);
- ファイルネームの先頭に、”_binary_”を追加して終わりに、”_start”を追加しています。
”music-16b-2c-44100hz.mp3” は曲データでそのまま使用します。
次は、componentsフォルダー
フォルダーの中は以下の様になっています。
- componentsファオルダ−
- my_boardフォルダー
- my_board_v1_0フォルダー
- board.h
- board_pin_config.c : ボードピン設定用ファイル
- my_board_v1_0フォルダー
- CMakeLists.txt : CMake用のファイル
- Kconfig.projbuild : ボード設定用のファイル
- my_boardフォルダー
最初にmy_board_v1_0フォルダー内の ”board_pin_config.c” を説明します。このファイルでI2Sのピンの指定を行っていました。
board_pin_config.c
#include "esp_log.h"
#include <string.h>
#include "audio_error.h"
#include "audio_mem.h"
#include "board_pins_config.h"
static const char *TAG = "MY_BOARD_V1_0";
esp_err_t get_i2s_pins(i2s_port_t port, i2s_pin_config_t *i2s_config)
{
AUDIO_NULL_CHECK(TAG, i2s_config, return ESP_FAIL);
if (port == I2S_NUM_0) {
// i2s_config->bck_io_num = GPIO_NUM_4;
// i2s_config->ws_io_num = GPIO_NUM_13;
// i2s_config->data_out_num = GPIO_NUM_16;
// i2s_config->data_in_num = GPIO_NUM_39;
i2s_config->bck_io_num = GPIO_NUM_26;
i2s_config->ws_io_num = GPIO_NUM_25;
i2s_config->data_out_num = GPIO_NUM_22;
} else if (port == I2S_NUM_1) {
i2s_config->bck_io_num = -1;
i2s_config->ws_io_num = -1;
i2s_config->data_out_num = -1;
i2s_config->data_in_num = -1;
} else {
memset(i2s_config, -1, sizeof(i2s_pin_config_t));
ESP_LOGE(TAG, "i2s port %d is not supported", port);
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t i2s_mclk_gpio_select(i2s_port_t i2s_num, gpio_num_t gpio_num)
{
if (i2s_num >= I2S_NUM_MAX) {
ESP_LOGE(TAG, "Does not support i2s number(%d)", i2s_num);
return ESP_ERR_INVALID_ARG;
}
if (gpio_num != GPIO_NUM_0 && gpio_num != GPIO_NUM_1 && gpio_num != GPIO_NUM_3) {
ESP_LOGE(TAG, "Only support GPIO0/GPIO1/GPIO3, gpio_num:%d", gpio_num);
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "I2S%d, MCLK output by GPIO%d", i2s_num, gpio_num);
if (i2s_num == I2S_NUM_0) {
if (gpio_num == GPIO_NUM_0) {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1);
WRITE_PERI_REG(PIN_CTRL, 0xFFF0);
} else if (gpio_num == GPIO_NUM_1) {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3);
WRITE_PERI_REG(PIN_CTRL, 0xF0F0);
} else {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2);
WRITE_PERI_REG(PIN_CTRL, 0xFF00);
}
} else if (i2s_num == I2S_NUM_1) {
if (gpio_num == GPIO_NUM_0) {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1);
WRITE_PERI_REG(PIN_CTRL, 0xFFFF);
} else if (gpio_num == GPIO_NUM_1) {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD_CLK_OUT3);
WRITE_PERI_REG(PIN_CTRL, 0xF0FF);
} else {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD_CLK_OUT2);
WRITE_PERI_REG(PIN_CTRL, 0xFF0F);
}
}
return ESP_OK;
}
- 13から16行:オリジナルコード ー> コメントアウト。
- 18から20行: BCK: GPIO26 WSIO: GPIO25 DIN: GPIO22に指定
- 35行以下:MP3再生の為のパラメータの設定を行う関数。直接は使用していないのですがコンパイルの時に参照される様です。
次は、”board.h”。今回のプログラムでは使用していませんが、コンパイル時に参照される様です。内容はオリジナルから変更しています。
board.h
#ifndef _AUDIO_BOARD_H_
#define _AUDIO_BOARD_H_
#include "board_pins_config.h"
#endif
続いて、my_boardフォルダーのファイル CMakeLists.txtです。
CMakeLists.txt
# Edit following two lines to set component requirements (see docs)
set(COMPONENT_REQUIRES)
set(COMPONENT_PRIV_REQUIRES esp_peripherals)
if(CONFIG_AUDIO_BOARD_CUSTOM)
message(STATUS "Current board name is " CONFIG_AUDIO_BOARD_CUSTOM)
list(APPEND COMPONENT_ADD_INCLUDEDIRS ./my_board_v1_0)
set(COMPONENT_SRCS
./my_board_v1_0/board_pins_config.c
)
endif()
register_component()
IF (IDF_VERSION_MAJOR GREATER 3)
idf_component_get_property(audio_board_lib audio_board COMPONENT_LIB)
set_property(TARGET ${audio_board_lib} APPEND PROPERTY INTERFACE_LINK_LIBRARIES ${COMPONENT_LIB})
ELSEIF (IDF_VERSION_MAJOR EQUAL 3)
set_property(TARGET idf_component_audio_board APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES $<TARGET_PROPERTY:${COMPONENT_TARGET},INTERFACE_INCLUDE_DIRECTORIES>)
ENDIF (IDF_VERSION_MAJOR GREATER 3)
- プログラムに合う様に、3,7,9行を変更しています。
最後は、Kconfig.projbuild。 menuconfigで使用されるファイルです。今回は変更していません。
play_mp3_controlフォルダーのCMakeLists.txt
このファイルは変更していません。
CMakeLists.txt
# (Automatically converted from project Makefile by convert_to_cmake.py.)
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
include($ENV{ADF_PATH}/CMakeLists.txt)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(play_mp3_control)
コンパイルして実行
モニターを開いて、~/esp/play_mp3_controlフォルダーに移動。先ずはターゲットの設定を行います。今回はESP32なので、idf.py set-target esp32 を実行。続いて menuconfig を実行して音源ボードを設定します。モニターで、idf.py menuconfig を実行し、Audio HAL —> Audio board (ESP32-Lyrat V4.3) —> と進み、その画面で ( ) Custom audio board を選択して 保存(s)して下さい。 設定はここだけです。
次はESP32をPCにつないでモニターから、idf.py -p [USB port] flash monitor と入力して下さい。コンパイルが終了すると曲が再生されます。
最後に
かなり苦労しましたが、ADFのインストールとMP3ファイルの再生が出来る所まで来ました。次回は本来のサンプルプログラムに有るキー入力処理を追加して行きます。ボタンを押すと曲が始まる様にプログラムを変更して行きます。
ここに今回のプロジェクトを保存します。