N.Yamazaki's blog

主に音声合成について思ったことを書いてみようと思います。
省RAM版の言語処理エンジンのプロトタイプ開発

- M5Stackで漢字テキストからの音声合成 -

 

Keywords: AqKanji2Koe, AquesTalk, 言語処理, M5Stack, ESP32, 組み込みシステム

 

■漢字の読み上げ



日本語テキストから音声合成をするには、言語処理エンジンが必要です。
言語処理エンジンAqKanji2Koeは、漢字仮名混じりの文字列を、AquesTalk用の音声記号列に変換するライブラリです。

この言語処理には5MB〜12MBの辞書データが必要で、これがネックになってハードウェア規模を小さくできませんでした。そのため、現在の最小動作環境は、Raspberry Pi程度となっています。
*「AquesTalk Pi」はRAM256MB〜のRaspberry Pi上で動作

 

現在、この言語処理エンジンを、1ランク小さい規模のハードウェアで動作をすることを目指して開発を進めています。具体的には、RAMは256KB以下、CPUはCortex-Mをターゲットにしています。

辞書データは削減できないので、現状、ワンチップマイコンの中に入れることはできません。
そこで、これはSDメモリカードやSPIフラッシュメモリなどの、外部メモリに配置して使うことを考えています。

 

target

 


■プロトタイピング



今回、M5Stackを使ってプロトタイプを作成しました。
M5Stackは、ハードウェア規模がターゲットに近く、SDスロットやオーディオ出力が付いているので今回のプロトタイピングにはぴったりです。

 

M5Stack
SoC ESP32 
CPU Xtensa LX6(Tensilica製),CLK:240MHz
RAM 520KiB
ROM 4MB(flash)
その他

320 x 240 TFTカラーディスプレイ、

microSDカードスロット、スピーカー

 

このプロトタイプでは、シリアルで漢字を含むテキスト(UTF8)を受信し、今回開発中の言語処理エンジンで音声記号列に変換して、それをAquesTalk pico for ESP32で音声データを生成して出力しています。
顔表示(+リップシンク)の部分は、「M5Stack-Avatar」を使わせていただきました。

 

<プロトタイプのデモ動画>

 


■アルゴリズムとデータ構造の全見直し



言語処理では、辞書データへの大量のアクセスが必要です。
一方で、SDメモリカードからのデータの読み出しは、メモリからのアクセスとは比較できないほど遅くなります。この点が大きな開発課題となります。
最初のプロトタイプは、1文を処理するのに1分ほどかかってしまい、とても実用になりませんでした。

 

辞書データには、LOUDSなどの簡潔データ構造が使われます。
これはデータサイズが小さく、高速な辞書引きができるメリットがありますが、データが分散しているという特徴があります。
RAM上に辞書データが展開されている場合は何の問題もないのですが、転送速度が遅く、ランダムアクセスが不得意なSDメモリカードに配置されている場合は、大きな問題となります。

 

今回、データ構造やアルゴリズムを全面的に見直すことにし、読み込み回数の削減や、なるべく連続的なアドレスでアクセスするようにして、なんとか実用的なレベルまでたどり着きました。

現プロトタイプでのコードサイズは100KB以下(ただし、C++の標準ライブラリは含まず)です。
RAMサイズは未だ調べていませんが、あれこれ含めて520KiBで動いている事実から目標に近くなっていると思っています。


今後はライブラリ製品としての開発フェーズに入ります。

 

■Link


| AquesTalk | 12:08 | - | - |
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(2) | - |
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 | - | - |
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