こんにちは、ENGかぴです。
音声合成ICであるATP3012(アクエスト)はシリアル通信で送信した文字列を音声データに変換して出力することができます。ESP32-WROOM-32Eをサーバーで動作させブラウザーのレスポンスによって任意の音声を出力させて動作確認しました。
ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。
音声合成ICはATP3012R5-PU(アクエスト製:秋月電子で購入)を使用しています。周辺回路で音声を増幅するためにD級アンプモジュールとしてAE-TPA2006モジュール(秋月電子)を使用しています。アクセスポイントにはWZR-HP-G300NH(バッファロー:生産中止)を使用しています。 下記記事では他のマイコンを使って音声再生したことをまとめています。
トワイライト(TWELITE)の無線を使って遠隔で音声再生する。
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eを使って動作確認したことをまとめています。
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
シリアル通信で音声を再生する
ATP3012はローマ字で送信したシリアルデータを音声に変換する音声合成ICです。アクエスト社の音声合成ミドルウェアであるAquesTalkをArduino UNOなどで使用されているマイコンに搭載した製品です。
Arduino UNOのマイコン(ATMEGA328P)を使用しているためArduino UNOの基板のマイコンを置き換えることで簡単に音声出力が確認できるのが特徴です。詳細はアクエスト社のHPをご確認ください。
AQUEST-音声合成LSI「AquesTalk pico LSI」
ATP3012シリーズを使用する
ATP3012シリーズはクリスタルなどの発振子が必要です。別シリーズのATP3011があり内蔵の発振子で音声を出力するものもあります。
2つのシリーズを比較するとATP3012シリーズの方が音声が聞き取りやすいと感じていますが、ATP3011でも音声が聞き取りにくいことはないので部品点数を減らしたいのならATP3011を選択し少しでも音声を明瞭にしたい場合はATP3012を選択するとよいと思います。
ATP3012シリーズにおいてATP3012F5とATP3012F6のどちらも購入して音声を確認しましたがATP3012F6の明瞭版の方がアナウンスに適した感じで業務用などには向いていると感じています。ATP3012F5は明瞭版よりも高い声なので趣味で遊ぶ分にはATP3012F5の方が好みでした。
今回使用するピンは以下の通りです。
ピン番号 | 機能 | 内容 |
---|---|---|
2 | RXD | マイコンからシリアル通信を受信する。 |
9、10 | XTAL1 XTAL2 | クリスタル発振子を接続10MHz/16MHzを実装する。クリスタルの仕様によっては12-22pFほどのコンデンサが必要となる。 |
11 | CLK16 | 動作クロック切り替え 16MHを使用する場合HIGHにする。 |
15 | AOUT | 音声出力端子。D級アンプモジュールで信号を増幅して音声再生する。 スピーカの容量(抵抗のインピーダンスが高い)によっては直付け可能 |
今回はシリアル通信を使用していますがSPIやWire(I2C)通信でも音声再生することが可能です。Arduino環境ではSerialライブラリで簡単に通信できるためシリアル通信がおすすめです。
音声再生の方法
void setup() {
//Serial2.begin(9600); //ATP3012のデフォルトのボーレート
Serial2.begin(9600,SERIAL_8N1,16,17); //ピンを指定して初期化
}
//使用例
Serial2.print("ohayo-gozaima_su.\r"); //おはようございます
ESP32-WROOM-32EのSerial2を使用します。Serialはシリアルモニタで使用するためSerial2を使用しています。ATP3012のデフォルトのボーレートが9600bpsなので9600を指定してbegin()関数でシリアル通信を開始します。ライブラリの更新でデフォルトピン変更になることがあるのでピンを指定して初期化する方が良い場合もあります。
ピンを指定して初期化する場合は第1引数にボーレート、第2引数にデータのフォーマット(Arduinoのデフォルトはデータビット8、パリティなし、ストップビット1)、第3引数に受信に使用するピン、第4引数に送信に使用するピンを指定します。
Serial2.pirint()で再生したい音声のローマ字入力を行います。改行コードである\rを入力すると文字列の終わりとして認識して音声再生を開始します。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
よく使う音声記号
今回使用したアクセントや区切り記号についてまとめました。音声記号仕様はデータシートに記載されているものを一部抜粋してまとめています。
記号 | 内容 |
---|---|
.(0x2E) | 無音区間が入り、文の終わりを示す。 |
?(0x3F) | 無音区間が入り、分の終わりとなるが文末の声が高めになる。 |
;(0x3B) | 次のアクセントが高い音で始める。 |
/(0x2F) | アクセントの区切りを指定する。 |
‘(0x27) | アクセント記号で音の高さが「高→低」に変化する部分につける。 |
_(0x5F) | 母音のi,uが振動を伴わずに発音させる。 データシートの読み記号表に定義されている音のみ対応 |
音声記号は半角文字で指定します。文字の横の()内の値は記号に対応したアスキーコード(テキスト)を16進数で示したものです。
Serial2.print("yorosiku;onegaisima_su.\r");
Serial2.print("genki+desuka? \r");
Serial2.print("konban'wa.\r");
アクセント記号を付けない場合は棒読みになってしまいますが、下手にアクセント記号を付けると棒読みよりもイントネーションがおかしくなり不自然な音声になってしまいます。
WebServerで表示するHTMLデータ
ESP32-WROOM-32EをWebServerを使って通信する方法については下記記事にまとめています。
ESP32-WROOM-32Eにサーバーを実装してレスポンスする
ここでは、htmlによる表とリストボックスの実装について説明しています。
表を実装する
<table border="1" style="background-color: lightyellow;">
<tr>
<th>項目</th>
<th>内容</th>
</tr>
<tr>
<td>任意</td>
<td>任意の文字を生成します。</td>
</tr>
<tr>
<td>再生リスト</td>
<td>再生リストから再生します</td>
</tr>
</table>
HTMLの<body>内に<table>タグを埋めると以下のように表示されます。見え方はブラウザーによって異なる場合があります。
項目 | 内容 |
---|---|
任意 | 任意の文字を生成します。 |
再生リスト | 再生リストから再生します |
HTMLでは表をエクセルのように表示はできますが書き方に特徴があります。タグと呼ばれる<>の記号を使ったパーツを使って表を構成します。表はテーブルタグ内で横方向(行)を指定してその中にセル(列)を指定して構成します。
<table>タグで表の範囲を指定します。<table>タグ内でborder=1にしていますがこれは表の枠線を表示する場合に使用します。ブラウザーによっては初期の条件で表示されることもあります。表のスタイルとして表の背景色を指定することで表を目立させています。
<tr>タグで行を追加します。<td>タグもしくは<th>タグで行を指定します。<th>タグを使用するとヘッダセル扱いになり文字が通常よりも大きく表示されます。
リストボックスを実装する
<select name="onseisel" style="font-size:1rem;" >
<option value="1">こんにちは</option>
<option value="2" selected>こんばんは</option>
<option value="3">おやすみなさい</option>
</select>
HTMLの<body>内に<table>タグを埋めると以下のように表示されます。見え方はブラウザーによって異なる場合があります。
selectタグを使用するとリストボックスが実装できます。selectタグ内に選択しをoptionタグを使って追加します。
selectタグでは属性としてnameにonseiselを指定しています。リストボックスで選択した値をセットするために使用します。例えばこんばんはを選択するとonseisel=”2″になります。
リストボックスの初期値をあらかじめセットする場合はoptionタグ内にselectedを指定します。
動作確認
ESP32-WROOM-32Eの28ピンはTX2に割り振られる(2024年6月中旬ではTX2はデフォルトで9(25)ピンです)ためATP3012F6のRXDと接続してシリアル通信によるコマンドを出力します。クリスタル発振子(16MHz)を使用していますが12~22pFのコンデンサを接続することが推奨されています。今回は手持ちのコンデンサがなかったので実装していません。
TPA2006の/SDを制御することで音声の出力を制御できます。/SDをLOWにするとTPA2006がDisable(動作停止)します。今回は常にONするためVCCに接続しています。
ATP3012の15ピンのAOUT部分にノイズ除去のためにR1とC4を実装しています。C4は0.047uF程度がデータシートで回路例として示されていますが手持ちのものがなかったため0.1uFで代用しています。
音声のボリュームを調整したい場合はC4よりTPA2006に向けた部分に可変抵抗を入れて信号電圧を調整するとよいでしょう。
スピーカのインピーダンスが高いものであれば直接接続することもできますが、大きな音を出したい場合はインピーダンスが低い8Ωまたは4Ωのスピーカを接続する場合があります。
インピーダンスが低いスピーカで大きな音を出したい場合はアンプを使用してATP3012の音声出力を信号増幅する必要があります。今回はTPA2006を使って信号増幅しています。TPA2006は負荷が8Ωであれば最大で1.45Wの出力が可能なD級アンプモジュールです。
EPS32-WROOM-32Eはアクセスポイントに対して接続要求を行います。接続が確立するとスマホなどのブラウザーでページを表示することができます。
再生リストから音声を再生する場合は項目の「再生リスト」のラジオボタンを選択してリストボックスから再生したい音声を選択します。
任意の音声を再生する場合は項目の「任意」のラジオボタンを選択してテキストボックスにコマンドを入力します。\rはソースコードで付加するため不要です。
通信開始ボタンを押すとシリアル通信を使用してATP3012にコマンドを送信して音声を再生します。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <Wire.h>
#include <WebServer.h>
#define SERVER_WAIT_MAX 180
#define TIME_RECONNCT_MAX 3000
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
const char *ssid = "xxxxxxxxxxxx"; //SSID
const char *pass = "yyyyyyyyyyyy"; //password
const IPAddress ip(192,168,11,20); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク
/* 変数宣言 */
String onseistr;
String onseisel;
String onseiRatio;
int16_t timreconnect = TIME_OFF;
uint32_t beforetimCnt = millis();
WebServer Wserver(80);
/* プロトタイプ宣言 */
void mainApp(void);
void mainTimer(void);
void HtmlSet(void);
void HtmlPost(void);
void handleNotFound(void);
void setup() {
Serial.begin(115200);
//Serial2.begin(9600);//ライブラリの更新によりデフォルト値変更
Serial2.begin(9600,SERIAL_8N1,16,17);
WiFi.mode(WIFI_STA); //ステーションモード
WiFi.config(ip, ip, subnet);
WiFi.begin(ssid, pass); //WiFiのアクセスポイントの設定
uint16_t wait = SERVER_WAIT_MAX;
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(wait);
Serial.print("..");
if ( --wait <= 0 ) {
ESP.restart(); //設定時間を過ぎたら強制ソフトリセットを行う
break;
}
}
Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
Wserver.on("/", HTTP_POST, HtmlPost); //URLを指定して処理する関数を指定
Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
Wserver.begin(); //Webサーバーの開始
onseiRatio = "1";
}
void loop() {
auto status = WiFi.status();
if( status != WL_CONNECTED ){
if( timreconnect == TIME_OFF ){
if( status == WL_DISCONNECTED ){
timreconnect = TIME_RECONNCT_MAX;
WiFi.reconnect();
Serial.println("Re-Connecting-start");
//reconnectflg = true;
}
}
}
else{
Wserver.handleClient();
}
if( timreconnect == TIME_UP ){
timreconnect = TIME_OFF;
}
}
/* タイマ管理 */
void mainTimer(void){
if ( millis() - beforetimCnt > BASE_CNT ){
beforetimCnt = millis();
if( timreconnect > TIME_UP ){
--timreconnect;
}
}
}
/* クライアントに返信するhtmlデータを生成 */
void HtmlSet(void){
String str = "";
str += "<html lang=\"ja\">";
str += "<head>";
str += "<meta charset=\"UTF-8\">";
str += "<title>音声動作確認</title>";
str += "</head>";
str += "<body>";
str += "<h1>音声動作確認</h1>";
str += "<form method='post'>";
str += "<label>音声パターンを入力します。</label>";
str += "<br>";
str += "<table border='1pt' style='background:lightyellow;'>";
str += "<tr><th>項目</th><th>内容</th></tr>";
str += "<td><input type='radio' name='onseiRatio' value='0'";
if(onseiRatio == "0") {
str += " checked"; }
str += ">任意 ";
str += "</td>";
str += "<td>任意の文字を再生します。<br>";
str += "<input type='text' id='onseistr' name='onseistr'";
str += "style='font-size:1rem; width:50rem; padding:16px; placeholder='konnitiwa'>";
str += "</td>";
str += "<tr><td><input type='radio' name='onseiRatio' value='1'";
if(onseiRatio == "1") {
str += " checked"; }
str += ">再生リスト";
str += "<td>音声リストから再生します。<br>";
str += "<select name='onseisel' style='font-size:2rem;'>";
str += "<option value='0'";
if( onseisel =="0"){
str += "selected";}
str += ">おやようございます。</option>";
str += "<option value='1'";
if( onseisel =="1"){
str += "selected";}
str += ">こんにちは</option>";
str += "<option value='2'";
if( onseisel =="2"){
str += "selected";}
str += ">こんばんは</option>";
str += "<option value='3'";
if( onseisel =="3"){
str += "selected";}
str += ">おやすみなさい</option>";
str += "<option value='4'";
if( onseisel =="4"){
str += "selected";}
str += ">いってらっしゃい</option>";
str += "<option value='5'";
if( onseisel =="5"){
str += "selected";}
str += ">おかえりなさい</option>";
str += "<option value='6'";
if( onseisel =="6"){
str += "selected";}
str += ">いただきます</option>";
str += "<option value='7'";
if( onseisel =="7"){
str += "selected";}
str += ">ごちそうさまでした</option>";
str += "<option value='8'";
if( onseisel =="8"){
str += "selected";}
str += ">よろしくおねがいします</option>";
str += "</select>";
str += "</td></tr>";
str += "</table>";
str += "<br><br>";
str += "<input type='submit' value='通信開始'";
str += "style='padding: 20px; min-width: 500px;border-radius: 5px;";
str += "font-family: inherit; background: lightgreen; font-size: 2rem;'>";
str += "</form>";
str += "</body>";
str += "</html>";
Wserver.send(200,"text/html", str);
//HTTPレスポンス200でhtmlデータとして送信
}
/* 送信を押したときに遷移する */
void HtmlPost(void){
String str = "";
uint8_t sel;
uint8_t rsel;
onseiRatio = Wserver.arg("onseiRatio");
onseisel = Wserver.arg("onseisel");
onseistr = Wserver.arg("onseistr");
rsel = onseiRatio.toInt();
sel = onseisel.toInt();
if( rsel ){
switch (sel)
{
case 0:
Serial2.print("ohayo-gozaima_su.\r");
break;
case 1:
Serial2.print("konniti'wa.\r");
break;
case 2:
Serial2.print("konban'wa.\r");
break;
case 3:
Serial2.print("oyasumina'sai.\r");
break;
case 4:
Serial2.print("ittera'sshai.\r");
break;
case 5:
Serial2.print("okaerina'sai.\r");
break;
case 6:
Serial2.print("itadakima_su.\r");
break;
case 7:
Serial2.print("gochisousamade'sita.\r");
break;
case 8:
Serial2.print("yorosiku;onegaisima_su.\r");
break;
default:
break;
}
}
else{
if( onseistr.length() > 0 ){ //書き込み
Serial2.print(onseistr);
Serial2.print("\r");
}
}
str += "<html lang=\"ja\">";
str += "<head>";
str += "<meta http-equiv='refresh' content='1;'>";
str += "<meta charset=\"UTF-8\">";
str += "<title>Sensor graph</title>";
str += "</head>";
str += "<body>";
str += "<h1>音声再生中</h1>";
str += "</body>";
str += "</html>";
Wserver.send(200, "text/html", str);
}
/* URLが存在しない場合の処理 */
void handleNotFound(void) {
String message = "File Not Found\n\n";
message += "URI: ";
message += Wserver.uri();
message += "\nMethod: ";
message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += Wserver.args();
message += "\n";
for (uint8_t i = 0; i < Wserver.args(); i++) {
message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
}
Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}
Serial2のRX2、TX2のデフォルトピンがライブラリの更新により記事作成時より変わっているためbegin()関数でデータビット及びピンを指定して初期化を行っています。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
最後まで、読んでいただきありがとうございました。
音声記号やアクセント記号をまだ使いこなせていないため、音声がぎこちない感じになっていますが、うまく使えるようになると聞き取りやすい音声パターンが作れそうです。