LoginSignup
10
10

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-30

今回は、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にブラウザから画像表示してみた、が。

(2020/5/4) 補足
aRESTを使っていますが、そのままでは使いにくく、この投稿 で示した改造をしている前提です。

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ファイル、テキストの音声再生などなどできます。(非常に助かります)

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

抜粋
//宣言
#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="こんにちは"

ソースコード

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

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;
}

以上

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10