今回は、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="こんにちは"
ソースコード
追加部分に、「//★追加」と付記しておきました。
#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;
}
以上