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

組み込みエンジニア

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

音声合成ICであるATP3012(アクエスト)はシリアル通信で送信した文字列を音声データに変換して出力することができます。Seeeduino XIAOのシリアル通信を使って任意の音声を出力させて動作確認したことをまとめています。

音声合成ICはATP3012R5-PU(アクエスト製:秋月電子で購入)を使用しています。周辺回路で音声を増幅するためにD級アンプモジュールとしてAE-TPA2006モジュール(秋月電子)を使用しています。

Seeeduino XIAOで動作確認したことについてリンクをまとめています。

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にする。
13/PLAY発音中にLOWになる。音声再生待機中はHIGHになるためHIGHになるのを確認して再生スタートする。
15AOUT音声出力端子。D級アンプモジュールで信号を増幅して音声再生する。
スピーカの容量(抵抗のインピーダンスが高い)によっては直付け可能
ATP3012F5で使用するピンと内容説明

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

音声再生の方法

#define PIN_PLAY 9

void setup() {
  Serial1.begin(9600); //ATP3012のデフォルトのボーレート
  pinMode(PIN_PLAY,INPUT_PULLUP);
  Serial1.print("yorosikuonegaisimasu.\r"); //よろしくお願いします。の音声
}

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

再生中や再生待機のステータスをDIポートで確認するため9ピンをプルアップ付きの入力ピンとしています。音声ICや周辺回路の動作が正常であるかを確認するため「よろしくおねがいします。」の音声を発生させています。

if( digitalRead(PIN_PLAY)){ //音声待機なら下の処理で再生
  switch( onseiSel.byte ){
    case 0:
      Serial1.print("#J.\r"); //ブザーを鳴らす
      break;
    case 1:
      Serial1.print("ohayo-u.\r");
      break;
    case 2:
      Serial1.print("konniti'wa.\r");
      break;
    //省略
else{
  Serial1.print("$"); //再生を取り消し
}

変数のonseiSelのパターンによって再生する音声を切り替える例を示しています。音声が再生中であればPIN_PLAYピンがLOWになっているため次の再生の処理を行わずに現在再生中であれば再生を取り消すためのコマンドを送信します。

よく使う音声記号

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

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

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

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

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

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

動作確認

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

Seeeduino XIAOのDIを4本使ってSW2で音声のパターンを切り替えます。SW1を押すとSW2で設定した音声のパターンを再生します。クリスタル発振子(16MHz)を使用していますが12~22pFのコンデンサを接続することが推奨されていますが、手持ちのコンデンサがなかったので実装していません。

TPA2006の/SDを制御することで音声の出力を制御できます。/SDをLOWにするとTPA2006がDisable(動作停止)します。今回は初期化時にDOをHIGHにして制御しているため常にONしている状態にしています。

ATP3012の15ピンのAOUT部分にノイズ除去のためにR1とC4を実装しています。C4は0.047uF程度がデータシートで回路例として示されていますが手持ちのものがなかったため0.1uFで代用しています。

音声のボリュームを調整したい場合はC4よりTPA2006に向けた部分に可変抵抗を入れて信号電圧を調整するとよいでしょう。

スピーカのインピーダンスが高いものであれば直接接続することもできますが、大きな音を出したい場合はインピーダンスが低い8Ωまたは4Ωのスピーカを接続する場合があります。

インピーダンスが低いスピーカを接続すると電流を多く引っ張る必要があるため直接接続するとマイコンに負担がかかるため注意が必要です。

インピーダンスが低いスピーカで大きな音を出したい場合はアンプを使用してATP3012の音声出力を信号増幅する必要があります。今回はTPA2006を使って信号増幅しています。TPA2006は負荷が8Ωであれば最大で1.45Wの出力が可能なD級アンプモジュールです。

SW2で音声のパターンを変えながらSW1を押すと音声が再生されるのを確認しました。また音声を再生中にSW1を押すと再生中の音声が停止することも確認できています。

Arduino環境では簡単にシリアル通信でデータを送信できます。手軽に音声を再生できて音声ガイダンスを作ったりできるためATP3012を使用するメリットは大きいと感じました。

ソースコード全体

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

#include <TimerTC3.h>

#define PIN_DI1 0
#define PIN_DI2 2
#define PIN_DI3 3
#define PIN_DI4 4
#define PIN_DI5 5
#define PIN_PLAY 9

#define PIN_DO1 1
#define DI_WAIT_CNT 1000

#define DI_FILT_MAX 4
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define FILT_MIN 1

struct DIFILT_TYP{
  uint8_t wp;
  uint8_t buf[DI_FILT_MAX];
  uint8_t di1;
};

typedef union{
  uint8_t  byte;
  struct {            
    bool  di1   :1; 
    bool  di2   :1; 
    bool  di3   :1; 
    bool  di4   :1; 
    bool  di5   :1; 
    bool  di6   :1; 
    bool  di7   :1; 
    bool  di8   :1; 
  }bit;
}DI_DATA;

// application use
bool comflg= false;
DIFILT_TYP difilt;
int8_t timdifilt = TIME_OFF;
int8_t cnt10ms;
bool btnflg1;
DI_DATA onseiSel;
bool  flg;

/*** Local function prototypes */
void TimerCnt();
void mainTimer();
void DiFilter();

void setup() {

    Serial.begin(115200);
    Serial1.begin(9600);

    pinMode(PIN_DI1,INPUT_PULLUP);
    pinMode(PIN_DI2,INPUT_PULLDOWN);
    pinMode(PIN_DI3,INPUT_PULLDOWN);
    pinMode(PIN_DI4,INPUT_PULLDOWN);
    pinMode(PIN_DI5,INPUT_PULLDOWN);
    pinMode(PIN_PLAY,INPUT_PULLUP);
    pinMode(PIN_DO1,OUTPUT);
    digitalWrite(PIN_DO1,HIGH);

    TimerTc3.initialize(1000);
    TimerTc3.attachInterrupt(TimerCnt);
    timdifilt = FILT_MIN;

    for( uint8_t i=0; i < 10; i++ ){
        mainTimer();
        DiFilter();
        delay(10);
    }
    Serial.println("start");
}

void loop() {

    mainTimer();
    DiFilter();

    if(difilt.di1 == 0){
        if(btnflg1){
            btnflg1 = false;

            if( digitalRead(PIN_PLAY)){
              switch( onseiSel.byte ){
                case 0:
                  Serial1.print("#J.\r");
                  break;
                case 1:
                  Serial1.print("ohayo-gozaima_su.\r");
                  break;
                case 2:
                  Serial1.print("konniti'wa.\r");
                  break;
                case 3:
                  Serial1.print("konban'wa.\r");
                  break;
                case 4:
                  Serial1.print("oyasuminasai.\r");
                  break;
                case 5:
                  Serial1.print("itterasshai.\r");
                  break;
                case 6:
                  Serial1.print("okaerinasai.\r");
                  break;
                case 7:
                  Serial1.print("itadakima_su.\r");
                  break;
                case 8:
                  Serial1.print("gotisousamadesita.\r");
                  break;
                case 9:
                  Serial1.print("yorosiku;onegaisima_su.\r");
                  break;
                case 10:
                  Serial1.print("denwaba'ngo-wa <NUM VAL=01-2345-6789>de_su.\r");
                  break;
                case 11:
                  Serial1.print("denki+desuka? \r");
                  break;
                case 12:
                  if( flg ==false ){
                    flg = true;
                    Serial1.print("aioti-kikino,sofutowokaihatusiteima_su,");
                    Serial1.print("waifai,moju-rutono;thuusinnwa,umakuittema_su.\r");
                  }
                  else{
                    flg = false;
                    Serial1.print("mondaiwa,hakeide-ta'nohenkannga'umakudekimasenn.");
                    Serial1.print("ijoude_su.\r");
                  }
                  break;
              }
            }
            else{
              Serial1.print("$"); //再生を取り消し
            }
        }
    }
    else{
    btnflg1 = true;
    }
}
/* callback function add */
void TimerCnt(){
    ++cnt10ms;
}
/* Timer Management function add */
void mainTimer(){

    if( cnt10ms >= BASE_CNT ){
        cnt10ms -=BASE_CNT; //10msごとにここに遷移する

        if( timdifilt > TIME_UP ){
            timdifilt--;
        }
    }
}
/* DiFilter function add */
void DiFilter(){

    if( timdifilt == TIME_UP ){
        difilt.buf[difilt.wp] = digitalRead(PIN_DI1);
    
        if( difilt.buf[0] == difilt.buf[1] &&
            difilt.buf[1] == difilt.buf[2] &&
            difilt.buf[2] == difilt.buf[3] ){ //4回一致を確認
              difilt.di1 = difilt.buf[0];
        }
      
        if( ++difilt.wp >= DI_FILT_MAX ){
            difilt.wp = 0;
        }

        timdifilt = FILT_MIN;
        onseiSel.bit.di1 = digitalRead(PIN_DI5);
        onseiSel.bit.di2 = digitalRead(PIN_DI4);
        onseiSel.bit.di3 = digitalRead(PIN_DI3);
        onseiSel.bit.di4 = digitalRead(PIN_DI2);
        onseiSel.byte &= 0x0F;
    }
}

Seeeduino XIAOは共用対で定義した時MSBのビットが下位ビットとして立つためLSBファーストでビットが配置されます。共用対で定義する場合は配置が逆になるため注意が必要です。

関連リンク

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

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

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

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

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

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

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