【ESP Arduinoライブラリのご紹介】ESP32-A2DP


# はじめに

今まで使ってきたESPマイコン用Arduinoライブラリについてご紹介したいと思います。
これらの記事がどなたかの何らかに貢献できれば幸いです。

# ESP32-A2DP Library

今回はESP32-A2DPライブラリをご紹介します。

https://github.com/pschatzmann/ESP32-A2DP.git

Thanks to [@pschatzmann](https://github.com/pschatzmann), author of the ESP32-A2DP library.


# 概要
スマートフォンからESP32へ音声データをBluetooth(A2DP)でストリーミングして、ESP32から出力したI2S信号をDAC(Digital Analog Converter）へ入力できます。つまり、このライブラリを使ってBluetoothスピーカーを構成できます。
A2DP（Advanced Audio Distribution Profile）とは無線通信を利用して音声データをストリーミングするBluetoothのプロファイルです。iOSのAirPlayはこのプロファイルに準拠しています。

はじめ、ESP-IDF開発環境のGitHubリポジトリにC言語で書かれたa2dp_sink[^1]ドライバが公開されていました。その後、[@pschatzmann](https://github.com/pschatzmann)さんが[ESP-IDF A2DP-SINK demo](https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/bluedroid/classic_bt/a2dp_sink)を使いやすいArduinoクラスライブラリに変換？してくれました。Thanks👍

[^1]: sink：吸収源≒受信 source：供給源≒送信
 
（かんたんな構成図）
[iPhone] ---(Bluetooth [Radio Frequency 2.4GHz])--->[ESP32]---(I2S)--->[DAC]---(RCA Cable)--->[AMP]---(Cu Cable)--->[Speaker]

# お試し環境

つぎの環境でこのライブラリの動作確認をおこないました。

- ソフト
    - VSCode
    - PlatformIO（VSCode拡張機能）
    - [arduino-esp32ライブラリ(#1.0.6)](https://github.com/espressif/arduino-esp32/tree/1.0.6)
- ハード
    - [ESP-WROOM-32 変換基板 KP-ESP32C ](https://eleshop.jp/shop/g/g402543/)
    - [ES9038Q2M中華DAC](https://ebay.us/maGNJ8)
    - iPhone

KP-ESP32Cと中華DAC(I2Sインターフェイス付き）
<img width="25%" alt="ESP-WROOM-32 変換基板　KP-ESP32C" src="https://eleshop.jp/PRODUCTS/CATALOG/MODULE/PHOTO/kpesp32ca.jpg"><img width="25%" alt="中華DAC" src="https://i.ebayimg.com/images/g/bt4AAOSwiEVeX0Dy/s-l1600.jpg">

# 動作の様子

iPhoneからApple Musicでダウンロードした曲をAirPlayで再生しています。
ESP32基板と中華DACをタカチのケースに収めました。

https://www.instagram.com/reel/CUhDjtDg6yR/?utm_source=ig_web_copy_link

電子回路的な配線の説明はここでは省略します。詳細はESP-IDFのGitHubリポジトリなどを参照してください。

# サンプルコード

こちらに私がモディファイしたサンプルがあります。よろしければご参照ください。

https://github.com/riraosan/ESP32_BT-A2DP_Receiver.git

```c++:Application.h

#include <Arduino.h>
#include <BluetoothA2DPSink.h>
#include <esp32-hal-log.h>

class Application {
   public:
    Application() {}

    ~Application() {}

    static void avrc_metadata_callback(uint8_t id, const uint8_t* text) {
        log_i("==> AVRC metadata rsp");
        String str((char*)text);

        switch (id) {
            case 0x01:
                log_i("==>　曲名：%s", str.c_str());
                break;
            case 0x02:
                log_i("==>　アーティスト：%s", str.c_str());
                break;
            case 0x04:
                log_i("==>　アルバム名：%s", str.c_str());
                break;
            case 0x20:
                log_i("==>　ジャンル：%s", str.c_str());
                break;
            default:
                log_i("==>　Unknown：%s", str.c_str());
                break;
        }
    }

    static void on_data_receive_callback(void) {
        static int count;
        if (++count % 100 == 0) {
            digitalWrite(_BLUE_LED_PORT, HIGH);
        } else {
            digitalWrite(_BLUE_LED_PORT, LOW);
        }
    }
#ifdef TEST
    BluetoothA2DPSink* getA2DPSink(void) {
        return &_a2dp_sink;
    }
#endif
    void setup(void) {
        pinMode(_BLUE_LED_PORT, OUTPUT);
        pinMode(_GREEN_LED_PORT, OUTPUT);

        i2s_pin_config_t pin_config = {
            .bck_io_num   = 26,
            .ws_io_num    = 22,
            .data_out_num = 25,
            .data_in_num  = I2S_PIN_NO_CHANGE  //Use in i2s_pin_config_t for pins which should not be changed
        };

        //Settings for ES9038Q2M VR1.07 DAC Board(eBay item number:263908779821)
        i2s_config_t i2s_config = {
            .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
            .sample_rate          = 44100,                       // corrected by info from bluetooth
            .bits_per_sample      = (i2s_bits_per_sample_t)16,   // set_bits_per_sample()
            .channel_format       = I2S_CHANNEL_FMT_RIGHT_LEFT,  // 2-channels
            .communication_format = I2S_COMM_FORMAT_I2S,         // I2S communication format I2S
            .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,        // default interrupt priority
            .dma_buf_count        = 8,                           // default
            .dma_buf_len          = 64,                          // default
            .use_apll             = false,                       // I2S using APLL as main I2S clock, enable it to get accurate clock
            .tx_desc_auto_clear   = true                         // I2S auto clear tx descriptor if there is underflow condition
        };

        _a2dp_sink.set_pin_config(pin_config);
        _a2dp_sink.set_i2s_config(i2s_config);
        _a2dp_sink.set_bits_per_sample(I2S_BITS_PER_SAMPLE_32BIT);  //for I2S : PCM 44.1K-384K 32BIT
        _a2dp_sink.set_on_data_received(on_data_receive_callback);
        _a2dp_sink.set_avrc_metadata_callback(avrc_metadata_callback);

        _a2dp_sink.start("Riraosan Player", true);
    }

    void handle(void) {
        if (_a2dp_sink.isConnected()) {
            digitalWrite(_GREEN_LED_PORT, HIGH);
        } else {
            digitalWrite(_GREEN_LED_PORT, LOW);
        }

        switch (_a2dp_sink.get_audio_state()) {
            case ESP_A2D_AUDIO_STATE_STARTED:
                break;
            case ESP_A2D_AUDIO_STATE_STOPPED:
            case ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND:
                digitalWrite(_BLUE_LED_PORT, LOW);
                break;
            default:
                break;
        }
    }

   private:
    BluetoothA2DPSink _a2dp_sink;
    constexpr static int _BLUE_LED_PORT  = 33;
    constexpr static int _GREEN_LED_PORT = 32;
};

```

```c++:main.cpp
#include <Application.h>

static Application app;

void setup() {
    app.setup();
}

void loop() {
    app.handle();
}
```

```platformio.ini
[platformio]
default_envs    = esp32_a2dp_debug

[env:esp32_a2dp_release]
build_type      = release
extends         = esp32, arduino-esp32, serial, Windows

build_flags =
        -D CORE_DEBUG_LEVEL=0

[env:esp32_a2dp_debug]
build_type      = debug
extends         = esp32, arduino-esp32, serial, Windows

monitor_filters = esp32_exception_decoder
build_flags =
        -D TEST
        -D CORE_DEBUG_LEVEL=4
        -D CONFIG_ARDUHAL_LOG_COLORS

test_port               = COM7
test_speed              = 115200
test_build_project_src  = no

[esp32]
board = esp32dev

;OTA or Serial
[ota]
;upload_protocol = espota
;upload_port    = url
;upload_port    = 192.168.xx.xx
;monitor_port   = /dev/tty.usbserial-1952FF03F3
;monitor_speed  = 115200
;targets        = monitor

[serial]
upload_protocol = esptool
upload_speed    = 115200
monitor_speed   = 115200
targets         = upload, monitor

;Serial Port(Mac or Linux or Windows)
[Mac]
upload_port     = /dev/tty.usbserial-1952FF03F3
monitor_port    = /dev/tty.usbserial-1952FF03F3

[Linux]
upload_port     = /dev/ttyUSB0
monitor_port    = /dev/ttyUSB0

[Windows]
upload_port     = COM7
monitor_port    = COM7

[arduino-esp32]
platform        = espressif32
framework       = arduino
platform_packages = framework-arduinoespressif32@https://github.com/espressif/arduino-esp32.git#1.0.6
build_flags =
        -std = gnu++14
        -D ARDUINO_ARCH_ESP32
        -D ESP32

build_unflags =
        ;-std = gnu++11

board_build.mcu         = esp32
board_build.f_cpu       = 240000000L
board_build.f_flash     = 80000000L
board_build.flash_mode  = qio
board_build.partitions  = default.csv

lib_deps =
        ;https://github.com/riraosan/ESP32-A2DP.git ;modified for ES9038Q2M VR1.07 DAC Board
        https://github.com/pschatzmann/ESP32-A2DP.git
```

# 気をつけること

- ESP8266はペリフェラル（周辺機器）にBluetoothが内蔵されていないので、このライブラリは使えません。
- このサンプルはarduino-esp32ライブラリ1.0.6で動作確認しています。最新のarduino-esp32ライブラリを使用するとI2SライブラリのI2S定数定義が変更されているので、コンパイルエラーとなります。
- I2Sポートは`i2s_pin_config_t`で設定しています。
- I2Sドライバは`i2s_config_t `で設定しています。お持ちのDACに応じて設定を変更してください。 
- コーデックはSBCのみです。AACと比べて音質の差は感じられませんでした。
- ジョン・ウィリアムズ指揮のオーケストラ映像をiPhoneで視聴した際に、指揮振り（映像）と音楽のタイミング差を感じました。（厳密に測定してませんが、体感で約0.5秒未満ぐらい遅延を感じます）リップシンクができればよいのですが。どうすればリップシンクできるのか、どなたか教えて下さい。🙏
- 一曲送る、一曲戻る、早送り、巻き戻し、一時停止、停止、再生、一曲の再生が終了して次の曲の再生が開始する時などに、「プツッ」というクリックノイズが聞こえることがあるかもしれません。その際は、ESP32-A2DPライブラリのDMAバッファを適切なタイミングでクリアしてください。

# さいごに

私はESP-IDF版のa2dp_sinkサンプル（C言語版）をいろいろ調べながら、動作するところまでこぎつけました。
その時に、「このC言語ドライバをクラス化したらいいのでは？」とは思っていましたが、その思いつきを完全に忘れてしまいました。
その後2年ぐらい経って、何気なくGitHubを調べてみたら、pschatzmannさんがArduino環境向けにクラスを作成してくれていて驚きました。私は彼と同じことを考えていたけれども、私には実装できなかったと思います。
[@pschatzmann](https://github.com/pschatzmann)さん、素敵なライブラリを作ってくれてありがとう。あなたの貢献に感謝します。👍

# See Also

- [ESP-IDF A2DP-SINK demo](https://github.com/espressif/esp-idf/tree/master/examples/bluetooth/bluedroid/classic_bt/a2dp_sink)
- [ES9038Q2M datasheet](http://file2.dzsc.com/product/18/05/25/829029_170233543.pdf)
- [ESP32でBluetoothスピーカーを作ってみた (I2S編)](https://qiita.com/Tw_Mhage/items/7473d694a31ca6bd9592)
- [ESP32でI2S+DACを使う](https://qiita.com/h_nari/items/b52c525f0c5b35aaf692)
- [ESP32でサウンド出力時のクリックノイズ対策（I2S+内蔵DAC）]
(http://blog-yama.a-quest.com/?eid=970190)

（了）