ArduinoでPCM5102Aを使って音楽を再生する

今回は、ESP32のArduinoにスピーカをつなげて音楽を再生します。
採用したボードは、前回の投稿に続き、以下です。

TTGO-TM-ESP32
　https://www.aliexpress.com/item/32848882218.html
　https://github.com/LilyGO/TTGO-TM-ESP32 

SPI接続のLCDとSDカードは使いましたが、今回はI2Sを使います。
組込みボードで音楽の再生は初めてだったので、備忘録として投稿してみました。

前回の投稿の続きです。
　[ArduinoのLCDにブラウザから画像表示してみた、が。](https://qiita.com/poruruba/items/9228f3421181a123a8de )

(2020/5/4) 補足
aRESTを使っていますが、そのままでは使いにくく、[この投稿](https://qiita.com/poruruba/items/34220a4dd6aaf48392aa) で示した改造をしている前提です。

# DACチップ：PCM5102A

DACチップはPCM5102Aです。割とメジャーなようです。
I2Sで接続します。マイコンに3つのGPIOをつなぐだけなので簡単です。

接続するのは以下の通り

　LRCK：Left Right Clock → ESP32のIO25
　DIN：PCM Data → ESP32のIO19
　BCK：Bit Clock → ESP32のIO26
※ESP32側は各自の環境に合わせてください。

# PCM5102A用ライブラリの使い方

Arduino用のライブラリも充実していて、以下を使いました。
zipをダウンロードして、ArduinoIDEのライブラリマネージャにインストールしておきます。

schreibfaul1/ESP32-audioI2S
　https://github.com/schreibfaul1/ESP32-audioI2S 

MP3再生ができるのはもちろん、SDカード上にあるMP3ファイルやWeb上にあるMP3ファイル、テキストの音声再生などなどできます。（非常に助かります）

以下にソースコードの一部を記載します。
たったこれだけです。

```c:抜粋
//宣言
#include "Audio.h"
Audio audio;

//PIN番号
#define I2S_DOUT      19
#define I2S_BCLK      26
#define I2S_LRC       25

//準備(setup)
  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  audio.setVolume(12); // 0...21

//ループ(loop)
  audio.loop();
```

I2S_DOUT、 I2S_BCLK、I2S_LRC は、搭載されているボードに合わせてください。
あとは適当なタイミングで、以下のいづれかを呼び出せばよいです。

・bool audio.connecttoSD(String command)
　機能：SDカードにあるMP3ファイルの再生を開始します。
　引数：command：SDカードにあるMP3のファイル名
　例：command="320k_test.mp3"

・bool audio.connecttospeech(String command, "ja")
　機能：文章を発声します。
　引数：command：発声したい文章
　例：command="こんにちは"

# ソースコード

追加部分に、「//★追加」と付記しておきました。

```c:DisplayServer.uno
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7789.h> // Hardware-specific library for ST7789
#include <SPI.h>

#include "Audio.h" //★追加

#include <WiFi.h>
#include <WiFiServer.h>
#include <aREST.h>

#include <SD.h>

// 編集はここから
const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";

// GET接続を待ち受けるポート番号
#define REST_PORT     80

// SDカードから読み出す画像ファイル名
const char* bgimage = "/bgimage.bmp";

// LCDの解像度
#define DISP_WIDTH    240
#define DISP_HEIGHT   320

// LCDの接続ポート(SPI接続)
#define TFT_CS         5
#define TFT_RST        17 
#define TFT_DC         16
#define TFT_MOSI       23  // Data out
#define TFT_SCLK       18  // Clock out

// SDカードの接続ポート(SPI接続)
#define SD_CS   13
#define SD_SCK  14
#define SD_MOSI 15
#define SD_MISO 2

// I2Sの接続ポート //★追加
#define I2S_DOUT      19
#define I2S_BCLK      26
#define I2S_LRC       25

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

// LCD画面を回転させるかどうか
#define DISP_ROTATE   1

// 編集はここまで

Audio audio; //★追加

SPIClass spi_sd(VSPI);

#define DISP_LENGTH   ((DISP_WIDTH >= DISP_HEIGHT) ? DISP_WIDTH : DISP_HEIGHT)
#define BUFFER_SIZE   DISP_LENGTH
uint16_t line_buffer[BUFFER_SIZE];

#define PARTS_SIZE  (3 * 10) // must 3 times

WiFiServer server(REST_PORT);
aREST rest = aREST();

// aREST function

// SDカードにあるMP3の再生 //★追加
String playBackground(String command){
  Serial.println("playBackground called");

  if( !audio.connecttoSD(command) )
    return "NG";

  return "OK";
}

// 文章の発声 //★追加
String playSpeech(String command){
  Serial.println("playSpeech called");

  if( !audio.connecttospeech(command, "ja") )
    return "NG";

  return "OK";
}

// SDカードにある画像ファイルの表示
String drawBackground(String command) {
  Serial.println("drawBackground called");
  
  if( drawBmp(command.c_str()) < 0 )
    return "NG";
    
  return "OK";
}

// 表示画像の転送(RGB565)
String drawBmp565(String command) {
  Serial.println("drawBmp565 called");

  uint16_t winsize[4];
  
  int len = parseRGB565(command, winsize, line_buffer, BUFFER_SIZE );
  int data_len = winsize[2] * winsize[3];

  if( len != data_len )
    return "NG";

  tft.startWrite();
  tft.setAddrWindow(winsize[0], winsize[1], winsize[2], winsize[3]);
  tft.writePixels(line_buffer, data_len);
  tft.endWrite();
    
  return "OK";
}

// 表示画像の転送(RGB332)
String drawBmp332(String command) {
  Serial.println("drawBmp332 called");

  uint16_t winsize[4];
  
  int len = parseRGB332(command, winsize, line_buffer, BUFFER_SIZE );
  int data_len = winsize[2] * winsize[3];

  if( len != data_len )
    return "NG";

  tft.startWrite();
  tft.setAddrWindow(winsize[0], winsize[1], winsize[2], winsize[3]);
  tft.writePixels(line_buffer, data_len);
  tft.endWrite();
    
  return "OK";
}

// 表示画像の転送(モノクロ)
String drawBmp1(String command) {
  Serial.println("drawBmp1 called");

  uint16_t winsize[4];
  
  int len = parseRGB1(command, winsize, line_buffer, BUFFER_SIZE );
  int data_len = winsize[2] * winsize[3];

  if( len != data_len )
    return "NG";

  tft.startWrite();
  tft.setAddrWindow(winsize[0], winsize[1], winsize[2], winsize[3]);
  tft.writePixels(line_buffer, data_len);
  tft.endWrite();
    
  return "OK";
}

// 解像度情報の取得
String getInfo(String command) {
  Serial.println("getInfo called");

  return String(DISP_ROTATE) + "," + String(tft.width()) + "," + String(tft.height());
}

// 初期化
void setup(void) {
  Serial.begin(9600);
  Serial.println(F("Hello! ST77xx TFT Test"));

  // SDカードのマウント
  spi_sd.end();
  spi_sd.begin(SD_SCK, SD_MISO, SD_MOSI);
  if(!SD.begin(SD_CS, spi_sd)){
      Serial.println("Card Mount Failed");
//      return;
  }

  // LCDの初期化
  tft.init(DISP_WIDTH, DISP_HEIGHT);           // Init ST7789 320x240
  tft.invertDisplay(false);
  tft.setRotation(DISP_ROTATE);

  Serial.println(F("Initialized"));

  // 初期画像の表示(SDカードからの読み出し含む)
  if( drawBmp(bgimage) < 0 )
    tft.fillScreen(ST77XX_BLACK);

  // Init variables and expose them to REST API

  // Function to be exposed
 // GETエンドポイントの定義
  rest.function("drawbg", drawBackground);
  rest.function("draw565", drawBmp565);
  rest.function("draw332", drawBmp332);
  rest.function("draw1", drawBmp1);
  rest.function("getInfo", getInfo);
  rest.function("playbg", playBackground); //★追加
  rest.function("playspeech", playSpeech); //★追加

  // Give name & ID to the device (ID should be 6 characters long)
  rest.set_id("0001");
  rest.set_name("esp32");

  // I2Sセットアップ //★追加
  audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  audio.setVolume(12); // 0...21

  // WiFiアクセスポイントへの接続
  WiFi.begin(wifi_ssid, wifi_password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(WiFi.localIP());

  // Webサーバ起動
  server.begin();
  Serial.println("Server started");
}

// ループ処理
void loop() {
  // 音声再生処理 //★追加
  audio.loop();

  WiFiClient client = server.available();
  if (client) {
    // GET呼び出しを検知
    for( int i = 0 ; i < 10000; i += 10 ){
      if(client.available()){
        // GET呼び出しのコールバック呼び出し
        rest.handle(client);
        return;
      }
      delay(10);
    }
    // まれにGET呼び出し受付に失敗するようです。
    Serial.println("timeout");
  }
}

// RGB332からRGB565への変換
uint16_t fromColor332(uint8_t val){
  return ((val & 0x00E0) << 8) | ((val & 0x001C) << 6) | ((val & 0x0003) << 3);
}

// 16進数文字列からuint16配列への変換のための関数軍
int char2int(char c){
  if( c >= '0' && c <= '9' )
    return c - '0';
  if( c >= 'a' && c <= 'f' )
    return c - 'a' + 10;
  if( c >= 'A' && c <= 'F' )
    return c- 'A' + 10;

  return 0;
}

char int2char(int i){
  if( i >= 0 && i <= 9 )
    return '0' + i;
  if( i >= 10 && i <= 15 )
    return 'a' + (i - 10);

  return '0';
}

uint16_t get_uint16b_from_str(String str, int offset){
    uint16_t value = char2int(str.charAt(offset)) << 12;
    value += char2int(str.charAt(offset + 1)) << 8;
    value += char2int(str.charAt(offset + 2)) << 4;
    value += char2int(str.charAt(offset + 3));

    return value;
}

uint8_t get_uint8b_from_str(String str, int offset){
    uint8_t value = char2int(str.charAt(offset)) << 4;
    value += char2int(str.charAt(offset + 1));

    return value;
}


// RGB565転送呼び出しのパラメータ解析
int parseRGB565(String str, uint16_t *win, uint16_t *array, int maxlen){
  int len = str.length();
  if( ((len - 16) % 4) != 0 )
    return -1;
  if( len > (16 + maxlen * 4) )
    return -1;

  for( int i = 0 ; i < 16 ; i += 4 ){
    win[i / 4] = get_uint16b_from_str(str, i);
  }

  for( int i = 0 ; i < len - 16 ; i += 4 ){
    array[i / 4] = get_uint16b_from_str(str, 16 + i);
  }

  return (len - 16) / 4;
}

// RGB332転送呼び出しのパラメータ解析
int parseRGB332(String str, uint16_t *win, uint16_t *array, int maxlen){
  int len = str.length();
  if( ((len - 16) % 2) != 0 )
    return -1;
  if( len > (16 + maxlen * 2) )
    return -1;

  for( int i = 0 ; i < 16 ; i += 4 ){
    win[i / 4] = get_uint16b_from_str(str, i);
  }

  for( int i = 0 ; i < len - 16; i += 2 ){
    uint8_t value = get_uint8b_from_str(str, 16 + i);
    array[i / 2] = fromColor332(value);
  }

  return (len - 16) / 2;
}

// RGBモノクロ転送呼び出しのパラメータ解析
int parseRGB1(String str, uint16_t *win, uint16_t *array, int maxlen){
  int len = str.length();
  if( ((len - 16) % 2) != 0 )
    return -1;
  if( len > (16 + maxlen / 4) )
    return -1;

  for( int i = 0 ; i < 16 ; i += 4 ){
    win[i / 4] = get_uint16b_from_str(str, i);
  }

  int col = 0;
  for( int i = 0 ; i < len - 16; i += 2 ){
    uint8_t value = get_uint8b_from_str(str, 16 + i);
    for( int j = 0 ; j < 8 ; j++ )
      array[col++] = ((value >> ( 7 - j )) & 0x0001) ? 0xffff : 0x0000;
  }

  return col;
}

// SDカードからビットマップファイルの取得およびLCD表示
int drawBmp(const char *filename) {
  File     bmpFile;
  int      bmpWidth, bmpHeight;   // W+H in pixels
  uint8_t  bmpDepth;              // Bit depth (currently must be 24)
  uint32_t bmpImageoffset;        // Start of image data in file
  uint32_t rowSize;               // Not always = bmpWidth; may have padding
  boolean  flip    = true;        // BMP is stored bottom-to-top

  Serial.println("BMP Loading: "); Serial.print(filename);

  // Open requested file on SD card
  if ((bmpFile = SD.open(filename)) == NULL) {
    Serial.println("File not found");
    return -1;
  }

  // Parse BMP header
  if(fread_uint16b(bmpFile) != 0x4D42){
    bmpFile.close();
    Serial.println("not BMP signature");
    return -1;
  }

  Serial.println("File size: "); Serial.println(fread_uint32b(bmpFile));
  fread_uint32b(bmpFile); // Read & ignore creator bytes
  bmpImageoffset = fread_uint32b(bmpFile); // Start of image data
  // Read DIB header
  Serial.println("Header size: "); Serial.println(fread_uint32b(bmpFile));
  bmpWidth  = fread_uint32b(bmpFile);
  Serial.println("bmpWidth size: "); Serial.println(bmpWidth);
  bmpHeight = fread_uint32b(bmpFile);
  Serial.println("bmpHeight size: "); Serial.println(bmpHeight);
  if(fread_uint16b(bmpFile) != 1){
    bmpFile.close();
    Serial.println("Not supported planes");
    return -1;
  }

  bmpDepth = fread_uint16b(bmpFile); // bits per pixel
  Serial.println("Bit Depth: "); Serial.println(bmpDepth);
  if((bmpDepth != 24) || (fread_uint32b(bmpFile) != 0)) { // 0 = uncompressed
    bmpFile.close();
    Serial.println("Not supported format");
    return -1;
  }

  Serial.println("Image size: ");
  Serial.print(bmpWidth);
  Serial.println(bmpHeight);

  // BMP rows are padded (if needed) to 4-byte boundary
  rowSize = (bmpWidth * 3 + 3) & ~3;

  // If bmpHeight is negative, image is in top-down order.
  // This is not canon but has been observed in the wild.
  if(bmpHeight < 0) {
    bmpHeight = -bmpHeight;
    flip      = false;
  }

  // Crop area to be loaded
  int w = bmpWidth;
  int h = bmpHeight;
  if(w > tft.width())
    w = tft.width();
  if(h > tft.height())
    h = tft.height();

  for (int row = 0; row < h; row++) { // For each scanline...
    uint32_t pos;
    if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
      pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
    else     // Bitmap is stored top-to-bottom
      pos = bmpImageoffset + row * rowSize;
    if(bmpFile.position() != pos) // Need seek?
      bmpFile.seek(pos);

    uint8_t buffer[PARTS_SIZE];
    int donesize = 0;
    int col = 0;
    while( donesize < rowSize ){
      int readsize = ((rowSize - donesize) > PARTS_SIZE) ? PARTS_SIZE : (rowSize - donesize);
      if( bmpFile.read(buffer, readsize) != readsize ){
        bmpFile.close();
        Serial.println("read failed");
        return -1;
      }

      for( int i = 0 ; i < readsize && col < w; i += 3 )
        line_buffer[col++] = tft.color565(buffer[i + 2], buffer[i + 1], buffer[i]);

      donesize += readsize;
    }

    tft.startWrite();
    tft.setAddrWindow(0, row, w, 1);
    tft.writePixels(line_buffer, w);
    tft.endWrite();
  } // end scanline

  bmpFile.close();

  Serial.println("BMP Loaded in ");

  return 0;
}

uint16_t fread_uint16b(File f) {
  uint16_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read(); // MSB
  return result;
}

uint32_t fread_uint32b(File f) {
  uint32_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read();
  ((uint8_t *)&result)[2] = f.read();
  ((uint8_t *)&result)[3] = f.read(); // MSB
  return result;
}
```

以上