気の向くままに辿るIT/ICT/IoT
webzoit.net
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円といったところかと。

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

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

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

EasyWordMall Arduino UNO R3互換ボード(USBケーブル付属)

EasyWordMall Arduino Pro Mini互換ボード Atmega328 5V 16MHz

Rasbeeオリジナル FT232RL互換 3.3V/5V FTDI/USB/TTL変換アダプタ

KKHMF Arduino Nano Ver 3.0互換ボード ATmega328P CH340G 5V 16MHz

HiLetgo Mini USB Nano V3.0 ATmega328P CH340G 5V 16MHz マイクロコントローラーボード Arduinoと互換 改良版(5個セット)

iBUFFALO USB2.0ケーブル (A to miniB) スリムタイプ ホワイト 1m BSUAMNSM210WH

iBUFFALO PC用スピーカー USB電源 ブラック BSSP29UBK

パイオニア Pioneer SE-A611 ヘッドホン オープン型/オンイヤー ブラック SE-A611 【国内正規品】

EasyWordMall パッシブブザー インピーダンス 16Ohm AC 2KHZ 3V 5V 12V 10個

クリップ,SODIAL(R) ワニグチクリップ 黒/赤 計100個

uxcell ワニグチクリップワイヤー ケーブル テストリード 45cm 10個

Rasbee 400穴 ブレッドボード 8.5*5.5cm 1個

HiLetgo 400穴 ブレッドボード 8.5*5.5cm 5個セット

HiLetgo 400穴 ブレッドボード 8.5*5.5cm 10個セット

Rasbee SY-170 ミニブレッドボード カラフルブレッドボード 5個

HiLetgo ジャンパーワイヤー(オス-オス) 20cmx40本 5セット

Rasbee ジャンパーワイヤー(オス-オス) 各種長さx65本

オリジナル音声

 配列データは端折ったが、実際には、自身の「おはよう」という音声データを変換した(方法については、前段の[録音・再生・ファイル出力・変換]のリンク先参照)。

 音声データは、サンプリング周波数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とスピーカー(やヘッドホン、圧電ブザー)だけでしゃべらせることはできた。

 各方面に感謝。

ウェブ造ホーム前へ次へ