オールインワンのマイコンボードM5StickC Plus(下図)を使って作成しようと思います。
ボイスチェンジャと言うのは主に、人の声を変換して、元の声とは異なる声に変換する装置のことです。一般的には、どういう事に使われるのでしょうね……?
「個人の特定を避けるため」でしょうか??
昔、ヘリウムボイスと言うおもちゃが販売されていました。ヘリウムの混合気体が入ったボンベから、ヘリウムガスを吸い込んだ状態で喋ると、聞き慣れた自分の声が、全く違った声に聞こえるのが楽しい、と言うおもちゃでした。
「ヘリウムガスなんて吸い込んで安全なの?」
等の心配や疑問が湧きますよね?今回のボイスチェンジャは、デジタル処理でリアルタイムに声を変えてスピーカから出力しますので、安全に遊ぶことができます。
※後ほど考案するアルゴリズムでは、ボイスチェンジャ通過後の音声から、元の音声をある程度再現できるかもしれませんが、それは是とします。即ち、「個人の特定を避けるため」の用途には向きません。あくまで、「おもちゃ」の分類です。
M5StickC Plusには、SPM1423というマイクが内蔵されているので、ここから元の音声を取り込みます。SPM1423の制御方法や応用例は以下の記事でご覧ください。
「#23-10 PDMマイクを使って見る」
「#24-1 音声メモを作る」
オールインワンのM5StickC Plusですが、マイクは内蔵しているのに、残念ながら出力装置(スピーカ)は内蔵されていません。出力は、アンプを内蔵した外部スピーカを想定します。M5StickC Plusとアンプ内蔵スピーカの間に、以下図のような、ちょっとした回路を挟む必要があります。
M5StickC PlusのMCUであるESP32に内蔵された8ビットDA変換器の出力(G26) → ボルテージフォロア(U1、オペアンプLM358) → 直流除去(C1、カップリングコンデンサ) → アンプ内蔵スピーカ(J1、ミニジャック)へ出力する回路です。
以降、ボイスチェンジャ通過後の音声を、「VC変換(Voice Change)後音声」等と記す場合があります。
VC変換は、リアルタイム変換とします。即ち、マイクから読み取った音声データをリアルタイムでVC変換して、そのままスピーカから出力します。自分で喋った音声が、リアルタイムで異なる声になってスピーカから流れた方が、おもちゃとしては面白いでしょうから。
VC変換のパラメータは無限に設定可能なので、その中から予めいくつか選んだパラメータをメニューから選択できるようにしておこうと思います。以下図にUIFlowのUIエディタで設計した画面イメージを示しておきます。^
ヘリウムボイスは、空気(約80% N2+……)よりも、音波を高速に伝えることのできるヘリウムガス(He)が気管内に充満することで、喋った言葉が高音になって聞こえるおもちゃでした。声を変える一番簡単な方法は、声の音程を上げたり/下げたりすることです。
ピッチ(音程)を変えると、音声の周波数が変わります(下図)。
元の音声(Original voice)を、高周波数側にシフトすると高音、低周波数側にシフトすると低音と、なります。元の音声とは周波数が異なるので、別人の声に聞こえます。これがボイスチェンジャの基本原理です。
ヘリウムボイスのエミュレーションであれば、前節で、音のピッチ(周波数)を上げるだけで実現可能です。更に異なる音声に変換するため、音程を変化させた音声を2音〜4音重ね合わせるというアイディアが浮かびます(下図)。
少しづつ異なるピッチ(周波数)の音声を複数合成すると、元の音声を推定し難くなる効果が期待できます。上図で、緑色の円内2音を合成したり、黄色の円内2音を合成することで、より複雑なVC変換が可能となります。更に、緑色円および黄色円内2音〜4音を合成するなど、組み合わせは無限に広がります。
以下に、スクリプトでVC変換をシミュレーションした音声をリンクしておきます。
WAVファイル:a251_vchgr.wav
※周波数を変えているだけなので、逆変換して周波数を調整して行くと、元の音声を推定できる可能性があることには留意してください。「個人の特定を避けるため」という用途には向かない可能性があります。
ボイスチェンジャ=元の声と異なる音声に変換する、と、言う定義からは外れてしまいますが、元の声をそのまま残して、そこへピッチを変えた別な音声を追加(合成)すると、あたかも二人の人間が同時に喋っているかの様な音声に変換することができます。エフェクトとしては面白いので、後ほどVC変換パラメータの中に作り込んでおこうと思います。
ここからがボイスチェンジャプログラム作成の肝の部分となります。
「ピッチを変えることで、元音声の周波数を上げたり/下げたりする」、ここまではご理解いただけたものと思います。
では、どうやってピッチを変えれば良いのでしょうか?
例えば、10kHzでサンプリングした音声を20kHzで再生すると、ピッチが上がります。逆に、10kHzでサンプリングした音声を5kHzで再生すると、ピッチが下がります。
この場合困ることは、ピッチ変換して再生する際の時間も一緒に変化してしまう事です。
今回のボイスチェンジャでは、リアルタイムで、マイクから読み出した音声データをVC変換して出力します。ピッチだけを変化させつつ、喋るスピードは変えずにVC変換する方法を考えなくてはなりません。
ピッチ変換方法考案:
非常に簡単なアルゴリズムなので、リアルタイムのVC変換にも十分耐え得ると考えられます。
ソフトウェアの詳細設計に移ります。
実行すべき仕事としては、
1)の処理では、マイクからバッファサイズ(=rec_SIZ)毎にデータが読み出されるので、VC変換もバッファサイズ毎に変換するのが適切です。一方、2)の処理は一定の出力レート(=rec_FS)毎に処理する必要があるので、1)と2)の処理が非同期となります。
この様な非同期処理を、スケジューリングしながら実行するプログラムを組むのは、とても骨💀の折れる作業なのですが、ここでもM5StickC Plusに搭載されたFreeRTOSの並列処理機能が真価を発揮します。
今回もFreeRTOSのスレッドを使って、以下の様に並列処理させようと思います。
今回の処理には共有リソースは無いので、セマフォを使う必要は無く、互いに非同期に処理して問題ありません。
サブスレッド側の処理です。
ボイスメモ(「#24-1 音声メモを作る」)の時と同様です。M5StickC Plus内蔵マイクの制御方法についてはこちらをご確認ください。
DMACがハンドリングしたデータを、DMACバッファからブロック毎に読み出します。
サブスレッドの占有リソースは、ESP32に搭載されたI2Sユニットと、M5StickC Plus搭載のマイクSPM1423です。
サブスレッド側の処理です。
マイクから読み出したブロック内のデータは16ビットPCMなのですが、バイアス値が不明です。こちらの実験によると、12ビット程度のPCMデータであると推定できますが、バイアス値は212÷2=2,048とは限らない様です。
よって、マイクからデータを読み出したら、以下のアルゴリズムで、バイアス値を推定します。
バイアス値の推定アルゴリズム:
マイクから読み出したブロック内データの時間はrec_SIZ÷rec_FSです。
バッファサイズrec_SIZ=256サンプル/バッファ
サンプリングレートrec_FS=11,025Hz
なので、
rec_SIZ÷rec_FS≒23ms
バイアス検出に要する時間はrec_SIZ÷rec_FS×vcg_BSIZです。
vcg_BSIZ=32区間なので、
rec_SIZ÷rec_FS×vcg_BSIZ≒0.74秒
この位の長い時間でバイアス検出しないと、バイアス点が激しく変化してしまう事で、それがノイズとなってしまいます。VC変換後の音声がすごくノイジィな音になってしまうので、長めに設定してあります。
const int rec_SIZ=256; // rec: buffer size for i2s_buf
const int rec_FS=44100/4; // rec: PCM sampling rate [Hz]
const int vcg_PSIZ=1024; // vcg: buffer size for vcg_pcm
const int vcg_BSIZ=32; // vcg: buffer size for vcg_bia
プログラム先頭付近にて、上コードの様に定数が定義されているので、定数を変更する場合には適切に設定してください。
サブスレッド側の処理です。
マイクから読み出してバイアス検知・自動スケーリングしたPCMデータ(8ビットPCM)をブロック毎にVC変換します。変換作業(サブスレッド)と、DA変換器への出力処理(メインプロセス)は非同期なので、変換中のバッファとは別に、変換が完了していて再生可能なデータを保持したバッファが必要となります。
この様なバッファ構成をダブルバッファと呼びます。
static uint8 vcg_buf[2][rec_SIZ]; // vcg: voice changer buffer
最初の「[2]」の部分がダブルバッファとなっています。
static volatile int vcg_vld; // vcg: valid index for vcg_buf
vcg_vld変数によって、バッファを切り替えながら処理します。vcg_vldは、0または1を取る変数です。それ以外の値は取りません。
vcg_vld変数は、サブスレッドが非同期に更新するため、「volatile」宣言しておく必要があります。
こちらで指摘した様に、VC変換の組み合わせは無限に可能なので、アイディア次第でご自身のVC変換パラメータを作って試せる様に、VC変換パラメータを指定する配列変数の仕様を決めておきます。
VC変換パラメータ仕様:
異なるピッチの音声データを合成する時、単純に加算すると音量が大きくなります。一方、合成を相加平均とすると、平均化することで音声波形のピーク値が減少して、音量としては小さくなります。
今回のプログラムは後者の相加平均ですので、合成後に、平均化によって小さくなった音量を増幅するためのパラメータVOLUMを持たせています。
また、別途、メニュー項目との対応を取るため、以下の配列も保持します。
const void *vcg_menu[]=
{
// "Menu item", parameter
"1) Original", vcg_original,
"2) Helium", vcg_helium,
"3) Bass", vcg_bass,
"4) 2 pitches", vcg_2pitch,
"5) 3 pitches", vcg_3pitch,
"6) 4 pitches", vcg_4pitch,
"7) 2 men low", vcg_2menlow,
"8) 2 men high", vcg_2menhigh,
// maximum 8 items
NULL, NULL
};
TFTディスプレイに表示するメニュー項目文字列とVC変換パラメータ配列が対になった配列変数です。NULLポインタで終端となり、TFTディスプレイの表示範囲の関係で、最大8項目までです。
メインプロセス側の処理です。
サブスレッドが並列処理でVC変換したデータは、ダブルバッファに蓄えられています。vcg_vld側のバッファが、再生可能なVC変換後のデータを保持しています。
このデータをloop()関数内のベストエフォートで、到達した時点の実時間(micros()関数値)に従い、出力すべきデータを拾い上げて、ESP32に内蔵されている8ビットDA変換器へ出力します。以下、該当コードの抜粋です。
// output changed voice to DAC
if (vcg_cur!=vcg_vld)
{
vcg_cur=vcg_vld;
vcg_usec_start=micros ();
}
vcg_ptr=(micros () - vcg_usec_start)*(1e-6*rec_FS);
if (vcg_ptr>rec_SIZ - 1)
vcg_ptr=rec_SIZ - 1;
dacWrite (ana_DACO, vcg_buf[vcg_cur][vcg_ptr]);
当方で、出力レートを実測してみたところ、約26,000Hzでした。マイクのサンプリングレートrec_FS=11,025Hzを上回っていますので、十分な実力があります。
loop()関数内で時間のかかる処理を追加してしまうと、出力レートが落ちて再生音声品質が低下するので注意してください。
メインプロセスの占有リソースは、ESP32搭載のDA変換器ユニット等です。
完成プログラムを以下にリンクしておきます。M5StickC Plus開発環境の立ち上げは、こちらを参照してください。
完成プログラム:m5cp_voice_chgr.ino
※コンパイルには、M5cp-PSライブラリが必要となります。リンクを辿ってインストールしておいてください。
コンパイルしてM5StickC Plusに書き込むと、シリアルモニタから以下のコマンドを受け付けています。
シリアルモニタコマンド仕様:
デバッグ用のコマンドなので、ボイスチェンジャとして使う場合は、シリアルモニタ無しで自立します。
U/Iとして、以下の動作をスイッチに割り付けてあります。
M5StickC Plusおよび、サウンド出力回路、アンプ内蔵スピーカ等を以下表の様に接続してください。
M5StickC Plus | 接続 | サウンド出力回路 | 接続 | アンプ内蔵スピーカ等 |
5V出力 | → | Vcc | ||
GND | ⇔ | GND | ||
G26(DA変換器出力) | → | G26 | ||
ミニジャック | → | ミニプラグ |
実験を進めて行きます。
実験サンプル:
当方の実験では、拙作sPeachで喋らせた合成音声を元の音声として使うため、PCにイヤホンを別途接続して内蔵マイクのそばに置き、スピーカは離れたところに設置して実験しました。下図が実験中の様子です。
元の音声を再生するためのイヤホンが左側に映っています。ご自身の声で実験する場合は、直接M5StickC Plusのマイクに向かって話してください。分解したことが無いので定かではありませんが、M5StickC Plusの内蔵マイクは、SW-Aの後ろ側辺りに内蔵されているらしいです。
実際にボイスチェンジャとして動作させている様子をデモ動画にしておきました。以下でご確認ください。
M5StickC Plusには、マイク(SPM1423)は内蔵されているけどスピーカが無いので、ちょっと残念、とか思っていたのですが、万一スピーカが内蔵されていたりすると、今回の様なリアルタイムで出力するアプリケーションではハウリングしてしまうので、これはこれで正しい仕様なんですね……。