FC2ブログ

スマフォのアプリを作りたい(29):スマフォだけで動作する音声認識⑤

   プログラミング [2020/07/19]
JuliusをAndroidスマフォ上で動作させたいと思ってます。
概ね
Ⅰ.まずはJavaコードからC関数を呼び出すようなアプリの作り方を調べます。
Ⅱ.Javaコード(Cライブラリ含む)をReact Nativeで使えるパッケージ化方法を調べます。
Ⅲ.React Nativeにパッケージを取り込んで、Build&Go。
の順番で進めてますが、5回目にもなるのに未だの途中です。

前回、AndroidStudioのデバッグ機能を有効に動作させるための設定に1週間程費やしてしまいました。
今回は、その続き。

なんとかデバッグできる環境にはなりましたが、未だ不足している部分があります。


◆標準出力に出るメッセージが見たい
JuliusLib(libjulius&linsent)内で、メッセージ出力にprintf/fprintfが使われています。
「それってAndroid上ではどうなるの?」と調べたら、「logcatに出力される。」という記述をどこぞのサイトを斜めに読んで見つけた。

でも、実際はそんな簡単ではなくて、「だまっててもlogcatに出力される」ようにはなっていませんでした。

「リダイレクトすれば見える」というような記述もあって、その通りやってみた。

----コマンドプロンプト-----------------------------
C:\WINDOWS\system32>cd /d d:\appmake\android/sdk/platform-tools

d:\AppMake\Android\Sdk\platform-tools>adb shell
root@generic_x86:/ # stop
root@generic_x86:/ # setprop log.redirect-stdio true
root@generic_x86:/ # start
root@generic_x86:/ #
---------------------------------------------------


でも、どうも出てるようには見えない。
さらに調べると、なんか色んなことが書いてあって却って面倒そう。

しかたなくJuliusLibのソースに少し付け足しすることに方針転換しました。
プリプロセッサのマクロ定義を使って、printf/fprintfをAndroidのlogcat出力関数に置き換えてみました。

まずは、julius.hの終わりにヘッダファイルの#includeを追加します。
----julius.h---------------------------------------


/* read libsent includes */
#include <sent/stddefs.h>


#include <julius/extern.h>

#if defined(__ANDROID__)
# include <julius/print-android-macro.h>
#endif


#endif /* __J_JULIUS_H__ */
---------------------------------------------------


新規追加したヘッダの中身は、こんな感じ。
----print-android-macro.h--------------------------
#include <jni.h>
#include <android/log.h>

#define printf(...) __android_log_print(ANDROID_LOG_INFO, MYTAG, __VA_ARGS__);

#define fprintf(fptr, ...) { \
if(fptr==_STD_OUT){__android_log_print(ANDROID_LOG_INFO, MYTAG, __VA_ARGS__);} \
else if(fptr==_STD_ERR){__android_log_print(ANDROID_LOG_DEBUG, MYTAG, __VA_ARGS__);} \
else {fprintf(fptr, __VA_ARGS__);} \
};

#define MYTAG "JuliusLib"
#define _STD_OUT stdout
#define _STD_ERR stderr

---------------------------------------------------

julius.hと同じフォルダ「(プロジェクト・フォルダ)\app\src\main\cpp\libjulius\include\julius」に追加しました。


ちなみに、マクロ・ヘッダの#includeは「#include <stdio.h>」より前にあるとダメです。
本体の関数定義も置き換えようとしちゃうから。

ちょっと、こんなことしていいのか判断できてないですがいちおう動いてはいるみたいです。
stdout/stderr以外を使って、通常のファイルに出力しようとするfprintf()があった場合に、ちゃんとファイル出力できてるか未確認です。


◆.so側に制御が渡ったときのカレントディレクトリ
JavaのコードからJNI(Java Native Interface)を経由してC/C++の.soを呼び出したとき、元のカレントディレクトリが引き継がれるのかと思いきや。
.so呼び出し直後のカレントディレクトリは、ルート「/」でした。
なので、APKのassetsからアプリ専用フォルダにコピーしたデータファイル※をアクセスするためには、そのパスを.so側に教えてあげる必要があります。
※詳細は、前回記事をご参照願います。

JNIの機構では、Java側の関数呼び出しの際に指定した引数が、C側の第3引数以降になるようです。
また、Javaの変数値をCの変数型の値に変換する関数が用意されているとのこと。
解説サイトを参考にMainActivity.javaとnative-lib.cppを以下のように変更しました。

----MainActivity.java------------------------------
package com.teburarec.call_julius;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;
import java.io.IOException;
import android.content.res.AssetManager;
import java.io.File;
import java.io.InputStream;
import java.io.FileOutputStream;
import android.util.Log;


public class MainActivity extends AppCompatActivity {

String myDataPath;

// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

File myDirObj = getFilesDir();
this.myDataPath = myDirObj.getPath();
Log.d("to dir path", this.myDataPath);


initDataFiles();

setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
String rtmsg = stringFromJNI(this.myDataPath);
tv.setText(rtmsg);
}

/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI(String curpath);

private void initDataFiles() {
AssetManager assetMgr = getResources().getAssets();
try {
byte[] buffer = new byte[4096];

String files[] = assetMgr.list("jdata");
for(int i = 0; i < files.length; i++) {
Log.d("assets file", files[i]);
String assetPath = "jdata/" + files[i];
InputStream infile = assetMgr.open(assetPath);
FileOutputStream outfile = openFileOutput(files[i], MODE_PRIVATE);
int n = 0;
while (-1 != (n = infile.read(buffer))) {
outfile.write(buffer, 0, n);
}
outfile.close();
}
}
catch (IOException e) {
e.printStackTrace();
}
}

}
---------------------------------------------------


渡されたパスをカレントディレクトリに設定すれば、後はJuliusLibを動作させるための各種データファイルをファイル名だけでアクセスできる・・・ハズ。
----native-lib.cpp---------------------------------
:
:
extern "C" JNIEXPORT jstring JNICALL
Java_com_teburarec_call_1julius_MainActivity_stringFromJNI(
JNIEnv* env,
jobject jObj /* this */,
jstring curpath
) {

const int argc = 3;
const char *argv[] = {"julius-sample", "-C", "simple_keyword.jconf"};



int ret;

const char *jdataPath = env->GetStringUTFChars(curpath, NULL);
chdir(jdataPath);
env->ReleaseStringUTFChars(curpath, jdataPath);


/* by default, all messages will be output to standard out */
/* to disable output, uncomment below */
//jlog_set_output(NULL);

/* output log to a file */
//FILE *fp; fp = fopen("log.txt", "w"); jlog_set_output(fp);



---------------------------------------------------


この対策をやって、「-C simple_keyword.jconf」と指定した、.jconfはアクセスされるようになりました。

logcat(printf/fprintf)に出力されるエラーメッセージもさほど詳しいものはないことが、この時点で分かった。
なので・・・


◆JuliusLibのlog出力機能の有効化
Juliusのコードを見ると、随所にログを出力する機能が入ってます。
サンプル(julius-simple.c)では、そのlog取得機能の初期化がコメントアウトされていました。
native-lib.cppの「FILE *fp; fp = fopen("log.txt", "w"); jlog_set_output(fp);」※を復活させます。
※前節のnative-lib.cppのコード抜粋の最後の方にあります。

jlog_set_output()で、ファイルポインタにNULLを指定すると、標準出力にログを出力するようになっていました。
ただし、使っている関数がvfprintf()だったので、追加したlogcat出力への変換マクロの対象外になってます。

この時点で実行させて何か出てないか確認しましたが、何も出力はされていないようでした。


◆.jconfから余計なパラメタを削除する
前節までのメッセージ関係の修正を入れながら、デバッガでエラー原因箇所を調べました。

.jconfを読み込んでJconf構造体にセットする処理と、それら動作パラメタをチェックする段階で、それぞれエラー終了してました。

原因は、.jconf内に以下が設定されていたためでした。

-fvad 3
-d model/lang_m/bccwj.60k.bingram

-fvadは、コンパイル時に何だか無効化(使わないことに)した機能のパラメタ?
-dは、ngram_filenameという変数名で管理されていて、-LMパラメタの関連の指定?

ともかく、セッティングが不十分で、不必要だったみたいです。
その他のmain.jconfに入っていて、意味も分からずそのままにしていたパラメタを全てコメントアウトしました。
そうすべきでした。


何やら認識したみたい! \(^o^)/
20200719_1.jpg


◆認識結果の確認
まずは、アプリ画面には最後まで走ったときに「COMPLETE」と出るようにしてありました。
認識結果のメッセージは、logcatに出ます。
AndroidStudioのLogcat画面だと他のメッセージと混ざって見つけにくいですが、以下のように出てました。

----Logcat-----------------------------------------

07-17 23:05:58.027 com.teburarec.call_julius I/JuliusLib: current dir=/data/data/com.teburarec.call_julius/files
07-17 23:11:29.546 com.teburarec.call_julius I/JuliusLib: sentence1:
07-17 23:11:29.546 com.teburarec.call_julius I/JuliusLib: 入浴
07-17 23:11:29.546 com.teburarec.call_julius I/JuliusLib: wseq1:
07-17 23:11:29.546 com.teburarec.call_julius I/JuliusLib: 入浴
07-17 23:11:29.546 com.teburarec.call_julius I/JuliusLib: phseq1:
07-17 23:11:29.546 com.teburarec.call_julius I/JuliusLib: silB
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: ny
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: u:
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: y
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: o
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: k
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: u
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: silE
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: cmscore1:
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: 0.704
07-17 23:11:29.547 com.teburarec.call_julius I/JuliusLib: score1: -118340.523438
---------------------------------------------------

あれ?これだけ?
録音ファイルは2個あったはずなのに。
それに、1個の録音で複数の単語を録音してあったのだけど・・・


JuliusLibが吐き出したログファイルには、長々とたくさん書き込まれてた。

----コマンドプロンプト-----------------------------
C:\WINDOWS\System32>cd /d d:\appmake\android/sdk/platform-tools

d:\AppMake\Android\Sdk\platform-tools>adb shell
root@generic_x86:/ # cd /data/user/0/com.teburarec.call_julius/files
root@generic_x86:/data/user/0/com.teburarec.call_julius/files # ls
invoicename.txt
jnas-mono-16mix-gid.binhmm
keywords1.wav
keywords2.wav
log.txt
simple_keyword.jconf
teburarecword.dict
root@generic_x86:/data/user/0/com.teburarec.call_julius/files # nl log.txt
1 STAT: include config: simple_keyword.jconf
2 STAT: jconf successfully finalized
3 STAT: *** loading AM00 _default


15 STAT: reading [teburarecword.dict]...
16 Stat: init_wordlist: reading in word list
17 Stat: init_wordlist: read 15 words
18 STAT: done


42 STAT: All init successfully done

43 STAT: ###### initialize input device


171 ------------------------------------------------------------
172 FrontEnd:

173 Input stream:
174 input type = waveform
175 input source = waveform file
176 input filelist = invoicename.txt
177 sampling freq. = 16000 Hz required
178 threaded A/D-in = not supported (live input may be dropped)
179 zero frames stripping = on
180 silence cutting = off
181 long-term DC removal = off
182 level scaling factor = 1.00 (disabled)
183 reject short input = < 800 msec
184 reject long input = off

185 ----------------------- System Information end -----------------------

186 Notice for feature extraction (01),
187 *************************************************************
188 * Cepstral mean normalization for batch decoding: *
189 * per-utterance mean will be computed and applied. *
190 *************************************************************
191 Stat: adin_file: input speechfile: keywords1.wav
192 Warning: strip: sample 28992-29008 has zero value, stripped
193 STAT: 724074 samples (45.25 sec.)
194 STAT: ### speech analysis (waveform -> MFCC)

root@generic_x86:/data/user/0/com.teburarec.call_julius/files #
---------------------------------------------------


ともかく、予想した認識結果ではなかったげど、何某か認識されたようです。
あとちょっとな感じなのですが・・・


◆複数の.wavを処理するには
JuliusBookの3.2節を良く読んでみました。

・1録音ファイルには1単語のみでなければならないのか?
→どうもそうだそうです。「デフォルトでは,1ファイルを1発話として認識を行う.」とあるので、単語認識させてるので最初に認識された単語のみ出力される。

・「-filelist」に指定するファイル名リストの書き方は?
→1行:1ファイル名で書くのが正しいみたい。
じゃあなぜ先頭の1ファイルしか処理しないのか?

native-lib.cpp(ベースはJuliusに含まれていたjulius-simple.c)をよくよく眺めたり、ステップ実行してみて動きを追っかけました。

なんかリストのファイル毎に処理するループが見当たらない。

どうも、rawfile指定でファイルリストを処理することは想定してない・・・?
なので以下の処理を追加しました。
----native-lib.cpp---------------------------------
:
:
/***********************************/
/* Open input stream and recognize */
/***********************************/
if (jconf->input.speech_input == SP_MFCFILE
|| jconf->input.speech_input == SP_OUTPROBFILE) {
/* MFCC file input */


}

else if (jconf->input.speech_input == SP_RAWFILE) {
/* Rawfile(wav) input */
while (0 == (ret = (j_open_stream(recog, NULL)))) {
ret = j_recognize_stream(recog);
if (ret == -1) {
//後処理しなくていいのか?
return env->NewStringUTF("ERROR: RECOGNIZE ERROR");
}
} //end while

if (ret == -1) {
fprintf(stderr, "error in input stream\n");
//後処理しなくていいのか?
return env->NewStringUTF("ERROR: INPUT STREAM");
}
else if (ret == -2) {
fprintf(stderr, "failed to begin input stream\n");
//後処理しなくていいのか?
return env->NewStringUTF("ERROR: FAILED TO BEGIN");
}
}

else {
/* raw speech input (microphone etc.) */


}


---------------------------------------------------

これで、複数の発話ファイルで、それぞれ単語認識する動作を確認できました。




当初想定のは、ここで「完了」とします。

次回からは、:音声データのバッファ渡しインタフェースを追加してReactNativeパッケージとして使えるように・・・
できるんでしょうか。

自信ねーなー。飽きてきたし。

では、ご機嫌よう。
(m__)m
スポンサーサイト





コメントの投稿

非公開コメント

カレンダー
01 | 2024/02 | 03
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 - -
プロフィール

さるもすなる

Author:さるもすなる
さるです。別HPサイト「さるもすなる」から侵食してきました。 山菜/きのこ、それとタイトルにしたPPバンド籠のことをメインに徒然に・・・・暇を持て余したさるの手仕事:男手芸のブログってことで。

最新記事
最新コメント
月別アーカイブ
カテゴリ
天気予報

-天気予報コム- -FC2-
本家のHPのトップ
山菜や茸の話です
PPバンドの籠作品と作り方です
投稿をお待ちしております



PVアクセスランキング にほんブログ村 にほんブログ村 ハンドメイドブログへ



マニュアルのお申し込み



検索フォーム
リンク
RSSリンクの表示
ブロとも申請フォーム

この人とブロともになる

QRコード
QR