PR

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

組み込みエンジニア
本記事はプロモーションが含まれています。

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

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

音声合成ICはATP3011M6-PU(アクエスト製:秋月電子で購入)を使用しています。周辺回路で音声を増幅するためにD級アンプモジュールとしてAE-PAM8012モジュール(秋月電子)を使用しています。 下記記事では他のマイコンを使って音声再生したことをまとめています。

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

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

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

Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。

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

Wire(I2C)通信で音声を再生する

ATP3011はローマ字で送信したシリアルデータを音声に変換する音声合成ICです。アクエスト社の音声合成ミドルウェアであるAquesTalkをArduino UNOなどで使用されているマイコンに搭載した製品です。

Arduino UNOのマイコン(ATMEGA328P)を使用しているためArduino UNOの基板のマイコンを置き換えることで簡単に音声出力が確認できるのが特徴です。詳細はアクエスト社のHPをご確認ください。

AQUEST-音声合成LSI「AquesTalk pico LSI」

ATP3011シリーズを使用する

ATP3011シリーズはマイコン内蔵の発振子で動作するため外部の発振子は必要ありません。内部クロックはRCクロックであるため電源電圧の変化や温度変化の影響を受けてしまいます。UART(シリアル通信)で音声を再生する場合は正常に動作しないこともあるので注意が必要です。

データシートにはクロックのボーレートに影響されにくいI2C/SPI通信を使用することが推奨されています。

別のシリーズとしてATP3012がありクリスタルなどの発振子で動作しますが、音声のサンプリングが滑らかになるため音声の質がよくなり電源電圧の変化や温度変化の影響を受けにくくなりますが発振子など部品点数が増えてしまうデメリットがあります。

2つのシリーズを比較するとATP3012シリーズの方が音声が聞き取りやすいと感じていますが、ATP3011でも音声が聞き取りにくいことはないので部品点数を減らしたいのならATP3011を選択し少しでも音声を明瞭にしたい場合はATP3012を選択するとよいと思います。

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

ピン番号機能内容
4SMOD0動作モードを選択します。
SMOD0をGNDに接続するとI2C通信が選択される。
12AOUT音声出力端子。D級アンプモジュールで信号を増幅して音声再生する。
スピーカの容量(インピーダンスが高い)によっては直付け可能
13/PLAY発音中にLOWになる。
音声再生待機中はHIGHになるためHIGHになるのを確認して再生スタートする。
27SDAI2Cデータ入出力ポート(プルアップが必要)
28SCLI2Cクロック出力ポート(プルアップが必要)
ATP3011M6で使用するピンの機能まとめ

今回はWire(I2C)を使用していますがSPIやシリアル通信でも音声再生することが可能です。ATP3011シリーズの内蔵クロックは使用環境の影響を受けやすいため、SCLのクロックに同期して動作するWire(I2C)やSPI通信がお勧めです。

広告

Wireの設定

#include <Wire.h>

void setup() {
    Wire.begin(); //begin()内にアドレスを入れるとスレーブになる
    //Wire.setClock(100000); //クロック周波数を設定する場合にセット初期は100kHz
}

I2Cを使用する場合は初めに「Wire.h」をインクルードします。初期化関数内でbegin()関数の引数の有無によってマスタで動作するかスレーブで動作するかが決まります。引数でスレーブアドレスを指定するとスレーブとして動作開始します。マスタとして使用するため引数は無しとします。

クロックは設定しなければ初期条件(ライブラリのソースコードを確認するとクロック周波数が100kHzになっている)となります。指定した値に近似した値がクロック周波数になりますが誤差が大きくなると通信エラーの原因になるため注意が必要です。

I2Cによる信号はプルアップする抵抗値にも影響するためクロックを早く設定しすぎると波形がなまってしまうこともあるので特に意識せず100kHzで十分だと思います。

音声再生の方法

void CmdSet(uint8_t no ){
  Wire.beginTransmission(SLAVE_ADRS); //スレーブが存在するか確認
  byte error = Wire.endTransmission();

  if( error == 0){
    Wire.beginTransmission(SLAVE_ADRS); //スタート・コンディションの発行
    Wire.print("ohayougozaima_su.\r");
    Wire.endTransmission(); //ストップ・コンディションの発行
  }
  else{
    //NGの場合の処理
  }
}

最初にスレーブが存在するか確認を行い、正常であればerrorが0になるため内部の処理を行います。スレーブが存在しない場合もしくはエラーを監視したい場合はNGの場合の処理を追加します。

正常な場合はbeginTransmission()関数で初期化とスレーブアドレスをセットします。print()関数で音声のフォーマットに従った文字列をセットします。音声再生の判断は\r(改行コード)で行われるため文字列の末端に\rをつける必要があります。

すべてのデータをセットしたらendTransmission()関数でスタート・コンディションからストップ・コンディションまでを含めたデータを送信するとセットした文字列の音声が再生されます。

ArduinoのWireは32バイトまでしか送信できないため発生させる文字列が長い場合は分割して送信する必要があります。

Wireで分割してもATP3011は127バイトまでの指定となるため文字列が127バイト以下になるように音声データを構成する必要があります。長い文字列を再生したい場合は127バイト以下の音声を連続して発生させるようにします。

広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

よく使う音声記号

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

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

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

//音声パターンの例
Wire.print("yorosiku;onegaisima_su.\r");
Wire.print("genki+desuka? \r");
Wire.print("konban'wa.\r");

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

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

広告

動作確認

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

SW1で音声のパターンを切り替えます。SW1を押すと音声を再生します。音声再生中にSW1を押すと音声を停止します。

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

Wire(I2C)通信で使用する入出力ポートにはプルアップ抵抗が必要ですが、スレーブとなるデバイスの仕様によって規定されている値を使用します。Arduinoライブラリのデフォルト(クロック周波数100kHz)程度であれば5k~10kΩ抵抗を実装するとよいでしょう。

音声のボリュームを調整したい場合はC2よりPAM8012モジュール側に可変抵抗を入れて信号電圧を調整するとよいでしょう。

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

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

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

SW1を押すと音声が再生されるのを確認しました。また音声を再生中にSW1を押すと再生中の音声が停止することも確認できています。

ATP3011M6は男性の声で音声が再生されますが、声が低い感じで聞き取りにくく感じることがありました。同一シリーズでF4があり音声を確認しましたが女性の声で高めの声でした。

スポンサーリンク

ソースコード全体

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

#include <Wire.h>

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

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

DIFILT_TYP difilt;
uint32_t beforetimCnt = millis();
int8_t timdifilt = TIME_OFF;
int8_t cnt10ms;
bool btnflg1;
uint8_t talkcnt;

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

void setup() {

  pinMode(PIN_PLAY,INPUT_PULLUP);
  pinMode(PIN_DI1,INPUT_PULLUP);
  
  Wire.begin();
  Serial.begin(11500);  

  //Serial.print("konnitiwa.\r");
  timdifilt =TIME_UP;
}

void loop() {

  mainTimer();
  DiFilter();

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

      if( digitalRead(PIN_PLAY)){
        CmdSet(talkcnt);
     
        if( ++talkcnt >= 2 ){
          talkcnt = 0;
        }
      }
      else{
        CmdSet(0xFF);
      }
    }
  }
  else{
    btnflg1 = true;
  }
}
/* Wireによるコマンド送信*/
void CmdSet(uint8_t no ){

  Wire.beginTransmission(SLAVE_ADRS); //スレーブが存在するか確認
  byte error = Wire.endTransmission();

  if( error == 0){
    Wire.beginTransmission(SLAVE_ADRS); //スタート・コンディションの発行
  
    switch(no){
      case 0:
        Wire.print("ohayougozaima_su.\r");
        break;
      case 1:
        Wire.print("kyouwa,shigotode,kanaritukareta");
        Wire.endTransmission(); //ストップ・コンディションの発行
        Wire.beginTransmission(SLAVE_ADRS); //スタート・コンディションの発行
        Wire.print("node,hayakunema_su.\r");
        break;
      case 0xFF:
        Wire.print("$");
        break;
    }

    Wire.endTransmission(); //ストップ・コンディションの発行
  }
}
/* Timer Management function add */
void mainTimer(void){

  if ( millis() - beforetimCnt >= BASE_CNT ){
    beforetimCnt = millis();

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

  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;
  }
}

関連リンク

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

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

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

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

PR:テックキャンプエンジニア転職

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

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