LoginSignup
3
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-01

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

128142224_2796301777326186_3281067876323078915_o.jpg
[リンク]再生した音声を録音したもの

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;
  }
}
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
[使用モジュール]
MicroSDカードモジュール
DACモジュール(PCM5102A)
TFT液晶モジュール(ILI9341)
水晶発振器12.000MHz(EXO-3)

[開発環境]
VisualStudioCode + PlatformIO

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対応

3
1
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
3
1