SHAREVOX core の API を探る(4)jasson

Explore the SHAREVOX core API (4) jasson


2022/12/22
藤田昭人


5日間連続投稿の4日目です。(あと2日)

この記事を今日初めてみた人のために…

この記事は5回シリーズで、 12/15 に京都ノートルダム女子大学で 僕が行った講義のために用意した ちょっとしたツールの作成過程について紹介しています。 なお、毎年僕を講義に呼んでくれる吉田智子先生が 先日の僕が担当した講義の様子をブログ( https://ndsi.kyo2.jp/d2022-12-18.html )で紹介してくれます。ご参考まで…

では、昨日に続き、対話コマンド t2s の話を続けます。


対話形式で喋ってくれるようにはなったのだが…

昨日の記事 でもチラッと触れましたが、 Model ディレクトリを変更して出てきた女性の声は おそらく「小春音アミ」の声なんですよね。

そもそも SHAREVOXの公式リリースには 次の4キャラクター6声が収録されています。

SHAREVOXとキャラクター

たしか「つきよみちゃん」は speaker_id = 4 のはず…

で、 月曜日の記事 に書いたAPI一覧の中から 「登録キャラクター&ボイスの情報を取得できるAPI」 を探したら metas が見つかりました。 でもこれ、戻り値の文字列は JSON なんですよね…


C言語で JSON を扱うには

普通 JSON と言えば 僕も迷わず JavaScript を使うんですが、 今回はC言語縛りなので…

C/C++ 用の JSON ライブラリは多数出回ってますが、 今回は jansson を使うことにしました。

jansson.readthedocs.io

…というのもマニュアルがしっかりしてそうだったから (英語だけど…)。 で、マニュアルを読み始めたのだけど、 jansson の Github Rep をチラ見したらこんなのが見つかった。

github.com

これは jansson のリリースに付属するサンプルなんですが、 対話形式の汎用 JSON パーサーです。 今回は metas の情報一覧を表示するだけなので 「これは丁度いい」 とばかりに、 コードをガシガシ書き換えて実装したのが print_metas.cpp です(後述します)。


収録されているキャラクターを一覧する実装

例によって修正したソースコードは diff の出力を掲載しています。

CMakeLists.txt
$ diff -u CMakeLists.txt.orig CMakeLists.txt
--- CMakeLists.txt.orig 2022-12-22 18:06:36.000000000 +0900
+++ CMakeLists.txt  2022-12-13 10:23:24.000000000 +0900
@@ -13,6 +13,7 @@
 set(OPENAL_DIR "/usr/local/opt/openal-soft")
 message(STATUS "OPENAL_DIR: ${OPENAL_DIR}")

-link_directories("${OPENAL_DIR}/lib")
-add_executable(t2s t2s.cpp play.cpp)
-target_link_libraries(t2s ${CORE_LIB} readline openal)
+include_directories(/usr/local/include)
+link_directories("${OPENAL_DIR}/lib" /usr/local/lib)
+add_executable(t2s t2s.cpp play.cpp print_metas.cpp)
+target_link_libraries(t2s ${CORE_LIB} readline openal jansson)
$ 
t2s.cpp
$ diff -u t2s.cpp.orig t2s.cpp
--- t2s.cpp.orig    2022-12-22 18:06:56.000000000 +0900
+++ t2s.cpp 2022-12-13 14:19:22.000000000 +0900
@@ -1,9 +1,10 @@
 #include <stdio.h>
+#include <ctype.h>
 #include <string.h>
 #include <stdlib.h>

 extern void play(uint8_t *output, int size);
-
+extern void print_table(const char *text);

 #include "../core/src/core.h"

@@ -16,19 +17,17 @@
   if (!initialize(model, false)) {
     printf("coreの初期化に失敗しました\n");
     exit(1);
-  } else {
-    printf("coreの初期化に成功しました\n");
   }

   //printf("metas: %s\n", metas());
-
+  //print_table(load_json(metas()));
+  //print_table(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);
-  } else {
-    printf("終了\n");
   }
 }

@@ -38,13 +37,13 @@
 {
   SharevoxResultCode result;

-  printf("音声生成中...");
+  //printf("音声生成中...");
   result = sharevox_tts(text, speaker, output_size, output);
   if (result != SHAREVOX_RESULT_SUCCEED) {
      printf("\n%s\n", sharevox_error_result_to_message(result));
      return(-1);
   }
-   printf("終了: %d bytes\n", *output_size);
+   //printf("終了: %d bytes\n", *output_size);
   return(0);
 }

@@ -115,7 +114,7 @@
   return(-1);
 }

-#define    MODEL       "../../model"
+#define    MODEL       "../model"
 #define    OPENJTALK_DIC   "../open_jtalk_dic_utf_8-1.11"
 #define    OUTPUT_WAV_NAME "audio.wav"

@@ -161,17 +160,25 @@
                    &output_binary_size,
                    &output_wav) < 0) exit(1);
       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 (isnumber(line[0])) {
+        speaker_id = line[0] - '0';
+        printf("speaker id: %lld\n", speaker_id);
+      } else if (strcmp(line, "i") == 0 ||
+                 strcmp(line, "id") == 0) {
+        printf("speaker id: %lld\n", speaker_id);
+      } else if (strcmp(line, "l") == 0 ||
+                 strcmp(line, "list") == 0) {
+        print_table(metas());
+      } else 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);
+                 strcmp(line, "play") == 0) {
+        if (output_wav != NULL) play(output_wav, output_binary_size);
       }
     } else {
       printf("[%d] %s\n", n, line);
$ 
play.cpp
$ diff -u play.cpp.orig play.cpp
--- play.cpp.orig   2022-12-22 18:07:17.000000000 +0900
+++ play.cpp    2022-12-11 23:49:05.000000000 +0900
@@ -62,7 +62,7 @@
 {
   wav_header *hp = (wav_header *) output;

-  printf("### play\n");
+  //printf("### play\n");
   //printf("RIFF: 0x%x\n", hp->RIFF);
    if (hp->RIFF != WAVH_RIFF) {
        printf("NOT match riff\n");
@@ -89,8 +89,8 @@
    short wavchannels   = hp->nChannels;
    // nSamplesPerSec(サンプリング周波数)と
    // nAvgBytesPerSec(1秒あたりのバイト数)の違いを説明
-   printf("hp->nSamplesPerSec: %d \n", hp->nSamplesPerSec);
-   printf("hp->nAvgBytesPerSec: %d \n", hp->nAvgBytesPerSec);
+   //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);
@@ -99,7 +99,7 @@
   short bitsParSample = hp->wBitsPerSample;
    //printf("bitsParSample: %d \n", bitsParSample);

-  printf("ov_datasize: %d\n", hp->ov_datasize);
+  //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");
@@ -111,14 +111,14 @@
   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);
+  //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);
+   //printf("playback_ms: %d msec \n", playback_ms);
    unsigned char *data = hp->data;

    ALuint source;
$ 

要はデバッグ用のプリントを止めただけです。 あとはキャラクターの一覧表示と選択のために 内部コマンドを追加しました (内部コマンドについては後述)。


こちらが、本稿のホットポイントの print_metas.cpp です。 既に述べたように jansson のリリースに付属する simple_parse.c を 手直しして作成しました。

/*
 * print_metas
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <jansson.h>


json_t *load_json(const char *text) {
    json_t *root;
    json_error_t error;

    root = json_loads(text, 0, &error);

    if (root) {
        return root;
    } else {
        fprintf(stderr, "json error on line %d: %s\n", error.line, error.text);
        return (json_t *)0;
    }
}

void
print_style(json_t *style, const char *primary)
{
  size_t size = json_object_size(style);
  const char *key;
  int id = -1;
  json_t *value;
  char secondary[8192];

  json_object_foreach(style, key, value) {
    if (strcmp(key, "name") == 0) {
      strcpy(secondary, json_string_value(value));
    } else if (strcmp(key, "id") == 0) {
      id = json_integer_value(value);
    }
  }
  if (id > -1) printf("%d: %s  %s\n", id, primary, secondary);
}

void
print_styles(json_t *styles, const char *primary)
{
  if (json_typeof(styles) != JSON_ARRAY) return;
  size_t i;
  size_t size = json_array_size(styles);
  for (i = 0; i < size; i++) {
    json_t *child = json_array_get(styles, i);
    print_style(child, primary);
  }
}

void
print_line(json_t *object)
{
  size_t size = json_object_size(object);
  const char *key;
  json_t *value;
  char primary[8192];
  
  json_object_foreach(object, key, value) {
    if (strcmp(key, "name") == 0) {
      strcpy(primary, json_string_value(value));
    } else if (strcmp(key, "styles") == 0) {
      print_styles(value, primary);
    }
  }
}

void
print_table(const char *text)
{
  size_t i, size;
  json_t *root = load_json(text);
  if (root == NULL) return;
  if (json_typeof(root) != JSON_ARRAY) return;
  size = json_array_size(root);
  for (i = 0; i < size; i++) {
    json_t *child = json_array_get(root, i);
    print_line(child);
  }
}

#if 0
#include <fcntl.h>

#define M1     1048576
#define DEF_PATH    "../sharevox_core-0.1.2/t2s/model/official/metas.json"

int
main()
{
  char buff[M1];

  int fd = open(DEF_PATH, O_RDONLY);
  if (fd < 0) exit(1);
  int n = read(fd, buff, M1);
  if (n < 0) exit(1);

  json_t *root = load_json(buff);
  if (root) {
    //print_json(root);
    print_table(root);
    json_decref(root);
  }

  close(fd);
  exit(0);
}
#endif

ちなみに末尾の '#if 0' の中身は print_metas.cpp を単体で動かすときの main ルーチンです。 SHAREVOX の Model ディレクトリにある metas.json (APIの metas を呼び出した時に 返ってくる JSON 文字列はこのファイルの内容では?) の内容を表示してくれます。


ビルド

昨日までと同様に cmake でビルドします。

$ cd sharevox_core-0.1.2/t2s/
$ 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/06_T2S_JSON
-- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/06_T2S_JSON/sharevox_core-0.1.2
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/06_T2S_JSON/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/06_T2S_JSON/sharevox_core-0.1.2/t2s/build
maverick:build fujita$ cmake --build .
[ 25%] Building CXX object CMakeFiles/t2s.dir/t2s.cpp.o
[ 50%] Building CXX object CMakeFiles/t2s.dir/play.cpp.o
[ 75%] Building CXX object CMakeFiles/t2s.dir/print_metas.cpp.o
[100%] Linking CXX executable t2s
[100%] Built target t2s
$ 


実行

いよいよ完成が近づいたので内部コマンドをちょっと整理しました。

さらに内部コマンドは次のとおりです。

  • 数字1文字:スピーカーIDを設定します。
  • id(i):現在設定されているスピーカーIDを表示します。
  • list(l):現在の model に収録されているキャラクタとスピーカーIDを表示します。
  • save:直前に発話したメッセージを audio.wav に格納します。
  • show:直前に発話したメッセージの WAV ヘッダー情報を表示します。
  • play(p):直前に発話したメッセージを再度発話します。
  • quit、exit(q):t2s を終了します。

まぁ「キャラクタとスピーカーIDの表示」「スピーカーIDの設定」 「発話メッセージ」「終了」ぐらいしか使わないと思いますが…

$ ./t2s
model: ../model
dic: ../open_jtalk_dic_utf_8-1.11
> l
0: 小春音アミ  ノーマル
1: 小春音アミ  喜び
2: 小春音アミ  怒り
3: 小春音アミ  悲しみ
4: つくよみちゃん  ノーマル
5: 白痴ー  ノーマル
6: 開発者  ノーマル
> 4
speaker id: 4
> こんにちは
> つくよみちゃんです
> q
exit
$



ようやく「つくよみちゃん」が喋ってくれました。

#つくよみちゃんを利用してフォロワー増やしたい

でも…

授業ではデモしなかったんです。 いや、できなかったってできなかったって言ったほうが良いかも。

その理由は…明日、説明します。

以上

SHAREVOX core の API を探る(3)OpenAL

Explore the SHAREVOX core API (3) OpenAL


2022/12/21
藤田昭人


昨日の「マクラでモタつくとその日のうちに書き上げるのは難しい」を教訓に…

毎年、僕を講義に呼んでくれる吉田さんが 先日の僕が担当した講義の様子をブログ( 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ファイルを再生する ツールだったのですが、 オンメモリ・データの再生に 改造しやすそうな次のサンプルを見つけました。

github.com

このプログラムには元ネタがあるようです*1

WAVファイルのヘッダーの情報などが きっちり定義されていて、 最初の入力ファイルの読み込み部分さえ 外せば上手くいきそう…と思ったのですが、 作業してみたらちょっと上手くないところがありました。 これは後述の「play.cpp」のところで説明します。


OpenAL

上記のサンプルは OpenAL なるライブラリの利用を仮定しています。
例によって、詳細は Wikipedia から…

ja.wikipedia.org

どうやら、ゲームプログラミングでは著名なライブラリのようです。 オリジナルを開発した Loki Software は既に廃業しているようで、今は次の団体が引き継いでるとか…

www.openal.org

でも、オープンソース版は 次のサイトからダウンロードできるようです*2

openal-soft.org

MacBook を使っている僕の場合、 例によってホームブリューで インストールするのが簡単ですね。

formulae.brew.sh


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);
}

OpenALOpenGL と よく似たインターフェースなんだそうですが、 要は関数 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 が動いている動画も 貼り付けるよう頑張ってみるつもりです。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:付属のメモランダムから拾い出した URL は次のページでした。

tips.hecomi.com

こちらはオリジナルの OpenJTalk+HTS を対象にしているようです。

*2:どういう事情かよく知りません。

*3:以前、紹介したと思いますが、 次の Github の SHAREVOX 0.1.0 のリポジトリ

github.com

…に格納されている sharevox_model-0.1.0.zip というZIPファイルです。

このファイルを解凍すると model ディレクトリが生成されますので sharevox_core-0.1.2/t2s/ に格納してください。

SHAREVOX core の API を探る(2)ReadLine

Explore the SHAREVOX core API (2) ReadLine


2022/12/20
藤田昭人


5日間連続投稿の2回目、 今回はテクニカル(?)なトピックから始めます。

実はこの春先まで僕は、 音声合成を含む機械学習アルゴリズムの実行には GPU を使った実行環境が必須だと思い込んでいました。 なので昨年、シンデレラの童話読み聞かせを作ったときも Chrome で標準的に使える Web Speech API を迷わず使っていました。 噂によればこの API は 裏で Google音声認識音声合成が 動いているのだとか… 「まぁ、そう言うもんなんだろう」 という認識だったのです。 っていうかぁ… 無料の割には音声認識の精度が高かったので 結構満足していた訳で、 難点はブラウザ内蔵のAPIの面倒くささ*1と、 あと iPhone では音声認識が動かない*2ぐらいかな? と思っていたのです。

ところが、 夏の椎間板ヘルニアで 寝たきりになっている時期、 本当にやれることが無くて 暇つぶしに スマホ相手にググりまくっている時期に たまたま見つけたのが VOICEVOX だった訳です*3

…って語ってると 明日のマクラがなくなるので、 本日はこの辺で止めて 本題に入ります。


【注】昨日の diff に誤りがありました

昨日の記事 の simple_tts.cpp の修正箇所に漏れがありました。 正しくはこちら。

$ diff -u ../example/cpp/unix/simple_tts.cpp simple_tts.cpp
--- ../example/cpp/unix/simple_tts.cpp  2022-10-28 17:21:34.000000000 +0900
+++ simple_tts.cpp  2022-12-20 13:35:13.000000000 +0900
@@ -2,7 +2,7 @@
 #include <iostream>
 #include <string>

-#include "../../../core/src/core.h"
+#include "../core/src/core.h"

 #define OUTPUT_WAV_NAME "audio.wav"

@@ -17,7 +17,7 @@

   std::cout << "coreの初期化中..." << std::endl;

-  if (!initialize("../../../model", false)) {
+  if (!initialize("../model", false)) {
     std::cout << "coreの初期化に失敗しました" << std::endl;
     return 1;
   }
# 

修正前のコードだと model ディレクトリのパスが違うので初期化でコケます。
それから OpenJTalk の辞書も simple_tts ディレクトリに コピーしておく必要があります。

関連ファイルが正しく配置できると…

$ ls
CMakeLists.txt          open_jtalk_dic_utf_8-1.11
build               simple_tts.cpp
maverick:simple_tts fujita$ ./build/simple_tts 'こんにちは'
coreの初期化中...
openjtalk辞書の読み込み中...
音声生成中...
音声ファイル保存中...
音声ファイル保存完了 (audio.wav)
maverick:simple_tts fujita$ ls
CMakeLists.txt          open_jtalk_dic_utf_8-1.11
audio.wav           simple_tts.cpp
build
$ 

見てのとおり audio.wav が生成されます。


Simple_TTS は遅い

さて、 この simple_tts は WAV ファイルを生成するだけ (再生はしてくれない) のも困りものですが、 とにかく実行が遅い。

実行時間を測ってみると…

$ time -p ./build/simple_tts 'こんにちは'
coreの初期化中...
openjtalk辞書の読み込み中...
音声生成中...
音声ファイル保存中...
音声ファイル保存完了 (audio.wav)
real 18.64
user 18.95
sys 0.40
$ 

なんと 18 秒もかかっています。 「ひょっとして初期化がノロいのでは?」 と考えたので次のようなプログラムを 書いてみました。

$ cat c.cpp
#include "../core/src/core.h"

int
main()
{
  initialize("../model", false);
  finalize();
}
$ g++ -o ccc c.cpp -Wl,-rpath,../core/lib ../core/lib/libcore.dylib
ld: warning: dylib (../core/lib/libcore.dylib) was built for newer macOS version (11.7) than being linked (11.0)
$ time -p ./ccc
real 18.88
user 17.97
sys 0.38
$ 

このプログラムは初期化処理(initialize)と 終了処理(finalize)だけを実行する2行のプログラムです。 ld コマンドが出力する waring は忘れてもらうとして…

どうやら simple_tts がノロいのは model ファイルを取り込む 初期化処理に非常に時間がかかっているようです。 言い換えれば WAVファイルを生成する sharevox_tts の実行はそれほど時間を要しない。

であれば…

起動時に初期化処理を行い、 その後は対話形式で text-to-speach を実行する 対話型のプログラムを作れば良いことになります。


コマンド t2s

…という事で、 simple_tts にちょこっとコードを追加して 対話型のコマンドにすることにしました。 名付けて t2s 。もちろん爺さん趣味です😁

GNU ReadLine

対話型インターフェースといえば GNU ReadLine です。詳細は Wikipedia を見てもらうとして…

ja.wikipedia.org

要はシェルのライン・エディティング機能を 提供してくれるライブラリーでして、 今回のように UTF-8 のコード入力が必須の場合 これをかました方が安全なんです。 ヒストリー機能も使えるしね。

Mac の場合、 インストールはホームブリューを使うのが 一番お手軽です。次のページに簡単な解説が 書いてあるので見てもうとして…

qiita.com

このページに掲載されている サンプルコードと simple_tts.cpp を ドッキングすることにしました。 (詳細は後述の「ソースコード」の項を参照)


CMakeLists.txt

コマンド t2s のディレクトリは 昨日の記事 の simple_tts ディレクトリの隣におくことにしました。 なので CMakeLists.txt は昨日作成したものをちょいと修正するだけです。

$ diff -u CMakeLists.txt.orig CMakeLists.txt
--- CMakeLists.txt.orig 2022-12-19 10:33:15.000000000 +0900
+++ CMakeLists.txt  2022-12-20 15:01:02.000000000 +0900
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.16)
-project(SimpleTTS)
+project(T2S)
 set(CMAKE_CXX_STANDARD 11)

 find_path(PARENT NAMES "sharevox_core-0.1.2" PATHS "../..")
@@ -9,5 +9,5 @@
 find_library(CORE_LIB NAMES core PATHS "${TOP_DIR}/core" PATH_SUFFIXES lib)
 message(STATUS "CORE_LIB: ${CORE_LIB}")

-add_executable(simple_tts simple_tts.cpp)
-target_link_libraries(simple_tts ${CORE_LIB})
+add_executable(t2s t2s.cpp)
+target_link_libraries(t2s ${CORE_LIB} readline)
$ 

名前を simple_tts から t2s に変更し、 target_link_libraries に readline を追加しました。


ソースコード

ソースコード t2s.cpp はこちら。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "../core/src/core.h"

#define MODEL       "../../model"
#define OPENJTALK_DIC   "../open_jtalk_dic_utf_8-1.11"
#define OUTPUT_WAV_NAME "audio.wav"

void
init()
{
  SharevoxResultCode result;

  if (!initialize(MODEL, false)) {
    printf("coreの初期化に失敗しました\n");
    exit(1);
  } else {
    printf("coreの初期化に成功しました\n");
  }

  printf("openjtalk辞書の読み込み中...");
  result = sharevox_load_openjtalk_dict(OPENJTALK_DIC);
  if (result != SHAREVOX_RESULT_SUCCEED) {
    printf("\n%s\n", sharevox_error_result_to_message(result));
    exit(1);
  } else {
    printf("終了\n");
  }
}

int
generate(const char *text, int64_t speaker, 
         int *output_size, uint8_t **output)
{
  SharevoxResultCode result;

  printf("音声生成中...");
  result = sharevox_tts(text, speaker, output_size, output);
  if (result != SHAREVOX_RESULT_SUCCEED) {
      printf("\n%s\n", sharevox_error_result_to_message(result));
      return(-1);
  }
    printf("終了: %d bytes\n", *output_size);
  return(0);
}

int
save(const char *name, uint8_t *output, int size)
{
  FILE* fp;

  printf("音声ファイル保存中...");
  fp = fopen(name, "wb");
  if (fp == NULL){
      printf("失敗\n");
      return(-1);
  }

  if (fwrite(output, size, 1, fp) < 1 ){
      printf("ファイルへの書き込みに失敗しました。\n");
      return(-1);
  }

  fclose(fp);
  printf("音声ファイル保存完了 (%s)\n", name);
  return(0);
}

void
term()
{
  finalize();
}


// readline

#include <readline/readline.h>
#include <readline/history.h>

int
numMB(char *str)
{
  if ((*str & 0x80) == 0x00) return(1);
  if ((*str & 0xE0) == 0xC0) return(2);
  if ((*str & 0xF0) == 0xE0) return(3);
  if ((*str & 0xF8) == 0xF0) return(4);
  if ((*str & 0xFC) == 0xF8) return(5);
  if ((*str & 0xFE) == 0xFC) return(6);
  return(-1);
}

int 
main()
{
  char *line = NULL;
  int n;

  SharevoxResultCode result;
  int64_t speaker_id = 0;
  int output_binary_size = 0;
  uint8_t *output_wav = nullptr;
  
  init();

  while (1) {
    line = readline("> ");
    if (line == NULL || strlen(line) == 0) {
      free(line);
      continue;
    }
    n = numMB(line);
    if (n == 1) {
      if (strcmp(line, "q") == 0 ||
          strcmp(line, "quit") == 0 ||
          strcmp(line, "exit") == 0) {
            free(line);
            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);
    } else {
      printf("[%d] %s\n", n, line);
    }
    add_history(line);
    free(line);
  }

  printf("exit\n");
  term();
  exit(0);
}

前述のとおり、 サンプルコードと simple_tts.cpp を シンプルにドッキングした内容です。 ミソは int numMB(char *str) なる関数。 この関数はマルチバイト文字のバイト数を判定します。 返り値が1なら1バイト文字(=英字)、 返り値が3なら3バイト文字(≒漢字)で、 t2s の対話インターフェースでは 3バイト文字から始まる文字列はWAVファイルを生成するテキスト、 1バイト文字から始まる文字列は t2s の内部コマンドと判定しています。 もっとも、このコードでは内部コマンドは exit/quit/q の1つだけです。


ビルドと実行

トップディレクトリの直下に t2s ディレクトリを作成し、 上記の open_jtalk_dic_utf_8-1.11 と CMakeLists.txt と t2s.cpp を作成したら、 昨日の記事 とほぼ同様の手順でビルドができます。

$ rm -r 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
-- PARENT: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine
-- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2/core/lib/libcore.dylib
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/04_T2S_ReadLine/sharevox_core-0.1.2/t2s/build
$ cmake --build .
[ 50%] Building CXX object CMakeFiles/t2s.dir/t2s.cpp.o
[100%] Linking CXX executable t2s
[100%] Built target t2s
$ 
$ ./t2s
coreの初期化に成功しました
openjtalk辞書の読み込み中...終了
> こんにちは
[ja] こんにちは
音声生成中...終了: 69504 bytes
音声ファイル保存中...音声ファイル保存完了 (audio.wav)
> q
exit
$ 



予想したとおり、一度 initialize を実行して model をロードすると、 sharevox_tts の実行は短時間です。 が、やはり WAV ファイルの再生機能がないと t2s コマンドは役に立ちそうにありません。

明日は WAV ファイルの再生機能を紹介します。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:クラッキング防止のためなんだそうですが…

*2:Apple vs Google の構図なんでしょうかね?

*3:正直に答えましょう。 僕が見たのはこちら…

www.youtube.com

そもそもニコ動に縁のなかった僕には この手の動画は当然ノーマークで… だって、こんなところから テクニカルな情報を拾えるなんて思ってなかったもん。

でも…

結局これが 「唯一ヘルニアになって良かったこと」 になりました😀

SHAREVOX core の API を探る(1)CMake

Explore the SHAREVOX core API (1) CMake


2022/12/19
藤田昭人


ここ数年は、 この時期になると ゲスト・スピーカーの準備に 追われるのが常で、 特に去年は例の音声チャットボットの プロトタイプ作成に追い立てられる 日々を過ごしてました。 今年は夏の3ヶ月完全休業を 取り返していたこともあって 「去年と同じメニューで授業」 と秋口あたりから相談してました。なので 「今年は平穏な年末を過ごせそうだ」 と鷹を括っていたのですが…

やはり今年も悪夢からは逃れられなかった😀

実は、昨年の実習教材が突如動かなくなったのでした*1。 その対応に追われて授業準備の予定は総崩れ。 おかげでブログの続きを公開するのが だいぶん遅れてしまいました。

さて…

今回から数回、 SHAREVOX core を使ったプログラミングを紹介していきます。 実は授業で 「ちょっとしたデモぐらい見せたい」 とコードと ブログの下書きを 書き溜めていたのです。 が、結構な分量になってしまったので トピックごと小出しして行くことにしました。


SHAREVOX core の API

まずはSHAREVOX core の API をざっくり概観していきます。 SHAREVOX core のREADME.mdによれば、 「APIは core/src/core.h を見ろ」と書いてますが、中身は doxygenフォーマットの インターフェース情報でした。 関数名だけ抜き書きすると…

関数名 説明
1 initialize 初期化する
2 load_model モデルをロードする
3 is_model_loaded モデルがロード済みかどうか
4 finalize 終了処理を行う
5 metas メタ情報を取得する
6 supported_devices 対応デバイス情報を取得する
7 variance_forward 音素ごとの音高と長さを求める
8 decode_forward 波形を求める
9 last_error_message 最後に発生したエラーのメッセージを取得する
10 sharevox_load_openjtalk_dict open jtalkの辞書を読み込む
11 sharevox_tts text to spearchを実行する
12 sharevox_tts_from_kana text to spearchをAquesTalkライクな記法で実行する
13 sharevox_wav_free voicevox_ttsで生成した音声データを開放する
14 sharevox_error_result_to_message エラーで返ってきた結果コードをメッセージに変換する

…と、音声合成に関わりがありそうな 14エントリーが定義されていました。

付属するサンプルソースexample/cpp/unix/simple_tts.cpp を眺めると、このうち initialize, sharevox_load_openjtalk_dict, sharevox_tts, sharevox_wav_free, finalize を呼び出しているようです。 このプログラムは比較的フラットで SHAREVOX core の API を探るための もっとも単純なサンプルとして 活用できそうです。


SimpleTTS のディレクトリを作り直す

早速 simple_tts.cpp を手直しして ライブラリーの挙動を確認することにしたのですが…

このプログラムは見ての通り ディレクトリ・ネストの深いところにあり( 前回 の手順を実行した方はご存知のとおり) ビルドするためには関連ライブラリーを ローカルディレクトリに コピーしてくる必要があります。


CMakeLists.txt を書いてみる

そこでトップディレクトリ直下に 新たなディレクトリを作り、 関連ライブラリーを移動することなく ビルドできるように CMakeLists.txt を作成します。

初心者向けのサンプルを探して いろいろチェックしてみたのですが、 Wikipedia 英語版のCMakeのページにある Example に掲載されている次のコードが 一番シンプルでわかりやすい例のようです。

// hello.cpp
#include <iostream>

int main()
{
    std::cout << "Hello, world!\n";
}

という C++ のコードをビルドするための CMakeLists.txt は次のようになります。

cmake_minimum_required(VERSION 3.5)
project(HelloWorld CXX)
add_executable(hello hello.cpp)

この記法、年寄りの僕には かつて書いていた BSD UnixMakefile を彷彿させます。 等価な Makefile を書くと、 確かこんな感じ…

PROG= hello
SRCS=  hello.c

ということで CMake によるビルド手順の記述方法を 感覚的に理解したところで simple_tts.cpp のために僕が用意したのは以下の CMakeLists.txt です。

cmake_minimum_required(VERSION 3.16)
project(SimpleTTS)
set(CMAKE_CXX_STANDARD 11)

find_path(PARENT NAMES "sharevox_core-0.1.2" PATHS "../..")
message(STATUS "PARENT: ${PARENT}")
set(TOP_DIR "${PARENT}/sharevox_core-0.1.2")
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}")

add_executable(simple_tts simple_tts.cpp)
target_link_libraries(simple_tts ${CORE_LIB})

要は SHAREVOX core のライブラリの格納位置は トップディレクトリから相対パスが決まっているとの 仮定の元に find_path なるコマンドで トップディレクトリを探しに行っています。あと message(STATUS, "XXX") なる記法は、かつての echo "XXX" と等価と考えれば良いみたい。


新しいディレクトリで simple_tts をビルドしてみる

CMakeLists.txt が用意できたら simple_tts をビルドしてみましょう。

【注】ターミナルソフトで UNIX コマンドを使ってください。

  • まずは 前回 紹介した手順で SHAREVOX core のビルドツリーを用意し、 ビルドしてください。
$ ls sharevox_core-0.1.2/core/lib
core.h              libonnxruntime.dylib
libcore.dylib           libopenjtalk.a
libonnxruntime.1.10.0.dylib
$ 

上記のように core/lib にライブラリのバイナリが確認できればOKです。

$ cd sharevox_core-0.1.2
$ mkdir simple_tts
$ cd simple_tts
$ vi CMakeLists.txt
<中略>
$ cp ../example/cpp/unix/simple_tts.cpp .
$ vi simple_tts.cpp
<中略>
$ diff -u ../example/cpp/unix/simple_tts.cpp simple_tts.cpp
--- ../example/cpp/unix/simple_tts.cpp  2022-10-28 17:21:34.000000000 +0900
+++ simple_tts.cpp  2022-12-20 13:35:13.000000000 +0900
@@ -2,7 +2,7 @@
 #include <iostream>
 #include <string>

-#include "../../../core/src/core.h"
+#include "../core/src/core.h"

 #define OUTPUT_WAV_NAME "audio.wav"

@@ -17,7 +17,7 @@

   std::cout << "coreの初期化中..." << std::endl;

-  if (!initialize("../../../model", false)) {
+  if (!initialize("../model", false)) {
     std::cout << "coreの初期化に失敗しました" << std::endl;
     return 1;
   }
$ 

トップディレクトリで simple_tts を作成し、 前述の CMakeLists.txt と simple_tts.cpp を置きます。 simple_tts.cpp はオリジナルをコピーしてきますが、 上記の通り、インクルードするヘッダーファイルのパスを変更してください。

$ 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
-- PARENT: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake
-- TOP_DIR: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake/sharevox_core-0.1.2
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake/sharevox_core-0.1.2/core/lib/libcore.dylib
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/03_T2S_CMake/sharevox_core-0.1.2/simple_tts/build
$ cmake --build .
[ 50%] Building CXX object CMakeFiles/simple_tts.dir/simple_tts.cpp.o
[100%] Linking CXX executable simple_tts
[100%] Built target simple_tts
$ ls
CMakeCache.txt      Makefile        simple_tts
CMakeFiles      cmake_install.cmake
$ 

これも 前回 と同じ手順になります。


ずいぶん丁寧な CMakeLists.txt の解説になっちゃいましたが…

元々 cmake コマンドは X Window の imake や GNU の autoconf など UNIX の make コマンドの Makefile を自動生成するコマンドだったはずで、 今では大幅に機能強化されていて「覚えるのが大変」なコマンドですが、 オリジナルの記法に立ち戻れば案外スラッと書けるなぁ…感慨しているところです😀

ちなみに、件の授業は 12/15 に終わってまして、 相方の吉田さんに「今回のブログは連続5日間連続投稿」と 啖呵を切ってしまったので、一発目はこのへんで…
明日もがんばります。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:蓋を開ければ大したことではなかったのですが、 やはり最初にみつけた時は衝撃的だったのは確か😀

SHAREVOX core をビルドしてみる

Try to build SHAREVOX core


2022/10/30
藤田昭人


前回YouTube の話から始めましたが…

今回はテレビの話。 完全に昭和世代の僕は 今もってテレビ依存の生活をしています。 最近、朝のニュースを付けてると 「1ドル=150円」だとか、 「マクドナルドが立て続けに値上げした」 とか、果ては 「ロシアに爆撃されたウクライナの街角」 の映像まで連日見せられています。 ティーンエイジャーの諸君にも 「円安」や「戦争」の話はピンと来なくても 「マクドナルドの値上げ」は あまり経験のない 実感的な話ではないでしょうか?

確かに、僕のような老人にも、 昨年と一昨年のパンデミックは未知の体験で、 正直かなりビビっていたのですが…

3年目のコロナ禍にもどこか慣れてしまった 今年の印象はだいぶん違ったものになりました。 例えば「円安」。 僕らは子供の頃 「1ドル=360円」 をリアルタイムで経験しているので、 若い方々ほどは衝撃的には受け止めません*1。 このところ毎日ウクライナから届く 「爆撃された街角の映像」 にも既視感があります*2。 こういった子供の頃の生活に根ざした体験は いつまでも記憶に残るものなんですね。

今年は…

なにやら50年前に舞い戻ったような 不安げな社会の空気を感じ取りつつ…
このブログでは子供の頃感じた 楽しげな感覚に突き動かされている気が しています😁


またまたファイルをダウンロードする。

さて…

本稿でのお題は SHAREVOX core を ソースコードからビルドします。 今回も僕が MacBook Pro (Retina, 15-inch, Mid 2014)/macOS Big Sur verion 11.7 で実行した手順を紹介します。 ちなみにビルドには cmake 3.20.2 を使いました*3

まずはライブラリのソースとバイナリのファイルをダウンロードするところから。 任意の作業ディレクトリに移動してください。

今回も SHAREVOX Core 0.1.2 を使用しました。次のファイルをダウンロードします。

今回はビルドするので、 ダウンロードするのはソースだけ。

次に ONNX Runtime v1.10.0 のライブラリをダウンロードします。

これも前回とおなじ。

一方、Open JTalk のソースコードは VOICEVOX のリポジトリからダウンロードします*4

このリポジトリはリリース・タグが設定されていないので、 上記の URL から「Code」「 ZIP ファイルをダウンロード」を指定してください。

さらに Open JTalk の辞書セット(UTF-8)をダウンロードします。

これは simple_tts を動かす際に必要になります。

いずれも前回同様、 作業ディレクトリの下の archive ディレクトリにダウンロードして、 その場でアーカイブファイルを 展開することにします。


ビルドのためのソースツリーを構築する

README.md の コアライブラリのビルド で紹介されている手順通りに エラー無しでビルドするには、 ソースツリーの構築に注意する必要があります。

【注】ターミナルソフトで UNIX コマンドを使ってください。

  • SHAREVOX Core 0.1.2のソースツリーを archive ディレクトリの隣に作る
$ cd <作業ディレクトリ>
$ cp -r archive/sharevox_core-0.1.2 .
$ 
  • sharevox_core-0.1.2/onnxruntimeをコピー
$ cp -r archive/onnxruntime-osx-universal2-1.10.0 sharevox_core-0.1.2/onnxruntime
$ 
  • open_jtalkを移動
$ cp -r archive/open_jtalk-1.11 sharevox_core-0.1.2/open_jtalk
$ 
  • core/src/engine/openjtalk.cpp を次のように修正
    text2mecabの引数に "8192" を追加します。
$ cd sharevox_core-0.1.2/core/src/engine/
$ cp openjtalk.cpp openjtalk.cpp.orig
$ diff -u openjtalk.cpp.orig openjtalk.cpp
--- openjtalk.cpp.orig  2022-10-28 17:26:57.000000000 +0900
+++ openjtalk.cpp       2022-10-28 17:27:13.000000000 +0900
@@ -15,7 +15,7 @@
 namespace voicevox::core::engine {
 std::vector<std::string> OpenJTalk::extract_fullcontext(std::string text) {
   char buff[8192];
-  text2mecab(buff, text.c_str());
+  text2mecab(buff, 8192, text.c_str());
   Mecab_analysis(&mecab, buff);
   mecab2njd(&njd, Mecab_get_feature(&mecab), Mecab_get_size(&mecab));
   njd_set_pronunciation(&njd);
$ 

これで、README.md の説明どおりに ビルドできます。


SHAREVOX core をビルドする

作業ディレクトリに戻って…

SHAREVOX core の README.md の コアライブラリのビルド の項目の手順にしたがってビルドします。

$ cd sharevox_core-0.1.2
$ mkdir build
$ cd build
$ cmake ..

<中略>

Required HTSEngine not found
-- Configuring done
-- Generating done

$ cmake --build . --config Release
[  2%] Building C object open_jtalk/src/CMakeFiles/openjtalk.dir/jpcommon/jpcommon.c.o

<中略>

[ 97%] Building CXX object core/CMakeFiles/core.dir/src/engine/synthesis_engine.cpp.o
[100%] Linking CXX shared library libcore.dylib
[100%] Built target core
$ 

ご存知のように、 この "cmake .." がビルドするための Makefile を用意するプロセスで、 続く "cmake --build . --config Release" が (実際にソースをコンパイルする) ビルドのプロセスです。

最後に "cmake --install ." で 所定のディレクトリにインストールすると…

$ cmake --install .

<中略>

$ cd .. 
$ ls sharevox_core-0.1.2/core/lib/
core.h              libonnxruntime.dylib
libcore.dylib           libopenjtalk.a
libonnxruntime.1.10.0.dylib
$

それまで存在しなかった lib ディレクトリが作成されて、 ビルドされたバイナリ・ライブラリがコピーされます。


SHAREVOX core をテストしてみる

さらに README.md の コアライブラリのビルド の「C++のテスト実行」の手順を実行してみましょう。

$ cmake -S . -B test_build -DBUILD_TEST=YES

<中略>

[100%] Linking CXX executable unit_test
dyld: Library not loaded: @rpath/libonnxruntime.1.10.0.dylib
  Referenced from: 02_sharevox_core/sharevox_core-0.1.2/test_build/tests/unit_test
  Reason: no suitable image found.  Did find:
    02_sharevox_core/sharevox_core-0.1.2/onnxruntime/lib/libonnxruntime.1.10.0.dylib: code signature in (02_sharevox_core/sharevox_core-0.1.2/onnxruntime/lib/libonnxruntime.1.10.0.dylib) not valid for use in process using Library Validation: library load disallowed by system policy
CMake Error at 02_sharevox_core/sharevox_core-0.1.2/test_build/_deps/catch2-src/extras/CatchAddTests.cmake:45 (message):
  Error running test executable
  '02_sharevox_core/sharevox_core-0.1.2/test_build/tests/unit_test':


    Result: Subprocess aborted
    Output: 



make[2]: *** [tests/unit_test] Error 1
make[2]: *** Deleting file `tests/unit_test'
make[1]: *** [tests/CMakeFiles/unit_test.dir/all] Error 2
make: *** [all] Error 2
$ 

テスト用のプログラムをリンクするところでコケます。 これは 前回 の脚注で説明したライブラリの証明書の問題です。 前回同様、 アップルメニューから「システム環境設定」を選択して 「セキュリティとプライバシー」 をクリックし、 ダイアログの「一般」パネルで「このまま許可」選んでください。 次のとおり cmake --build test_build が再実行できます。

$ cmake --build test_build
Consolidate compiler generated dependencies of target openjtalk
[ 31%] Built target openjtalk
Consolidate compiler generated dependencies of target core
[ 38%] Built target core
Consolidate compiler generated dependencies of target Catch2
[ 96%] Built target Catch2
Consolidate compiler generated dependencies of target Catch2WithMain
[ 98%] Built target Catch2WithMain
Consolidate compiler generated dependencies of target unit_test
[ 99%] Linking CXX executable unit_test
[100%] Built target unit_test
$ ctest --test-dir test_build --verbose
Internal ctest changing into directory: 02_sharevox_core/sharevox_core-0.1.2/test_build
UpdateCTestConfiguration  from :02_sharevox_core/sharevox_core-0.1.2/test_build/DartConfiguration.tcl
UpdateCTestConfiguration  from :02_sharevox_core/sharevox_core-0.1.2/test_build/DartConfiguration.tcl
Test project 02_sharevox_core/sharevox_core-0.1.2/test_build
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
test 1
    Start 1: extract_one_character

1: Test command: 02_sharevox_core/sharevox_core-0.1.2/test_build/tests/unit_test "extract_one_character"
1: Test timeout computed to be: 10000000
1: Filters: extract_one_character
1: Randomness seeded to: 3858558281
1: ===============================================================================
1: All tests passed (6 assertions in 1 test case)
1: 
1/1 Test #1: extract_one_character ............   Passed    0.01 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.02 sec
$ 

以上で「C++のテスト実行」は終了です。

この状態で 前回 の「SHAREVOX core のサンプルプログラムをビルドする」の手順を 実行すれば、再び男性の声を聞くことができます。


今回は SHAREVOX core のビルドの手順を紹介しました。 「前回、バイナリライブラリで動かしたのに、何で?」 と思ってらっしゃる方も多いかと…

そもそも僕は そういう無駄な努力が 大好きなヤツですが…*5 *6

ソースからのビルドは コンパイル対象となるソースコードを スキャンしていくプロセスですので、 注意深く観察していれば 対象となるソフトウェアについて より多くの情報が引き出せます。

例えば、今回の SHAREVOX core の場合、 前回 紹介した simple_tts をビルドするためには onnxruntime のライブラリと open_jtalk の辞書ファイルが必要だったので この二つが関連することは分かっていたのですが、 ソースのビルド・プロセスから さらに次のことがわかります。

  • SHAREVOX core は比較的小さなライブラリである
  • 下位で open_jtalk と onnxruntime を呼び出している?
  • 各々はトップディレクトリ以下の次のサブディレクトリに格納されている
  • onnxruntime は公開されているコードを無修正で利用している

…とまぁ、 現時点で分かっているのはこれくらい。

とはいえ、simple_tts でちゃんと WAV ファイルが生成できることが確認できたので、 次は model ディレクトリを差し替えると つくよみちゃんの声に切り替わる理由を 調べてみたいと思っています。

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:「なんで、子供に為替がわかるんだ!!」 とのツッコミがありそうですが…
僕が子供の頃は アメリカやヨーロッパから輸入されるお菓子は とんでもない高級品で、 危篤な人がその年のお歳暮に送ってくれないと、 僕たちの口には入らない代物だったのです😀

*2:僕らはベトナム戦争をリアルタイムで体験した世代、 つまり日常的な報道番組で 戦場の様子を(テレビ越しに) 目撃した世代ですから。

*3:README.md によれば CMake 3.16以上とのことです。

*4:どうやら VOICEVOX/SHAREVOX で使用する Open JTalk のソースコードは 独自に修正されているようです。

*5:実際、中学生の僕といえば…

ゴミ捨て場で拾った 壊れたトランジスタラジオを 再び鳴らそうとハンダゴテを振り回したり…

組立説明書が無くなったプラモデルを引き取って 箱に印刷されている完成写真と睨めっこして、 ちゃんと完成すべく、 ああでもない、 こうでもないと…

無駄なことに労力を注ぎ込む パッとしない変わった趣味の男子でした😁

*6:これが、先日の 「今、16歳の自分に出会ったら、 今の自分は何を語りかけるのか?」 というお題に僕は引っかかり続けている理由でもあります😀

前々回 にもチラッと触れたように、今の僕は 「これから就職を控えた学生諸君に何かをいう」 立場でもあるのですが、 自分の経験から言えることは 「16歳で 自分の人生の目標が定まっている人なんて ほとんどいない」 ということだけなんです。

実際「16歳の僕」がやっていた 周りの人たちには全く理解されない 「無駄な努力」と「自己満足」が (というか「それができる能力」が) その後の僕の人生を切り開いたことは 否定しようがない事実。 だって、僕が16歳の頃は プログラマなんて職業はみんな 知らなかったからね (もちろん親だって)。 プログラマが社会的に広く認知される 職業になったのはそこから10年あまり 経過した後でした。

それに、 当時は周囲から「無駄な努力」と 言われ続けていたことを 「16歳の僕」は何か後ろめたく 感じていたのも本当の話。 なので、今の僕は…

「迷うな。ブレるな。 自分を信じろ。やがて、その力が お前の人生を切り開く時が必ずやってくる!!」

…と彼に語ってやりたいと 思うのが正直なところです。

が、それは単に 僕が運が良かっただけ かもしれない。 これが今の僕の 酷く悩ましい問題 でもあります。

SHAREVOX core を動かしてみる

Try to run SHAREVOX core


2022/10/19
藤田昭人


前回 「楽しげなことを、コツコツと…」と語りましたが…

苦しくなると 「72時間連続コーディング」 とか無意味なパワープレーを 繰り出していたのも今は昔、 最近では MacBook に1時間程度向かうたびに 休憩を入れるようになっています。 休憩と言ってもテレビで YouTube を見るだけなんですけどね。 画面からの距離を長めに取ると 目が少し休まる気がするのです。 リモートワークが定着して良かった良かった。

お気に入りは「YOASOBI 切り抜き」ですね。 間にCMが挿入される回数が少ないので 流しっぱなしにしてます。 YOASOBI というと ボカロPとボーカリストの 「小説を音楽にするユニット」 なんだそうですが、 ふたりはトークも上手なんですねぇ*1。 おかげでポケモンの最近のキャラクターを 知りましたました😀

この映像、 彼らのオールナイトニッポンのDJを短く切って 映像を付けているようです。 オールナイトニッポンと言えば、 僕は「笑福亭鶴光」だとか「あのねのね」だとか、 あるいは「ビートたけし」が リアルタイムの世代なんですが、 彼らのワイルドなトークだけではなく、 AYASE氏とIKURA嬢のマイルドなトークでも 違和感なく聴けるのが不思議なところ。 やはりDJ特有の会話のリズム感が 魅力なんでしょうかねぇ?

さてさて…


SHAREVOX core というライブラリ

音声合成といえば有料サービスが花盛りですが…

本稿では、 前回チラッと触れた SHAREVOX core の動かし方を紹介してみようかと思います。

このライブラリには、 その核になる音声合成に関わる 機械学習のコードが ギュッと詰まっています。 もうひとつの特徴は 機械学習の実行環境を選ばないことです。 実は「機械学習というとGPUを搭載してない コンピュータでは動かない」と思い込んでたのですが、 このライブラリをリンクしたテストプログラムは 手持ちの Intel MacBook Pro でも あっさり動いてしまいました。

このライブラリ 「タダで音声合成をしたい人」 には朗報なんではないでしょうか?

という事で、 やや無謀にも 手持ちの Intel Mac Book Pro を使って ライブラリのバイナリ配布を動かしてみます。 やはり「論より RUN」でしょう😀


ライブラリのソースとバイナリのファイルをダウンロードする

Github においてある SHAREVOX core の ソース には README.md が用意されており、 ライブラリのダウンロード、展開、ビルドの手順が紹介されています。 が、ここでは僕が MacBook Pro (Retina, 15-inch, Mid 2014)/macOS Big Sur verion 11.7 で実行した手順を紹介します。

まずはライブラリのソースとバイナリのファイルをダウンロードするところから。 任意の作業ディレクトリに移動してください。

今回は SHAREVOX Core 0.1.2 を使用しました。次のファイルをダウンロードします。

次に ONNX Runtime v1.10.0 のライブラリをダウンロードします。

さらに Open JTalk の辞書セット(UTF-8)をダウンロードします。

作業ディレクトリの下に archive サブディレクトリを作成し、 以上の4つのファイルをダウンロードして、 各々を展開しました。


SHAREVOX core のサンプルプログラムをビルドする。

元の作業ディレクトリに戻って…
まずは SHAREVOX core のソースツリーを展開します。

先ほどダウンロードしてきたソースの ZIP ファイルを移動、

$ mv archive/sharevox_core-0.1.2 .
$ cd sharevox_core-0.1.2/

あるいはコピーします。

$ cp -r  archive/sharevox_core-0.1.2 .
$ cd sharevox_core-0.1.2/

以降、このソースツリーを作業のベースに、SHAREVOX core の C++ サンプルコード の手順に従って そのほかのバイナリ・ファイルを所定のディレクトリにコピーしていきます*2

【注】ターミナルソフトで UNIX コマンドを使ってください。

前述のダウンロード・展開したバイナリファイルを example/cpp/unix にコピーします。

  • sharevox_core のバイナリ・ライブラリ
$ cp  ../archive/sharevox_core-osx-universal2-cpu-0.1.2/libcore.dylib example/cpp/unix/
  • ONNX Runtime の共有ライブラリ
$ cp -r ../archive/onnxruntime-osx-universal2-1.10.0/lib/*.dylib example/cpp/unix/
$ cp -r ../archive/open_jtalk_dic_utf_8-1.11 example/cpp/unix/

以上のコピーを終えると、 example/cpp/unix/ には 以下のファイル・フォルダが存在することになります。

$ ls example/cpp/unix/ | cat
CMakeLists.txt
README.md
libcore.dylib
libonnxruntime.1.10.0.dylib
libonnxruntime.dylib
open_jtalk_dic_utf_8-1.11
simple_tts.cpp
$ 

C++サンプルプログラムのディレクトリに移動して、 CMake(3.16 以上)を使ったビルドを行います。

  • "cmake -S . -B build" と "cmake --build build" を実行する
$ cd example/cpp/unix/
$ cmake -S . -B build
-- 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
-- CORE_LIB: /Users/fujita/xtr/BookBot/BookBot3/02_sharevox_core/sharevox_core-0.1.2-A/example/cpp/unix/libcore.dylib
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/fujita/xtr/BookBot/BookBot3/02_sharevox_core/sharevox_core-0.1.2-A/example/cpp/unix/build
$ cmake --build build
[ 50%] Building CXX object CMakeFiles/simple_tts.dir/simple_tts.cpp.o
[100%] Linking CXX executable simple_tts
[100%] Built target simple_tts
$ 

これで build/simple_tts がビルドできました。 実行してみましょう*3

$ ls build/simple_tts
$ build/simple_tts これはテストです
coreの初期化中...
openjtalk辞書の読み込み中...
音声生成中...
音声ファイル保存中...
音声ファイル保存完了 (audio.wav)
$ 

引数で渡した日本語のフレーズを 発話した WAV ファイルが生成されます。 Mac Book には GPU が搭載されていないので 「coreの初期化中...」 は少し待たされます。

WAV ファイルが生成されたら 発話させてみましょう。

$ afplay audio.wav
$ 

男性の声で「これはテストです」と 読み上げてくれたでしょうか?


サンプルプログラムでつきよみちゃんに発話させてみる

調べてみると simple_tts は 学習済みの言語モデルを使って 引数で渡されるテキストを読み上げているようです。 そこで言語モデルを差し替えると 声色が変わるのか試してみました。

実は SHAREVOX Core 0.1.0 には SHAREVOX のリリースに付属する言語モデルのセットも収録されてます。

これをダウンロード、展開すると model というフォルダが現れますので、 SHAREVOX Core 0.1.2 の model と差し替えてみます。

元の sharevox_core-0.1.2 ディレクトリに戻って…

$ mv model model-0.1.2
$ mv ../archive/model .
$ 

これで言語モデルの差し替えができました。

さらに simple_tts.cpp の変数 speaker_id を 0 から 4 に変更します。

$ cd example/cpp/unix
$ (エディタで simple_tts.cpp を修正)
$ diff -u simple_tts.cpp.orig simple_tts.cpp
--- simple_tts.cpp.orig 2022-10-19 10:39:11.000000000 +0900
+++ simple_tts.cpp  2022-10-19 10:39:29.000000000 +0900
@@ -34,7 +34,7 @@

   std::cout << "音声生成中..." << std::endl;

-  int64_t speaker_id = 0;
+  int64_t speaker_id = 4;
   int output_binary_size = 0;
   uint8_t *output_wav = nullptr;

$ 

修正が終わったら simple_tts をリビルドしましょう。

$ cd build/
$ make
Consolidate compiler generated dependencies of target simple_tts
[ 50%] Building CXX object CMakeFiles/simple_tts.dir/simple_tts.cpp.o
[100%] Linking CXX executable simple_tts
[100%] Built target simple_tts
$ cd ..
$ 

先ほどと同じように WAV ファイルを生成し、 発話させてみましょう。

$ build/simple_tts これはテストです
coreの初期化中...
openjtalk辞書の読み込み中...
音声生成中...
音声ファイル保存中...
音声ファイル保存完了 (audio.wav)
$ afplay audio.wav
$

いかがでしょう? 女性の声色に変わりましたね。 これがつくよみちゃん( speaker_id = 4 )の声です。 少々早口のような気もしますが…

それではお約束の…

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:YOASOBI については 他にももう少し語りたいのですが、 それはまた後日。

*2:実は Python サンプルコードにもトライしましたが、 うまく実行できませんでした。
ケアレスミスのようなので、 後日リトライしてみるつもりです。

*3:build/simple_tts の初回実行時には次のダイアログが表示されます。

これはリンクした libcore.dylib の証明書がないため表示されます。 その際には、 アップルメニューから「システム環境設定」を選択して 「セキュリティとプライバシー」 をクリックし、 以下のダイアログの「一般」パネルで「このまま許可」選んでください。

このダイアログは libonnxruntime.1.10.0.dylib でも表示されます。 なので、 build/simple_tts を実行するためには 都合2回「このまま許可」を選択することになります。

音声チャットボットの取り組み、あれこれ

Voice chatbot initiatives this and that


2022/10/08
藤田昭人


実に1年ぶり(?)のブログ記事の公開であります*1

本当はもっと早く復帰したかったのですが…

実は椎間板ヘルニアのおかげで、 ほぼ3ヵ月間完全に活動停止に追い込まれてました。 執刀医のコロナ感染とかの不運もあって、結果的に 発症から手術に至るまでに2ヶ月半待ちました。 実際、3ヵ月間も運動を大きく制限されていると、 身体のあちらこちらに支障出てくる訳でして、 今は脚力と体力の回復に勤しんでいる訳です。 もちろん還暦過ぎの老体なので、リハビリは 「腰の様子確かめながらコツコツと」 って感じですかね。


音声チャットボットによるAI体験

という訳で…

今年の僕の夏はとんだ大ブレーキの ロスト・サマーだったのですが、 不運ばかりではなかったのでした。 ちょうど昨年の今頃始めた 音声チャットボットの試みが 作った僕自身の予想を超えて 注目を集めてくれたからです。

実は、数年前から 京都ノートルダム女子大学の 「インターネット社会論」の授業に ゲストスピーカーとして参加させてもらっていて、 AI技術に関するQ&Aをしてきました。 で、毎年出るのが 「このままAI技術が進歩すると 人間にできる仕事は無くなりませんか?」 という質問。 毎年それなりに準備していくのですが、 僕の答えには どうにも納得してもらえてる感じがしないのです。

そこで「コレならどうだ‼️」と 昨年捻り出したのが、 学生の皆さんにチャットボットのルールを 作ってもらう実習授業でした。 学生の皆さんのスマホで簡単に動かせて、 もちろんボットから音声で話しかけてくれて… 授業の内容を説明し始めると長くなるので、 気になる方は次の論文をどうぞ。

conference.ciec.or.jp

一般教養の授業なので、 学生の皆さんの日常に如何に迫れるか? が大きな課題でした。


授業を書き残しておこう

この昨年末に実施した実習授業は 学生のみなさんにも好評だったので、 その場は毎年授業に呼んでくれる 吉田智子さんと喜んだのですが、 そこは大学でのこと。年が明けたら 「実習授業について書き残しておこう」 ってことになりました。

先に紹介したのは、 僕が実習授業のために書き下ろした ウェブ・アプリケーション の概要に関する論文です。 で、吉田さんが書いたのは次の論文。

conference.ciec.or.jp

彼女は情報教育の専門家なので、この時は 「情報工学の僕とはだいぶん趣きが違う」 としか思ってなかったのですが…

そこは大学でのこと。 論文が書けたのなら どこかのカンファレンスに投稿することになる訳で、 その準備をしている6月ごろに 僕の椎間板ヘルニアが発症してしまって… そのあとのこと、 ポスターセッションの原稿の用意とかは、 全部吉田さんがやってくれました*2

論文は8月初旬のカンファレンスで 発表されたらしいのですが (僕はヘルニアでのたうち回ってたので 正直なところわからない) 、なんと優秀賞をもらったそうです。

2022PCカンファレンス受賞者喜びの声 (1) 論文賞 | Special | CIEC

京都ノートルダム女子大学のニュースはこちら。

web.archive.org

いや、この写真は8月末に撮影したのだけど、 コルセットを巻いて、痛みを堪えて、 なんとか作り笑いをしたつもりだったのだけど、 全然笑えてない😁

といった顛末があったので…

論文発表に関わるイベントが全て終わるまでは、 この取り組みをブログで報告できなかったのでした。 しかし、1年もかかるとは思ってなかったなぁ。


今年はどうする?

昨年は実習授業の前日まで 実習教材のコーディングをしているといった ドタバタの有り様だったので 当日の授業は僕が担当をしたのですが、今年は (まだ僕が長時間のコーディングに耐えない状態なので) 昨年の教材をそのまま使って実習授業をすることになってます。

ただ 「読み聞かせる童話は『シンデレラ』から差し替えたい」 という話は(春ごろ、ヘルニア発症前に)相談してました。 というのも『シンデレラ』は次のページの翻訳を使っていたのですが…

www.grimmstories.com

これ、オリジナルの直訳バージョン、 いわゆる「本当は恐ろしいグリム童話」でして、 物語の中にはグロテスクなシーンがたびたび登場します。 で、学生の皆さんが書いたルールを見てると どうしてもそこがひっかかるらしく、 「どう思いますか?」とか「グロいよね?」といった 質問やコメントが増えてしまう…といった傾向がありました。 「これはちょっとどうにかしたい」 というのが僕の意見で、 腰の状態が予想外に早くに改善して、 長時間のコーディングに耐える状態になった時の 努力目標になってます。

で「もし他の童話に差し替えるとしたら?」 という相談もしてたのですが、 吉田さんから出たのは ワイルドの『幸福の王子』。 この童話については 著名な翻訳家の結城浩さんの日本語版が公開されてます。

www.hyuki.com

この翻訳であれば『シンデレラ』のような 童話の本筋以外に関心が集中するような トラブルは出ないように思うのですが… 新たに見つけた課題は、この童話は 「王子とツバメの会話」 が軸となる会話劇であることです。 セリフとセリフの間の説明が少なく、 童話の字面を追っただけでは 誰が語ったセリフなのか? (幼児には)わかりにくい。 (人間の)図書館司書が読み聞かせる時には 声色を変えるのだろうなぁ…と思わせる童話です。

いやぁ 「できの良い音声合成機能さえあれば 簡単に実現できる」 って思っていた 「童話の読み聞かせ」ですが、 実際に読み聞かせをする際に 人間はいろんな事をするんですねぇ。 奥が深いなぁ😀


いくつかの声色を切り替えるには?

ヘルニアの発症当初は (痛くて10〜20分間程度しか椅子に座ってられなくて) ベットに横たわりスマホググるしかやれることがなかったので、 音声合成でいくつかの声色を使い分ける方法を探しまくってました。

当初考えていたのは Web Speech API の SpeechSynthesis のパラメータを変更すること。 この方法だと 確かに声色が変わってることは認識できるのですが、 物語の登場人物のキャラクターまでを 想像するのは難しい感じがしました。

例えば『幸福の王子』の場合、 王子は王子らしい声色で、 ツバメはツバメがイメージできる声色で 読み上げて欲しいところです。 もちろんナレーションの声色とも 識別できなければなりません*3


VOICEVOX と SHAREVOX

何か、うまい手はないものかと さらに探しまくっていたところ、 YouTube で見かけた 「フリーの音声合成ソフト紹介」 的映像をキッカケに 見つけたのが VOICEVOX でした。

voicevox.hiroshiba.jp

Githubソースコードもおいてあります。

github.com

使い方の解説も見つけたので…

voicevox.hiroshiba.jp

1時間ぐらい椅子に座ってられるようになったら ちょっと試してみようと考えたのですが…

突如、SHAREVOX が公開されました。

www.sharevox.app

Github にソースも置いてある。

github.com

…となると、コード比較をしたくなるのが 職業プログラマの性なのですが、 チョロっと見た限りでは SHAREVOX の方が よく整備されているように見えます。

腰の痛みがなく、体調が良ければ、 もう少しガツガツ行きたいところなんだけどなぁ…

止む無く公開されているソースコードスマホ片手にツラツラと眺めてました。特に sharevox_core は、機械学習による音声合成エンジンの Open JTalkニューラルネットワークモデルの交換フォーマットの onnxruntime のラッパーになっているらしく 大変興味深いものです。 今後、ブログ記事が数回は書けそうです。

還暦も過ぎたので、そろそろ 「楽しげなことをコツコツと…」 に路線を変更しなくちゃね😀


最後に・・・

…とまぁ、 ブログを1年間もサボっていた言い訳を 長々と語ってきましたが、 これを復帰の前口上にさせてください。

それと最後にもうひとつだけ!!

京都検定の問題と模範解答を読み上げる 音声チャットボットについてです。

www.kyotokentei.ne.jp

実は昨年の実習授業の知見から、 スマホ+音声チャットボット+学習支援 は面白いテーマだなぁ…と思ってまして、 今年の4月ごろには試験問題と模範解答を 読み上げる音声チャットボットを作るつもりでいました。

でも、試験問題と模範解答の著作権って どうなってるのかわからなくて 「実験で作るだけなのにお金を取られるのはヤダなぁ…」 などと考えていたところ、京都検定を思い出しました。

この検定は京都商工会議所が運営しているのですが、 ツテを頼って聞いてもらったところ 「公平性を期すため、いずれの事業者ともタイアップしない」 との方針なんだとか*4

なので、比較的時間に余裕のある夏の間にシステムを開発して、 2022年の検定試験がある12月の1ヶ月前ぐらいに 公開する計画を立ててました。

残念なことに、 予期せぬロストサマーのために 夏の3ヶ月を棒に振ったので 計画は放棄寸前になっていたのですが…

SHAREVOX のリリースコードの中に 「つくよみちゃん」の音声があったので、 今は「2022年の検定試験には間に合わなくても、 彼女に問題を読み上げてもらいたい」と考え始めています。

tyc.rei-yumesaki.net

ということで、この文章にもハッシュタグを追加しておきます。

#つくよみちゃんを利用してフォロワー増やしたい

以上

*1:はてブmarkdown 記法をすっかり忘れてしまってる。

*2:ちゃんとお礼を言ってなかったから、 この場を借りて 「吉田さん、本当にありがとう」

*3:これ、文字通り 「人間の認知とは何か?」 というお題です。 理詰めで考えると堂々巡りになりそうな。 「ツバメがイメージできる声色」 と書くのは簡単だけど、 実際のツバメは言葉は喋らないですからね。 学習のしようがありません。

*4:つまり昔の大学入試問題と同じ扱いで今も運営しているそうです。

京都では毎年12月に1級、2級、3級の試験がありますが、 試験後は京都新聞に問題と模範回答が掲載されます。

地元では講習会を企画する企業や スマホアプリを開発しているITベンチャーも存在しますが、 みんな京都新聞の掲載情報を見て 勝手にやってるみたいです。