M5AtomからI2Sでステレオ音を出力する(UDA1334A)
TL;DR
- M5AtomはI2Sにより音を出力できるものの、公式のキットに搭載されているNS4168はモノラルアンプなのでステレオ音源に対応できません
- ステレオ対応DACであるUDA1334Aのモジュールを使うと、M5Atomからステレオ出力できることを確認しました
- 動作確認用のコードを、GitHubに公開しています
M5Atomと音声出力
M5Atomは、M5シリーズの中でも最小のサイズと、サイズにしては多めのGPIOピンを特徴とする開発モジュールです。
スイッチサイエンスさんでatomを検索すれば分かるとおり、挿すだけで使える拡張モジュールがたくさん発売されています。
M5Atom向けには、音を扱えるモジュールとしてATOM EchoとATOM SPKが出ています。
M5Atom標準のNS4168はモノラルのみ
これらモジュールはNS4168というアンプチップを搭載しています。I2S(Inter-IC Sound)インタフェースを通してM5Atomから音信号を送り込み、音楽を流したり、合成音声を喋らせたりできるというわけです。
I2Sは、3本の信号線によって左チャンネル・右チャンネルのステレオ出力ができる規格です。
ところが、NS4168はモノラル出力しかできません。データシートの回路図を見れば明らかなように、そもそも出力ピンがスピーカ1個分しかありません。
ステレオ信号が入力されても、左か右のどちらかしか再生できないのです。
NS4168データシートより
M5Atomでもステレオ出力したい!
ATOM EchoやATOM SPKの想定用途は自作スマートスピーカーやオーディオプレイヤーのようです。スピーカーから喋らせたりちょっと音楽を鳴らしたりするだけならモノラルでもよいかもしれません。ただ、I2Sはせっかくステレオに対応できるのですから、やはりステレオで音楽を聴いてみたいものです。
もう1つの理由は限られた人にしか当てはまりませんが、振動触覚アクチュエータ(振動子)を使う場合にもステレオ出力ができると便利です。この秋、触感デバイス開発モジュール“hapStak“が発売され、M5AtomからI2Sで振動を制御できるようになりました。
hapStak
このモジュールもNS4168を使っているので、1個のM5Atomに対して1個の振動子しか使えません。もしM5Atomからステレオ出力ができれば、1個のAtomで2つの振動子に別々の振動を送る、スピーカから音を出しながら振動子も動かすなど、使い方が広がります。
検証環境
ハードウェア
- M5Atom Matrix
- UDA1334A搭載 I2S ステレオDACモジュール
- ジャンパケーブル5本、ブレッドボード
- 有線のイヤホンかヘッドホン
ソフトウェア
-
Visual Studio Code & PlatformIO
- Arduino IDEでも本記事と同様の操作は可能です。適宜読み替えてください
- ただ、M5StackはArduino IDEを使うと書き込みがとても遅く、平気で数分かかります。PlatformIOを導入して慣れる手間を惜しまないほうが、生産性が上がって幸せになれると考えます
UDA1334Aモジュールを使ってみる
「I2S ステレオ」でググると真っ先に出てくるのが、AdafruitのUDA1334A搭載 I2S ステレオDACモジュールです。3.5mmステレオミニジャックと音声出力ピンを備え、I2S以外のフォーマットにも対応するそうです(今回は普通にI2Sで使います)。
ピン配置についてはAdafruitのドキュメントが分かりやすいのでご覧ください。たくさんピンが付いてますが、I2Sで使う分には電源に2本・I2Sに3本の5本だけ繋げば動きます。
左からVIN(電源) / GND(グラウンド) / WSEL(左右ch選択用) / DIN(音声信号用) / BCLK(ビットクロック)
I2Sのピン名称にはいくつかの表記があるので、役割を覚えておくのがいいです。例えば、WSELピンは左右の信号のどちらを送るか選択する用途ですが、英語でWord Selectとも、Left Right Clockとも呼ばれます。
M5Atom側の表記 | モジュール側の表記 |
---|---|
LRCK | WSEL |
Data | DIN |
BCLK | BCLK |
接続した様子(GPIOは22, 19, 33を使用)
罠になりやすいポイント
M5AtomとUDA1334Aの組み合わせは、基本的には「繋いでサンプルを書き込めば動く」です。
ただ、ミスしやすいポイントを踏むと音が出なくて悩むことになります。
(この後2回ほどハマりポイントを踏んだ。慢心には気をつけよう!)
ATOM EchoとATOM SPKで、I2Sに使うピン番号が異なる
ATOM EchoとATOM SPKは、共にNS4168を使ったモジュールです。しかし、使っているGPIOピンが異なるため互換性がありません。
- | BCLK | LRCK | Data |
---|---|---|---|
ECHO | G19 | G33 | G22 |
SPK | G22 | G21 | G25 |
今回拡張モジュールそのものは使わないものの、使うサンプルコードの想定と異なるピン番号にケーブルを挿していると、無音になったり謎の雑音が再生されたりとうまく動きません。使うサンプルコードのピン番号指定を必ず確認し、コードを書き換えるか、ケーブル位置を変えるかのどちらかで対応しましょう。
ケーブルのちょっとした挿し具合でノイズが入ったりする
これは筆者が用いたジャンパケーブルの品質の問題かもしれませんが、ブレッドボードに挿したケーブルの角度が少し変わるだけで、音が消えてサーというノイズだけになる・無音になるなどの現象が起きました。
サンプルで音が出なくても、指でケーブルの根本を掴んだら正常な音になることもあります。同様の現象が起きた場合はお試しください。
また、お試しレベルではなく、もしこのセットを常用されるのであれば、もう少ししっかりケーブルを固定するなどの措置を取るのがいいでしょう。
ATOM SPKのサンプルを動かす
まずは、ATOM SPKのサンプルPlayRawPCM
を書き込んでみましょう(使うGPIOピンは上の写真から22, 21, 25に変わります)。
読者の方は後述するESP8266Audioのサンプルをいきなり試してもOKですが、SPKのサンプルはコードの書き換えをほぼせずに「書き込めば動く」ものですので、買った直後や不具合時の動作確認にお役立てください。
PlatformIOを使う場合は、.pio/libdeps/{環境名}/M5Atom/examples/TOM_BASE/ATOM_SPK/PlayRawPCM
と辿ればファイルが入っているはずです。お試しなので、サンプルのファイル4つをsrc/
フォルダにコピーしてPlayRawPCM.ino
を開き、ビルド・書き込みしてみます。
chocobo_loop_r.c(直球)
ただしく接続できていれば、M5Atomのボタンを1回押すとサンプル音楽がエンドレスで流れるはずです。音が出た証拠として、PCのライン入力にオーディオケーブルを繋いだ様子を載せておきます。
ステレオ化する
ESP8266Audioのサンプルを動かす
まず、ATOM SPKのサンプルを拡張してステレオにできないか検討しました。
しかしながら、AtomSPL.cppを見れば分かるように、このサンプルはESP-IDFのI2S機能を直接叩いています。これを改造していってもステレオ対応できるのかもしれませんが、もう少しライブラリに頼った開発をした方が楽だと感じました。
そこで、ESPシリーズのSoCのためのオーディオライブラリESP8266Audioを使ってみましょう。ESP8266Audioにはたくさんのサンプルコードがあり、どれが簡単なのか筆者はよく分かっていません。ひとまず、PlayMP3FromSPIFFS
を使ってみることにしました。
コードの修正
先ほどのATOM SPKサンプルとは違い、コードに数か所修正が必要です。
- Arduino.h -> M5Atom.h
@@ -1,4 +1,4 @@
-#include <Arduino.h>
+#include <M5Atom.h>
#ifdef ESP32
#include <WiFi.h>
#include "SPIFFS.h"
- AudioOutputI2SNoDAC -> AudioOutputI2S
-
AudioOutputI2SNoDAC
はDACがない環境でもオーディオ再生ができるためのクラスのようです。今回はDACがあるので、NoDACを使ってしまうと謎の雑音が再生されてうまくいきません - 下で示しているdiff含め3箇所あるので忘れずに全部変えましょう
-
@@ -8,7 +8,7 @@
#include "AudioFileSourceSPIFFS.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
-#include "AudioOutputI2SNoDAC.h"
+#include "AudioOutputI2S.h"
// To run, set your ESP8266 build to 160MHz, and include a SPIFFS of 512KB or greater.
// Use the "Tools->ESP8266/ESP32 Sketch Data Upload" menu to write the MP3 to SPIFFS
@@ -18,7 +18,7 @@
AudioGeneratorMP3 *mp3;
AudioFileSourceSPIFFS *file;
-AudioOutputI2SNoDAC *out;
+AudioOutputI2S *out;
AudioFileSourceID3 *id3;
-
out->SetPinout(19, 33, 22);
- M5Atomで使っているピン番号を指定します。引数は左からBCLK, LRCK, Dataです。
- また、この1行上の
new AudioOutputI2SNoDAC();
のNoDACを忘れずに取りましょう。これらのクラスは暗黙の型変換ができ、もしミスしていてもコンパイルが通ってしまいます(2敗)。
@@ -56,7 +56,8 @@ void setup()
file = new AudioFileSourceSPIFFS("/pno-cs.mp3");
id3 = new AudioFileSourceID3(file);
id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
- out = new AudioOutputI2SNoDAC();
+ out = new AudioOutputI2S();
+ out->SetPinout(19, 33, 22);
mp3 = new AudioGeneratorMP3();
mp3->begin(id3, out);
}
SPIFFSにファイル書き込み
先ほどのATOM SPKの例と異なり、PlayMP3FromSPIFFS
ではオーディオファイル(pno-cs.mp3)をSPIFFSというファイルシステムから参照します。PlatformIOでは、data/
フォルダをプロジェクト内に作って書き込みたいファイルを設置し、メニューから"Upload Filesystem Image"を実行してしばらく待てばOKです。
SPIFFS書き込みボタン
動きました。結構音質もよくて驚きです。
PlayMP3FromSPIFFSでステレオになっているか確認する
サンプルの音源は左右で同じ信号になっているので、ちゃんとステレオになっているか確認しましょう。pno-cs.mp3を編集し、2秒おきに左右の音量バランスが極端に変わるようにして書き込みます。
pno-cs-pan.mp3
ファイル名を変える場合は、サンプルコードのファイル名も忘れずに変えておく必要があります。
@@ -53,7 +53,7 @@ void setup()
Serial.printf("Sample MP3 playback begins...\n");
audioLogger = &Serial;
- file = new AudioFileSourceSPIFFS("/pno-cs.mp3");
+ file = new AudioFileSourceSPIFFS("/pno-cs-pan.mp3");
id3 = new AudioFileSourceID3(file);
id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
out = new AudioOutputI2S();
モジュールからの出力をPCで録音した結果
上のmp3編集画面とあまりにも似た波形ですが、これはM5AtomとUDA1334Aモジュールから出た音を録音したものです。きちんとステレオで再生できています!
IMUでインタラクティブに左右の音量バランスを変えてみる
ステレオ出力にするだけなら、サンプルコードに毛が生えただけでできてしまいました。もう少しM5Atomならではの実験として、Atom Matrixに内蔵されている慣性センサ(IMU)MPU6886を使って、本体の傾きで左右の音量を変えられるようにしてみます。
ESP8266Audioのリポジトリを検索した限りは、左右の音量を独立に操作する機能が見つかりません。その代わりに、AudioOutputMixer
クラスを使えば複数のファイルを同時に再生し、それぞれの音量も変えられるようです。
そこで、左だけ聞こえる音源と右だけ聞こえる音源を用意し、改造したMixerSample
コードでIMUから取得した傾きに応じて音量を変更するようにしてみました。
コードは長いのでGitHubでご覧ください。
筆者がMixerの使い方を理解していないためか、1秒おきにクリックノイズが混入してしまっています。それでも、左右の傾きでインタラクティブに音量バランスが変わっているのが分かります。
[Tips] ESP8266Audioではloop()に大きなdelay()を入れてはいけない
READMEに書いてあるように、ESP8266Audioはloop()
関数内でdelay()
を使ってしまうと正常に音が出ません。もし頻度を減らしたい処理があるなら、一定時間おきになるよう条件分岐を使うなどして対処しましょう。
下の例は、100ミリ秒おきに実行されます。
if(millis() % 100 == 0) {
M5.IMU.getAttitude(&pitch, &roll);
Serial.println((pitch + 45.0) / 90.0);
stub[0]->SetGain((pitch + 45.0) / 90.0);
stub[1]->SetGain((- pitch + 45.0) / 90.0);
}
[Tips] ループ再生させる
ESP8266Audioのサンプルでは、オーディオファイルの最後まで再生したら停止するようになっています。ループ再生させるためにもう一度begin()
を呼んでも、最初から再生にはなりません(筆者が試したときは、ループのタイミングでCPUパニックが起きリセットがかかる挙動でした)。
安全にループ再生させたいなら、その都度ファイルや再生用のオブジェクトを全部作り直すのがいいようです。筆者は初期化部分を関数に分離し、ループのたびに呼ぶようにしました。
void initializeAndStartMP3(const char* filename, int id) {
Serial.printf("Starting %s\n", filename);
file[id] = new AudioFileSourceSPIFFS(filename);
id3[id] = new AudioFileSourceID3(file[id]);
id3[id]->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
stub[id] = mixer->NewInput();
stub[id]->SetGain(0.5);
mp3[id] = new AudioGeneratorMP3();
mp3[id]->begin(id3[id], stub[id]);
}
この処理は少し時間がかかってしまい、その間は音の再生が止まってしまいます。従って、途切れずにずっとループ再生させることはできていません。
おわりに
UDA1334AステレオDACモジュールを使い、M5Atomからステレオオーディオ出力をしてみました。細かい罠はあるものの、ピン同士の接続やコード書き込みはそこまで難しい作業なく進められました。
ステレオ出力の使い道は、主には音楽関係になると思います。携帯音楽プレーヤーとか、有線イヤホンをBluetoothで無線化するレシーバーを自作してみるのはいかがでしょうか?
Discussion