気の向くままに辿るIT/ICT/IoT
IoT・電子工作

ArduinoでWAV/MP3オリジナル音声の発話

ホーム前へ次へ
Arduinoって?

ArduinoでWAV/MP3オリジナル音声の発話

ArduinoでWAV/MP3オリジナル音声の発話

Arduinoとスピーカーで音声合成回路
2018/09/17

 Arduinoで自分で作った音声をしゃべらせてみるページ。

 前回、しゃべるArduinoしゃべるESP8266/ESP32のデモをやってみましたが、そうなると試してみたくなるのが、オリジナルの音声をArduinoやESPにしゃべらせること。

録音・再生・ファイル出力・変換

 音声の録音・再生・各種ファイルへの保存・変換には、Audacity、ALSA(arecord/aplay)、SoX(rec/play)を使う方法などがあります。

SPIFFS・PROGMEM

 Arduino IDEでArduinoボードに書き込むにあたり、ESP用ライブラリearlephilhower/ESP8266Audioのサンプルスケッチを見る限り、PROGMEMのみならず、SPIFFS上に置いた.mp3や.wavファイルを直接再生できそうも、自身はまだ出来ていないこともあり、PROGMEMを使う方法をとることにしました。

C/C++配列

 となると音声データは、Arduino IDE構文がベースとしているC/C++の配列として格納したいところですが、テキストエディタvimに含まれるxxdコマンドには、C言語の配列を作成するiオプションがあり、標準入力からなら16進値(16進数)の配列の中身を、PCMベースのWAVやMP3などからなら入力ファイル名を配列名としたインクルードファイル(ヘッダファイル)形式で出力してくれます。

マルチプラットフォーム

 Linuxに限らず、WindowsでもAudacityはもちろん、xxdもVIMをインストールすれば使えます。

前提

 結果が同じなら手段は何でも良いですが、今回は、Audacityとxxdコマンドを使ってオリジナル音声を録音、.wav/.mp3で出力、xxdでCインクルードファイルを出力、これをコピーし、PROGMEMキーワードを加えたものを含め、Arduino IDEでスケッチを書く方法をとりました。

 よってAudacity、vim(xxd)、Arduino IDEがOSにインストールされていること。

 今回、実際には、先にarecordコマンドで録音、出力した.wavファイルを元にAudacityで編集しましたが、Audacityだけで事足りるので別にarecordを使う必要はありません。

必要なもの・今回使ったもの

 今回使ったスピーカーは、100均セリアで買った1つでモノラル、2つでステレオになるというもの、USBケーブルもセリア、Nano互換機は、Amazonマーケットプレイス直で買ったもので全部で500〜600円といったところかと。

 スピーカーを使う場合、スピーカーにつながっているステレオ(ミニ)プラグの根元にマイナス、他の極にプラスをつなげば、音を出す準備は完了。(電源を要するものは電源も入れる。)

 出力については、スピーカーでなくとも、ヘッドフォンやパッシブブザーでもいけますが、圧電ブザーを使う場合は、ワニグチクリップは不要な代わりに別途ブレッドボードが必要となったり、スケッチに該当するプログラム行の追記が必要になります。

 また、ヘッドホンだと聴こえすぎるほど大きく、スピーカーは、少し小さいが聴こえるには聴こえる、一方、ブザーだと耳を当てないと聴こえないほど、ちょっと音が小さいのでトランジスタを噛ませた方がよいかもしれません。

オリジナル音声

 配列データは端折りましたが、実際には、自身の「おはよう」という音声データを変換しました(方法については、前段の[Audacity、ALSA(arecord/aplay)、SoX(rec/play)]のリンク先参照)。

 音声データは、サンプリング周波数8kHz(8000Hz)、1ch モノラルで、かつ、以下4つのファイルを作成、変換元にしたのは、非圧縮ファイル。

 録音の仕方もあるでしょうが、この「おはよう」という音声データは、WAV 32bitで108.1kB、WAV 16bitで54kB、非圧縮で27kB、MP3で12.5kBのファイルサイズとなり、驚いたことに、非圧縮とMP3をxxd -iで16進ダンプすると、それぞれ166.8kB、77.3kBと肥大化し、Arduinoには、でかすぎるか?と思いましたが、他のライブラリのサンプルスケッチなどを確認するともっと大きなものもあり、結果、どちらも正常に機能しました。

 圧縮や間引きされてるとは言え、音声データの方がファイルサイズが小さいというのは、意外でした。

 一方、MP3とWAVでこんなにもファイルサイズが違うということを知りました。

 なぜ、非圧縮よりWAVのファイルサイズが大きいのだろう...。

スケッチ・回路

const unsigned char tmp_ohayo_wav[] PROGMEM = {
 0x52, 0x49, 0x46, 0x46, 0x24, 0x00, 0x00, 0x80, 0x57, 0x41, 0x56, 0x45,
 ...
 0x7e, 0x7d, 0x7c, 0x7c, 0x7d, 0x7e, 0x7f, 0x7f
};
unsigned int tmp_ohayo_wav_len = 27044;
 
void setup()
{
// pinMode(3, OUTPUT);
 DDRD |= B00001000;  //PD3(OC2B):Arduino D3
 TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
 TCCR2B = _BV(CS20);
 
 play();
}
 
void play() {
 for (int i = 0; i < tmp_ohayo_wav_len; i++) {
  OCR2B = pgm_read_byte_near(&tmp_ohayo_wav[i]);
  delayMicroseconds(125);
 }
}
 
void loop() {
}

 スケッチの書き方は、Arduinoにしゃべらせてみたトレース&PCMデータの作り方に倣いつつ、delayMicroseconds(125);の値は、どうやって決めるのかと思いきや、結果、方法も丸かぶりの【Arduino】WAVまたはMP3ファイルを再生するに「サンプリング周波数8000Hzなら、サンプリング間隔は1秒/8000=125us、1データ再生する毎にdelayMicrosecondsで125usの間をあけてる」旨書いてある、なるほど。

 配列には、constとPROGMEMキーワードを付けること。

 前者は、少なくともArduino IDE 1.8.6では付けないとエラーになる、後者は、スケッチと同じSRAMではなく、より容量の大きいフラッシュメモリに格納するためにあり、少なくとも今回のデータだとSRAMに入り切らず、これもないと、やはり、エラーになります。

 前回、Talkieライブラリのサンプルスケッチを使わせて頂いた時は、出力ピンの設定はなかったように思われる中、D3でないと機能しないという情報があり、素直にそれに沿ってみました。

 今回は、明示的にD3を指定する必要があって、[pinMode(3, OUTPUT);]の代わりに[DDRD |= B00001000;]のように書けることは、わかったが、Talkieライブラリのサンプルスケッチには、それすらもなかった...どういう仕組みなんだろ...。

スケッチの解析を試みる...

...
 DDRD |= B00001000;  //PD3(OC2B):Arduino D3
 TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
 TCCR2B = _BV(CS20);
 
...
}
 
...
 for (int i = 0; i < tmp_ohayo_wav_len; i++) {
  OCR2B = pgm_read_byte_near(&tmp_ohayo_wav[i]);
  delayMicroseconds(125);
 }
...

 一見、アセンブラにも見えなくもありませんが、さて、何が書いてあるのか...。

 このあたりの理解が、まだ、不足していたのですが、以前、わからないままPCMAudioデモしてみましたが、気づけばコメントにsoxコマンドまで書いてあるPCMAudioソース、Arduino 日本語リファレンスArduinoのTimerを初心者が1からなんとなくわかるためのメモArduinoプログラムの高速化AVRでのタイマとPWMの使い方あたりの情報が参考になりました。

 つまり、Arduinoにおけるサウンド再生に当たっては、PWM/Pulse Width Modulationを使うのが妥当であり、これには、UnoやNanoだとTimer0/Timer1/Timer2と3つあるタイマーとレジスタが密接に関わっていて、これらコードは、Arduinoに載っているATmega328PなどAVR IC上のレジスタを操作している...という理解でよさ気。

 PCMAudioソースにもありますが、サンプリング周波数は8KHzくらいなら、Arduinoで再生して聴くに十分ということで、これが、なんとなく目安になっている模様。

...
 DDRD |= B00001000;  //PD3(OC2B):Arduino D3
...

 これは、レジスタDDRは、B/C/Dの3つのポートDDRB(D13-D8)/DDRC(アナログ)/DDRD(D7-D0)を持っており、ポートDのレジスタDDRDは、おそらく2進数を表わすBに続き、左からD7,D6,D5,D4,D3,D2,D1,D0ピンの0/1(INPUT/OUTPUT)を表わし、よって、ここでビットが立っているのは、D3ということらしい。

...
 TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
 TCCR2B = _BV(CS20);
 
...

 ここでは、3つのタイマーの内、Timer2を使っていて、Timer2においては、タイマーピン設定は、TCCR2AかTCCR2Bで行ない、タイマーピン出力は、OCR2AかOCR2Bになされ、タイマーピンにはPB3とPD3が割り当てられおり、ボードのピンアサインなどでも確認できますが、Uno系ボードでは、PB3がD11、PD3がD3に当たる。

 _BV()は、特定のピン1つの状態が、HIGH(=1)となる8ビットを返すマクロ(~_BV()はLOW(=0))であり、COMビットは、COM2BのCOM2B1とCOM2B0の2ビットから成るPWM波生成の方法を、WGMビットは、WGM2にWGM22/WGM21/WGM20と3ビットから成る波形生成の方式を、CSビットはクロックの分周比(prescaler)を決めるものとのこと。

 よって、ここでは、ピン設定TCCR2Aでは、PWM波生成方法の内、COM2B1を1(HIGH)とし、非反転(non-inverting)モードで一致した際にCOM2Bをクリア(LOW)、BOTTOMでセット(HIGH)とし、波形生成方法のWGM01とWGM00ビットに1を設定することで波形がBOTTOMの時にOCR2Bをクリア(LOW)、MAXの時にOCR2Bをセット(HIGH)としつつ、デューティ比を調整し、通常のdigitalWriteでは出せない高い周波数の『高速PWM』を使うモードが1に、TCCR2Bには、CA22/CA21/CA20と3ビットある内、CA20のプリスケーラーが1に設定された『プリスケーラなし』の状態...。

...
 for (int i = 0; i < tmp_ohayo_wav_len; i++) {
  OCR2B = pgm_read_byte_near(&tmp_ohayo_wav[i]);
  delayMicroseconds(125);
 }
...

 [include/avr/pgmspace.h]によるとpgm_read_byte_near()で16ビットで1バイトずつ読み込み、タイマー出力ピンOCR2Bに書き込む(出力)、前述の通り、サンプリング周波数8000Hzにおけるサンプリング間隔1秒/8000=125us分間を置き、配列要素数分ループしている...。

ということになりますが、もうちょっとちゃんと読まないとよくわからない...。

 とにもかくにもオリジナルの日本語の音声をArduinoとスピーカー(やヘッドホン、圧電ブザー)だけでしゃべらせることはできました。

 各方面に感謝。

ホーム前へ次へ