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
$



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

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

でも…

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

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

以上