Webミーティングや、ゲームでのボイスチャットの機会が増えて、ボイスチェンジャーを自作しようと思い立ち、夏休みの自由研究的に実装してみたときの記録
使い慣れているということもありますが、実装速度的にもPythonのようなインタープリタ言語より、一般的に処理速度が高速とされるコンパイラ言語を用いた方が、リアルタイムの処理を要求されるボイスチェンジャーのようなアプリケーションには向いているのではないかと思ったので、今回はC/C++を使って実装してみました。
目的:
- リアルタイムに音声を処理可能なボイスチェンジャー(オーディオコンバータ)を自作する
- C/C++の簡単な知識
https://github.com/sukima-log/RealTimeSoftVoiceChanger
リモートワークや、ゲーム内ボイスチャット、配信サービスなどの影響からか、ボイスチェンジャーで検索すると、今や無料で公開されているものが沢山見つかります。
今回はあえてアプリを自作することで、音声処理への理解の向上と、自分だけのオリジナルな設定を盛り込むことができる自作の優位さを生かした、声質変化のバリエーションもいくつも作りこんでみました。
開発環境の構築
Windows上で動作するアプリケーションの開発環境としては、大きく分けて以下のようなものが考えられます。
- Windows+MinGW
- Linux+MinGW
今回は、開発環境の再現性の容易さと、仮想環境を用いることで簡単に無料で環境構築が行えるという利点から、あえて、Linux(Ubuntu)上でWindows上で動作するボイチェン(リアルタイムオーディオコンバーター)を開発します。
Linux上でWindowsアプリケーションを開発
あえてLinux上でWindows上で動作可能な実行ファイルをクロスコンパイルする関係で、以下のようなメリットとデメリットが伴います。
Linux上で開発環境を構築するメリット
- 開発環境の再現性
- 操作のバックワード互換性と再利用性
- 技術屋にとってなじみのある環境
- 環境構築が容易
Linux上で開発環境を構築するデメリット
- クロスコンパイル設定の手間
- CLIに馴染みの無い場合
開発環境の構築方法
Githubで共有しているコード内の「environment/environment.sh」内に環境構築のためのコマンドをまとめているので、スクリプトを実行するだけで環境が自動的に構築されます。
また、Dockerイメージを作成するためのDockerfileもありますので、Dockerで仮想環境を作成して、クロスコンパイルすることも可能です。
環境構築の大まかな手順としては以下の流れです。
- クロスコンパイルのため「MinGW」インストール
- 「CMake」を用いたWindows用のビルド自動化環境構築
- 「toolchain.cmake」の記述
- 必要なライブラリおよび、ヘッダーファイルのインストール
- コードの記述
- 「CMakeLists.txt」の記述
- コンパイル
開発環境構築の詳細については、以下にまとめていますのでそちらをご覧ください。
関連するライブラリ等とその説明
ボイスチェンジャーを実装していくうえで使用したライブラリ等について、まとめておきます。実際にコーディングしていくのは、以下図のAudio I/O Libraryの内側になります。
以下に挙げるライブラリやソフトウェアは基本的にすべて無料で使うことができます。
音声の入出力(PortAudio)
リアルタイムのオーディオ処理用のオープンソースライブラリである、PortAudioを用います。マイクなどの音声入力用インターフェースや、ヘッドフォンやスピーカー等の音声出力用インターフェースなどのハードウェアオーディオインターフェースから、音声を取得したり、出力したりすることができる関数を用いることができます。
以下によると、PortAudioはオープンソースのクロスプラットフォームオーディオI/Oライブリで、Windows、Macintosh、Unixなどのプラットフォームで利用でき、C/C++でプログラムを作成し、実行することができるとしています。
このライブラリを用いることで、プログラム内でWindowsの「サウンド設定」などで設定している、出力と入力のデバイスを標準入出力として選択し、処理する音声の取得および、処理した音声の出力が可能です。
使用例とその詳細については、処理の説明の章で後述します。
音声の処理(SoundTouch)
ボイスチェンジャーの中心の実装部分である、音声処理部分は「SoundTouch」というライブラリを用います。
SoundTouchは、オープンソースのライブラリでタイムストレッチや、ピッチシフトなどを行うことができます。このように、オーディオのピッチを調節したり、テンポや再生速度を変更したりすることで、簡単に入力した音声を変換することが可能です。
音声処理ライブラリの候補
音声信号処理を行うライブラリには、以下のような候補があります。
ライブラリ名 | 説明 |
---|---|
SoundTouch |
|
Rubber Band |
|
World |
|
STRAIGHT |
|
HTS |
|
RubberBandなどの他の音声処理のためのライブラリと比較しても、動作が比較的軽く、リアルタイム処理向きだとされているため、今回はSoundTouchを選びました。
自作の強みとして基本的にPortAudioで音声の入出力を行い、音声変換処理部のみほかのライブラリや、自作処理に置き換えるという方法も取ることができます。
インターフェース(VB-Audio Virtual Cable)
自作したアプリケーションで処理した音声出力をZoomやSteamなどに入力するには、仮想オーディオデバイスをコンピュータ内に構築する必要があります。そこで、今回は「VB-Audio Virtual Cable」を用いています。
これによって、以下図のようにコンピュータ内で仮想的にケーブルの取り回しを行い、アプリケーションどうしを接続することが可能になります。
出典:https://vac.muzychenko.net/en/index.htm
※このソフトウェアは、Windows上にインストールするものであって、今回自作しているプログラム中に含まれるものではありません。
ボイスチェンジャー処理の概要
ここでは、具体的な処理の詳細についてまとめていきます。コードすべてについて説明していると長いので、コードの全体ではなく、ボイスチェンジャーを実装するうえで特に注目すべき点に焦点を当ててまとめています。
コード全文は、以下のGitHub上で公開しているmian.cpp内をご覧ください。コードは全部で500行にも満たないので、読んだ方が早いかもしれません。散らかっているなりにコメントをマメに入れているので、なんとなく雰囲気は掴めると思います。
以下のまとめを読んでから、コードを見るとより要点が掴みやすいかと思います。
PortAudioのI/O設定
音声の入出力デバイスを取得して、設定を行っています。
入力はLRの区別のある特殊なステレオマイクや、いわゆるバイノーラルマイクなどを使っていない限り、ヘッドセットなどではモノラルインプットとして、出力は、スピーカーやヘッドフォン、イヤホンなどではステレオになると思われます。
C/C++:
// 入出力デバイス
PaStreamParameters inputParameters;
PaStreamParameters outputParameters;
// PortAudioストリームを指定
PaStream *stream;
// PA_APIエラーコード格納
PaError err;
//PA_APIの初期化
err = Pa_Initialize();
// デフォルト入力デバイス取得
inputParameters.device = Pa_GetDefaultInputDevice();
inputParameters.channelCount = 1; /* モノラルインプット */
inputParameters.sampleFormat = PA_SAMPLE_TYPE;
inputParameters.suggestedLatency = Pa_GetDeviceInfo( inputParameters.device )->defaultLowInputLatency;
inputParameters.hostApiSpecificStreamInfo = NULL;
// デフォルト出力デバイス取得
outputParameters.device = Pa_GetDefaultOutputDevice();
outputParameters.channelCount = 2; /* ステレオアウトプット */
outputParameters.sampleFormat = PA_SAMPLE_TYPE;
outputParameters.suggestedLatency = Pa_GetDeviceInfo( outputParameters.device )->defaultLowOutputLatency;
outputParameters.hostApiSpecificStreamInfo = NULL;
11行目の「Pa_GetDefaultInputDevice()」でWindowsで設定しているデフォルトの入力デバイスを取得して、17行目の「Pa_GetDefaultOutputDevice()」でデフォルトの出力デバイスを取得します。
このとき、Windows側の設定で出力先を「VB-Audio Virtual Cable」として、Zoomなどのアプリケーション側の設定で入力先を「VB-Audio Virtual Cable」とすることで、この自作ボイチェンアプリと、任意のアプリケーションを接続することができます。
SoundTouchのパラメータ設定
SoundTouchライブラリで音声を処理するときに使われる、各種パラメータを設定しています。これらのパラメータを適切に設定することで、声を高くしたり、低くしたり、ピッチやテンポの調節が可能です。
また、処理に用いるバッファ長などのパラメータを変更することで、処理の高速化ないしはスムーズな音声変換などにも対応させることができます。
C/C++:
soundTouch.setChannels(2); // 入力チャンネル数
soundTouch.setSampleRate(SAMPLE_RATE); // サンプルレート(Hz)
soundTouch.setTempo(*tempo); // テンポ:ピッチに影響(def:1)
soundTouch.setTempoChange(*tempo_ch); // テンポ:変化率指定,相対的な変化量(def:1)
soundTouch.setPitch(*pitch); // ピッチ:変化率を倍率指定:倍速で再生され高低変化
soundTouch.setPitchSemiTones(*pitchsemitone); // ピッチ:半音単位指定:音の高さ変化量を半音数指定:(正->高,負->低)
soundTouch.setRateChange(*rate_ch); // 再生速度:倍率指定,ピッチや音質に影響しない(def:1)
soundTouch.setRate(*rate); // 再生速度指定:ピッチや音質に影響(def:1)
soundTouch.setSetting(SETTING_USE_AA_FILTER, *aa_filter); // エイリアシングノイズ除去(0無効/1有効)
soundTouch.setSetting(SETTING_USE_QUICKSEEK, *quick_seek);// クイックシーク(0/1,無効/有効)(品質犠牲)
soundTouch.setSetting(SETTING_SEQUENCE_MS, *sequencems); // 内部バッファ長(小->処理遅延小)(リソース大)
soundTouch.setSetting(SETTING_SEEKWINDOW_MS, *seekwind); // シークウィンドウ長(大->精度向上)(処理遅延増)
soundTouch.setSetting(SETTING_OVERLAP_MS, *overlap); // オーバーラップ長(大->滑らか/遅延)
パラメータ | デフォルト | 簡易説明 |
---|---|---|
setTempo() | 1.0f |
|
setTempoChange() | 1.0f |
|
setPitch() | 1.0f |
|
setPitchSemiTones() | 0.0f |
|
setRateChange() | 1.0f |
|
setRate() | 1.0f |
|
SETTING_USE_AA_FILTER | 0 (無効) |
|
SETTING_USE_QUICKSEEK | 1 (有効) |
|
SETTING_SEQUENCE_MS | 0 (自動設定) |
|
SETTING_SEEKWINDOW_MS | 0 (自動設定) |
|
SETTING_OVERLAP_MS | 8 |
|
※適切なパラメータの設定が必要
I/Oコールバック関数
アプリケーションのメイン処理部の説明に入ります。処理の根幹だと言っても、まったく複雑な処理はありません。
コールバック関数呼び出し
ストリームを開いて、8行目でコールバック関数を呼び出しています。9行目に任意の値や配列、構造体のポインタを渡すことで、コールバック関数に変数を渡すことができます。
C/C++:
err = Pa_OpenStream(
&stream // ストリーム情報格納
, &inputParameters // 入力デバイスの設定
, &outputParameters // 出力デバイスの設定
, SAMPLE_RATE // サンプリング周波数
, FRAMES_PER_BUFFER // 1回バッファリングあたりのフレーム数
, 0 // ストリームオプション:デフォルト0
, ioCallback // 音声入出力イベントが発生するたび呼び出される関数
, NULL // コールバック関数に渡されるデータのポインタ
);
コールバック関数内部処理
先ほどからコールバック関数と言っているこの関数内で入力バッファから音声の取得をしてコンバージョン(音声変換)処理を行い、その後、音声を出力バッファに書き出すという一連の流れを記述します。
C/C++:
static int ioCallback(
const void *inputBuffer // 入力バッファのポインタ
, void *outputBuffer // 出力バッファのポインタ
, unsigned long framesPerBuffer // バッファあたりフレーム数
, const PaStreamCallbackTimeInfo* timeInfo // ストリームの現在の時刻
, PaStreamCallbackFlags statusFlags // ストリームの状態を示す型のビットマスク
, void *userData // 引数で渡された任意のユーザデータポインタ
) {
SAMPLE *out = (SAMPLE*)outputBuffer; // オーディオサンプル取得用
const SAMPLE *in = (const SAMPLE*)inputBuffer; // オーディオサンプル読み取り用
unsigned int i;
(void) timeInfo; /* Prevent unused variable warnings. */
(void) statusFlags;
(void) userData;
/* inputステレオ変換 */
SAMPLE stereo_in[framesPerBuffer*2];
for (i=0; i<framesPerBuffer; i++) {
stereo_in[2*i] = in[i];
stereo_in[2*i+1] = in[i];
}
/* 入力バッファからデータを読み取り */
soundTouch.putSamples(stereo_in, framesPerBuffer);
/* 出力バッファにデータを書き込む */
soundTouch.receiveSamples(out, framesPerBuffer);
// 処理が成功したかどうかチェック
if (soundTouch.numSamples() == 0) {
for (i=0; i<framesPerBuffer*2; i++) {
out[i] = 0;
}
}
// 入出力処理継続
return paContinue;
}
2、3行目のバッファーがそれぞれ入出力に用いるバッファ(データの一時格納場所)で、inputBufferに入力してきた音声が書き込まれ、それを取り出して処理した後にoutputBufferに書き出すことで、音声の入出力が可能になります。
ここで取得する音声はモノラルで、出力する音声はステレオなので、16~20行目で一度、入力音源をステレオ音源に書き換えています。具体的には、ステレオ音源は音声が配列中に「左、右、左、右...」の順で入っているので、2倍の大きさの配列を用意して、片側の音しか持たないモノラル音源の配列から同じ音を2回連続した番地(左右)に割り当てています。
21~24行目では、ボイスチェンジャーの主要な機能として、音声の変換を行い、出力に書き込んでいます。ここで、音声の変換に使用されるパラメータは上のSoundTouchのパラメータ設定で決めたものが使用されます。
最後に、処理が成功したかどうかを26行目でチェックして、成功していたらreturnに進み終了。失敗していたら、出力バッファに0を書き込むようにしています。
返り値を「paContinue」とすることで、これらの処理が継続して実行されます。
以上、コードポイント説明になりました。実際の動作例については、また別途記事でまとめます。
ボイチェン実装における今後の課題
ひとまず、動作するものができましたが、改善点として以下のような課題が見えました。
処理速度の課題
意識しないと気にならない程度ですが、若干出力される音にぷつぷつ間、途切れがあるようにも感じるので、パラメータを調節する、ダブルバッファや並列処理を用いて処理を高速化するなどの工夫をすると、より良いかもしれないなと思います。
また、音声信号処理のような、データがストリームで入力されてくる処理は、今回のようにソフトウェアで処理するより、ハードウェアを用いて処理を行った方が高速に処理可能だと考えられるため、FPGAなどを用いたハードウェアボイスチェンジャーも検討できればよいですが、、FPGAなどの価格も考慮すると、できるだけソフトウェア実装で頑張りたいところです。
クオリティの課題
ナチュラルな音声の変換、特に声質の性別変換に自然なパラメータの選択と処理が難しいので、「World」といった周波数包括変換が容易なライブラリを用いることを検討しても良いかもしれないです。
参考:
https://ddddakyl.blogspot.com/2017/03/ubuntu-studioportaudio.html
http://2ten1ryu.blog90.fc2.com/blog-entry-21.html
以上、ありがとうございました。