N.Yamazaki's blog

主に音声合成について思ったことを書いてみようと思います。
<< 音声記号列を簡単に記述する方法 | main | 隠しコマンドでATP3011R4のチューニング >>
Arduinoで音声出力をはじめよう!「入門編」

keyword: Arduino 音声出力 サウンド 音声合成LSI

音声合成LSIを使えば簡単にArduinoで音声出力ができます!

demo

■Arduinoで音声を出力するいくつかの方法

電子工作を手軽に始められるマイコンボード『Arduino』。モノづくりやプロトタイピングで数多く利用されています。Arduinoからの出力の基本は、なんといってもLチカですね。誰もがLEDを点滅させることから始めたことと思います。その後、もう少し詳細な情報を出力するのにLCD(液晶ディスプレイ)を使う方法がありますが、Lチカに比べるとだいぶ難しくなってきます。

一方で、サウンドを使いたいということもあるでしょう。音は、なんらかの警告や注意喚起に最適ですし、見ないで操作する装置だってありますよね。

そこで、Arduinoででサウンドを出力する方法を考えてみると、おおむね以下の方法があります。(これらの特徴や使い方に関しては、書籍「Prototyping Lab」の31章に詳しく書かれています)

1.ポートのON/OFFによるブザー音
2.PCMAudioライブラリでPWM再生
3.Wave ShieldでSDカード上のサウンドデータを出力


■音声合成LSIを使う方法

今回は上記とは全く別な方法、音声合成LSIを使って音声メッセージを出力する方法を紹介します。この方法は、以下の特徴があります。

- 必要な機能がデバイス1つにまとめられているので配線が簡単
- スケッチにテキストを記述するだけで任意のメッセージを発声できる。
- あらかじめ音声メッセージ用の音声データを用意する必要がない。
- 大きな音声データのせいでArduinoのプログラム領域が占有されることが無い。
- 数値を簡単に桁読みで出力できる。
- 単語の置き換えなど、プログラムで文を構成すればメッセージのパタンも無限大。


それでは、とにかく作ってみましょう。今回のお題は「ラーメンタイマー」です。
ボタンを押して時間を分単位でセットし、時間になるとメッセージで知らせてくれます。
なお、以降の内容は、Arduinoを使ってなんらかのスケッチを動かしたり、ブレッドボードを使用して電子回路を組める方を想定しています。もしまだであれば、オライリーから出ている書籍「Arduinoをはじめよう」などを参考に、一通り動かせるようにしてください。


■用意するもの

- Arduino Uno×1
- ブレッドボード×1
- 音声合成LSI ATP3011F4-PU×1
- タクトスイッチ×1
- 抵抗4.7KΩx2
- 抵抗1KΩx1
- コンデンサ 47nF(0.047uF) x1
- コンデンサ 100nF(0.1uF) x1
- アクティブスピーカー ボリューム付×1
- ワニ口クリップ(コード付 アクティブスピーカー接続用)×2
- 配線素材(適量)

ここで使用する音声合成LSI(以降"LSI"とする)は、「AquesTalk pico LSI」 型番:ATP3011F4-PUです。28ピンのDIPパッケージの部品で、現在、秋月電気通商で¥850で入手可能です。なお、この女声のバージョン以外にも声種の異なる製品が販売されています(アクエストより)。

アクティブスピーカーとは、アンプ内蔵のスピーカのことです。パソコン用やiPod用などの外部スピーカーもこれに該当します。ボリュームがついているものを使うのが簡単ですが、もしボリュームがついていない場合は、別途ブレッドボード上に音量調整用の半固定抵抗を追加してこれで調整します(後述)。

■配線する

以下のように配線します。

haisen

回路図はこちら。
circuit

ArduinoとLSIの通信はI2Cで行います。I2Cとは、デバイス間を2本の線(+GND)で通信するもので、複数のデバイスも並列につなげられる特徴があります。そのため、I2CインターフェースのLCDモジュール、フラッシュメモリ、時計モジュールなども同時に使用可能です。いずれにせよ、ArduinoではI2C通信用のライブラリが用意されてますので、詳細を知らなくても簡単に使うことができます。

R1、R2の抵抗(プルアップ抵抗と呼ぶ)の値は、かなり適当で大丈夫です。今回の回路だったら1KΩ〜数10KΩでも動くでしょう。

100nFのコンデンサは、電源ラインを安定させるためのもので、LSIの7ピン-8ピン間になるべく近くなるように配線する必要があります。

時間設定用のタクトスイッチは、ArduinoのD2端子に接続しています。ボタンを押したときに信号レベルがLOWになります。

LSIの12ピンから1KΩの抵抗を通して、アクティブスピーカに接続します。この抵抗と47nFのコンデンサは、高域のノイズを除去するためのフィルタの役割があります。

アクティブスピーカは、通常ミニプラグが使われていますので、接続にはワニ口クリップを用いました。他にも市販の「ステレオミニジャックDIP化キット」を使っても良いでしょう。
このミニプラグ(オス)は、通常3極になっています。一番内側(コード側)はGNDに接続します。もう一方は端子のどちらかを指先で軽く触れたとき「プチッ」と鳴る方に接続します。もし、どちらも鳴るならお好きな側に。

「ミニプラグとクリップの写真」
clip


アクティブスピーカにボリュームが無ければ、5KΩ程度の半固定抵抗をブレッドボードに追加して、これで音量を調整するようにします。
haisen_vr
「半固定抵抗を接続した配線」

■スケッチを書き込む

Androidでは、動かすプログラムのことをスケッチと呼びます。
以下の手順で、ラーメンタイマーの動作を行うスケッチをArduino基板に書き込んでみます。

1. ここから今回のスケッチramen_timer.zipをダウンロードして任意の場所にファイル保存。
2. ramen_timer.zipを任意のフォルダに解凍
3. arduinoアプリを起動(Windowsなら arduino.exeを起動)
4. メニュー:ファイル>開く で、解凍したファイル中のramen_timer.ino を開く
5. メニュー:ファイル>マイコンボードに書き込む

以上で間違いが無ければ、パソコン上には「マイコンボードへの書き込みが完了しました。」と表示され、「ポン!」と起動時に音が鳴ると思います。その後、タクトスイッチを押すたびに「1分」「2分」と発声し、指定の時間が経つと「○分たったよ。ラーメン、ラーメン、ラーメンできた。・・・」と数回発声して終了します。

「完成動画」


■スケッチの音声出力している部分を読む

今回使用したラーメンタイマーのスケッチを読んで、音声出力の書き方をみましょう。
次のコードが、ラーメンタイマーのプログラムです。

「スケッチ」ramen_timer.ino

/////////////////////////////////
// ramen_timer - ラーメン・タイマー AquesTalk pico LSIを使用
//   
// by N.Yamazaki AQUEST Corp.  <http://www.a-quest.com>
#include <Wire.h>    // I2C通信用ライブラリ
const int ledPin = 13;    // LED のピン番号
const int swPin = 2;    // タクトスイッチのピン番号
int sec10;        // 残時間カウンタ 単位は[0.1秒]
int lastState;    // 一つ前のタクトスイッチの状態
int setMinute;    // セット時間 単位は[分]
void setup()
{
Wire.begin();  // ArduinoをI2C Masterとして初期化
pinMode(swPin, INPUT_PULLUP);    // スイッチ端子を入力(プルアップあり)に
pinMode(ledPin, OUTPUT);        // LED端子を出力に
sec10 = -5;
lastState=HIGH;
while(AquesTalk_IsBusy()) ; // Ready待ち
AquesTalk_Synthe("#J"); // 「ポン!」チャイム音出力
}
void loop()
{
/////////////////////////////////////
// タクトスイッチの処理
/////////////////////////////////////
int state = digitalRead(swPin);   
if(lastState==HIGH && state==LOW){ // ボタンが押された
setMinute = (sec10+650)/600; // 1分追加(65秒追加して分単位に切り捨て)
sec10 = setMinute*600;    // 0.1秒単位のカウンタ
String strMsg = GenMinuteMessage(setMinute);
while(AquesTalk_IsBusy()) ; // Ready待ち
AquesTalk_Synthe(strMsg); // 「1分」、「2分」、...
}
lastState = state; //現在のスイッチ状態を保存
/////////////////////////////////////
// カウンタ値に応じた処理
/////////////////////////////////////
if(sec10>0) { // カウントダウン中
sec10--;    // 0になるまでカウントダウン
LedBrink(); // カウントダウン中はLED点滅する
}
else if(sec10==0){ // Time up
if( !AquesTalk_IsBusy() ){
String strMsg = GenMinuteMessage(setMinute) + "+ta'ttayo.";
AquesTalk_Synthe(strMsg);    // 「○分たったよ!」
sec10--; 
digitalWrite(ledPin, LOW); // LED OFF
}
}
else if(-5<sec10 && sec10<0){ // Time up 以降 4回繰り返す
if( !AquesTalk_IsBusy() ){
// 「ラーメン、ラーメン、らーめんで・き・た!」
AquesTalk_Synthe("ra'-menn/ra'-menn/ra,ame-nn-/de',ki,ta'/...");
sec10--;
}
}
delay(100);  // 0.1sec 待ち
}
/*----------------------------------------------------
ローカル関数
-----------------------------------------------------*/
// 「○分」の音声記号列生成
//  例) "<NUMK VAL=3 COUNTER=fun>"
String GenMinuteMessage(int min)
{
String str = "<NUMK VAL=";
str += String(min, DEC);
str += " COUNTER=fun>";
return str;
}
// 10回呼び出しにつき1回、LEDを点灯
void  LedBrink()
{
static int count=0;
// counter update
count++;
if(count==10) count=0;
if(count==0) digitalWrite(ledPin, HIGH);
else         digitalWrite(ledPin, LOW);
}
/*----------------------------------------------------
音声合成用の関数
-----------------------------------------------------*/
#define I2C_ADDR_AQUESTALK 0x2E // AquesTalk pico LSIのデフォルトのI2Cアドレス
// LSIがコマンドを受信可能かチェック
// 戻り値 0:Ready 1:Busy 2:Error
int AquesTalk_IsBusy()
{
delay(10); // Busy応答は10msec以上待つ必要がある 連続して呼ばれた場合のため
Wire.requestFrom(I2C_ADDR_AQUESTALK, 1);
if(Wire.available()>0){
byte c = Wire.read();
if(c=='>')    return 0;    // Ready応答
else        return 1;    // busy応答
}
else {
return 2; //ERR: NOACK または応答が無い。I2Cの配線をチェックすべし
}
}
// 音声合成開始    引数に音声記号列を指定
void AquesTalk_Synthe(String &strMsg)
{
char msg[256];
strMsg.toCharArray(msg, 256);
AquesTalk_Synthe(msg);
}
// 音声合成開始    引数に音声記号列を指定
// 最後に"¥r"を送信
void AquesTalk_Synthe(const char *msg)
{
AquesTalk_Cmd(msg);
AquesTalk_Cmd("¥r");
}
// LSI にコマンド送信
void AquesTalk_Cmd(const char *msg)
{
// Wireの制約で、一度に送れるのは32byteまで
// AquesTalk picoへは一度に128byteまで送れるので、
// Wire.beginTransmission()〜Wire.endTransmission()を複数回に分けて呼び出す
const char *p = msg;
for(;*p!=0;){
Wire.beginTransmission(I2C_ADDR_AQUESTALK);
// Wireの制約で、一度に送れるのは32byteまで
for(int i=0;i<32;i++){
Wire.write(*p++);
if(*p==0) break;
}
Wire.endTransmission(); // 実際はこのタイミングで送信される
}
}
 

全体の構成としては、前半にArduinoスケッチの基本の、setup()とloop()関数があります。
中間部分にはローカル関数でアプリ固有の関数が2つあります。
そして、音声出力関係の関数は後半にまとめられています。(「音声合成用の関数」以下の部分)

実際に音を出しているところを見てみましょう。
setup()中の以下の部分が、起動時に「ポン」と効果音を鳴らしているところです。

 while(AquesTalk_IsBusy()) ; // Ready待ち
 AquesTalk_Synthe("#J"); // 「ポン!」チャイム音出力


AquesTalk_IsBusy()は、LSIがメッセージを受信できる状態かチェックする関数です。発声中は1が返るので、これで、発声の終了もチェックできます。whileループでLSIの準備ができるまで待ちます。
続くAquesTalk_Synthe()は、LSIにコマンド(またはメッセージ)を送る関数です。
ここでは、"#J"を指定していますが、これはLSIからチャイム音を出力するためのコマンドです。チャイム音は他に"#K"も指定できます。この関数の呼び出しで発声を開始し、関数からは発声完了を待たずに戻ります。

次に、loop()内の次の部分は、タクトスイッチを押下したときに、「1分」、「2分」と発声させている部分です。

String strMsg = GenMinuteMessage(setMinute);
while(AquesTalk_IsBusy()) ; // Ready待ち
AquesTalk_Synthe(strMsg); // 「1分」、「2分」、...

GenMinuteMessage()はローカル関数で、引数に数値を指定すると"<NUMK VAL=3 COUNTER=fun>"のような文字列を返す関数です。この文字列はLSIに数値を読ませるための特殊な記述で、

 桁読みで数値3を助数詞「分」をつけて読む"

という意味で、この例では「さんぷん」と発声し、数値の文字を変えれば任意の数を「いっぷん」「にふん」のように正しい読みとアクセントで読み上げます。数値の読み方やアクセントは数によって複雑に変化するのですが、それらはLSIが自動でやりますので簡単です。
助数詞の部分は他にも時分秒、年月日などいろいろ用意されています。詳細はLSIのデータシートの「音声記号列仕様」に詳しく示されています。
その後、先と同様にAquesTalk_IsBusy()でLSIが受信可能状態かをチェックします。
このチェックが無くても動くのですが、発声中にAquesTalk_Synthe()を呼び出すと機能しないので、チェックするようにしてください。
AquesTalk_Synthe()の引数に文字列"<NUMK VAL=X COUNTER=fun>"を指定して、「1分」、「2分」と発声させています。

つづいて、loop()内の次の部分は、指定時間が経過したときに「○分たったよ」と発声する部分です。

String strMsg = GenMinuteMessage(setMinute) + "+ta'ttayo.";
AquesTalk_Synthe(strMsg); // 「○分たったよ!」

基本的に前述と同じですが、ここでのポイントは先ほどの数値を読ませる文字列の後ろに"+ta'ttayo."という文字列を指定知る部分です。もうお気づきのことと思いますが、「たったよ」を指定しています。この文字列は音声記号列と呼ばれ、記述方法は基本ローマ字で表記して、さらにアクセントや文節の区切りを指定することができます。

助詞の「は」を"wa"と音として表記するなどいくつか気をつけない点があります。また誤った指定をするとエラーになり発声しません。記述方法の詳しくはLSIのデータシートの「音声記号列仕様」に書かれていますので、是非参照してください。

■オリジナルのスケッチで音声出力する

自分のアプリに音声出力機能を追加する方法です。

まず、ラーメンタイマーのスケッチの後半の「音声合成用の関数」以下をざっくりコピーします。
次に、スケッチの先頭に次の行を追加してください。この行は、I2C通信を使うために必要なおまじないです。

 #include <Wire.h>

また、setup()の最初に次の行を忘れずに追加してください。これはI2C通信の初期化関数です。

 Wire.begin();

あとは、スケッチの中で音声出力をしたい部分に、AquesTalk_IsBusy()でLSIの状態を確認してからAquesTalk_Synthe()でメッセージを送信するだけです。簡単でしょ!

音声出力のHelloWorldとなるもっともシンプルなスケッチを、以下に示しておきます。(音声合成用の関数は省略)

「スケッチ  」HelloTalk.ino

// Hello Talk
#include <Wire.h>
void setup()
{
Wire.begin();
}
void loop()
{
while(AquesTalk_IsBusy()) ;
AquesTalk_Synthe("konnnichiwa.");
}
2012/11/15 追記
音声合成を使うスケッチを自分で書く場合は、
音声合成LSIライブラリを使うと簡単です。

■音声記号列を試行錯誤するためのツール

音声記号列は慣れるとさくっと書けるようになるのですが、最初のうちはアクセントや区切り記号の指定に迷って、試行錯誤が必要でしょう。そのたびにスケッチを転送して実行を繰り返すと効率が悪いので、そんなときは以下のAquesTalkTermスケッチを使うと楽です。これはArduino基板をシリアルモニタとLSIの中継に使い、シリアルモニタから任意のコマンドや音声記号列をLSIに送信することができます。

使い方は、このスケッチをArduinoに書き込んだ後、シリアルモニタを起動します(メニュー:ツール>シリアルモニタ)。画面下の改行コードが[CRのみ]、ボーレートが[9600bps]であることを確認し、上のエディトボックスにLSIに送るコマンド(たとえば、"konnnichiwa.")を入力して送信ボタンのクリックで転送できます。"kkk"などと誤った音声記号列を指定すると、"E105"とちゃんとエラーコードが返ります。

スケッチのダウンロード AquesTalkTerm.ino

■音声記号列を記述するポイント

読みの指定は音として意識する必要があります。前述の助詞の「は」を"wa"と表記するほかにも、二重母音の「えい」「おう」を長音化させる。例えば「合成」は"gousei"とするのでなく"go-se-"としたほうがわかりやすくなります。
また、母音の無声化も効果的です。無声化とは、例えば文末の「〜ます。」の「す」がスーッと抜けた音になり、物理的には声帯の振動しない音になることをいいます。この例では、"masu." でなく"ma_su." と指定します。文末以外の文中でも無声化は起こり、例えば「スケッチ」を"sukecchi"の代わりに"_sukecchi"と無声音を使ったほうが聴きやすくなります。おおまかですが、無声化は次の条件で発生します。

 キ、ク、シ、ス、チ、ツ、ヒ、フ、ピ、プ、シュなどが、カ、サ、タ、ハ、パ行の直前に来るとき

まとめると、以下に注意します

・助詞を音にあわせる
・二重母音の長音化
・無声化

そういや、これを忘れていました。漢字のテキストを音声記号に変換するWebサービスがあります。詳しくは、こちらの記事を参照してください。

「音声記号列を簡単に記述する方法」
http://blog-yama.a-quest.com/?eid=970149

■発声速度を変えるには

次の関数を追加し、これを呼び出せば、それ以降、発話速度が指定の値に変更されます。
なお、この変更は電源を切ってもそのまま有効になります。 

 AquesTalk_Speed() 2012/10/10修正

// 発話速度を変更
//    speed: 50-300  default:100 50:最遅 300:最速
void AquesTalk_Speed(int speed)
{
String hex;
String str;
if(speed<50) speed=50;
else if(speed>300) speed = 300;
hex = String(speed%256, HEX);
if(hex.length()==1)  str = "#W0020" + hex;
else                 str = "#W002"  + hex;
str.toUpperCase();  // 大文字で指定 2012/10/11 追記
while(AquesTalk_IsBusy()) ;
AquesTalk_Synthe(str);
hex = String(speed/256, HEX);
if(hex.length()==1)  str = "#W0030" + hex;
else                 str = "#W003"  + hex;
while(AquesTalk_IsBusy()) ;
AquesTalk_Synthe(str);
}

■発声を途中で中断するには

次の関数を追加し、これを呼び出せば、発声の途中で止めることができます。

 

AquesTalk_Break()
// 発声を中断する
void AquesTalk_Break()
{
AquesTalk_Cmd("$");
}


■アンプを自作する(オーディオ回路)

ここまではアクティブスピーカーを使用しましたが、アンプをつけてスピーカをならしてみます。お勧めは、データシートの付録にも示している1.2Wのアンプの回路です。

circuit amp
「アンプ回路図」

このアンプのICには、秋月電子通商の「ミニモノアンプ基板」¥500を使いました。
なお、ブレッドボード配線での注意点として、アンプの消費電流は大きいので動作中に電源電圧が変動して動作が不安定(とぎれとぎれの変な音になったりすることもある)になることがありました。これを防ぐためにアンプIC近くの電源+5VとGNDの間に100uFの電解コンデンサを追加しています。

「アンプを実装」
amp

なお、アンプ回路は他でも構わないのですが、LSIからの出力ゲインは4Vp-pと比較的大きいので歪まないように注意して設計してください。

■スピーカーは必ず筐体に入れて

最後に、自作の場合スピーカを剥き出しで動かすことが多いのですが、これだと残念な音しかでません。試しに、手でスピーカーの周りを覆って鳴らしてみてください。こうすると低音からしっかり鳴りだすことがわかります。装置が完成のときには、スピーカをちゃんと筐体にいれて鳴らしてくださいね。

speaker「手でスピーカの周りを覆ってみたら」



■外部LINK

今回のラーメンタイマーのスケッチ
http://www.a-quest.com/download/package/ramen_timer.zip

かんたんマイコン「Arduino」の始め方
http://pc.watch.impress.co.jp/docs/2008/1218/musashino023.htm

秋月電子通商「音声合成LSI ATP3011F4-PU」
http://akizukidenshi.com/catalog/g/gI-05665/

音声合成LSI使用レポート
http://www.eleki-jack.com/KitsandKids2/2012/04/lsi.html

ATP3011F4-PUデータシート
http://www.a-quest.com/download/manual/atp3011_datasheet.pdf

ArduinoでI2C制御LCD ACM1602を使う
http://www.eleki-jack.com/mycom2/2012/05/arduinoi2clcd_acm16021.html

 

[Arduino] AquesTalk ライブラリ
http://blog-yama.a-quest.com/?eid=970151

| AquesTalk pico LSI | 20:53 | - | - |
PROFILE
Follow
CATEGORIES
LATEST ENTRIES
SEARCH THIS SITE
RECOMMEND
RECOMMEND
RECOMMEND
RECOMMEND
RECOMMEND
RECOMMEND
RECOMMEND
SONY MDR-CD900ST
SONY MDR-CD900ST (JUGEMレビュー »)

普段これで開発しています。
RECOMMEND
RECOMMEND
RECOMMEND
RECOMMEND