ESP32-WROOM-32Eのシリアル通信で音声を再生する

組み込みエンジニア

こんにちは、ENGかぴです。

音声合成ICであるATP3012(アクエスト)はシリアル通信で送信した文字列を音声データに変換して出力することができます。ESP32-WROOM-32Eをサーバーで動作させブラウザーのレスポンスによって任意の音声を出力させて動作確認しました。

音声合成ICはATP3012R5-PU(アクエスト製:秋月電子で購入)を使用しています。周辺回路で音声を増幅するためにD級アンプモジュールとしてAE-TPA2006モジュール(秋月電子)を使用しています。アクセスポイントにはWZR-HP-G300NH(バッファロー:生産中止)を使用しています。 下記記事では他のマイコンを使って音声再生したことをまとめています。

Arduinoライブラリと音声合成ICで音声を再生する

トワイライト(TWELITE)の無線を使って遠隔で音声再生する。

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

シリアル通信で音声を再生する

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の方が好みでした。

今回使用するピンは以下の通りです。

ピン番号機能内容
2RXDマイコンからシリアル通信を受信する。
9、10XTAL1 XTAL2クリスタル発振子を接続10MHz/16MHzを実装する。クリスタルの仕様によっては12-22pFほどのコンデンサが必要となる。
11CLK16動作クロック切り替え 16MHを使用する場合HIGHにする。
15AOUT音声出力端子。D級アンプモジュールで信号を増幅して音声再生する。
スピーカの容量(抵抗のインピーダンスが高い)によっては直付け可能
ATP3012F5で使用するピンと内容説明

今回はシリアル通信を使用していますがSPIやWire(I2C)通信でも音声再生することが可能です。Arduino環境ではSerialライブラリで簡単に通信できるためシリアル通信がおすすめです。

音声再生の方法

void setup() {
  Serial2.begin(9600); //ATP3012のデフォルトのボーレート
}
//使用例
Serial2.print("ohayo-gozaima_su.\r"); //おはようございます

ESP32-WROOM-32Eのシリアル2を使用します。Serialはシリアルモニタで使用するためSerial2を使用しています。ATP3012のデフォルトのボーレートが9600bpsなので9600を指定してbegin()メソッドでシリアル通信を開始します。

Serial2.pirint()で再生したい音声のローマ字入力を行います。改行コードである\rを入力すると文字列の終わりとして認識して音声再生を開始します。

音声再生できる文字数は127バイト分なので127バイトを超える長い文字列を再生したい場合は分割して音声再生する必要があります。

よく使う音声記号

今回使用したアクセントや区切り記号についてまとめました。音声記号仕様はデータシートに記載されているものを一部抜粋してまとめています。

記号内容
.(0x2E)無音区間が入り、文の終わりを示す。
?(0x3F)無音区間が入り、分の終わりとなるが文末の声が高めになる。
;(0x3B)次のアクセントが高い音で始める。
/(0x2F)アクセントの区切りを指定する。
‘(0x27)アクセント記号で音の高さが「高→低」に変化する部分につける。
_(0x5F)母音のi,uが振動を伴わずに発音させる。
データシートの読み記号表に定義されている音のみ対応
ATP3012でよく使う音声記号まとめ(データシート抜粋)

音声記号は半角文字で指定します。文字の横の()内の値は記号に対応したアスキーコード(テキスト)を16進数で示したものです。

Serial2.print("yorosiku;onegaisima_su.\r");
Serial2.print("genki+desuka? \r");
Serial2.print("konban'wa.\r");

アクセント記号を付けない場合は棒読みになってしまいますが、下手にアクセント記号を付けると棒読みよりもイントネーションがおかしくなり不自然な音声になってしまいます。

音声記号やアクセント記号をまだ使いこなせていないため、音声がぎこちない感じになっていますが、うまく使えるようになると聞き取りやすい音声パターンが作れそうです。

すき間時間で資格をゲット【STUDYing(スタディング)】

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を指定します。

コーチング主体のプログラミングスクール【CODEGYM Monthly(コードジムマンスリー)】まずは無料相談

動作確認

動作確認用の回路図
動作確認用の回路図

ESP32-WROOM-32Eの28ピンはTX2に割り振られるため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にコマンドを送信して音声を再生します。

WiFi通信を使用してリストボックスをつかって固定の音声を再生でき手打ちで任意の音声を再生させられるのは面白いのですが、コマンドが間違っていると音声が再生されないか変なイントネーションの音声になるため注意が必要だと感じました。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#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 = "001D73912B41"; //SSID
const char *pass = "y013xxrctu6f7"; //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);

  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); //テキストファイルであることを示している。
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

GEEKJOB-未経験からITエンジニアに【オンライン無料体験】

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました