N.Yamazaki's blog

主に音声合成について思ったことを書いてみようと思います。
M5Stackの音量を抵抗1つで調節する

keywords: M5Stack, Volume, 音量、スピーカー


 

■概要



M5Stackは、内蔵のオーディオアンプへの入力信号のレベルが大きすぎるため音が割れます。
一方、内蔵DACは量子化精度が8bitしかないので、ソフト的に音量を下げることは厳しいです。
今回は、1つの抵抗でM5Stackの音量を調整してみます。

 

 

■しくみ


 

DACの出力をGPIO26から出力し、抵抗を経て、GPIO25につながっているアンプに送る。

 

ESP32には2つのDACがあり、GPIO25とGPIO26にそれぞれつながっています。
M5Stackでは、DAC1(GPIO25)がアンプに接続されています。


ここで、DAC1を止めて、DAC2の出力をGPIO26から出し、抵抗で減衰させてGPIO25に戻せば、音量が調整できます。このときESP32のGPIO25はフローティング状態(電気的に絶縁)しておく必要があります。

 

上の動画では、10kΩと22KΩで切り替えています。10KΩの場合は、そのまま出力した場合とほとんど音量が変わらず、22KΩの場合は、約16dB(1/6)小さくなりました。

 

■プログラム



ベースはM5Stackのspeakerサンプルプログラムです。関数playMusicVolume()は、M5StackのライブラリのSpeaker.cから起動音の再生ルーチンを抜き出し、以下を修正したものです。


・最初にGPIO25を入力に設定する関数を呼び出す
・DACへの出力はDAC1からDAC2(GPIO26)に変更

 

// M5_volume.ino - M5Stack volume control
#include <M5Stack.h>
void setup() {
// Initialize the M5Stack object
M5.begin();
M5.Lcd.printf("M5Stack Speaker test:¥r¥n");
M5.Speaker.setVolume(8);
M5.Speaker.playMusic(m5stack_startup_music, 25000);
}
void loop() {
if(M5.BtnA.wasPressed()) {
M5.Lcd.printf("wasPressed A ¥r¥n");
playMusicVolume(m5stack_startup_music, 25000); //play the M5Stack startup sound
}
if(M5.BtnB.wasPressed())
{
M5.Lcd.printf("wasPressed B ¥r¥n");
M5.Speaker.tone(3000, 200); //frequency 3000, with a duration of 200ms
}
if(M5.BtnC.wasPressed())
{
M5.Lcd.printf("wasPressed C ¥r¥n");
M5.Speaker.setVolume(10);	// max
M5.Speaker.playMusic(m5stack_startup_music, 25000); //play the M5Stack startup sound
}
M5.update();
}
void playMusicVolume(const uint8_t* music_data, uint16_t sample_rate) 
{
// disconnect PWM & input on GPIO25
ledcDetachPin(SPEAKER_PIN);
pinMode(SPEAKER_PIN, INPUT);
uint32_t length = strlen((char*)music_data);
uint16_t delay_interval = ((uint32_t)1000000/sample_rate);
for(int i=0; i<length; i++) {
dacWrite(GPIO_NUM_26, music_data[i]);
delayMicroseconds(delay_interval);
}
for(int t=music_data[length-1]; t>=0; t--) {
dacWrite(GPIO_NUM_26, t);
delay(2);
}
ledcAttachPin(SPEAKER_PIN, TONE_PIN_CHANNEL);
}

 

■GPIO25がOFFにならない!?



アンプの入力インピーダンスが28KΩ(typ)なので、ESP32側のGPIO25が完全に切り離されているなら、22KΩでも半分程度の減衰になるはずです。しかし、実際は1/6。


ESP32側のGPIO25のGND間の内部抵抗が、なぜか10KΩ程度とかなり低くなっていました。オープンドレイン出力なども試したのですが、フローティング状態にできませんでした。なにか方法をご存知でしたらコメントください。

 


M5Stackはアンプとスピーカーを内蔵していますが、今のままではあまりにもったいない。
音が歪まないように改良して欲しいところです。
 

| 電子工作 | 18:05 | comments(0) | - |
ESP32のPDMでサウンド出力+ノイズ対策

keywords: ESP32、PDM、オーディオ、音声

 

■概要


 

ESP32は、PDM(Pulse-density modulation)の変調器を内蔵しています。
前の記事では内蔵DACでしたが、今回はこのPDMで音を出してみます。

PDMは1bitのデジタル信号ですが、パルスの密度がアナログ信号の振幅に直接対応しているので、アナログアンプに直に与えても鳴ります(アンプ自体がLPFの役割を担っているため)。

今回の実験は、ESP32-DevCとM5Stackで行ないました。


 

 

■崩れたPDM波形!?


 

最初のPDM出力波形はこんなでした。

 

 

波形がひどく崩れています。
立ち上がりは急峻で良いのですが、立下りが指数的に下がっていて、これでは、まともな音は出ません。

ネットでも同様の現象が報告されていたので、そんなものなのかと一時は諦めましたが・・・

 

実は、なんてことはない、単純なミスでした。
内蔵DAC用のコードを改良して使っていたため、出力ピンの指定を忘れていました。
i2s_set_pin()でこれを指定すれば、下のきれいな矩形のPDM信号が得られるようになりました。

 

 

 

 

■無音の再生時のノイズ



ゼロの値を連続して出力すると、ピーという周期性のノイズが現れます。
このノイズ、常に同じ高さでなく毎回違った高さの音が出ます。
どうも、前に出力した波形データに影響しているようです。

 

ESP32のデータシートをみると、PDMの処理は次の図で示されています。

これだけでは、実際どんな処理が行われているのか把握できませんが、
このPDM処理の部分で周期性ノイズが発生していることには間違いありません。

 

(ESP32 Technical Reference Manualより)

 

 

このノイズ対策には、ディザを加えれば良いようです。
ゼロの値を連続する代わりに、例えば、0,1,0,1,...とすることで、この周期性ノイズはなくなります。
ホワイトノイズは残りますが、これは信号出力時の背景ノイズと同じ大きさなので、我慢するしかないでしょう。
後述のプログラムでは、再生する信号にもディザを加えています。合成音声や小さい音などで量子化誤差がランダムでなくなる信号に対しても周期的ノイズを削減する効果が得られます。

 

 

■クリックノイズ対策



PDMの停止中の出力は0Vの状態が続くため、PDMの開始と終了時にクリックノイズが発生します。
DAC出力のときに行ったように、開始時には0VからVDD/2まで、終了時はその逆のVDD/2から0Vまでのスロープ状に値を変化させることでクリックノイズを削減できます。

 

ただ、PDMではスロープ状の値をデータとして指定しても、その出力はスロープ状になりません。先に示したようにPDMの変調器にはHPFやLPF、アップサンプリングが使われているので、DACの場合と異なり、入力の値がそのままストレートに出力されません。

 

そこで、PDMでスロープを表現するのはあきらめ、この部分は内蔵DACを用いることにします。
同じピンにDACとPDMの出力を割り当て、開始時には、内蔵DACで0からVDD/2[V]でスロープを出力した後、PDMに切り替えます。終了時はその逆です。

 

 

■サンプルプログラム



上で検討したことを盛り込んだ、PDMでサウンド/オーディオ/音声出力するライブラリ(ソースプログラム)を公開します。サンプルプログラムは、上の動画で使ったものです。

 

ダウンロード    TestPDM.zip (TestPDM.ino, PDMout.c, PDMout.h, magic07.h)

 

関数は以下の通りです。

 

  • PDMOut_create(): DACにより出力を0VからVDD/2までスロープさせる。
  • PDMOut_release(): DACにより出力をVDD/2から0Vまでスロープさせる。
  • PDMOut_start(): PDMの初期化後、出力をDACからPDMに切り替える。
  • PDMOut_stop(): PDMを停止し、DACに切り替える(VDD/2を維持)。
  • PDMOut_write(): 引数に1サンプルのデータを指定し、I2Sに送る。PDM変調器にはI2SのDMAで送られる。
  • PDMOut_clear(): I2SのDMAバッファをすべてクリア

内蔵DACも使っているので、出力ピンはGPIO25かGPIO26のいずれかになります。

 

write()のPCMデータは符号付き16bitです。内蔵DACのときは符号無しなので注意してください。

 

PDM変調器のほかに内蔵DACも使用しているので、本プログラムを動かした後はこれらのペリフェラルの状態が使用前と異なる可能性があります。M5Stackでは、PDM出力の終了後にM5.Speaker.tone()関数で音が出なくなりました。M5.begin()での内部状態が変わるためです。これについては、次の関数を呼び出せば、再び出るようになります。

(M5.begin()でGPIO25にPWMを繋げた状態にしているのは、今後変更されるかもしれません)

   ledcAttachPin(SPEAKER_PIN, TONE_PIN_CHANNEL);

 

 

■PDMでステレオ



ここで扱っているのはモノラルの信号ですが、pin_configの指定でPDMのクロックを出力し、i2s_push_sample()でLR別々の値を指定すればステレオのPDMを出力できます。
ただ、PDMのフォーマットでは1つのデータラインで左右の信号が交互に入るので、今回のようにPDM出力を直にアナログ信号として利用する方法ではステレオ化できません。
PDMのデジタルデータに対応したアンプ(MAX98356やSSM2537など)を使う必要があります。

 

 

■PDMは内蔵の8bitDACより量子化精度が高い?



信号を1/2づつ小さくしながら、どこまで聞こえるか試してみました。その結果、PDMの量子化は10bit以上あるようです。
ただ、S/N比(ピーク信号対雑音比)を測定してみると(FS:24KHz, 1023Hzのsin波、最大振幅)、49dBとなり、8bitDACと同等となりました。

 


■M5Stackで使う場合



現在のM5Stackは、アンプに入力する信号レベルが大きすぎて音割れが酷いです。PDMの場合も同じように音が割れます。
PDMの出力にLPFを通した波形のpeak-to-peakの最大振幅は約3.3Vで、DACとほとんど変わりません。

M5Stackで使用しているアンプ(NS4148)にPDMのデジタル信号を直接与えた場合、アンプ自体のLPF特性を利用して鳴るのですが、振幅が3.3Vのパルス信号を与えるのはかなり無理があるようです。特に音量が小さくなると、ノイズが絶対的に大きくなるようです。


試しに、PDM出力を1次のLPFを通してアナログ値にし、抵抗の分圧でゲインを下げててからアンプに入れたら、ずいぶん良くなりましたが・・・

 

 

■さいごに



量子化ビット数は10bit程度ありますが、S/N比が8bitDACと同じのため、音質の差はほどんどありません。
外付けLPFが必要なことを考えると、DACの代わりにPDMを使用するメリットは無さそうです。とほほ・・・

 

 

■リンク



・  前の記事「ESP32でサウンド出力時のクリックノイズ対策(I2S+内蔵DAC)

・  ESP32のデータシート
・  M5Stackのオーディオアンプ「NS4148
 

 

| 電子工作 | 15:04 | - | - |
AquesTalk-ESPを簡単に使うクラス(プログラム)

keywords: ESP32, 音声合成, マルチタスク, M5Stack
 

■概要


 

前記事では、ESP32用の音声合成ライブラリ AquesTalk pico for ESP32(AquesTalk-ESP)を紹介しました。
今回は、これを用いてバックグラウンドで内蔵DACから音声を出力するプログラム "AquesTalkTTS" を紹介します。

 

このAquesTalkTTSは、AquesTalk-ESPで音声を生成してI2S経由で内蔵DACから出力するまでを一つにまとめたC++のクラスです。初期化と発声の最短2ステップで簡単に音声合成できます。

 

当初はAquesTalk-ESPを非同期処理で使うテクニックを書こうと思っていたのですが、
そもそもESP32はFreeRTOS上で動いているので、素直にこのマルチタスク機能を使います。


 

 


■サンプルプログラム


 

M5Stackで動作します。

 

機能:

  • 時刻を表示
  • 左ボタン:年月日を読み上げ
  • 中ボタン:時分秒を読み上げ
  • 右ボタン:発声を中止

 ※ 読み上げ中も、時刻表示は毎秒変化します。
   ※ 発声中に別のボタンを押すと、即座に発声が切り替わります。

 

ダウンロード

 サンプルプログラム「M5_TalkingClock.zip」(AquesTalkTTSのソースコードも入っています)

 

 

■コード説明



サンプルプログラムをもとに、AquesTalkTTSクラスの使い方を示します。

 

M5_TalkingClock.ino(抜粋)

14 void setup()
15 {
・・・
21   iret = TTS.create(licencekey);
・・・
30 }
31 
32 void loop()
33 {
・・・
38   if(M5.BtnA.wasPressed()){
39     if(getLocalTime(&timeinfo)){
40       // 年月日の読み上げ
41       sprintf(koe,"<NUMK VAL=%d COUNTER=nenn>/<NUMK VAL=%d COUNTER=gatu>/<NUMK VAL=%d COUNTER=nichi>.",
42         timeinfo.tm_year+1900,timeinfo.tm_mon+1, timeinfo.tm_mday);
43       iret = TTS.play(koe, 100);
・・・
60   else if(M5.BtnC.wasPressed()){
61     TTS.stop();
62   }
63 
64   M5.update();
65 }

setup()の中で、AquesTalkTTSを初期化します(21行目)

 

loop()の中で、左ボタンが押されたら(38行目)、時間を取得し、数値読みタグで年月日の音声記号列を生成します(41行目)。あとは、TTS.play()を呼び出すだけです(43行目)。これで年月日が読み上げられます。音声出力はバックグラウンドで処理するので、TTS.play()はすぐに戻ります。

 

右ボタンが押されたら(49行目)、TTS.stop()を呼び出します。これで発声が停止します。発声中以外は特に何も起こりません。


※ あらかじめArduinoIDE上にAquesTalk-ESPをインストールしておく必要があります。
    インストール方法は、前記事の「ビルド準備 - ArduinoIDEの場合」の項を参照ください。
※ ライセンスキーが未指定の場合は、「ゆっくりしていってぬ」になります。

 


■AquesTalkTTSクラス(AquesTalkTTS.cpp/.h)の動作


 

AquesTalk-ESPとI2S経由で内蔵DACから音声出力するまでを1つのクラスにしています。
メソッドは create(), play(), stop(), release() の4つだけです。
create()で初期化、play()で音声合成開始、stopで発声停止、release()でメモリ解放します。

 

create()

AquesTalk-ESPの初期化を行っています。ワークバッファしてヒープ上に400byte確保されます。I2Sの初期化は行っていません。

 

play():

引数の音声記号列をAquesTalk-ESPにセットしてから、音声出力用のタスク(task_TTS_synthe())を起動します。

 

task_TTS_synthe():  (内部のタスク関数)

I2Sの初期化(DMAバッファが確保される)後、フレーム単位での音声合成を行いI2Sへ書き込みます。
最後のフレームまで生成したら、I2Sを解放します。
I2Sと内蔵DACは発声中だけ使用しますので、それ以外のときにビープなど他の音を出力することもできます。

 

stop():

不正な音声記号("#")を指定して、わざとAquesTalk-ESPがエラーで終了するようにしています。

 

※ 発声中にplay()を行うと、発声中のメッセージはその場で終了し、新たに指定したメッセージが始まります。
※ ArduinoIDE環境で動作確認しましたが、ESP-IDFでも動くと思います。
※ クリックノイズ対策済みです。

 

 

■リンク



「ESP32で音声合成(AquesTalk pico for ESP32)」
http://blog-yama.a-quest.com/?eid=970188

 

「ESP32でサウンド出力時のクリックノイズ対策(I2S+内蔵DAC)」
http://blog-yama.a-quest.com/?eid=970190

 

| AquesTalk | 22:10 | - | - |
ESP32でサウンド出力時のクリックノイズ対策(I2S+内蔵DAC)

keywords: ESP32, サウンド, DAC, I2S

 

■概要



ESP32の内蔵DACから音を出すとき、前後のプチプチというノイズが気になってので、これを防ぐ方法を検討してみました。

 

 

■クリックノイズの原因


 

ESP-IDFのI2Sドライバは無音時の値を0としている。
内蔵DACにおける値0は負の最大値である。

 

通常I2Sの16bitデータは符号付き(無音時の値が0、最小が-32768、最大が32767)で表現します(図1-A)。ESP-IDFのI2Sドライバも、その前提で書かれています。
しかし、内蔵DACは0Vから3.3Vの範囲を16bitの符号無し整数(0から65535)で指定します。
そのため、音声信号では無音時の値を32768、負の最大値を0、正の最大値を65535になるように変換して与える必要があります。

 

ESP-IDFのI2Sドライバは無音時の値が0という前提で書かれていますので、初期状態や動作の開始・終了の状態の変化のときに、この値0を出力することがあります。これは内蔵DACからみれば負の最大値であり、これがクリックノイズの原因です(図1-B)。

 

Waves

図1

 


■対策


 

図1-Cのように、再生の前後の立ち上がりと立下りを滑らかにします。

 

 

■Tips <ここが本記事のメイン



・立ち上がり、立下りは値を256づつ直線的に変化させる

このとき、より滑らかにしようと傾きを緩やかにするのはNGです。
内蔵DACの分解能が8bitしかないので(下位8bitは切り捨てられる)、同じ値が繰り返されるとステップノイズの基本周波数が低くなり、余計にノイズが目立つようになります。

 

・i2s_start()直後は、DMAバッファ1つ分のダミーを出力する

タイミングによりますが、先頭のデータが抜けることがあるために必要です。

 

・i2s_stop()の前に、DMAバッファすべてを0で埋める

再びi2s_start()で再開したときに異音が出ないようにするためです。

 

・i2s_stop()は、DMAバッファすべてとFIFOの長さの値を書き込んでから呼び出す

出力されるタイミングは値を書き込んだ時ではなく、実際はDMAバッファとI2S内のFIFOバッファ分の遅延があります(I2S内には32bitx64のFIFOがあります)。完全に立ち下がってからi2s_stop()を呼び出す必要があります。

 

・i2s_start()の操作で値0が一瞬出力されてしまう

たとえDMAバッファをすべて中間値(32768)で埋めても、値0が瞬間的に出てしまいます。そのため、停止時に中間値で維持する方法は使えません。

 

・DMAバッファを32768で埋めてi2sを止めない方法

この方法はありですが、音を出さないときにもI2Sを動してると、M5Stackでノイズが気になりました。

 

 ※現時点(2018/03/27)のESP-IDFなので今後動作が変わるかもしれません。

 


■サンプルプログラム


 

クリックノイズの対策を行ったサンプルプログラムを公開します。
正弦波のデータを4回に分けて出力しています。

 

・「TestClickNoise.ino
 

・「TestClickNoiseBef.ino」(クリックノイズ対策無し。比較用)

 


■リンク


 

 次の記事「ESP32のPDMでサウンド出力+ノイズ対策」

    http://blog-yama.a-quest.com/?eid=970192

 

「EPS-IDF Programming Guide -I2S」
     http://esp-idf.readthedocs.io/en/latest/api-reference/peripherals/i2s.html

 

「ESP-IDFのI2Sドライバソース i2s.c]
    https://github.com/espressif/esp-idf/blob/master/components/driver/i2s.c

 

「ESP32 Technical Reference Manual」    https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf

 

「ESP32で音声合成(AquesTalk pico for ESP32)」
    http://blog-yama.a-quest.com/?eid=970188

 

| 電子工作 | 23:19 | - | - |
ESP32で音声合成(AquesTalk pico for ESP32)

keywords: 音声合成 AquesTalk ESP32  I2S 
 

概要


 

AquesTalk pico for ESP32 (以下、AquesTalk-ESP)は、ESP32向けの音声合成ライブラリです。
これを使って、ESP-WROOM-32の内臓のDA(I2S経由)から合成音声を出力します。
アンプ・スピーカーを内蔵しているM5Stackなら、ソフトだけで簡単に音声合成できます。

 

 

 

AquesTalk pico for ESP32 (AquesTalk-ESP)の入手


 

アクエストのサイトからダウンロードできます。

 

・アクエスト > ダウンロード
   https://www.a-quest.com/download.html

 

ダウンロードしたZIPの中身は、ヘッダファイル aquestalk.h、ライブラリ libaquestalk.a、あとはドキュメントファイルです。
なお、ここでいうライブラリとはコンパイルしたオブジェクトを指し、ArduinoIDEのライブラリ(ソースコード等から構成されるもの)ではありません。また、AquesTalk-ESPは有償ソフトウェアであり、評価版ではナ行とマ行がすべてヌと発声される制限があります。

 

 

ビルド準備  ArduinoIDEの場合


 

ArduinoIDEによる開発環境上に、AquesTalk-ESPをインストールする手順を以下に示します。ArduinoIDEや、ArduinoIDEでESP32をビルドするための「Arduino core for the ESP32」はすでにインストールされているものとします。

 

1.  次の場所にライブラリとヘッダをコピーします。

{PATH_Arduino_ESP32}¥tools¥sdk¥lib¥libaquestalk.a
{PATH_Arduino_ESP32}¥tools¥sdk¥include¥aquestalk¥aquestalk.h

ここで、{PATH_Arduino_ESP32}は、「Arduino core for the ESP32」をインストールした場所です。 私の環境では次のとおりでした。

C:¥Program Files (x86)¥arduino¥hardware¥espressif¥esp32

aquestalk¥aquestalk.hのaquestalkフォルダは作成してください。

 

2.  次の場所にplatform.local.txtを作成します。同じ場所にplatform.txtがあるはずです。

{PATH_Arduino_ESP32}¥platform.local.txt

platform.local.txtには、以下を記述します。これは、コンパイル時のインクルードパスにaquestalkを追加し、リンク時にlibaquestalk.aを加える指定です。

compiler.c.extra_flags="-I{compiler.sdk.path}/include/aquestalk"
compiler.cpp.extra_flags="-I{compiler.sdk.path}/include/aquestalk"
compiler.c.elf.libs=-lgcc -lopenssl -lbtdm_app -lfatfs -lwps -lcoexist -lwear_levelling -lhal -lnewlib -ldriver -lbootloader_support -lpp -lsmartconfig -ljsmn -lwpa -lethernet -lphy -lapp_trace -lconsole -lulp -lwpa_supplicant -lfreertos -lbt -lmicro-ecc -lcxx -lxtensa-debug-module -lmdns -lvfs -lsoc -lcore -lsdmmc -lcoap -ltcpip_adapter -lc_nano -lrtc -lspi_flash -lwpa2 -lesp32 -lapp_update -lnghttp -lspiffs -lespnow -lnvs_flash -lesp_adc_cal -llog -lexpat -lm -lc -lheap -lmbedtls -llwip -lnet80211 -lpthread -ljson  -lstdc++ -laquestalk

compiler.c.elf.libsの部分は、Arduino core for the ESP32のバージョンによって異なるかもしれません。platform.txtからcompiler.c.elf.libsの部分をコピペして、最後に -laquestalk を加えます。

以上で準備は終わりです。

 


ビルド準備   ESP-IDFの場合


 

ESP-IDFの場合は、ArduinoIDEの場合とは異なり、アプリのプロジェクトの中にAquesTalkのライブラリとヘッダをコピーします。

具体的には次のようにプロジェクトのファイルを構成します(ここではプロジェクト名をhello_aquestalkとしています。~はmingw32ターミナル上でのホームディレクトリ)。


[ダウンロード hello_aquestalk.zip]

~/esp/hello_aquestalk/
    Makefile
    main/
        main.c
        aquestalk.h
        libaquestalk.a
        component.mk

なお、私のWindows上では以下のディレクトリになりました。

C:¥msys32¥home¥{USER_NAME}¥esp¥hello_aquestalk

MakefileはESP32-IDFの通常のままで、AquesTalkを使う上で特別な部分はありません。次の2行を記述します。

PROJECT_NAME := hello_aquestalk
include $(IDF_PATH)/make/project.mk

 

mainディレクトリを作成し、そのなかに、main.c, aquestalk.h,libaquestalk.a,component.mkを配置します。

component.mkには以下の記述を追加します。これは、リンク時にlibaquestalk.aを追加する指定です。この指定を忘れると、make時にAquesTalkの関数が未定義とのエラーになります。

COMPONENT_ADD_LDFLAGS += $(COMPONENT_PATH)/libaquestalk.a

以上で準備は終わりです。

 

 

サンプルコード


 

サンプルプログラムは、3つのメッセージを連続して音声出力するものです。
ArduinoIDEとESP-IDFの2つ用意していますが、ソースコードはほとんど同じです。
上のビルド準備が整っていれば、ArduinoIDEでは [マイコンボードに書き込む]、ESP-IDFでは 
$ make flash monitor で、GPIO25かGPIO26端子から音声信号が出力されるはずです。

 

・サンプルコードのダウンロード
    helllo_aquestalk(ArduinoIDE用)
    helllo_aquestalk(ESP-IDF用)

 


コード説明


 

以下はサンプルコードの抜粋です。
 

5|   #define LEN_FRAME 32
6|   uint32_t workbuf[AQ_SIZE_WORKBUF];
...
8|   void setup() {
...
12|   	Serial.println("Initialize AquesTalk");
13|   	iret = CAqTkPicoF_Init(workbuf, LEN_FRAME, "XXX-XXX-XXX");
...
17|   
18|   	DAC_Create();
...
21|   	Play("konnnichiwa.");
...
26|   	DAC_Release();
...
28|   }
...
34|   void Play(const char *koe)
35|   {
...
39|   	int iret = CAqTkPicoF_SetKoe((const uint8_t*)koe, 100, 0xffffU);
...
42|   	for(;;){
43|   		int16_t wav[LEN_FRAME];
44|   		uint16_t len;
45|   		iret = CAqTkPicoF_SyntheFrame(wav, &len);
46|   		if(iret) break; // EOD
47|   		
48|   		DAC_Write((int)len, wav);
49|   	}
50|   }
...
53|   //i2s configuration 
54|   const int i2s_num = 0; // i2s port number
55|   i2s_config_t i2s_config = {
56|   		 .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
57|   		 .sample_rate = 24000,
...
65|   };
66|   
67|   void DAC_Create()
68|   {
69|   	AqResample_Reset();
70|   
71|   	i2s_driver_install((i2s_port_t)i2s_num, &i2s_config, 0, NULL);
72|   	i2s_set_pin((i2s_port_t)i2s_num, NULL);
73|   }
...
81|   int DAC_Write(int len, int16_t *wav)
82|   {
83|   	int i;
84|   	for(i=0;i<len;i++){
85|   		// upsampling x3
86|   		int16_t wav3[3];
87|   		AqResample_Conv(wav[i], wav3);
88|   
89|   		// write to I2S DMA buffer
90|   		for(int k=0;k<3; k++){
91|   			uint16_t sample[2];
92|   			uint16_t us = ((uint16_t)wav3[k])^0x8000U;	// signed -> unsigned data 内蔵DA Only
93|   			sample[0]=sample[1]=us; // mono -> stereo
94|   			int iret = i2s_push_sample((i2s_port_t)i2s_num, (const char *)sample, 100);
95|   			if(iret<0) return iret; // -1:ESP_FAIL
96|   			if(iret==0) break;	//	0:TIMEOUT
97|   		}
98|   	}
...

 

CAqTkPicoF_Init()でAquesTalkを初期化します(13行目)。
引数にはワークバッファ、フレーム長、ライセンスキーを指定します。
ワークバッファは、AquesTalkが内部処理で使うメモリで、サイズが AQ_SIZE_WORKBUF の uint32_t 配列を指定します。AQ_SIZE_WORKBUF は aquestalk.h 内で定義されています。ワークバッファは音声合成処理を行っている時に必要で、一連の処理後は解放できます。これによりRAMを効率よく利用できます。

AquesTalkは、非同期処理にも対応できるように分割して音声を生成します。フレーム長とは、このときに一回に生成する音声波形の長さ(サンプル数)で、初期化時に指定します。指定可能な値の範囲は30-300で、ここでは32を指定しています。
最後の引数にはライセンスキーを指定し、評価版として使用するときはNULLを指定します。

 

AqTkPicoF_SetKoe()で、発声するメッセージ(音声記号列)を指定します(39行目)。
第2引数は発話速度でデフォルトが100。値を大きくするほど早い発声になります。
第3引数は最後の無音区間の長さです。通常デフォルトの0xFFFFUを指定すれば良いでしょう。

 

CAqTkPicoF_SyntheFrame()で、1フレームの音声データを取得します(45行目)。
サンプリング周波数が8KHzで、フレーム長に32サンプルを指定したので、32/8KHz=4ms毎に音声を生成することになります。戻り値が0以外なるまでこの関数を複数回呼び出します。
第1引数には16bit/PCM/モノラルの音声データが返されます。
第2引数は生成したデータのサンプル数が返ります。通常はフレーム長に等しく、最後のフレームだけフレーム長より小さくなることがあります。

 

DAC_Create()で、I2Sの初期化をします(67行目)。この中で、I2S経由で内蔵DACから出力する設定を指定しています。
AquesTalk picoの出力音声データは8KHzのサンプリング周波数ですが、AquesTalk-ESP32ライブラリには、これを3倍にアップサンプリングするサンプリング周波数変換関数(AqResample)が含まれています。
アップサンプリングにより、ローパスフィルタが使われていないオーディオ系に出力するときに、チロチロとした折り返し雑音を抑制することができます。もし、DA出力側にfc4KHz程度のLPFを入れられるのならば、アップサンプリングをせず8KHzのまま出力したほうがCPU負荷は少なくなります。

 

AqResample_Reset()は、アップサンプリング処理の初期化関数です(69行目)。
また、アップサンプリングに伴い、i2s_driver_install()で指定するconfigurationのサンプリング周波数には8KHzでなく24KHzを指定しています(57行目)。

 

DAC_Write()で、1フレーム(32サンプル)の音声をI2Sに出力しています(81行目)。
AqResample_Conv()のアップサンプリング処理により、1サンプルのデータが3サンプルに変換されます。これを1サンプルづつi2s_push_sample()でI2Sのバッファに出力します。

 

 

I2Sには符号無しデータを与える?!(内蔵DACの例外)



本来、I2Sのデータは符号付きです。16bitデータの場合、値は-32768〜32767の範囲で、センターの値は0となります。ところが、内臓DAの場合は、0-65535(下位8bitは無視)の範囲で、センターの値は32768にする必要があります。サンプルコードではこの変換を行っていますが(92行目)、外部接続のI2C-DACを使うときは、この変換をしてはNGです。

 


外付けDACを使うには


 

内蔵DACを使うと、I2Sの初期化時と、音声出力の最初にポップノイズが発生しています。原因の予想はついていますが、これについてはもう少しちゃんと調べてから書こうと思います。
コードを少し変更することで、ESP32の内蔵DACの代わりにPCM5102A搭載などの外付けI2S-DACを用いることもできます。
変更点は、

  • i2s_config.modeでI2S_MODE_DAC_BUILT_INフラグを外す
  • i2s_set_pin()で使用するピンの設定を行なう
  • 符号無しデータに変換している処理(92行目)をしない。

 

実際にRaspberry Pi用のPCM5102A搭載32bit 384kHz DAC完成基板を用いて動かしてみました。ただ、DACの設定がI2Sモードでは動かず、left justifiedにする必要があり、ちょっとモヤモヤしています。

 

 

ライセンスキーの入手



AquesTalk-ESPの評価版はナ行とマ行がすべてヌと発声される制限があります。ライセンスキーを購入することでこの制限が解除されます。

ライセンスキーの入手は、アクエストOnline Storeで「AquesTalk pico for ESP32 使用ライセンス」を購入します。このとき備考欄に使用するESP32モジュールのMACアドレスを記載する必要があります。その後に郵送されるライセンス証にライセンスキーが記載されており、このライセンスキーをCAqTkPicoF_Init()に指定します。

MACアドレスが不明の場合は、次のスケッチを実行します。

 

・アクエストOnline Store「AquesTalk pico for ESP32 使用ライセンス

 

[GetMAC.ino]

// Get default MAC Address
void setup() {
  Serial.begin(115200);
  uint8_t mac[6];
  esp_efuse_mac_get_default(mac);
  Serial.printf("¥nMAC Address = %02X:%02X:%02X:%02X:%02X:%02X¥r¥n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
void loop() {
}  

 

最後に


 

AquesTalk picoは、Flash(ROM)28KB、RAM:500B という極めて小さなリソースなので、他の処理に大きな負担をかけずに音声合成できます。アラートなどアプリのちょっとした部分に使用することもできると思います。
今回のコードは音声出力している間は他の処理ができない同期処理ですが、コードの書き方で音声出力と同時に表示を行うなど非同期の処理も可能です。この非同期で使う方法は次記事にする予定です。

 

次記事   「AquesTalk-ESPを簡単に使うクラス(プログラム)」

  http://blog-yama.a-quest.com/?eid=970191


リンク


 

・Arduino core for the ESP32 のインストール方法

  https://www.mgo-tec.com/arduino-core-esp32-install


・ESP-IDF ( ESP32 開発環境 ) の使い方

  https://www.mgo-tec.com/esp32-idf-howto-01


・ESP32でI2S+DACを使う

  https://qiita.com/h_nari/items/b52c525f0c5b35aaf692


・ESP-IDF Programming Guide - Build System
      http://esp-idf.readthedocs.io/en/latest/api-guides/build-system.html
 

| AquesTalk | 19:11 | - | - |
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