SHAREVOX core の API を探る(3)OpenAL
藤田昭人
昨日の「マクラでモタつくとその日のうちに書き上げるのは難しい」を教訓に…
毎年、僕を講義に呼んでくれる吉田さんが 先日の僕が担当した講義の様子をブログ( https://ndsi.kyo2.jp/d2022-12-18.html )で紹介してくれました。 でも、この「音声チャットボット」について 僕が語り始めるとまたまた収まらなくなるので…
昨日の t2s の続きを進めます。
sharevox_tts が生成するデータ
昨日の記事 でも書きましたが、 せっかく対話インターフェースをつけたのに 読み上げてくれないと興醒めなので、 WAVデータを再生する機能が欲しいところ。
そこで、 まずはWAVデータを生成する sharevox_tts の出力を見てみました。 次のソースコード(抜粋)の箇所です。
result = sharevox_tts(text.c_str(), speaker_id, &output_binary_size, &output_wav); if (result != SHAREVOX_RESULT_SUCCEED) { std::cout << sharevox_error_result_to_message(result) << std::endl; return 1; } std::cout << "音声ファイル保存中..." << std::endl; std::ofstream wav_file(OUTPUT_WAV_NAME, std::ios::binary); wav_file.write(reinterpret_cast<const char*>(output_wav), output_binary_size); sharevox_wav_free(output_wav);
コードからもわかるように、 どうやら (ヘッダーを含む) WAVデータが丸々メモリー上に 格納されているようです。 で、音声データを再生する 類似の実装を幾つかチェックしてみたのですが、 データを一旦ファイルに格納し 音声データを再生するコマンド (macOSの場合はafplayとか) を起動する実装が一般的でした。 なんかイマイチなやり方ですねぇ…
WAVファイルの再生方法
そこでオンメモリのWAVデータを (ファイルに格納する事なく) 再生できるオープンソースを 探してみました。 いずれもWAVファイルを再生する ツールだったのですが、 オンメモリ・データの再生に 改造しやすそうな次のサンプルを見つけました。
このプログラムには元ネタがあるようです*1。
WAVファイルのヘッダーの情報などが きっちり定義されていて、 最初の入力ファイルの読み込み部分さえ 外せば上手くいきそう…と思ったのですが、 作業してみたらちょっと上手くないところがありました。 これは後述の「play.cpp」のところで説明します。
OpenAL
上記のサンプルは OpenAL なるライブラリの利用を仮定しています。
例によって、詳細は Wikipedia から…
どうやら、ゲームプログラミングでは著名なライブラリのようです。 オリジナルを開発した Loki Software は既に廃業しているようで、今は次の団体が引き継いでるとか…
でも、オープンソース版は 次のサイトからダウンロードできるようです*2。
MacBook を使っている僕の場合、 例によってホームブリューで インストールするのが簡単ですね。
t2s を喋らせる実装
今回は 昨日の記事 に対するアップデートです。
CMakeLists.txt
CMakeLists.txt の修正箇所は次のとおりです。
$ diff -u CMakeLists.txt.orig CMakeLists.txt --- CMakeLists.txt.orig 2022-12-21 10:42:15.000000000 +0900 +++ CMakeLists.txt 2022-12-10 23:26:14.000000000 +0900 @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.16) project(T2S) set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_FLAGS "-Wno-deprecated-declarations -Wno-writable-strings") +message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") find_path(PARENT NAMES "sharevox_core-0.1.2" PATHS "../..") message(STATUS "PARENT: ${PARENT}") @@ -8,6 +10,9 @@ message(STATUS "TOP_DIR: ${TOP_DIR}") find_library(CORE_LIB NAMES core PATHS "${TOP_DIR}/core" PATH_SUFFIXES lib) message(STATUS "CORE_LIB: ${CORE_LIB}") +set(OPENAL_DIR "/usr/local/opt/openal-soft") +message(STATUS "OPENAL_DIR: ${OPENAL_DIR}") -add_executable(t2s t2s.cpp) -target_link_libraries(t2s ${CORE_LIB} readline) +link_directories("${OPENAL_DIR}/lib") +add_executable(t2s t2s.cpp play.cpp) +target_link_libraries(t2s ${CORE_LIB} readline openal) $
G++ のコンパイル・オプションを追加し、 OpenAL のライブラリもリンク対象に含めました。
t2s.cpp
t2s.cpp は内部コマンドを幾つか追加しました。
$ diff -u t2s.cpp.orig t2s.cpp --- t2s.cpp.orig 2022-12-21 10:42:34.000000000 +0900 +++ t2s.cpp 2022-12-11 23:36:11.000000000 +0900 @@ -2,26 +2,28 @@ #include <string.h> #include <stdlib.h> -#include "../core/src/core.h" +extern void play(uint8_t *output, int size); -#define MODEL "../../model" -#define OPENJTALK_DIC "../open_jtalk_dic_utf_8-1.11" -#define OUTPUT_WAV_NAME "audio.wav" + +#include "../core/src/core.h" void -init() +init(const char *model, const char *dic) { SharevoxResultCode result; - if (!initialize(MODEL, false)) { + printf("model: %s\n", model); + if (!initialize(model, false)) { printf("coreの初期化に失敗しました\n"); exit(1); } else { printf("coreの初期化に成功しました\n"); } - printf("openjtalk辞書の読み込み中..."); - result = sharevox_load_openjtalk_dict(OPENJTALK_DIC); + //printf("metas: %s\n", metas()); + + printf("dic: %s\n", dic); + result = sharevox_load_openjtalk_dict(dic); if (result != SHAREVOX_RESULT_SUCCEED) { printf("\n%s\n", sharevox_error_result_to_message(result)); exit(1); @@ -75,6 +77,27 @@ } +// wave format + +void +show(uint8_t *wav, int wsiz) +{ + printf("%d bytes\n\n", wsiz); + printf("RIFF: %c%c%c%c\n", wav[0], wav[1], wav[2], wav[3]); + printf("size: %d\n", *((int32_t *) &wav[4])); + printf("WAVE: %c%c%c%c\n", wav[8], wav[9], wav[10], wav[11]); + printf(" fmt: %c%c%c%c\n", wav[12], wav[13], wav[14], wav[15]); + printf("wFormatLength: %d\n", *((int32_t *) &wav[16])); + printf("wFormatTag: %d\n", *((int16_t *) &wav[20])); + printf("nChannels: %d\n", *((int16_t *) &wav[22])); + printf("nSamplesPerSec: %d\n", *((int32_t *) &wav[24])); + printf("nAvgBytesPerSec: %d\n", *((int32_t *) &wav[28])); + printf("nBlockAlign: %d\n", *((int16_t *) &wav[32])); + printf("wBitsPerSample: %d\n", *((int16_t *) &wav[34])); + printf("ov_data: %c%c%c%c\n", wav[36], wav[37], wav[38], wav[39]); + printf("ov_datasize: %d\n", *((int32_t *) &wav[40])); +} + // readline #include <readline/readline.h> @@ -92,8 +115,12 @@ return(-1); } +#define MODEL "../../model" +#define OPENJTALK_DIC "../open_jtalk_dic_utf_8-1.11" +#define OUTPUT_WAV_NAME "audio.wav" + int -main() +main(int argc, char *argv[]) { char *line = NULL; int n; @@ -101,9 +128,16 @@ SharevoxResultCode result; int64_t speaker_id = 0; int output_binary_size = 0; - uint8_t *output_wav = nullptr; + uint8_t *output_wav = NULL; - init(); + char *model = MODEL; + char *dic = OPENJTALK_DIC; + + if (argc > 1) { + model = argv[1]; + } + + init(model, dic); while (1) { line = readline("> "); @@ -120,17 +154,29 @@ break; } } + if (n == 3) { - printf("[ja] %s\n", line); if (generate(line, speaker_id, &output_binary_size, &output_wav) < 0) exit(1); - if (save(OUTPUT_WAV_NAME, output_wav, output_binary_size) < 0) exit(1); - sharevox_wav_free(output_wav); + if (output_wav != NULL) play(output_wav, output_binary_size); + //printf("# %d bytes\n", output_binary_size); + } else if (n == 1) { + if (strcmp(line, "save") == 0) { + if (save(OUTPUT_WAV_NAME, output_wav, output_binary_size) < 0) exit(1); + sharevox_wav_free(output_wav); + output_wav = NULL; + } else if (strcmp(line, "show") == 0) { + if (output_wav != NULL) show(output_wav, output_binary_size); + } else if (strcmp(line, "p") == 0 || + strcmp(line, "play") == 0) { + if (output_wav != NULL) play(output_wav, output_binary_size); + } } else { printf("[%d] %s\n", n, line); } + add_history(line); free(line); } $
追加した内部コマンドについては後述します。
play.cpp
さて、本稿のホットポイントの play.cpp です。 関数 play はOpenAL を使って WAVデータを再生する関数です。 関数の前半では WAVヘッダーから情報を取り出し、 後半では OpenAL で再生しています。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <OpenAL/al.h> #include <OpenAL/alc.h> #define WAVH_HEADER_SIZE 44 #define WAVH_CHANNELS_MONO 1 #define WAVH_CHANNELS_STEREO 2 #define WAVH_SAMPLINGRATE_CD 44100 #define WAVH_BITSPERSAMPLE8 8 #define WAVH_BITSPERSAMPLE16 16 ALenum getFormat(int wavChannels, int wavBit) { ALenum format; if(wavChannels == WAVH_CHANNELS_MONO){ if(wavBit == 8) { format = AL_FORMAT_MONO8; }else if(wavBit == 16) { format = AL_FORMAT_MONO16; } }else if(wavChannels == WAVH_CHANNELS_STEREO){ if(wavBit== 8){ format = AL_FORMAT_STEREO8; }else if(wavBit == 16) { format = AL_FORMAT_STEREO16; } } return format; } typedef struct tag_wav_header { int RIFF; // 'R','I','F','F' int size; // size of wave file from here on int WAVE; // 'W','A','V','E' int fmt; //'f','m','t',' ' int wFormatLength; // The length of the TAG format short wFormatTag; // should be 1 for PCM type ov_data short nChannels; // should be 1 for MONO type ov_data int nSamplesPerSec; // should be 11025, 22050, 44100 int nAvgBytesPerSec; // Average Data Rate short nBlockAlign; // 1 for 8 bit ov_data, 2 for 16 bit short wBitsPerSample; // 8 for 8 bit ov_data, 16 for 16 bit int ov_data; // 'd','a','t','a' int ov_datasize; // size of ov_data from here on unsigned char data[0]; } wav_header ; const int WAVH_RIFF = 0x46464952; // "RIFF" const int WAVH_WAVE = 0x45564157; // "WAVE" const int WAVH_FMT = 0x20746D66; // "'fmt" const int WAVH_OV_DATA = 0x61746164; // "ov_data" const int WAVH_WFORMATLENGTH = 16; const short WAVH_WFORMATTAG_PCM = 1; void play(uint8_t *output, int size) { wav_header *hp = (wav_header *) output; printf("### play\n"); //printf("RIFF: 0x%x\n", hp->RIFF); if (hp->RIFF != WAVH_RIFF) { printf("NOT match riff\n"); return; } //printf("size: %d\n", hp->size); //printf("WAVE: 0x%x\n", hp->WAVE); if (hp->WAVE != WAVH_WAVE) { printf("NOT match wave\n"); return; } //printf(" fmt: 0x%x\n", hp->fmt); if (hp->fmt != WAVH_FMT) { printf("NOT match fmt\n"); return; } //printf("wFormatLength: %d\n", hp->wFormatLength); //printf("wFormatTag: %d\n", hp->wFormatTag); if (hp->wFormatTag != WAVH_WFORMATTAG_PCM) { printf("wFormatTag should be 1\n"); return; } short wavchannels = hp->nChannels; // nSamplesPerSec(サンプリング周波数)と // nAvgBytesPerSec(1秒あたりのバイト数)の違いを説明 printf("hp->nSamplesPerSec: %d \n", hp->nSamplesPerSec); printf("hp->nAvgBytesPerSec: %d \n", hp->nAvgBytesPerSec); int samplesPerSec = hp->nSamplesPerSec; int byteParSec = hp->nAvgBytesPerSec; //printf("byteParSec: %d \n", byteParSec); short blockAlign = hp->nBlockAlign; //printf("blockAlign: %d \n", blockAlign); short bitsParSample = hp->wBitsPerSample; //printf("bitsParSample: %d \n", bitsParSample); printf("ov_datasize: %d\n", hp->ov_datasize); //printf("ov_data: 0x%x\n", hp->ov_data); if (hp->ov_data != WAVH_OV_DATA) { printf("NOT match ov data\n"); return; } int wavChannels = wavchannels; int wavBit = bitsParSample; int wavSize = hp->ov_datasize; int wavSamplingrate = samplesPerSec; printf("wavChannels: %d \n", wavChannels); printf("wavBit: %d \n", wavBit); printf("wavSize: %d \n", wavSize); printf("wavSamplingrate: %d \n", wavSamplingrate); //int time_playback = (float)wavSize / (float)(4*wavSamplingrate); int playback_ms = ((float)wavSize / (float)byteParSec) * 1000.0F; printf("playback_ms: %d msec \n", playback_ms); unsigned char *data = hp->data; ALuint source; ALuint buffer; ALCdevice *device = alcOpenDevice(NULL); if (!device) { printf("alcOpenDevice Faild\n"); return; } ALCcontext *context = alcCreateContext(device, NULL); if (!context) { printf("alcCreateContext Faild\n"); return; } alcMakeContextCurrent(context); alGenSources (1, &source); alGenBuffers(1, &buffer); ALenum format = getFormat(wavChannels, wavBit); alBufferData(buffer, format, data, wavSize, wavSamplingrate); alSourcei(source, AL_BUFFER, buffer); alSourcePlay(source); //printf("alSourcePlay \n"); int time_count; for (time_count = playback_ms; time_count > 0; time_count--) { usleep(1000); } alDeleteBuffers(1, &buffer); alDeleteSources(1, &source); }
OpenAL は OpenGL と よく似たインターフェースなんだそうですが、 要は関数 alSourcei でPCMデータ(WAVデータ)を ハードウェアにセットし、 関数 alSourcePlay で音声再生をキックする… デバイスドライバーみたいなコードを書くようです。 音声再生自体はハードウェアで実行されるのですが、 ソフトウェアから見れば バックグラウンドで 実行されているように見えます。 この時、ソフトウェアは データの再生時間を計算して ウェイトループに入ります。 再生時間が過ぎると 関数 alDeleteBuffers をコールして ハードウェアの音声再生を停止します。
で、前述のオリジナル・サンプルの 問題点ですが、ひとつは nSamplesPerSec(サンプリング周波数)とnAvgBytesPerSec(1秒あたりのバイト数) の意味を取り違えてるように思える事、 もうひとつは再生時間の単位を1秒にしている事です。 そのためオリジナル・サンプルのコードのままだと WAVデータを最後まで再生せずに 処理を終了してしまいます。 (つまり尻切れトンボ状態になります) そこで再生待ちのループをミリ秒単位に変更しました。 音声の場合、ミリ秒ぐらいまで 人間は聞き分けてしまいますからね。
ビルドと実行
ビルドの手順は 昨日の記事 と変わりません。
$ cd sharevox_core-0.1.2/t2s/ $ ls CMakeLists.txt play.cpp model t2s.cpp open_jtalk_dic_utf_8-1.11 $ rm -rf build $ mkdir build $ cd build $ cmake .. -- The C compiler identification is AppleClang 13.0.0.13000029 -- The CXX compiler identification is AppleClang 13.0.0.13000029 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- CMAKE_CXX_FLAGS: -Wno-deprecated-declarations -Wno-writable-strings -- PARENT: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL -- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL/sharevox_core-0.1.2 -- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL/sharevox_core-0.1.2/core/lib/libcore.dylib -- OPENAL_DIR: /usr/local/opt/openal-soft -- Configuring done -- Generating done -- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/05_T2S_OpenAL/sharevox_core-0.1.2/t2s/build maverick:build fujita$ cmake --build . [ 33%] Building CXX object CMakeFiles/t2s.dir/t2s.cpp.o [ 66%] Building CXX object CMakeFiles/t2s.dir/play.cpp.o [100%] Linking CXX executable t2s [100%] Built target t2s $
但し、t2s ディレクトリには SHAREVOX の公式リリースに 添付されている model ディレクトリも配置しました*3。この公式リリースの model ディレクトリには 4キャラクターの声が格納されています。
次に実行方法なんですが…
昨日の記事 でも説明したように、 起動後は初期化処理のため20秒弱待たされます。 また内部コマンドは1バイト文字(=英字)、 3バイト文字(≒漢字)は発話テキストと 認識されることは変わりません。
さらにこのバージョンでは次の model を選択できるようにしました。 コマンド起動時に第1引数として model ディレクトリのパスを受け付けます。 デフォルト(引数なし)では "../../model" が指定されます。 上記の build ディレクトリからだと "sharevox_core-0.1.2/model" が指定され SHAREVOX core に付属する 男性の声のみが収録されています。 引数に "../model" を指定すると "sharevox_core-0.1.2/t2s/model" 前述のSHAREVOX公式リリースの 4声が発声できるようになります。
音が聞こえないので あまり意味はないですが コマンドのログは以下のとおりです。
$ ./t2s model: ../../model coreの初期化に成功しました dic: ../open_jtalk_dic_utf_8-1.11 終了 > こんにちは 音声生成中...終了: 69504 bytes ### play hp->nSamplesPerSec: 48000 hp->nAvgBytesPerSec: 96000 ov_datasize: 69460 wavChannels: 1 wavBit: 16 wavSize: 69460 wavSamplingrate: 48000 playback_ms: 723 msec > q exit $ ./t2s ../model model: ../model coreの初期化に成功しました dic: ../open_jtalk_dic_utf_8-1.11 終了 > こんにちは 音声生成中...終了: 69848 bytes ### play hp->nSamplesPerSec: 48000 hp->nAvgBytesPerSec: 96000 ov_datasize: 69804 wavChannels: 1 wavBit: 16 wavSize: 69804 wavSamplingrate: 48000 playback_ms: 727 msec > q exit $
…ということで
ようやく t2s を喋らせることができました。 明日は t2s の最後のお色直しとして、 model ディレクトリに収録されている キャラクターの一覧を表示する機能などを 追加します。 あと、t2s が動いている動画も 貼り付けるよう頑張ってみるつもりです。
それではお約束の…
#つくよみちゃんを利用してフォロワー増やしたい
以上