MP3 Player Shield + ESP32-S2
2025.12.08

オーディオ用オペアンプの音質比較のために、MP3音源を元にした高品質なアナログ音源を取得、活用してみました。
SparkFun MP3 Player Shield に搭載されているVS1053は、デルタシグマ(ΔΣ)変調方式のD/Aコンバーター(DAC)を内蔵しています。
オーバーサンプリング: VS1053は、入力されたデジタルオーディオデータを128倍にオーバーサンプリング処理します。
デルタシグマ変調: オーバーサンプリングされたデータは、最終的に48kHz、18ビット相当のデータとなり、デルタシグマ変調によってアナログ信号に変換されます。
内蔵ボリューム: DAC変換された後、内蔵のボリューム回路を経て音声出力されます。
この方式により、原音が装飾されることなくオペアンプ比較に最適な音源として利用することができます。
ここでは、SparkFun MP3 Player Shield を ESP32-S2 で制御します。
本来であれば、WAV音源を使いたいのですが、ファイルサイズが大きく、44.1KHz,ステレオ16bit音源を用いたところ、データ転送が追いつきませんでした。
●SparkFun MP3 Player Shield

MP3 Player Shield Hookup Guide V15
SparkFun MP3プレーヤーシールドは、VS1053B MP3オーディオデコーダーICを使用してオーディオファイルをデコードします。
VS1053は、Ogg Vorbis/MP3/AAC/WMA/MIDIオーディオのデコード、IMA ADPCMおよびユーザーがロード可能なOgg Vorbisのエンコードも可能です。
VS1053は、シリアル入力バス(SPI)を介して入力ビットストリームを受信します。ストリームはICによってデコードされた後、オーディオは3.5mmステレオヘッドホンジャックと2ピン0.1インチピッチヘッダーの両方に出力されます。
SparkFun MP3 Player Shield は Arduino UNO などの拡張シールドとして使うのが一般的ですが、ESP32で制御することにより利便性が向上します。
楽曲ファイル名に漢字を含むロングネームを使用でき、シールドを使用する際にファイル名を変更する必要はありません。
また、WiFi機能を利用してリモート操作ができるのでファイルセレクター用スイッチを取り付ける手間が省けます。
●Unexpected Maker FeatherS2
FeatherS2 は、Unexpected Maker が提供しているAdafruit FeatherシリーズのESP32-S2ベース開発ボードです。
FeatherS2 は ESP32-S2チップを搭載し、SPI Flash 16MB、PSRAM 8MB を実装しています。
メインメモリにPSRAMがマップされているため、大きなメモリを割り当てると、自動的に外部PSRAMに割り当てられます。
これにより、画像処理などの多くのメモリー空間を必要とする処理を可能にしています。
Pinout
Specifications
・32-bit 240 MHz single-core processor
・16 MB SPI Flash
・8 MB extra PSRAM
・2.4 GHz Wi-Fi - 802.11b/g/n
・3D antenna
・2x 700 mA 3.3 V LDO regulator
・Optimised power path for low-power battery usage
・LiPo battery management
・Power (red), Charge (orange) & IO13 (blue) LEDs
・21x GPIO
・USB-C
・APA102 RGB LED (CLK IO45, DATA IO40)
・ALS-PT19 Ambient Light Sensor (IO14)
・QWIIC/STEMMA connector
●microSD Card Module

3.3V専用のmicroSDカードモジュールです。
MP3 Player Shield では、基板実装のmicroSDカードスロットとVS1053の制御にSPI通信を利用しています。
このmicroSDスロットをESP32で使おうとすると、ESP32用のSDライブラリ内でエラーが発生してしまいます。
そこでESP32-S2の2系統目のSPIを外付けmicroSDカードモジュールに割り当てます。
●ArduEZ ONE
MP3 Player Shield は、本来UNOに被せて使用するのですが、ここではESP32をシールドに被せて使用しています。

超軽量で薄型のArduino用アドオンブレッドボードです。
挿入穴が表裏で貫通していますので、どちらからでも部品配置/配線が可能です。
Arduinoの穴位置にスタッキングコネクタを挿入して使用します。

組み立てるとこんな感じになります
●配線
| microSD | | ESP32-S2 | | MP3 Shield |
| | | 17 | - | 2:MP3-DREQ |
| | | 18 | - | 6:MP3-CS |
| | | 12 | - | 7:MP3-DCS |
| | | 14 | - | 8:MP3-RST |
| | - | 35:SDO | - | 11: |
| | - | 37:SDI | - | 12: |
| | - | 36:SCK | - | 13: |
| 3V3 | - | 3V | - | +5V |
| CS | - | 11 | | |
| MOSI | - | 10 | | |
| CLK | - | 7 | | |
| MISO | - | 3 | | |
| GND | - | GND | - | GND |
●サンプルコード

sparkfun/MP3_Player_Shield
・firmware/MP3_PlayTrack/MP3_PlayTrack.ino
・firmware/MP3_RawPlay/MP3_RawPlay.ino
github内の上記2つのコードを参考にして、ESP32用に書き換えました
開発環境には、PlatformIOを使用しています。
Arduino開発環境構築 PlatformIO
$ pio init -b um_feathers2
$ vi platformio.ini
[env:um_feathers2]
platform = espressif32
board = um_feathers2
framework = arduino
build_flags =
-DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
-DCORE_DEBUG_LEVEL=4
upload_port = /dev/ttyACM0
●ESP32-S2用サンプルコード
#include <SPI.h>
#include <SD.h>
File dir;
File file;
char stack[2048];
char mp3_dirpath[127];
char trackName[64];
#define SPI_MISO 37
#define SPI_MOSI 35
#define SPI_SCK 36
SPIClass SPI2(HSPI);
#define SPI2_CS 11
#define SPI2_MOSI 10
#define SPI2_SCK 7
#define SPI2_MISO 3
#define SDSPEED 40000000
#define MP3_DREQ 17
#define MP3_XCS 18
#define MP3_XDCS 12
#define MP3_RESET 14
//VS10xx SCI Registers
#define SCI_MODE 0x00
#define SCI_STATUS 0x01
#define SCI_BASS 0x02
#define SCI_CLOCKF 0x03
#define SCI_DECODE_TIME 0x04
#define SCI_AUDATA 0x05
#define SCI_WRAM 0x06
#define SCI_WRAMADDR 0x07
#define SCI_HDAT0 0x08
#define SCI_HDAT1 0x09
#define SCI_AIADDR 0x0A
#define SCI_VOL 0x0B
#define SCI_AICTRL0 0x0C
#define SCI_AICTRL1 0x0D
#define SCI_AICTRL2 0x0E
#define SCI_AICTRL3 0x0F
void setup()
{
pinMode(MP3_DREQ, INPUT);
pinMode(MP3_XCS, OUTPUT);
pinMode(MP3_XDCS, OUTPUT);
pinMode(MP3_RESET, OUTPUT);
Serial0.begin(115200); //Use serial for debugging
Serial0.println("MP3 Testing");
//Setup SPI for VS1053
SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, -1);
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
//From page 12 of datasheet, max SCI reads are CLKI/7. Input clock is 12.288MHz.
//Internal clock multiplier is 1.0x after power up.
//Therefore, max SPI speed is 1.75MHz. We will use 1MHz to be safe.
SPI.setClockDivider(SPI_CLOCK_DIV16); //Set SPI bus speed to 1MHz (16MHz / 16 = 1MHz)
SPI.transfer(0xFF); //Throw a dummy byte at the bus
//Initialize VS1053 chip
digitalWrite(MP3_XCS, HIGH); //Deselect Control
digitalWrite(MP3_XDCS, HIGH); //Deselect Data
digitalWrite(MP3_RESET, LOW); //Put VS1053 into hardware reset
delay(10);
digitalWrite(MP3_RESET, HIGH); //Bring up VS1053
Mp3SetVolume(20, 20); //Set initial volume (20 = -10dB) LOUD
//Mp3SetVolume(40, 40); //Set initial volume (20 = -10dB) Manageable
//Mp3SetVolume(80, 80); //Set initial volume (20 = -10dB) More quiet
//Let's check the status of the VS1053
int MP3Mode = Mp3ReadRegister(SCI_MODE);
int MP3Status = Mp3ReadRegister(SCI_STATUS);
int MP3Clock = Mp3ReadRegister(SCI_CLOCKF);
Serial0.print("SCI_Mode (0x4800) = 0x");
Serial0.println(MP3Mode, HEX);
Serial0.print("SCI_Status (0x48) = 0x");
Serial0.println(MP3Status, HEX);
Serial0.print("SCI_ClockF = 0x");
Serial0.println(MP3Clock, HEX);
int vsVersion = (MP3Status >> 4) & 0x000F; //Mask out only the four version bits
Serial0.print("VS Version (VS1053 is 4) = ");
Serial0.println(vsVersion, DEC); //The 1053B should respond with 4. VS1001 = 0, VS1011 = 1, VS1002 = 2, VS1003 = 3
//Now that we have the VS1053 up and running, increase the internal clock multiplier and up our SPI rate
Mp3WriteRegister(SCI_CLOCKF, 0x60, 0x00); //Set multiplier to 3.0x
//From page 12 of datasheet, max SCI reads are CLKI/7. Input clock is 12.288MHz.
//Internal clock multiplier is now 3x.
//Therefore, max SPI speed is 5MHz. 4MHz will be safe.
//SPI.setClockDivider(SPI_CLOCK_DIV4); //Set SPI bus speed to 4MHz (16MHz / 4 = 4MHz)
SPI.setFrequency(4000000);
SPI2.begin(SPI2_SCK, SPI2_MISO, SPI2_MOSI, -1);
pinMode(SPI2_CS, OUTPUT);
if (!SD.begin(SPI2_CS, SPI2, SDSPEED)) {
Serial0.printf("initialization failed!\n");
while(1);
}
Serial0.printf("initialization done.\n");
playMP3("/sound/12_ARPEGGIO.mp3");
}
void loop() {}
void playMP3(char* fileName)
{
uint8_t mp3DataBuffer[32]; //Buffer of 32 bytes. VS1053 can take 32 bytes at a go.
Serial0.printf("trackName = %s\n",fileName);
if (!SD.begin(SPI2_CS, SPI2, SDSPEED)) {
Serial0.printf("initialization failed!\n");
while(1);
}
if (!(file = SD.open(fileName))) {
Serial0.printf("%s failed to open!\n", fileName);
return;
}
Serial0.println("Track open");
Serial0.println("Start MP3 decoding");
while(1) {
while(!digitalRead(MP3_DREQ));
int readBytes = file.read(mp3DataBuffer, sizeof(mp3DataBuffer));
if (readBytes<=0) break;
digitalWrite(MP3_XDCS, LOW); //Select Data
for(int y = 0 ; y < readBytes; y++) {
while(!digitalRead(MP3_DREQ)); //If we ever see DREQ low, then we wait here
SPI.transfer(mp3DataBuffer[y]); // Send SPI byte
}
digitalWrite(MP3_XDCS, HIGH); //Deselect Data
}
file.close(); //Close out this track
//End of file - send 2048 zeros before next file
digitalWrite(MP3_XDCS, LOW); //Select Data
for (int i = 0 ; i < 2048 ; i++) {
while(!digitalRead(MP3_DREQ)); //If we ever see DREQ low, then we wait here
SPI.transfer(0);
}
while(!digitalRead(MP3_DREQ)) ; //Wait for DREQ to go high indicating transfer is complete
digitalWrite(MP3_XDCS, HIGH); //Deselect Data
Serial0.printf("Track %s done!\n", fileName);
}
//Write to VS10xx register
//SCI: Data transfers are always 16bit. When a new SCI operation comes in
//DREQ goes low. We then have to wait for DREQ to go high again.
//XCS should be low for the full duration of operation.
void Mp3WriteRegister(unsigned char addressbyte, unsigned char highbyte, unsigned char lowbyte){
while(!digitalRead(MP3_DREQ)) ; //Wait for DREQ to go high indicating IC is available
digitalWrite(MP3_XCS, LOW); //Select control
//SCI consists of instruction byte, address byte, and 16-bit data word.
SPI.transfer(0x02); //Write instruction
SPI.transfer(addressbyte);
SPI.transfer(highbyte);
SPI.transfer(lowbyte);
while(!digitalRead(MP3_DREQ)) ; //Wait for DREQ to go high indicating command is complete
digitalWrite(MP3_XCS, HIGH); //Deselect Control
}
//Read the 16-bit value of a VS10xx register
unsigned int Mp3ReadRegister (unsigned char addressbyte){
while(!digitalRead(MP3_DREQ)) ; //Wait for DREQ to go high indicating IC is available
digitalWrite(MP3_XCS, LOW); //Select control
//SCI consists of instruction byte, address byte, and 16-bit data word.
SPI.transfer(0x03); //Read instruction
SPI.transfer(addressbyte);
char response1 = SPI.transfer(0xFF); //Read the first byte
while(!digitalRead(MP3_DREQ)) ; //Wait for DREQ to go high indicating command is complete
char response2 = SPI.transfer(0xFF); //Read the second byte
while(!digitalRead(MP3_DREQ)) ; //Wait for DREQ to go high indicating command is complete
digitalWrite(MP3_XCS, HIGH); //Deselect Control
int resultvalue = response1 << 8;
resultvalue |= response2;
return resultvalue;
}
//Set VS10xx Volume Register
void Mp3SetVolume(unsigned char leftchannel, unsigned char rightchannel){
Mp3WriteRegister(SCI_VOL, leftchannel, rightchannel);
}
プログラムの書込みに失敗する場合は、FeatherS2をRaspberry Pi にUSB接続後に、[BOOT]を押しながら[RESET]をクリックして、デバイスをダウンロードモードにします
続いてオーディオアンプを用意します。
●NJM4580DD使用ヘッドホンアンプキット

2回路入りのオペアンプ、NJM4580DDを使ったヘッドホンアンプキットです、
相互接続性を考慮し入出力および電源のGNDが共通になっております。
オペアンプの入門用、学習用に最適です。

このキットはオペアンプをピン互換品に交換することができます

対応確認されているオペアンプ一覧です

オペアンプ・キットのXH電源コネクターに繋げる、ON/OFFスイッチ付き電源ケーブルを作りました。
●オーディオテクニカ ATH-M20x プロフェッショナル モニターヘッドホン

モニターヘッドホンとは、音楽制作や録音などの現場で、原音に忠実な音を正確にモニタリング(確認)するために使用されるヘッドホンです。
通常のリスニング用ヘッドホンとは異なり、音に色付けをせず、低音から高音までフラットな周波数特性と高い解像度を持つ点が大きな特徴です。
このため、音のバランスや細かい音の違いを正確に聴き取ることができます。

最初にヘッドホンアンプキット標準添付のNJM4580DDで鳴らせて、続いてTI(テキサスインスツルメンツ、旧バーブラウン)のOPA2134に差し替えて聴いてみました。
私の耳でも明らかに違いがわかります。敢えて、NJM4580DDに差し戻して使おうとは思えません。
●応用例
ESP32はWiFi機能を実装しているので、リモートで操作してみました。
TeraTermを起動し、Telnet経由でメニュー制御できるようにしました。

ジャンルを選択します。ファイル名はソートして表示しています。

アーティストを選択します

アルバムを選択します

曲を選択します

再生が始まります

'p'を押すと曲を一時停止し、再度'p'を押すと再生を再開します
'q'を押すと曲を停止しメニューに戻ります
●FeatherS2補足
補足:PSRAM(擬似SRAM)
FeatherS2 は、PSRAMを搭載していますが、PSRAM用のメモリー関数 ps_malloc()やps_calloc()を使用すると、コアを吐いてしまいます。
CORRUPT HEAP: Bad tail at 0x3ffde428. Expected 0xbaad5678 got 0x20736563
ESP32-S2ブランチを使用する場合、メインメモリにPSRAMがマップされているため、大きなメモリの割り当ては自動的に外部PSRAMに割り当てられます。
したがって、メモリー確保は通常のメモリー関数 malloc(), calloc()を使用します。
Ref.How to use PSRAM with tasks #4210
補足:Power Management
USB電源が供給されると、自動的にUSB電源に切り替わり、バッテリー(接続されている場合)の充電も開始されます。これは「ホットスワップ」方式で行われるため、LiPolyを常に接続しておき、「バックアップ」電源として使用できます。LiPolyはUSB電源が失われた場合にのみ使用されます。また、USBジャックの横にはCHG LEDがあり、バッテリーの充電中に点灯します。バッテリーが接続されていない場合、このLEDが点滅することがあります。
補足:Measuring Battery
LiPolyバッテリーは4.2Vで最大電圧に達し、バッテリー駆動時間の大部分は3.7V前後で動作しますが、その後、保護回路が作動して電圧が遮断される直前までゆっくりと3.2V程度まで低下します。
BAT ━ 200KΩ ┳ 200KΩ
┃
VBATPIN(GPIO10)
検出可能な最大電圧が3.3Vなので分圧して1/2にします
#define LED_PIN 13
#define VBATPIN 10
void setup()
{
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
}
void loop()
{
float measuredvbat = analogReadMilliVolts(VBATPIN);
measuredvbat *= 2; // we divided by 2, so multiply back
measuredvbat /= 1000; // convert to volts!
//Serial.print("VBat: " ); Serial.println(measuredvbat);
if (measuredvbat < 4.16) {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
} else {
digitalWrite(LED_PIN, HIGH);
}
}
補足:APA102 2020のライブラリ
APA102 2020のライブラリーをビルドして実行しようとすると、下記のようなメッセージが表示され、ブート・ループしてしまいました。
esptool.py でフラッシュを消去しても回復できませんでした。Circuit Pythonをインストールして初期状態に戻し、LED関連とは別のArduinoのソースコードをビルド、アップロードすることで、正常に動作しました。
ESP-ROM:esp32s2-rc4-20191025
Build:Oct 25 2019
rst:0x7 (TG0WDT_SYS_RST),boot:0x8 (SPI_FAST_FLASH_BOOT)
Saved PC:0x40010d75
SPIWP:0xee
mode:QIO, clock div:1
load:0x3ffe8100,len:0x4
ets_loader.c 65
Circuit Python
FeatherS2をラズベリーパイに接続するだけで、ファイルシステムにUSBフラッシュドライブとして認識されます。
$ sudo fdisk -l
Disk /dev/sda: 11.7 MiB, 12255744 bytes, 23937 sectors
Disk model: FeatherS2
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000
Device Boot Start End Sectors Size Id Type
/dev/sda1 1 23936 23936 11.7M 1 FAT12
ドライブの中身をみてみます。
$ sudo mount /dev/sda1 /mnt/target
$ cd /mnt/target
$ ls -l
-rw-r--r-- 1 pi pi 0 12月 4 2016 .Trashes
drwxr-xr-x 2 pi pi 2048 12月 4 2016 .fseventsd
-rw-r--r-- 1 pi pi 0 12月 4 2016 .metadata_never_index
-rw-r--r-- 1 pi pi 6174 6月 28 18:43 adafruit_dotstar.py
-rw-r--r-- 1 pi pi 68 12月 4 2016 boot_out.txt
-rw-r--r-- 1 pi pi 1430 6月 28 18:43 code.py
-rw-r--r-- 1 pi pi 1242 6月 28 18:43 feathers2.py
drwxr-xr-x 2 pi pi 2048 12月 4 2016 lib
-rw-r--r-- 1 pi pi 373 6月 28 15:14 test_results.txt
$ cat boot_out.txt
Adafruit CircuitPython 6.3.0 on 2021-06-01; FeatherS2 with ESP32S2
FeatherS2には、初期状態において、UF2ブートローダーとESP32-S2をサポートする CircuitPython 6.3.0 がバンドルされています。
libディレクトリは空です。
補足:Circuit Python の環境を復元する
Circuit Python の環境に戻す場合は、まずパッケージを取得します。

→ askpatrickw/ESP32-S2-CircuitPython-Getting-Started.md
パッケージ一覧の中から、CircuitPython 6.3.0をダウンロードします。
2021-06-01T22:48:44.000Z 1.3MB adafruit-circuitpython-unexpectedmaker_feathers2-en_GB-6.3.0.bin
~/.platformio/packages/tool-esptoolpy/esptool.py と同じディレクトリに保存します。
esptool.py に関しては、Unexpected Maker TinyS2 を参考にしてアップデートしてください。
パッケージをFlashに焼き付けます。
$ cd ~/.platformio/packages/tool-esptoolpy
$ python3 esptool.py --chip esp32s2 -p /dev/ttyAMA0 --no-stub -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode qio --flash_freq 40m --flash_size 16MB 0x0000 adafruit-circuitpython-unexpectedmaker_feathers2-en_GB-6.3.0.bin
リセットボタンを押して、再起動します。
●参考
Unexpected Maker TinyS2
|
Raspberry Pi(ラズベリー パイ)は、ARMプロセッサを搭載したシングルボードコンピュータ。イギリスのラズベリーパイ財団によって開発されている。
たいていのことは100日あれば、うまくいく。長田英知著
「時間がなくて、なかなか自分のやりたいことができない」
「一念発起して何かを始めても、いつも三日坊主で終わってしまう」
「色んなことを先延ばしにしたまま、時間だけが過ぎていく」
そこで本書では、そんな著者が独自に開発した、
まったく新しい目標達成メソッド「100日デザイン」について、
その知識と技術を、余すところなくご紹介します。
まんがで納得ナポレオン・ヒル 思考は現実化する
OLとして雑務をこなす日々に飽き足らず、科学者だった父が残した薬品を商品化すべく、起業を決意した内山麻由(27)。彼女はセミナーで知り合った謎の女性からサポートを得ながら、彼女と二人三脚でナポレオン・ヒルの成功哲学を実践し、さまざまな問題を乗り越えていく。
ヒル博士の<ゴールデンルール>に従い、仕事に、恋に全力疾走する彼女の、成功への物語。
今日は人生最悪で最高の日 1秒で世界を変えるたったひとつの方法 ひすいこたろう著
偉人の伝記を読むと、最悪な日は、不幸な日ではなく、新しい自分が始まる日であることがわかります。最悪な出来事は、自分の人生が、想像を超えて面白くなる兆しなのです。偉人伝を読むことで、このときの不幸があったおかげで、未来にこういう幸せがくるのかと、人生を俯瞰する視線が立ち上がるのです。
ご飯は私を裏切らない heisoku著
辛い現実から目を背けて食べるご飯は、いつも美味しく幸せを届けてくれる。
29歳、中卒、恋人いない歴イコール年齢。バイト以外の職歴もなく、短期バイトを転々とする日々。ぐるぐると思索に耽るけど、ご飯を食べると幸せになれる。奇才の新鋭・heisokuが贈るリアル労働グルメ物語!
【最新版Gemini 3に対応!】できるGemini (できるシリーズ)
Geminiを「最強の知的生産パートナー」として使いこなすための、実践的なノウハウを凝縮した一冊です。
基本的な操作方法から、具体的なビジネスシーンでの活用、日々の業務を自動化するGoogle Workspaceとの連携、さらには自分だけのオリジナルAIを作成する方法まで余すところなく解説します。
Rustプログラミング完全ガイド 他言語との比較で違いが分かる!
Rustの各手法や考え方を幅広く解説!
500以上のサンプルを掲載。実行結果も確認。
全24章の包括的なチュートリアル。
ポチらせる文章術
販売サイト・ネット広告・メルマガ・ブログ・ホームページ・SNS…
全WEB媒体で効果バツグン!
カリスマコピーライターが教える「見てもらう」「買ってもらう」「共感してもらう」すべてに効くネット文章術
小型で便利な Type-C アダプター USB C オス - USB3.1 オスアダプター
Type-C端子のマイコンボードをこのアダプタを介して直接Raspberry Piに挿すことができます。ケーブルなしで便利なツールです。
Divoom Ditoo Pro ワイヤレススピーカー
15W高音質重低音/青軸キーボード/Bluetooth5.3/ピクセルアート 専用アプリ/USB接続/microSDカード
電源供給USBケーブル スリム 【5本セット】
USB電源ケーブル 5V DC電源供給ケーブル スリム 【5本セット】 電源供給 バッテリー 修理 自作 DIY 電子工作 (100cm)
|