[AquesTalk][Electron] Macで動作するゆっくり系ボイス再生アプリMYukkuriVoiceでAquesTalk10に対応した

いや、正確には、既に作ってあって、

そのアプリでAquesTalk10(https://www.a-quest.com/products/aquestalk.html)の音声再生・音声録画を使えるようにした。



https://github.com/taku-o/myukkurivoice





AquesTalkは、動画などで"ゆっくり"と呼ばれているキャラの声などを出せるライブラリです。

AquesTalk10は、その最新バージョン。

バージョンは上だが、下位のバージョンの機能を全て持っているわけではない。

むしろ、声が違うので、違うライブラリと見た方が自然か。



試しに使ってみたが、AquesTalk10の声は、なんか舌ったらずな感触がある。

自分の調音がイケてないせいだろうか。

少なくとも今までの"ゆっくり"の調音だと不自然感があるかも?

ただ、慣れると気にならなくなる、という話も。





動画で使うようなアプリなので、宣伝には動画が必要になった(断言)


[node.js][javascript] node ffiで構造体を扱う

node ffiで構造体を扱うなら、ref-structを使用する

npm install --save ffi
npm install --save ref
npm install --save ref-struct
var StructType = require('ref-struct');

// 型を定義
var AQTK_VOICE = StructType({
  bas: ref.types.int,
  spd: ref.types.int,
  vol: ref.types.int,
  pit: ref.types.int,
  acc: ref.types.int,
  lmd: ref.types.int,
  fsc: ref.types.int
});
// ポインタ
var ptr_AQTK_VOICE = ref.refType(AQTK_VOICE);

// 構造体作る
var aqtk_voice_val = new AQTK_VOICE;
aqtk_voice_val.bas = bas;
aqtk_voice_val.spd = spd;
aqtk_voice_val.vol = vol;
aqtk_voice_val.pit = pit;
aqtk_voice_val.acc = acc;
aqtk_voice_val.lmd = lmd;
aqtk_voice_val.fsc = fsc;
// ポインタ
ptr_aqtk_voice_val = aqtk_voice_val.ref();

あとはこいつを使って、色々やりとりすれば良い。

var ptr_int   = ref.refType(ref.types.int);
var ptr_uchar = ref.refType(ref.types.uchar);

// AquesTalk10
// unsigned char * AquesTalk_Synthe_Utf8(const AQTK_VOICE *pParam, const char *koe, int *size)
var framework_path = 'path/AquesTalk.framework/Versions/A/AquesTalk';
var ptr_AquesTalk10_Synthe_Utf8 = ffi.DynamicLibrary(framework_path).get('AquesTalk_Synthe_Utf8');
var fn_AquesTalk10_Synthe_Utf8  = ffi.ForeignFunction(ptr_AquesTalk10_Synthe_Utf8, ptr_uchar, [ ptr_AQTK_VOICE, 'string', ptr_int ]);

// AquesTalk_Synthe_Utf8の呼び出し
var alloc_int = ref.alloc('int');
var r = fn_AquesTalk10_Synthe_Utf8(ptr_aqtk_voice_val, text, alloc_int);
var buf_wav = ref.reinterpret(r, alloc_int.deref(), 0);

おわり

Finderからzip圧縮したファイルと、コマンドからzip圧縮したファイルでサイズが違う

調べると、別のアルゴリズムが使われているらしい。

Finderからzip圧縮したファイルの方がサイズが小さくなる。

試したら倍くらい違う。

で、実際、Finderからの圧縮と同じzipファイルを作るには、
dittoコマンドを使えば良いそう。

ditto -c -k --sequesterRsrc --keepParent release release.zip

dittoはmac独自のコマンドらしい。

古いバージョンのElectronで作られたアプリのElectronライブラリを更新する

Electron製アプリのElectronのバージョンがだいぶ古くなったので更新してみる。

https://github.com/taku-o/myukkurivoice



Electronのバージョンを上げても、特に機能が増えたりしないのがツライところだが。

後になるほど、更新作業の量が増えたり、インターネット上の資料が参考にならなくなったりするので、

適度に更新していくべきではあるだろう。


まず現在使用中のバージョンを確認する!

バージョンを確かめるにはnpm outdatedコマンドで。

npm outdated -g
> Package            Current  Wanted  Latest  Location
> electron            1.4.12   1.7.9   1.7.9
> electron-packager    8.4.0   9.1.0   9.1.0
> electron-prebuilt   1.4.12  1.4.13  1.4.13
> node-gyp             3.4.0   3.6.2   3.6.2
> npm                 3.10.9   5.5.1   5.5.1



順番に更新していく

update nvm
cd ~/.nvm
git fetch
git pull origin master
source ~/.nvm/nvm.sh
nvm --version



update node

どのバージョンに更新すべきかは、electronの .node-version見れば良いのだろうか。

nvm ls-remote
nvm install v8.2.1
nvm use v8.2.1



update npm
npm update -g npm
npm outdated -g



electron

electronも入れ直しになる。

今回の作業にあたり、下調べしたらelectron-prebuiltとか無くなったらしい?

あと、インストールドキュメントではelectronをローカルにインストールするように書かれていたが、グローバルに入れてしまう。

npm install -g electron
npm install -g electron-packager



node modules

アプリで使用していたライブラリも、electronのremoteあたりの書き方が変わったりして

そのままでは動かなくなったりしている。

面倒だから全部installコマンドで更新してしまう。

npm install --save https://github.com/connors/photon
npm install --save angular
npm install --save angular-input-highlight
npm install --save electron-json-storage
npm install --save electron-config
npm install --save electron-log
npm install --save electron-localshortcut
npm install --save ref
npm install --save ffi
npm install --save intro.js
npm install --save temp
npm install --save wave-recorder
npm install --save tunajs



electron-localshortcutのパラメータ修正

electron-localshortcutというnode modulesを使用していたが、

過去に使用していたバージョンと、最新バージョンで、ファンクションのパラメータの渡し方が違っていた。

ソースコードを書き換える。

- var r = localShortcut.register('Command+Q', function() {
+ var r = localShortcut.register(mainWindow, 'Command+Q', function() {
    app.quit();
});



photonkitのアイコン置き換え

icon-docsというアイコンが表示されなくなってしまった。

原因追うの面倒だから、別のアイコンに置き換えて対応終わり。

これが遊びと仕事の違いである。てきとー。



npm rebuild

使っているModuleのバージョンと、使おうとしているModuleのバージョンが違うぞ、と怒られる。

表示メッセージを記録していなかったのは致命的なミスである。

Module version mismatch.とか、bindins.jsとか、その辺のメッセージが表示されていたと思う。

Electronのバージョンと、モジュールのバージョンを指定して、npm rebuildすると良い。

electron --version
> v1.7.9
npm rebuild --runtime=electron --target=1.7.9 --disturl=https://atom.io/download/atom-shell --abi=54



これで完了!

ひとまずこれで動作したが、しばらく様子見は必要かな。


動画作成用の音声再生アプリと、ライブ放送読み上げ用の音声再生アプリでは要件が違う話

動画に再生する音声メッセージでキモいボイスを流さなくても済むように、
指定されたメッセージを読み上げるアプリを作成した。機械音声なら美声でなくても安心である。


さて、昨今、こういった機械音声はライブ放送や実況などでも利用されているが、
動画作成用に作った音声再生アプリをライブ放送のコメント読み上げに使おうとしても、
そのままではうまくいかない。


ちょっと試してみたところ、
動画作成用の音声再生アプリと、ライブ放送のコメント読み上げアプリでは、
どちらも似たような音声再生のアプリでありながら、アプリの要件が違っていたのです。

どんな要件の違いが?

・動画作成用の音声再生アプリは、動画の制作者の指定どおり忠実に音声を再生することが期待されている。
・ライブ放送のコメント読み上げアプリは、一部のメッセージをライブ放送用に読み替える機能が期待されている。

w → ワラワラ


・ライブ放送では動画の運営コマンドが飛んでくる。これは読み上げてはいけない
・動画作成用ではそのまま読み上げて欲しい。読めない語があっては困る。

/hidden
@コテハン


・並列で多数同時にメッセージが飛んでくる。
ライブ放送では、これらをキューにいれて順番に読み上げてあげる必要がある。
(試しに書かれたそばから再生してみたら凄かった。まるで動物園のようだった。)


・たとえば、辞書機能があるとして、動画作成用と、ライブ放送用では、辞書に入れるデータがだいぶ違ってくる。
・動画作成用では、頻出ワードとか辞書に登録する
・ライブ放送用では、こう読んで欲しい、みたいな語を辞書に登録する
・この2つの辞書は絶対共有できないですよね

設計的な話で言うと

ライブ放送用の音声再生アプリでは、音声再生のキューの処理機能と、一部音声のフィルター機能が要るが、
動画再生用の音声再生アプリには不要。
ライブ放送用、動画再生用で作りを変えるのは、嬉しくない。


ライブ放送用のアプリと、動画再生用の音声再生アプリを一緒にすると、
結構面倒なことになってしまいますね。

ところがギッチョン

棒読みちゃんなどのメジャーな音声アプリは、その辺、一つのアプリで両方に対応していて、
あ、ご苦労様です、苦労してたんですね、という感じなのでありました。

Web Audio APIで作ったaudio/wav形式の音声を保存する

この(↓)のアプリを作っている間に得た知見
https://github.com/taku-o/myukkurivoice


Web Audio APIで作った音声を保存する方法は、調べると色々出てくるのですが、

  • Recorder.js
  • MediaStream Recording API
  • ScriptProcessorNode


この記事では、自分にはこれが簡単そうだなと思った方法を書きます。

node wave-recorder を使う

node wave-recorder
https://www.npmjs.com/package/wave-recorder


このライブラリを使うと、おおよそこんな感じのコード(↓)で、Web Audio APIで加工した音声データをファイルに保存できます。
YATTAZE簡単!!
少なくとも他の方法よりは。そう見える。

var fs = require('fs');

function to_array_buffer(buf_wav) {
  var a_buffer = new ArrayBuffer(buf_wav.length);
  var view = new Uint8Array(a_buffer);
  for (var i = 0; i < buf_wav.length; ++i) {
    view[i] = buf_wav[i];
  }
  return a_buffer;
};

var a_buffer = to_array_buffer(buf_wav);

var audioCtx = new window.AudioContext();
audioCtx.decodeAudioData(a_buffer).then(
  function(decodedData) {
    // source
    var sourceNode = audioCtx.createBufferSource();
    sourceNode.buffer = decodedData;
    sourceNode.onended = function() {
      recorder.end();                                    // ここでend()しないと超巨大なファイルが出来る
    };

    // gain
    var gainNode = audioCtx.createGain();
    gainNode.gain.value = volume;

    // recorder
    var recorder = WaveRecorder(audioCtx, {              // ← recorder作って
      channels: 1,                                       // ←
      bitDepth: 16                                       // ←
    });                                                  // ←
    recorder.pipe(fs.createWriteStream(wav_file_path));  // ←

    // connect and start
    sourceNode.connect(gainNode);
    gainNode.connect(recorder.input);                    // ← 繋げる
    sourceNode.start(0);
});

でも、なんか作った音声ファイルの最後の方が切れるんだけど

ただし、良いことだけでなく、node wave-recorderを試してみると問題も見つかったのです。
作成した音声ファイルの、最後の方がファイルに保存されないのです。
(自分の技量の問題もあるでしょう)

# 音声
こんにちわ

# ファイルに保存された状態 (最後の方が欠ける)
こんにちw


下のコードで、音声データの加工が終わった時にファイルへの書き出しを閉じているのですが、
onendedイベントのタイミングでは、ストリームを閉じるのが少し早かったりするのでしょう。

// source
var sourceNode = audioCtx.createBufferSource();
sourceNode.buffer = decodedData;
sourceNode.onended = function() {
  recorder.end();
};


なお、これを見て、自分が取った解決策が↓である。

// source
var sourceNode = audioCtx.createBufferSource();
sourceNode.buffer = decodedData;
sourceNode.onended = function() {
  // onendedのタイミングでは出力が終わっていない
  setTimeout(function(){
    recorder.end();
  }, 100);
};

汚い!!
でも、事件は現場で起きているんだ!!
(これは、時間があるときに解決を試みることにする)

おまけ。node-wav

node-wav
https://www.npmjs.com/package/node-wav


このようなライブラリも見つけた。wave-recorderから呼び出されていた。
このライブラリはWeb Audio APIで作った音を保存するものではない。


が、ストリームをファイルに保存する機能がある。
audio/wav形式のファイルのヘッダとか、フォーマットとか、その辺の処理をやってくれる。
加工した音声をストリームの形式に持っていけるなら、便利に扱えそうです。
良さそうだったが、今回は使用しなかった。


READMEにはその辺の機能の説明は載っていないが、
落としてみて、lib/file-writer.js とか読むと幸せになれそう。

おわり

最終的には、MediaStream Recording API に移行したい。