昔、レコードからの出力をパソコンに取り込んで、多くのWAVファイルを作成しました。これまでは市販の音楽プレーヤーに入れて聴いてました。今回、ESP32でダラダラ聴き流せるようになりました(MP3のときのようにバグることも少ない)。それも音質を落とさず、録音時のままで❕さらに、SD内でのルート/サブディレクトリを問いません。さらにさらに、漢字・かなの長いファイル名にも対応しています(私の環境では)。
チャンネル 2(ステレオ)
サンプルレート 44100Hz
サンプル当たりのビット数 16
ビットレート 44100Hz × 16bit × 2 ÷ 1000 = 1411.2kbps
元ネタはここです。
上記サイトの SD WAV CODE を下敷きに工夫しました。環境や構成は以下。
1.Windows 11 Pro
2.Arduino IDE 2.3.6
3.ESP32 Dev Module(Arduino IDE 内でのボード名)
4.SPI接続 microSDカードスロットモジュール
5.32GBまでの適当なmicroSDカード
6.PCM5102A使用 32bit/384kHz DAC モジュール
https://store.shopping.yahoo.co.jp/nfj/h135.html
7.ボードマネージャは
esp32 by Espressif Systems version 3.0.1
にバージョンダウン
8.ツール
> Partition Scheme:"Custom"
9.ステレオミニジャックに外部アクティブスピーカーを接続。イヤホンを繋ぐ場合は音量にご注意。
ピンの接続です。
ESP32 | PCM5102A | microSDスロット |
---|---|---|
25 | DATA | |
26 | LRCK | |
27 | BICK | |
5V | VCC | |
5 | SC(CS) | |
23 | MOSI | |
19 | MISO | |
18 | SCK(CLK) | |
3V3 | 3V3 | |
GND | GND | GND |
Arduino IDE でのツール
の設定は次のようにしました。
//------------------------------------------------------------------------------------------------------------------------
//
// Title: SD Card Wav Player
//
// Description:
// Simple example to demonstrate the fundementals of playing WAV files (digitised sound) from an SD Card via the I2S
// interface of the ESP32. Plays WAV file from SD card. To keep this simple the WAV must be stereo and 16bit samples.
// The Samples Per second can be anything. On the SD Card the wav file must be in root and called wavfile.wav
// Libraries are available to play WAV's on ESP32, this code does not use these so that we can see what is happening.
// This is part 3 in a tutorial series on using I2S on ESP32. See the accompanying web page (which will also include
// a tutorial video).
//
// Boring copyright/usage information:
// (c) XTronical, www.xtronical.com
// Use as you wish for personal or monatary gain, or to rule the world (if that sort of thing spins your bottle)
// However you use it, no warrenty is provided etc. etc. It is not listed as fit for any purpose you perceive
// It may damage your house, steal your lover, drink your beers and more.
//
// http://www.xtronical.com/i2s-ep3
//
//------------------------------------------------------------------------------------------------------------------------
// Includes
#include "SD.h" // SD Card library, usually part of the standard install
#include "driver/i2s.h" // Library of I2S routines, comes with ESP32 standard install
#include <string.h>
#include <vector>
// SD Card
#define SD_CS 5 // SD Card chip select
#define SPI_MOSI 23
#define SPI_MISO 19
#define SPI_SCK 18
// I2S
#define I2S_DOUT 25 // i2S Data out oin
#define I2S_BCLK 27 // Bit clock
#define I2S_LRC 26 // Left/Right clock, also known as Frame clock or word select
#define I2S_NUM 0 // i2s port number
// Wav File reading
#define NUM_BYTES_TO_READ_FROM_FILE 1024 * 4 // How many bytes to read from wav file at a time
//------------------------------------------------------------------------------------------------------------------------
// structures and also variables
// I2S configuration
static const i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 44100, // Note, this will be changed later
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // high interrupt priority
.dma_buf_count = 8, // 8 buffers
.dma_buf_len = 64, // 64 bytes per buffer, so 8K of buffer space
.use_apll = 0,
.tx_desc_auto_clear = true,
.fixed_mclk = -1
};
// These are the physical wiring connections to our I2S decoder board/chip from the esp32, there are other connections
// required for the chips mentioned at the top (but not to the ESP32), please visit the page mentioned at the top for
// further information regarding these other connections.
static const i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCLK, // The bit clock connectiom, goes to pin 27 of ESP32
.ws_io_num = I2S_LRC, // Word select, also known as word select or left right clock
.data_out_num = I2S_DOUT, // Data out from the ESP32, connect to DIN on 38357A
.data_in_num = I2S_PIN_NO_CHANGE // (-1)we are not interested in I2S data into the ESP32
};
struct WavHeader_Struct {
// RIFF Section
char RIFFSectionID[4]; // Letters "RIFF"
uint32_t Size; // Size of entire file less 8
char RiffFormat[4]; // Letters "WAVE"
// Format Section
char FormatSectionID[4]; // letters "fmt"
uint32_t FormatSize; // Size of format section less 8
uint16_t FormatID; // 1=uncompressed PCM
uint16_t NumChannels; // 1=mono,2=stereo
uint32_t SampleRate; // 44100, 16000, 8000 etc.
uint32_t ByteRate; // =SampleRate * Channels * (BitsPerSample/8)
uint16_t BlockAlign; // =Channels * (BitsPerSample/8)
uint16_t BitsPerSample; // 8,16,24 or 32
// Data Section
char DataSectionID[4]; // The letters "data"
uint32_t DataSize; // Size of the data that follows
} WavHeader;
//------------------------------------------------------------------------------------------------------------------------
// Global Variables/objects
static const i2s_port_t i2s_num = I2S_NUM_0; // i2s port number
// The file we're currently playing
File f, WavFile; // Object for root of SD card directory
std::vector<String> mfList;
static uint16_t _BytesRead = 0;
long nmax = 0;
// Recursively scan the card and make a list of all MP3 files
// in all dirs
void scanDirectory(const char* dirname) {
File root = SD.open(dirname);
while (true) {
f = root.openNextFile();
if (!f) break;
String n = f.name();
n.toLowerCase();
String path = dirname;
path += f.name();
if (f.isDirectory()) {
if (f.name()[0] == '.') { // Ignore . and ..
continue;
}
String sub = dirname;
sub += f.name();
sub += "/";
scanDirectory(sub.c_str());
} else if (strstr(n.c_str(), ".wav")) {
mfList.push_back(path);
Serial.printf("WAV: %s\n\r", path.c_str());
} else {
//Serial.printf("SKP: %s\n\r", path.c_str());
}
f.close();
}
root.close();
}
//------------------------------------------------------------------------------------------------------------------------
void setup() {
Serial.begin(115200); // Used for info/debug
SD.begin(SD_CS, SPI, 24000000, "/sd"); //24000000
scanDirectory("/");
nmax = mfList.size();
Serial.println("Music List ----------");
for (int i = 0; i < nmax; i++) Serial.println(mfList[i].c_str());
Serial.println("---------------------");
i2s_driver_install(i2s_num, &i2s_config, 0, NULL);
i2s_set_pin(i2s_num, &pin_config);
}
long n = -1;
void loop() {
if (PlayWav() > 0)
; // Have to keep calling this to keep the wav file playing
else {
// get the wav file from the SD card
n = random(nmax); // shuffle play <- n++;
WavFile = SD.open(mfList[n].c_str()); // Open the wav file
if (WavFile == false)
Serial.println("Could not open 'wavfile(*.wav)'");
else {
Serial.println("--------------------------------------");
Serial.println(mfList[n].c_str());
WavFile.read((byte*)&WavHeader, 44); // Read in the WAV header, which is first 44 bytes of the file.
// We have to typecast to bytes for the "read" function
DumpWAVHeader(&WavHeader); // Dump the header data to serial, optional!
if (ValidWavData(&WavHeader)) // optional if your sure the WAV file will be valid.
i2s_set_sample_rates(i2s_num, WavHeader.SampleRate); //set sample rate
}; // Your normal code to do your task can go here
}
}
uint16_t PlayWav() {
static bool ReadingFile = true; // True if reading file from SD. false if filling I2S buffer
static byte Samples[NUM_BYTES_TO_READ_FROM_FILE]; // Memory allocated to store the data read in from the wav file
// Num bytes actually read from the wav file which will either be
// NUM_BYTES_TO_READ_FROM_FILE or less than this if we are very
// near the end of the file. i.e. we can't read beyond the file.
if (ReadingFile) // Read next chunk of data in from file if needed
{
_BytesRead = ReadFile(Samples); // Read data into our memory buffer, return num bytes read in
if (_BytesRead == 0) return 0;
ReadingFile = false; // Switch to sending the buffer to the I2S
} else
ReadingFile = FillI2SBuffer(Samples, _BytesRead); // We keep calling this routine until it returns true, at which point
// this will swap us back to Reading the next block of data from the file.
// Reading true means it has managed to push all the data to the I2S
// Handler, false means there still more to do and you should call this
// routine again and again until it returns true.
return _BytesRead;
}
uint16_t ReadFile(byte* Samples) {
static uint32_t BytesReadSoFar = 0; // Number of bytes read from file so far
uint16_t BytesToRead; // Number of bytes to read from the file
if (BytesReadSoFar + NUM_BYTES_TO_READ_FROM_FILE > WavHeader.DataSize) // If next read will go past the end then adjust the
BytesToRead = WavHeader.DataSize - BytesReadSoFar; // amount to read to whatever is remaining to read
else
BytesToRead = NUM_BYTES_TO_READ_FROM_FILE; // Default to max to read
WavFile.read(Samples, BytesToRead); // Read in the bytes from the file
BytesReadSoFar += BytesToRead; // Update the total bytes red in so far
if (BytesReadSoFar >= WavHeader.DataSize) // Have we read in all the data?
{
// WavFile.seek(44); // Reset to start of wav data.(to Endless Loop)
BytesReadSoFar = 0; // Clear to no bytes read in so far
BytesToRead = 0;
}
return BytesToRead; // return the number of bytes read into buffer
}
bool FillI2SBuffer(byte* Samples, uint16_t BytesInBuffer) {
// Writes bytes to buffer, returns true if all bytes sent else false, keeps track itself of how many left
// to write, so just keep calling this routine until returns true to know they've all been written, then
// you can re-fill the buffer
size_t BytesWritten; // Returned by the I2S write routine,
static uint16_t BufferIdx = 0; // Current pos of buffer to output next
uint8_t* DataPtr; // Point to next data to send to I2S
uint16_t BytesToSend; // Number of bytes to send to I2S
// To make the code eaier to understand I'm using to variables to some calculations, normally I'd write this calcs
// directly into the line of code where they belong, but this make it easier to understand what's happening
DataPtr = Samples + BufferIdx; // Set address to next byte in buffer to send out
BytesToSend = BytesInBuffer - BufferIdx; // This is amount to send (total less what we've already sent)
i2s_write(i2s_num, DataPtr, BytesToSend, &BytesWritten, 1); // Send the bytes, wait 1 RTOS tick to complete
BufferIdx += BytesWritten; // increasue by number of bytes actually written
if (BufferIdx >= BytesInBuffer) {
// sent out all bytes in buffer, reset and return true to indicate this
BufferIdx = 0;
return true;
} else
return false; // Still more data to send to I2S so return false to indicate this
}
bool ValidWavData(WavHeader_Struct* Wav) {
if (memcmp(Wav->RIFFSectionID, "RIFF", 4) != 0) {
Serial.print("Invalid data - Not RIFF format");
return false;
}
if (memcmp(Wav->RiffFormat, "WAVE", 4) != 0) {
Serial.print("Invalid data - Not Wave file");
return false;
}
if (memcmp(Wav->FormatSectionID, "fmt", 3) != 0) {
Serial.print("Invalid data - No format section found");
return false;
}
if (memcmp(Wav->DataSectionID, "data", 4) != 0) {
Serial.print("Invalid data - data section not found");
return false;
}
if (Wav->FormatID != 1) {
Serial.print("Invalid data - format Id must be 1");
return false;
}
if (Wav->FormatSize != 16) {
Serial.print("Invalid data - format section size must be 16.");
return false;
}
if ((Wav->NumChannels != 1) & (Wav->NumChannels != 2)) {
Serial.print("Invalid data - only mono or stereo permitted.");
return false;
}
if (Wav->SampleRate > 48000) {
Serial.print("Invalid data - Sample rate cannot be greater than 48000");
return false;
}
if ((Wav->BitsPerSample != 8) & (Wav->BitsPerSample != 16)) {
Serial.print("Invalid data - Only 8 or 16 bits per sample permitted.");
return false;
}
return true;
}
void DumpWAVHeader(WavHeader_Struct* Wav) {
if (memcmp(Wav->RIFFSectionID, "RIFF", 4) != 0) {
Serial.print("Not a RIFF format file - ");
PrintData(Wav->RIFFSectionID, 4);
return;
}
if (memcmp(Wav->RiffFormat, "WAVE", 4) != 0) {
Serial.print("Not a WAVE file - ");
PrintData(Wav->RiffFormat, 4);
return;
}
if (memcmp(Wav->FormatSectionID, "fmt", 3) != 0) {
Serial.print("fmt ID not present - ");
PrintData(Wav->FormatSectionID, 3);
return;
}
if (memcmp(Wav->DataSectionID, "data", 4) != 0) {
Serial.print("data ID not present - ");
PrintData(Wav->DataSectionID, 4);
return;
}
// All looks good, dump the data
Serial.print("Total size :");
Serial.println(Wav->Size);
Serial.print("Format section size :");
Serial.println(Wav->FormatSize);
Serial.print("Wave format :");
Serial.println(Wav->FormatID);
Serial.print("Channels :");
Serial.println(Wav->NumChannels);
Serial.print("Sample Rate :");
Serial.println(Wav->SampleRate);
Serial.print("Byte Rate :");
Serial.println(Wav->ByteRate);
Serial.print("Block Align :");
Serial.println(Wav->BlockAlign);
Serial.print("Bits Per Sample :");
Serial.println(Wav->BitsPerSample);
Serial.print("Data Size :");
Serial.println(Wav->DataSize);
}
void PrintData(const char* Data, uint8_t NumBytes) {
for (uint8_t i = 0; i < NumBytes; i++)
Serial.print(Data[i]);
Serial.println();
}
最後まで見ていただきありがとうございました。