ESP32-WROOM-32DでのSD内のwavファイル再生(16bit, 44.1kHz, stereo)の試行

ESP32-WROOM-32Dでのwave再生(16bit, 44.1kHz, stereo)の試行。
driver/i2s.hを使用して音声データをDACであるPCM5102Aに流し込んでいる。

![128142224_2796301777326186_3281067876323078915_o.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/e78e9d0a-8f16-1591-409b-2b3c6a3aac42.jpeg)
<a href= "https://youtu.be/HQxarfZlkIs">[リンク]再生した音声を録音したもの</a>

```c++:main.cpp
#if 0
#include <Adafruit_GFX.h>    // Core graphics library by Adafruit
#include <Arduino_ST7789.h> // Hardware-specific library for ST7789 (with or without CS pin)
#endif
#include <SPI.h>
#include <SD.h>
#include <driver/i2s.h>

#ifdef USE_WIFI
#include <WiFi.h>
#endif

#define PCM5102_LCK ( 2)
#define PCM5102_DIN ( 21)
#define PCM5102_BCK (16)
#define PCM5102_SCL (17)

hw_timer_t *timer = NULL;
hw_timer_t *timer2 = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

volatile unsigned long cnt = 0;

#define SD_BUFF_SIZE   (512)
#define SD_BUFF_NUM (8) 
unsigned char sdBuff[SD_BUFF_NUM][SD_BUFF_SIZE];
int readBuffNum = 0;
int writeBuffNum = 0;
typedef enum {
  BUFF_EMPTY,
  BUFF_READING,
  BUFF_WRITING,
  BUFF_FULL,
} buff_status_t;
int buffStatus[SD_BUFF_NUM] = {BUFF_EMPTY};

// output sound on I2S interface.
void IRAM_ATTR sound_out() {
  portENTER_CRITICAL_ISR(&timerMux);
  size_t written = 0;
  size_t accumWritten = 0;

  if(buffStatus[readBuffNum] == BUFF_FULL && cnt == 0) {
    buffStatus[readBuffNum] = BUFF_READING;
  }
  if(buffStatus[readBuffNum] == BUFF_READING) {
    do {
      i2s_write(I2S_NUM_0, (char *)&sdBuff[readBuffNum][cnt], 4, &written, 0);
      cnt += written;
      accumWritten += written;
    } while(accumWritten == 4);

    if(cnt >= SD_BUFF_SIZE) {
      buffStatus[readBuffNum] = BUFF_EMPTY;
      int tmp_readBuffNum = readBuffNum;
      ++tmp_readBuffNum %= SD_BUFF_NUM;
      if(buffStatus[tmp_readBuffNum] == BUFF_FULL) {
        cnt = 0;
        ++readBuffNum %= SD_BUFF_NUM;
      }
    }
  }
  portEXIT_CRITICAL_ISR(&timerMux);
}

// special SD I/F definition for my ESP32!
#define SD_SCK  (33)
#define SD_MISO (19)
#define SD_MOSI (23)
#define SD_SS   ( 5)

void setup(void) {
  SPI.end();
  SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_SS);
  
  Serial.begin(115200);
  if(!SD.begin(SD_SS, SPI, 15600000)) {
    Serial.println("SD card mount failed!!");
  }

  int i2sRet;
  const i2s_config_t config = {
    (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    44100,
    I2S_BITS_PER_SAMPLE_16BIT,
    I2S_CHANNEL_FMT_RIGHT_LEFT,
    (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
    ESP_INTR_FLAG_LEVEL1,
    16,
    512,
    false,
    false,
    0    
  };
  
  i2sRet = i2s_driver_install(I2S_NUM_0, &config, 0, NULL);
  i2s_pin_config_t pinConfig = {16, 2, 21, I2S_PIN_NO_CHANGE};
  i2sRet = i2s_set_pin(I2S_NUM_0, &pinConfig);
    
  timer = timerBegin(1, 4, true);
  timerAttachInterrupt(timer, sound_out, true);
  timerAlarmWrite(timer, 882, true);
  timerAlarmDisable(timer);

  Serial.println("Initialized");
}

void loop() {
  static int wavNum = 0;
  #define FILE_NUM  ( 7)
  const char *fileName[FILE_NUM] = {
    // 16bit stereo 44.1kHz
    "/ry.wav",
    "/gangnam.wav",
    "/rtype16st.wav",
    "/sorpart.wav",
    "/scc16st.wav",
    "/tomakepsg16st.wav",
    "/shil16st.wav",
  };
  bool flg_skipHeader = true;
  bool flg_waitForBufferFilled = true;
  
  #if 1
  File file = SD.open(fileName[wavNum], FILE_READ);
  #endif

  if(!file) {
    Serial.println("FILE READ error...");
  } else {
    // get length of data for playback.
    int rest = file.size();
    cnt = 0;
    readBuffNum = 0;
    writeBuffNum = 0;
    memset(buffStatus, BUFF_EMPTY, sizeof(buffStatus));

    while(rest > 0) {
      if(flg_skipHeader) {
        // read wav header.
        unsigned char wavHeader[44];
        file.read(wavHeader, 44);
        rest -= 44;
        flg_skipHeader = false;
        //wavHeader[0x16] == 0x02 ? stereoOffset = 1 : stereoOffset = 0;

        // read data into all the buffers.
        do {
          file.read(sdBuff[writeBuffNum], rest < SD_BUFF_SIZE ? rest : SD_BUFF_SIZE);
          rest -= SD_BUFF_SIZE;
          ++writeBuffNum;
        } while(writeBuffNum < SD_BUFF_NUM);
          
        for(int i = 0; i < SD_BUFF_NUM; i++) {
          buffStatus[i] = BUFF_FULL;
        }
        readBuffNum = 0;
        writeBuffNum = 0;

        if(flg_waitForBufferFilled) {
          // start playback.
          timerAlarmEnable(timer);
          flg_waitForBufferFilled = false;
          //for(;;) {};
        }
      }
      // read data into an empty buffer.
      if(buffStatus[writeBuffNum] == BUFF_EMPTY) {
        buffStatus[writeBuffNum] = BUFF_WRITING;
        file.read(sdBuff[writeBuffNum], rest < SD_BUFF_SIZE ? rest : SD_BUFF_SIZE);
        rest -= SD_BUFF_SIZE;
        buffStatus[writeBuffNum] = BUFF_FULL;
        ++writeBuffNum %= SD_BUFF_NUM;
      }
    }
SKIP_FILE:
    ++wavNum %= FILE_NUM;
  }
}
```

```ini:platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
; enlarge flash capacity:
board_build.partitions = no_ota.csv
; set frequency to 240MHz
board_build.f_cpu = 240000000L
; for debugger
monitor_speed = 115200
upload_speed = 921600
debug_tool = minimodule
```

飾りのスペアナ表示部とWebブラウザからの選曲機能を追加したバージョンを以下に示す。
![wavplayer.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/919305/e77a9cce-7037-d9d8-321d-471fa9031525.jpeg)
[使用モジュール]
MicroSDカードモジュール
DACモジュール(PCM5102A)
TFT液晶モジュール(ILI9341)
水晶発振器12.000MHz(EXO-3)

[開発環境]
VisualStudioCode + PlatformIO

```c++:main.cpp
#include <SPI.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <Arduino.h>

#include <SD.h>
#include <driver/i2s.h>

#include <WiFi.h>
#include <WebServer.h>

#define FFTN (256)
#define STP  (1)

volatile float realSpec[FFTN];
volatile float imagSpec[FFTN];
volatile bool fftReady = false;

// スマホ側リモコン画面コンテンツ
const char html[] = "<html><body><font size=15><a href='/p'><=</a> <a href='/n'>=></a></font></body></html>";

hw_timer_t *timer = NULL;   // 音声再生用
hw_timer_t *timer2 = NULL;  // パイロットランプ用
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

volatile unsigned long cnt = 0;

#define SD_BUFF_SIZE   (512)
#define SD_BUFF_NUM (8) 
unsigned char sdBuff[SD_BUFF_NUM][SD_BUFF_SIZE];
unsigned char dummyBuff[8];

int readBuffNum = 0;
int writeBuffNum = 0;
typedef enum {
  BUFF_EMPTY,
  BUFF_READING,
  BUFF_WRITING,
  BUFF_FULL,
} buff_status_t;
int buffStatus[SD_BUFF_NUM] = {BUFF_EMPTY};

// 音声データ出力(I2S)
void IRAM_ATTR sound_out() {
  portENTER_CRITICAL_ISR(&timerMux);
  size_t written = 0;
  size_t accumWritten = 0;

  if(buffStatus[readBuffNum] == BUFF_FULL && cnt == 0) {
    buffStatus[readBuffNum] = BUFF_READING;
  }
  if(buffStatus[readBuffNum] == BUFF_READING) {
    do {
      i2s_write(I2S_NUM_0, (char *)&sdBuff[readBuffNum][cnt], 256, &written, 0);
      cnt += written;
      accumWritten += written;
    } while(accumWritten == 256);

    if(cnt >= SD_BUFF_SIZE) {
      buffStatus[readBuffNum] = BUFF_EMPTY;
      int tmp_readBuffNum = readBuffNum;
      ++tmp_readBuffNum %= SD_BUFF_NUM;
      if(buffStatus[tmp_readBuffNum] == BUFF_FULL) {
        cnt = 0;
        ++readBuffNum %= SD_BUFF_NUM;
      }
    }
  }
  portEXIT_CRITICAL_ISR(&timerMux);
}

// SD
#define SD_SCK  (18)
#define SD_MISO (19)
#define SD_MOSI (23)
#define SD_SS   ( 5)
SPIClass hSpi(HSPI);

//////////// TFT ////////////
#define TFT_DC    (26)
#define TFT_RST   (25)
#define TFT_CS    ( 4)  // jtag tdo   (15)
#define TFT_MOSI  (32)  // jtag clock (13)
#define TFT_SCLK  (33)  // jtag mode  (14)
SPIClass vSpi(VSPI);
Adafruit_ILI9341 tft = Adafruit_ILI9341(&vSpi, TFT_DC, TFT_CS, TFT_RST);

volatile  float fReal[FFTN], fImag[FFTN];
volatile  float fftBuff[FFTN];
float oldFftBuff[FFTN];

// FFT計算本体(特別感謝：FPGAマガジン)
inline void calcFFT(volatile float *iW, volatile float *oR, volatile float *oI) {
    short N = FFTN;
    short stg;
    short halfN = N / 2;
    short i, j, k, kp, m, h;
    float w1, w2, s1, s2, t1, t2;

    // ステージ数を求める
    i = N;
    stg = 0;
    while (i != 1) {
        i = i / 2;
        stg++;
    }

    // データ入力
    for (i = 0; i < N; i += STP) {
      float x = (float)i / N;
        // ブラックマン窓を適用
        // fReal[i] = iW[i] * (0.42 - 0.5 * cos(2 * PI * x) + 0.08 * cos(4 * PI * x));
        // ナットール窓を適用
        fReal[i] = iW[i] * (0.355768 - 0.487396 * cos(2 * PI * x) + 0.144232 * cos(4 * PI * x) - 0.012604 * cos(6 * PI * x));
        fImag[i] = 0;
    }

    // ビット逆順ソート
    j = 0;
    for (i = 0; i <= N - 2; i += STP) {
        if (i < j) {
            t1 = fReal[j];
            fReal[j] = fReal[i];
            fReal[i] = t1;
        }
        k = halfN;
        while (k <= j) {
            j = j - k;
            k = k / 2;
        }
        j += k;
    }

    // バタフライ演算
    for (i = 1; i <= stg; i++) {
        m = pow(2, i);
        h = m / 2;
        for (j = 0; j < h; j++) {
            float w1in = j * (N / m);
            float w2in = w1in + halfN;
                w1 = cos(w1in / (FFTN / (2 * PI)));
                w2 = sin(w2in / (FFTN / (2 * PI)));
            for (k = j; k < N; k += m) {
                kp = k + h;
                s1 = fReal[kp] * w1 - fImag[kp] * w2;
                s2 = fReal[kp] * w2 + fImag[kp] * w1;
                t1 = fReal[k] + s1;
                fReal[kp] = fReal[k] - s1;
                fReal[k] = t1;
                t2 = fImag[k] + s2;
                fImag[kp] = fImag[k] - s2;
                fImag[k] = t2;
            }
        }
    }

    // 結果を出力用配列に格納
    for (i = 0; i < N; i += STP) {
        oR[i] = fReal[i];
    }
}

// FFT計算は別コアで
void fft(void *param) {
  for(;;) {
    fftReady = false;
    // short -> float 変換
    for(int i = 0; i < FFTN; i++) {
      fftBuff[i] = (fftBuff[i] + 0.5) / 32767.5;
    }
    calcFFT(fftBuff , realSpec, imagSpec);
    fftReady = true;
    delay(25);
  }
}

WebServer server(80);
volatile bool prev = false;
volatile bool next = false;

void handleRoot() {
  char buf[1024];
  sprintf(buf, "%s", html);
  server.send(200, "text/html", buf);
}

void handlePrev() {
  char buf[1024];
  prev = true;
  next = false;
  sprintf(buf, html);
  server.send(200, "text/html", buf);
}

void handleNext() {
  char buf[1024];
  prev = false;
  next = true;
  sprintf(buf, html);
  server.send(200, "text/html", buf);
}

File gRoot;
int gFileCnt = 0;
#define PLAY_LIST_MAX (256)
char *playList[PLAY_LIST_MAX] = {0};

// プレイリストにファイルパスを格納
void getFileNames(char **playList, File dir) {
  for(;;) {
    File item = dir.openNextFile();
    if(!item || gFileCnt >= PLAY_LIST_MAX) {
      break;
    } 

    if(item.isDirectory()) {
      // 特定ディレクトリは中に入らないよう除外
      if(strcmp(item.name(), "/System Volume Information") != 0 &&
         strcmp(item.name(), "/hidden") != 0) {
        getFileNames(playList, item);
      }
    } else {
      playList[gFileCnt] = (char *)malloc(256);
      strcpy(playList[gFileCnt], item.name()); 
      gFileCnt++;
    }
  }
}

// プレイリスト作成
void makePlayList(char **playList) {
  File root = SD.open("/");
  getFileNames(playList, root);
  root.close();
}

void setup(void) {
  // FFTを別コアで実行するよう指定
  xTaskCreatePinnedToCore(fft, "fft", 4096 * 4, NULL, 1, NULL, 0);

  Serial.begin(115200);

  WiFi.begin("rs500m-14aaf8-1", "6a6a227fb3f17");
  while(WiFi.status() != WL_CONNECTED) {
    delay(250);
  }
  Serial.print("IP Addr. : ");
  Serial.println(WiFi.localIP());

  // リモコンのハンドラ登録
  server.on("/", handleRoot);
  server.on("/n", handleNext);
  server.on("/p", handlePrev);
  server.begin();

  // SDカード
  vSpi.end();
  vSpi.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS);
  vSpi.setFrequency(80000000);
  vSpi.setDataMode(SPI_MODE1);

  // LCD
  tft.begin(80000000);
  tft.setRotation(3);
  tft.fillRect(0, 0, 320, 240, ILI9341_BLACK);

  
  // (撮影用)ウェイト
  delay(5000);

  tft.setCursor(70, 30);
  tft.setTextSize(1);
  tft.setTextColor(ILI9341_RED);
  tft.println("Powered by");  
  tft.setCursor(100, 40);
  tft.setTextSize(3);
  tft.setTextColor(ILI9341_DARKGREY);
  tft.println("PCM5102A");  

  hSpi.end();
  hSpi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_SS);
  hSpi.setFrequency(15800000);
  hSpi.setDataMode(SPI_MODE1);

  if(!SD.begin(SD_SS, hSpi, 15800000)) {
    Serial.println("SD card mount failed!!");
  }

  makePlayList(playList);

  // I2S設定
  int i2sRet;
  const i2s_config_t config = {
    (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    44100,
    I2S_BITS_PER_SAMPLE_16BIT,
    I2S_CHANNEL_FMT_RIGHT_LEFT,
    (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
    ESP_INTR_FLAG_LEVEL1,
    64,
    256,
    true,
    false,
    12000000  // 12.0MHz外部オシレータ使用時の設定値    
  };

  i2sRet = i2s_driver_install(I2S_NUM_0, &config, 0, NULL);
  i2s_pin_config_t pinConfig = {
    16, // BCK
     2, // WS
    21, // DAT
    I2S_PIN_NO_CHANGE};
  i2sRet = i2s_set_pin(I2S_NUM_0, &pinConfig);

  // タイマー割り込み設定 
  timer = timerBegin(1, 4, true);
  timerAttachInterrupt(timer, sound_out, true);
  timerAlarmWrite(timer, 882 << 5, true);
  timerAlarmDisable(timer);

  Serial.println("Initialized");
}

float oldSpec[60] = {0};
float peaks[60] = {0};
unsigned char oldPeaks[60] = {0};

void loop() {
  // プレイリストのインデックス
  static int wavNum = 0;

  bool flg_skipHeader = true;
  bool flg_waitForBufferFilled = true;

  //LCD
  static int x = 0;
  static int y = 0;
  static bool first = true;
  static int segment = 0;

  // 描画範囲指定
  typedef struct {
    int from;
    int to;
  } T_RANGE;
  static T_RANGE ranges[4] = {{ 0,  8},
                              { 8, 16},
                              {16, 24},
                              {24, 32},
  };

  File file = SD.open(playList[wavNum], FILE_READ);

  if(!file) {
    Serial.println("FILE_READ error...");
  } else {
    // 再生データ長を取得
    int rest = file.size();
    cnt = 0;
    readBuffNum = 0;
    writeBuffNum = 0;
    memset(buffStatus, BUFF_EMPTY, sizeof(buffStatus));

    while(rest > 0) {
      if(flg_skipHeader) {
        // wavヘッダ部簡易読み込み
        unsigned char wavHeader[44];
        file.read(wavHeader, 44);
        rest -= 44;
        flg_skipHeader = false;

        // バッファ全面にデータを読み込んでおく
        do {
          file.read(sdBuff[writeBuffNum], rest < SD_BUFF_SIZE ? rest : SD_BUFF_SIZE);
          rest -= SD_BUFF_SIZE;
          ++writeBuffNum;
        } while(writeBuffNum < SD_BUFF_NUM);

        for(int i = 0; i < SD_BUFF_NUM; i++) {
          buffStatus[i] = BUFF_FULL;
        }
        readBuffNum = 0;
        writeBuffNum = 0;

        if(flg_waitForBufferFilled) {
          // 再生開始
          timerAlarmEnable(timer);
          flg_waitForBufferFilled = false;
        }
      }
      // 空のバッファにデータ読み込み
      if(buffStatus[writeBuffNum] == BUFF_EMPTY) {
        buffStatus[writeBuffNum] = BUFF_WRITING;
        file.read(sdBuff[writeBuffNum], rest < SD_BUFF_SIZE ? rest : SD_BUFF_SIZE);
        rest -= SD_BUFF_SIZE;
        buffStatus[writeBuffNum] = BUFF_FULL;

        // 描画
        if(first) {
          for(int i = 0; i < 60; i++) {
            peaks[i] = 240;
          }
          memset(oldFftBuff, 120, 60);
        }         
        // loop() 1サイクルで全部描画せず区画を分けて描画(速度問題対策)
        for(x = ranges[segment].from; x < ranges[segment].to; x++) {
          if(fftReady) {
            for(int i = 0; i < FFTN; i += 4) {
              fftBuff[i] = sdBuff[readBuffNum][cnt + i + 0] + sdBuff[readBuffNum][cnt + i + 1] * 256;
            }
          }

          int x8 = (x << 3) + 32;
          //tft.drawFastVLine(x8,  oldSpec[x],  -80, ILI9341_BLACK);
          tft.drawFastHLine(x8 - 1, oldPeaks[x], 2, ILI9341_BLACK);

          int spec;    
          // バーの長さ補正
          if(x < 2) {
            spec = realSpec[x + 1] /  1;
          } else if(x < 8) {
            spec = realSpec[x + 1] *  9;
          } else {
            spec = realSpec[x + 1] * 17;
          }
          // バーの長さをクリップ
          if(abs(spec) > 160) {
            spec = 0;
          }

          y = 240 - (spec * 1);

          // 頂点の押し出し
          if(peaks[x] > y) {
            peaks[x] = y;
          }

          // 痕跡消去
          tft.drawFastVLine(x8, y, 80 - y, ILI9341_BLACK);
          // 書き込み
          tft.drawFastVLine(x8, 240, 240 - y, ILI9341_BLUE);
          tft.drawFastHLine(x8 - 1, peaks[x],  2, ILI9341_WHITE);

          oldSpec[x] = y;
          oldPeaks[x] = peaks[x];
          if(peaks[x] < 240) {
            peaks[x] += 0.5;
          } else {
            peaks[x] = 240;
          }
          first = false;
        }
        ++segment %= 4;

        // デバッグ用表示
        if(writeBuffNum == 0) {
          tft.fillRect(0, 0, 60, 10, ILI9341_BLACK); 
          tft.setCursor(0,0);
          tft.setTextSize(0);
          tft.setTextColor(ILI9341_WHITE);
          tft.printf("%d", rest);
        } 
        tft.fillRect(0, 10, 30, 10, ILI9341_BLACK); 
        tft.setCursor(0,10);
        tft.setTextSize(0);
        tft.setTextColor(ILI9341_GREEN);
        tft.printf("%d:%d", readBuffNum, writeBuffNum);

        ++writeBuffNum %= SD_BUFF_NUM;
      } else {
        ;
      }
      // 選曲制御
      if(prev == true) {
        prev = false;
        --wavNum;
        if(wavNum == -1) {
          wavNum = gFileCnt - 1;
        }
        goto SKIP_FILE;
      }
      if(next == true) {
        next = false;
        ++wavNum %= gFileCnt;
        goto SKIP_FILE;
      }
      server.handleClient();
    }
    ++wavNum %= gFileCnt;
SKIP_FILE:
    // ダミー
    ;
  }
}
```

[今後の予定]
ESP32内蔵DAC版の試行
mp3対応