FC2ブログ

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

   プログラミング [2020/09/09]
前回の続きになります。
でも、だいぶ間があいちゃいましたね。

前回割と中途半端なところで話を切ったので、この回から読み始めても「さっぱり何言ってんのかわかんない」と思います。
(それじゃなくても、独り言に近い備忘録なので、他の人が読んでもどうにか理解できるのかはあまり気に掛けてません。)

なので、もし類似の何かを調べしている方は、
「スマフォだけで動作する音声認識⑥」から、あるいは「スマフォだけで動作する音声認識①」からお読みください。


前回までで、JavaからC/C++のライブラリをコールして、さらにライブラリ側からJava側のコールバックを呼び出すところまでを実験的にやってみました。
今回は、今までの内容を整形(コードをそれなりの内容にまで整える)して、さらにReact Native用パッケージに仕立てるところ・・・
行けるかどうか分かりませんが、始めます。


◆単語認識ライブラリのコールバックの実装
ず~~~っと前に作ったGoogle音声認識と同時に録音するJavaコード(さるもすなる版react-native-google-speech-api)では、音声データをバッファで渡すことができます。
データ型はbyte配列(byte[])です。
ただし、そのバッファを握ったまま音声認識の処理が終わるまで離さないということは、どう考えても効率が悪そうだし、そんな制御は却って難しそうです。
なので、認識を開始した後に渡す音声データは、コールバックで渡す段階で認識側専用のバッファ(char *の先)にデータをコピーするのがまあ普通な考え方かと。

じゃあ、そのコピーはJava側で行うのか、C側で行うのか・・・

Java側でやるなら、
Java側にC側で用意したバッファのポインタをそのまま渡せるのか?できない?
前回記事でお試しコードを書いたときは、わざわざJavaのbyte型配列を新しく確保して渡した。
これだと、データコピーが2回発生してしまう。

C側でコピーするとしたら
Java側のコールバック関数のリターン値をデータアドレスとデータ長の2種類を含むものにしないといけなくなる。
①C言語でいうところの参照型の引数を使ってJava側のバッファアドレスを貰って来ることはできるのか?
②バッファを渡した側がデータのコピーの完了を知る術がない(そのバッファの解放判断ができない)んじゃない?

①:そもそも、Javaにポインタという概念はないらしい。
byte[]型のオブジェクトそのものはリターン値として使えて、そこから中身のレングスとポインタはJNIの関数で取り出せるみたいです。

②:でもでも、リターンのタイミングでそれを渡したとすると、コピーの完了を知るすべがないということになります。
Java側のバッファの「用済み」がいつなのか分からないと、バッファを解放できないということになります。
データコピーの完了を知らせるために別の仕掛けを用意しなくちゃいけなくなってしまいます。
認識が完了するまで、Java側バッファも保持しつづける?
それは、後々厄介なことになりそう。

悩んだ末に、こんなことを考えました。
Javaのコールバックを呼ぶんだけど、そのコールバックは再度C関数を呼び出して、そのC関数がデータのコピーをやっちゃうって仕掛けです。

まずは、やってみました。
ここでも結局、Java側のコールバック関数を経由して、C側のバッファのポインタはどう渡すんだ?
と、振り出しに戻るような疑念が出ました。

結局そこはC側にグローバル変数を用意して、自分のバッファとその長さを保持して
コールバックから呼ばれたデータコピー関数は、グローバル変数を参照することで、済ませてしまいました。
かなりベタというか、美しくありません。
これだと、マルチスレッドでこのコールバック処理が非同期に実施されるようなことがあれば、アウトです。

ともあれ、この作りで一旦先に進みます。

続きます。この後も長ーいです。


◆Javaライブラリ化
これまで、JavaコードからC++/Cで書かれたライブラリ(.so)をコールする「アプリケーション」をビルドする環境として試作してきました。
今度は、Javaコードを入り口とするライブラリ化が必要・・・なのかなぁと思います。

Java側へのインタフェースは、jlib_call_startjlib_call_recognizejlib_call_stop
と今回追加した、バッファのデータをコピーするC関数をコールするjlib_call_sendの4種類とします。

まずは、これまでアプリのメイン(クラス)としていたJava側(MainActivity)を他のJavaから使用されるJuliusライブラリへの入口として書き直す必要があります。


(1)パッケージ名を変更
これは、特に必要というわけではありません。
さるの場合、テンプレートを作成する際のプロジェクト名を何の気なしに「call_julius」と付けたため、そのままパッケージの最後の名前もcall_juliusとなっていました。すると、JNIを使用してコールするC関数の名前のパッケージ名部分の最後が「・・・call_1julius」になるのが何だか気に入らなかっただけです。

なのでアンダーバーなしの「calljulius」に変更します。
やり方は、以下を参考にしました。
参考:https://po-what.com/android-studio-package-rename

-AndroidStudioでプロジェクトを開く
-左側Projectエクスプローラの上部の選択欄から「Android」を選択
-ペイン上の「app」-「java」-(パッケージ名)を右クリックし、プルダウンから「Refactor」-「Rename...」を選択
-「Warning」メッセージに対して[Rename package]ボタンをクリック
-「Rename」ダイアログの「Rename package '・・・」下の入力フィールドに新しい名前を入力し、[Refactor]ボタンをクリック
-AndroidStudioウィンドウの下の方に「Refactoring Preview」タブが表示されるので、タブの下にある[Do Refactor]ボタンをクリック

-左側Projectエクスプローラの上部の選択欄から「Project」を選択
-ペイン上の(プロジェクト名)-「app」-「src」-「build.gradle」をダブルクリック
-右側にbld.gradleの内容が表示されるので、「android{・・・defaultConfig { applicationId "パッケージ名" ・・・}・・・}」の最後の名前を変更
-ペイン上の(プロジェクト名)-「app」-「src」-「main」-「java」下のパッケージ名が変わっているか確認
-さらにその下のコードファイル名(MainActivity)をダブルクリック
-右側にMainActivity.javaの内容は表示されるので、先頭行の「package com.teburarec.calljulius;」が新しい名前になっているか確認

-C/C++側のJNI対応の関数名中のパッケージ名を変更

あっちこっち見た感じで、これくらいの手順だったと思います。


(2)Javaのclass名の変更
これも、必ずしもやらなくていい気がしますが、テンプレートのまま(MainActivity)というのは、ちょっと体(タイ)を表してないので、変更します。

-AndroidStudioでプロジェクトを開く
-左側Projectエクスプローラの上部の選択欄から「Project」を選択
-ペイン上の(プロジェクト名)-「app」-「src」-「main」-「java」-(パッケージ名)-「MainActivity」を右クリックし、「Refacor]-「Rename...」を選択
-「Rename」ダイアログの「Rename class '・・・」下の入力フィールドに新しい名前を入力し、[Refactor]ボタンをクリック
さるの場合、上記の手順(javaファイル名が変更になる)をパスってこの下からやったたら、ビルドでエラーになりました。
-ペイン上の(プロジェクト名)-「app」-「src」-「main」-「java」-(パッケージ名)-(新しいクラス名)をダブルクリック
-(新しいクラス名).javaの内容が表示されるので、「public class (クラス名) extends AppCompatActivity {」を新しいクラス名に変更

-ペイン上の(プロジェクト名)-「app」-「src」-「main」の下の「AndroidManifest.xml」をダブルクリック
-右側に内容が表示されるので、「<activity android:name="パッケージ名.クラス名"・・・」のクラス名を新しいクラス名に変更

-C/C++側のJNI対応の関数名中のクラス名を変更

これくらいの手順だったと思います。

ちなみにプロジェクト名自体の変更は、AndroidStudio上ではできませんでした。
AndroidStudioを抜けてからはできるみたいです。
参考:https://qiita.com/emabust/items/04184e780f7407b9907f

ここで、一旦ビルドして、動きが以前のままか確認します。


(3)JavaライブラリとしてのI/Fのコーディング
アプリとして設計されていた、テンプレートベースのコードに手を入れていきます。

念のため、手を入れる前のプロジェクトフォルダごとバックアップします。
プロジェクト(フォルダ)の名前は、「call_juilius」のままとします。

jlib_call_startjlib_call_recognizejlib_call_stop、およびjlib_call_sendの4種類をpublicメソッドとして定義します。

中身を考え始めたときに引っ掛かったのがJavaコード同士のコールバックです。
Cと違って関数ポインタを単に引数で渡すということはできないみたいです。
そもそも、ポインタの概念がないそうだから、そうなんですね。

やりかたを色々ぐぐってみましたが、どこもさらっと呼んでもなんだか良くわかりません。
サンプルコードを見ても、どっちがコール元で、どっちがコールされてコールバックする側なんだか・・・
interfaceの定義は、importさせれば、別ソースから使えるのか? とか。

分からないながらも、以下を参考にコーディングしてみました。
参考:https://qiita.com/softbase/items/94984fa7821ecbc02f5b

でもその結果を、試してみる前に次に引っ掛かったのが、
準備したI/Fメソッドを呼び出す側がマルチスレッドであるという点です。

「認識準備しなさい(start)」というのと、「認識を始めなさい(recognize)」と指示するが、別スレッドになる可能性が大です。
片やJuliusライブラリ側の入り口部分は、マルチスレッドとか意識してません。シングルスレッド前提になっちゃって?ます。
仮に準備したI/F毎にスレッドが違った場合にチャント動くかどうかは確認していない。
いずれ、処理が追い越されたら(前のrecognizeが終わる前にrecognizeされたら)うまく動くはずもない。
よって、JuliusLib側はシーケンシャルに動作させる必要があります。

ということは、
・このJavaライブラリは、単一スレッドを起動してその中で、Cライブラリをコールするようにする。
・起動されたスレッドは、Cライブラリ側の初期化(jlibStart)を実行した後、「認識開始」の指示が来るまでwaitする。
jlib_call_startは、音声データを引き渡すコールバック関数の設定のみ行う。
jlib_call_recognizeは、待ち状態のスレッドを起こしてやって、スレッド側はjlibRecognizeを実行させる。
・CライブラリのjlibRecognizeの結果は、やはりコールバックでJavaの呼び出し元に伝える。
jlib_call_stopは、待ち状態のスレッドを起こしてやって、スレッドのjlibRecognizeループを終了させて、jlibStopを実行するように仕向ける。

こんな感じにするのかなー。
Java素人のさるにとっては、かなりハードルが上がりましたが、やむをえません。
ともかく、それっぽくコードは書いてみました。


で、これをライブラリとしてビルドするには?

Javaライブラリの作り方は、Android本家のページを参考にしました。
参考:https://developer.android.com/studio/projects/android-library?hl=ja

「(プロジェクトフォルダ)\app\build.gradle」をエディタで開いて編集します。
-「android{・・・defaultConfig { applicationId "com.teburarec.calljulius" ・・・}・・・}」のapplicationIdapplicationIdの行を削除
-先頭の「apply plugin: 'com.android.application'」の記述を「apply plugin: 'com.android.library'」に変更

でビルドしてみたら、.jaaファイルが「(プロジェクトフォルダ)\app\build\outputs\aar」内に作られました。

ところが、.jaaファイルはAndroidStudio内でのみ使える形式らしい。
別のプロジェクトに持ち込む場合は、.jarとの記述が見つかりました。


んー。
AndroidStudioで作ったJavaライブラリ(C/C++ライブラリを含む)をどうやって、ReactNativeの環境に持ち込むのか?
ちょっと、行き詰まりました。


ここから先がかなり試行錯誤になりそうなので、またまた一旦話を切ります。

今回のコードの変更箇所をおさらい的に以下に。

JuliusLib(C/C++)本体
path:(プロジェクトフォルダ)\app\src\main\cpp

Javaコールバック関数を呼び出すCライブラリ内の関数のポインタをRecogテーブルに追加。
----libjulius\include\julius\recog.h------------


//### was added by sarumosunaru. ->
#ifdef __ANDROID__
void *callback_for_adin;
#endif
//### was added by sarumosunaru. <-

} Recog;

#endif /* __J_RECOG_H__ */
---------------------------------------------------


Cライブラリ内のコールバックのポインタを引数として、標準入力を音声入力元とする関数に渡すようにした。
----libjulius\src\m_adin.c-------------------------
:
:
boolean
adin_initialize(Recog *recog)
{
:
switch(jconf->input.speech_input) {
:
case SP_STDIN:
//### was modified by sarumosunaru. ->
#ifdef __ANDROID__
arg = (char *) recog->callback_for_adin;
#else

arg = NULL;
#endif
//### was modified by sarumosunaru. <-

break;
:
:
//### was modified by sarumosunaru. ->
if (arg != NULL && jconf->input.speech_input != SP_STDIN) {
free(arg);
}
//### was modified by sarumosunaru. <-


return TRUE;
}
---------------------------------------------------


標準入力用関数(adin_stdin_read)をコールバックを呼び出すように変更した。
----libsent\src\adin\adinfile.c--------------------
:
:

//### was added by sarumosunaru. ->
#ifdef __ANDROID__
typedef int (*MY_ADIN_FUNC)(char *buffer, int buf_size);
MY_ADIN_FUNC inputVoiceIF_fromCaller;
#endif
//### was added by sarumosunaru. <-



/**
* Initialization for speech input via stdin.
*
* @param freq [in] required sampling frequency.
* @param arg dummy, ignored
*
* @return TRUE on success, FALSE on failure.
*/
boolean
adin_stdin_standby(int freq, void *arg)
{
/* store sampling frequency */
sfreq = freq;
//### was added by sarumosunaru. ->
#ifdef __ANDROID__
if (!arg) {
jlog("Error: adin_stdin: the callback address was not set.\n");
return FALSE;
}
inputVoiceIF_fromCaller = (MY_ADIN_FUNC) arg;
#endif
//### was added by sarumosunaru. <-

return(TRUE);
}

/**
* @brief Begin reading audio data from stdin
*
* @param pathname [in] dummy
*
* @return TRUE on success, FALSE on failure.
*/
boolean
adin_stdin_begin(char *pathname)
{
//### was added by sarumosunaru. ->
#ifdef __ANDROID__
wav_p = FALSE;
has_pre = TRUE;
return TRUE;
#else
//### was added by sarumosunaru. <-

if (feof(stdin)) { /* already reached the end of input stream */
jlog("Error: adin_stdin: stdin reached EOF\n");
return FALSE; /* terminate search here */
} else {
/* open input stream */
if (adin_file_open(NULL) == FALSE) {
jlog("Error: adin_stdin: failed to read speech data from stdin\n");
return FALSE;
}
jlog("Stat: adin_stdin: reading wavedata from stdin...\n");
}
return TRUE;
#endif //### was added by sarumosunaru
}

/**
* Try to read @a sampnum samples and returns actual sample num recorded.
*
* @param buf [out] samples obtained in this function
* @param sampnum [in] wanted number of samples to be read
*
* @return actural number of read samples, -1 if EOF, -2 if error.
*/
int
adin_stdin_read(SP16 *buf, int sampnum)
{
int cnt;

//### was added by sarumosunaru. ->
#ifdef __ANDROID__
cnt = (*inputVoiceIF_fromCaller)((char*)buf, sizeof(SP16)*sampnum);
if (cnt == 0) return -1; /* EOF */
else if (cnt < 0) {
jlog("Error: adin_stdin: an error occured while reading callback\n");
return -2; /* error */
}
#else
//### was added by sarumosunaru. <-

if (wav_p) {
:
:
}
#endif //### was added by sarumosunaru
:
:
---------------------------------------------------


JuliusLib制御のC/C++側
path:(プロジェクトフォルダ)\app\src\main\cpp
----julius-entry.cpp-------------------------------
#define MY_RETURN_MAX 240

char g_ret_str[MY_RETURN_MAX+16]; //認識結果リターン文字列
jmethodID g_inVoiceCallback; //JAVA側コールバックメソッドのID
JNIEnv* g_env; //コールバック用JNIインスタンス
jobject g_jObj; //

Jconf* g_jconf; //configuration parameter holder
Recog* g_recog = NULL; //Recognition instance

int g_tmpsize = 0; //一時保存データ長
char g_tmpbuf[MAXSPEECHLEN]; //一時保存バッファ320kB(最大バッファの半分)
//コピー関数用の共用変数
char* g_dst_curp; //カレントのバッファポインタ
int g_dst_lng; //バッファの残りのサイズ


//******************************************************************************
// function :Callback to be called when start waiting speech input.
// parameter :recog -認識エンジンのインスタンス
// dummy -未使用パラメタ
// return :
//******************************************************************************
static void
status_recready(
Recog* recog,
void* dummy
)
{
if (recog->jconf->input.speech_input == SP_MIC
|| recog->jconf->input.speech_input == SP_NETAUDIO) {
fprintf(stderr, "<<< please speak >>>");
}
else {
fprintf(stderr, "*** It's ready. >>>");
}
}

//******************************************************************************
// function :Callback to be called when speech input is triggered.
// parameter :recog -認識エンジンのインスタンス
// dummy -未使用パラメタ
// return :なし
//******************************************************************************
static void
status_recstart(
Recog* recog,
void* dummy
)
{
if (recog->jconf->input.speech_input == SP_MIC
|| recog->jconf->input.speech_input == SP_NETAUDIO) {
fprintf(stderr, "\r \r");
}
else {
fprintf(stderr, "*** Start the recognizing. >>>");
}
}

//******************************************************************************
// function :Sub function to output phoneme sequence.
// parameter :seq -音素シーケンスのアドレス
// n -
// winfo -
// return :なし
//******************************************************************************
static void
put_hypo_phoneme(
WORD_ID* seq,
int n,
WORD_INFO* winfo
)
{
int i,
j;
WORD_ID w;

static char buf[MAX_HMMNAME_LEN];

if (seq != NULL) {
for (i=0;i if (i > 0) printf(" |");
w = seq[i];
for (j=0;jwlen[w];j++) {
center_name(winfo->wseq[w][j]->name, buf);
printf(" %s", buf);
}
}
}
printf("\n");
}

//******************************************************************************
// function :Callback to output final recognition result.
// This function will be called just after recognition of an input ends
// parameter :recog -認識エンジンのインスタンス
// vp-ret -リターン文字列バッファ
// return :なし
//******************************************************************************
static void
output_result(
Recog* recog,
void* vp_ret
)
{
int i,
j;
int len;
WORD_INFO* winfo;
WORD_ID* seq;
int seqnum;
int n;
Sentence* s;
RecogProcess* r;
HMM_Logical* p;
SentenceAlign* align;

char* cur_p = (char *) vp_ret;
int ret_len;

sprintf(cur_p, "RETURN:");
ret_len = strlen(cur_p);
cur_p += ret_len;

//all recognition results are stored at each recognition process instance
for(r=recog->process_list;r;r=r->next) {
//skip the process if the process is not alive
if (! r->live) continue;

//result are in r->result. See recog.h for details
//check result status
if (r->result.status < 0) { //no results obtained
//outout message according to the status code
switch(r->result.status) {
case J_RESULT_STATUS_REJECT_POWER:
printf("<input rejected by power>\n");
break;
case J_RESULT_STATUS_TERMINATE:
printf("<input teminated by request>\n");
break;
case J_RESULT_STATUS_ONLY_SILENCE:
printf("<input rejected by decoder (silence input result)>\n");
break;
case J_RESULT_STATUS_REJECT_GMM:
printf("<input rejected by GMM>\n");
break;
case J_RESULT_STATUS_REJECT_SHORT:
printf("<input rejected by short input>\n");
break;
case J_RESULT_STATUS_REJECT_LONG:
printf("<input rejected by long input>\n");
break;
case J_RESULT_STATUS_FAIL:
printf("<search failed>\n");
break;
}
//continue to next process instance
continue;
}

//output results for all the obtained sentences
winfo = r->lm->winfo;

for(n = 0; n < r->result.sentnum; n++) { //for all sentences
s = &(r->result.sent[n]);
seq = s->word;
seqnum = s->word_num;

//output word sequence like Julius
printf("sentence%d:", n+1);
for(i=0;i printf(" %s", winfo->woutput[seq[i]]);
if (ret_len + strlen(winfo->woutput[seq[i]]) < MY_RETURN_MAX) {
sprintf(cur_p, "%s", winfo->woutput[seq[i]]);
ret_len += strlen(cur_p);
cur_p += strlen(cur_p);
}
}
printf("\n");
sprintf(cur_p, "\t");
ret_len++;
cur_p++;
//LM entry sequence
printf("wseq%d:", n+1);
for(i=0;iwname[seq[i]]);
printf("\n");
sprintf(cur_p, "\t");
ret_len++;
cur_p++;
//phoneme sequence
printf("phseq%d:", n+1);
put_hypo_phoneme(seq, seqnum, winfo);
printf("\n");
//confidence scores
printf("cmscore%d:", n+1);
for (i=0;i printf(" %5.3f", s->confidence[i]);
if (ret_len + 6 < MY_RETURN_MAX) {
sprintf(cur_p, "%d ", (int) (s->confidence[i]*1000));
ret_len += strlen(cur_p);
cur_p += strlen(cur_p);
}
}
printf("\n");
sprintf(cur_p, "\t");
ret_len++;
cur_p++;
//AM and LM scores
printf("score%d: %f", n+1, s->score);
if (r->lmtype == LM_PROB) { //if this process uses N-gram
printf(" (AM: %f LM: %f)", s->score_am, s->score_lm);
}
printf("\n");

if (r->lmtype == LM_DFA) { //if this process uses DFA grammar
//output which grammar the hypothesis belongs to when using multiple grammars
if (multigram_get_all_num(r->lm) > 1) {
printf("grammar%d: %d\n", n+1, s->gram_id);
}
}

//output alignment result if exist
for (align = s->align; align; align = align->next) {
printf("=== begin forced alignment ===\n");
switch(align->unittype) {
case PER_WORD:
printf("-- word alignment --\n"); break;
case PER_PHONEME:
printf("-- phoneme alignment --\n"); break;
case PER_STATE:
printf("-- state alignment --\n"); break;
}
printf(" id: from to n_score unit\n");
printf(" ----------------------------------------\n");
for(i=0;inum;i++) {
printf("[%4d %4d] %f ", align->begin_frame[i], align->end_frame[i], align->avgscore[i]);
switch(align->unittype) {
case PER_WORD:
printf("%s\t[%s]\n", winfo->wname[align->w[i]], winfo->woutput[align->w[i]]);
break;
case PER_PHONEME:
p = align->ph[i];
if (p->is_pseudo) {
printf("{%s}\n", p->name);
} else if (strmatch(p->name, p->body.defined->name)) {
printf("%s\n", p->name);
} else {
printf("%s[%s]\n", p->name, p->body.defined->name);
}
break;
case PER_STATE:
p = align->ph[i];
if (p->is_pseudo) {
printf("{%s}", p->name);
} else if (strmatch(p->name, p->body.defined->name)) {
printf("%s", p->name);
} else {
printf("%s[%s]", p->name, p->body.defined->name);
}
if (r->am->hmminfo->multipath) {
if (align->is_iwsp[i]) {
printf(" #%d (sp)\n", align->loc[i]);
} else {
printf(" #%d\n", align->loc[i]);
}
} else {
printf(" #%d\n", align->loc[i]);
}
break;
} //end switch
} //end for < align->num

printf("re-computed AM score: %f\n", align->allscore);

printf("=== end forced alignment ===\n");
} //end for s->align chain
} //end for < result.sentnum
} //end for process_list

//flush output buffer
fflush(stdout);
}


//******************************************************************************
// function :音声データ入力用コールバック関数への入り口
// parameter :buffer -バッファアドレス
// buf_size -バッファサイズ
// return :音声データ長
//******************************************************************************
int InVoiceBridge(
char* buffer,
int buf_size
)
{
char* dst_curp = buffer;
int dst_lng = buf_size;
int cpy_lng;
int ret_lng;

try {
//一時保存があればコピー
if (g_tmpsize > 0) {
cpy_lng = min(buf_size, g_tmpsize);
memcpy(dst_curp, g_tmpbuf, cpy_lng);
//それでも一時ファイルにデータがあった場合、一時保存データシフト
g_tmpsize -= cpy_lng;
if (g_tmpsize > 0) {
memcpy(g_tmpbuf, g_tmpbuf+cpy_lng, g_tmpsize);
}
//入力バッファがフルの場合、一旦リターン
if (buf_size <= cpy_lng) return cpy_lng;
dst_curp += cpy_lng;
dst_lng -= cpy_lng;
}
//バッファに空きがあればコールバックする
while (dst_lng > 0) {
//Javaコールバックのコール
g_dst_lng = dst_lng;
g_dst_curp = dst_curp;
ret_lng = (int) g_env->CallIntMethod(g_jObj, g_inVoiceCallback);
if (ret_lng < 0) break;
dst_lng = ret_lng;
}
return (buf_size - dst_lng);
}
catch (std::exception &e) {
strcpy(g_ret_str, "ERROR: EXCEPTION AT CALLBACK");
return 0;
}
}

//******************************************************************************
// function :コールバック内で使用する、音声データのコピー処理
// parameter :env -JNIインタフェース構造体へのポインタ
// jObj -呼出し元JAVAクラスのインスタンス
// jvoicebuf -Java音声データバッファ
// return :コピー先バッファの残りのサイズ
//******************************************************************************
extern "C" JNIEXPORT jint JNICALL
Java_com_teburarec_calljulius_JlibGate_jlibCopy(
JNIEnv* env,
jobject jObj,
jbyteArray jvoicebuf //データ格納先Javaバッファ
)
{
int dst_lng = g_dst_lng;
char* dst_curp = g_dst_curp;
int cpy_lng;
char* src_buf;
int src_lng;
jboolean isCopy;

src_lng = (int) g_env->GetArrayLength(jvoicebuf);
if (src_lng <= 0) return -1;
//jbyteArrayバッファポインタの取得
src_buf = (char *) g_env->GetByteArrayElements(jvoicebuf, &isCopy);
cpy_lng = min(dst_lng, src_lng);
memcpy(dst_curp, src_buf, cpy_lng);
dst_curp += cpy_lng;
dst_lng -= cpy_lng;
//バッファに入りきらなかった分を一時保存
src_lng -= cpy_lng;
if (src_lng > 0) {
memcpy(g_tmpbuf, src_buf+cpy_lng, src_lng);
g_tmpsize = src_lng;
}
if (isCopy) {
//jbyteArrayバッファポインタの解放
g_env->ReleaseByteArrayElements(jvoicebuf, (jbyte *)src_buf, JNI_ABORT);
}

return (jint) dst_lng;
}

//******************************************************************************
// function :動作パラメタの読込と解析、認識インスタンスの初期化
// parameter :env -JNIインタフェース構造体へのポインタ
// jObj -呼出し元JAVAクラスのインスタンス
// curpath -カレントディレクトリ・パス
// return :結果メッセージ(文字列)
//******************************************************************************
extern "C" JNIEXPORT jstring JNICALL
Java_com_teburarec_calljulius_JlibGate_jlibStart(
JNIEnv* env,
jobject jObj,
jstring curpath//, //データファイル格納先ディレクトリ
//void* voicesrc //
)
{
//動作パラメタはすべてsimple_keyword.jconfに記載する
const int argc = 3;
const char* argv[] = {"juliusword", "-C", "simple_keyword.jconf"};

int ret;

//2重起動チェック
if (g_recog != NULL) {
return env->NewStringUTF("ERROR: WAS STARTED ALREADY");
}

//.jconf、単語辞書、音響モデルの格納先をカレントディレクトリに設定する
const char* jdataPath = env->GetStringUTFChars(curpath, NULL);
chdir(jdataPath);
env->ReleaseStringUTFChars(curpath, jdataPath);

//## test code
char cwd[512] = "?";
getcwd(cwd, sizeof(cwd));
fprintf(stdout, "current dir=%s", cwd);

//ログファイルのオープン
//jlog_set_output(NULL);
FILE* fp;
fp = fopen("log.txt", "w");
if (fp == NULL) {
fprintf(stderr, "ERROR: faile to open the log.txt.");
}
jlog_set_output(fp);

//動作パラメタの解析/セッティング
g_jconf = j_config_load_args_new(argc, (char **) argv);
if (g_jconf == NULL) {
fprintf(stderr, "ERROR: found any invalid parameter in JCONF.\n");
return env->NewStringUTF("ERROR: MISSING JCONF");
}

//認識インスタンスの生成
g_recog = j_create_instance_from_jconf(g_jconf);
if (g_recog == NULL) {
fprintf(stderr, "ERROR: faile to startup\n");
return env->NewStringUTF("ERROR: FAILED TO CREATE INSTANCE");
}

//コールバック関数の設定
callback_add(g_recog, CALLBACK_EVENT_SPEECH_READY, status_recready, NULL);
callback_add(g_recog, CALLBACK_EVENT_SPEECH_START, status_recstart, NULL);
callback_add(g_recog, CALLBACK_RESULT, output_result, (void *) g_ret_str);

//Javaコールバック関数のメソッドIDの取得
jclass javagate = env->GetObjectClass(jObj);
g_inVoiceCallback = env->GetMethodID(javagate, "inputVoice", "()I");
if (!g_inVoiceCallback) {
return env->NewStringUTF("ERROR: FAILED TO GET CALLBACK");
}

//音声データ入力用のC側コールバックの指定
g_recog->callback_for_adin = (void *) &InVoiceBridge;

//音声入力デバイスの初期化
if (j_adin_init(g_recog) == FALSE) {
return env->NewStringUTF("ERROR: CAN NOT USE INPUT DEVICE");
}

//認識システム情報のログへの出力
j_recog_info(g_recog);

return env->NewStringUTF("");
}


//******************************************************************************
// function :音声の入力と認識の実行
// parameter :env -JNIインタフェース構造体へのポインタ
// jObj -呼出し元JAVAクラスのインスタンス
// return :結果メッセージ(文字列)-認識結果かエラーメッセージ
//******************************************************************************
extern "C" JNIEXPORT jstring JNICALL
Java_com_teburarec_calljulius_JlibGate_jlibRecognize(
JNIEnv* env,
jobject jObj
)
{
int ret;

//初期化済みチェック
if (g_recog == NULL) {
return env->NewStringUTF("ERROR: IS NOT STARTED");
}

//コールバック用にenvを保存
g_env = env;
g_jObj = jObj;

//音声入力と認識
//Raw file(wav) input
if (g_jconf->input.speech_input == SP_RAWFILE) {
ret = j_open_stream(g_recog, NULL);
if (ret == 0) {
ret = j_recognize_stream(g_recog);
if (ret == -1) {
return env->NewStringUTF("ERROR: RECOGNIZE ERROR");
}
}
else 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");
}
} //end raw speech process

//※本来、以下はエラー条件。結果は保証しない。
//MFCC file or OUTPROB file input
else if (g_jconf->input.speech_input == SP_MFCFILE
|| g_jconf->input.speech_input == SP_OUTPROBFILE) {
static char speechfilename[MAXPATHLEN]; //speech file name for MFCC file input

while (get_line_from_stdin(speechfilename, MAXPATHLEN, (char *) "enter MFCC filename->") != NULL) {
if (verbose_flag) printf("\ninput MFCC file: %s\n", speechfilename);
//open the input file
ret = j_open_stream(g_recog, speechfilename);
switch (ret) {
case 0: //succeeded
break;
case -1: //error
//go on to the next input
continue;
case -2: //end of recognition
//return;
return env->NewStringUTF("END: RECOGNITION");
}
//recognition loop
ret = j_recognize_stream(g_recog);
if (ret == -1) //return -1;
return env->NewStringUTF("ERROR: RECOGNIZE ERROR");
//reach here when an input ends
}
}

//other raw speech input (microphone etc.)
else {
switch(j_open_stream(g_recog, NULL)) {
case 0: //succeeded
break;
case -1: //error
fprintf(stderr, "error in input stream\n");
//return;
return env->NewStringUTF("ERROR: INPUT STREAM");
case -2: //end of recognition process
fprintf(stderr, "failed to begin input stream\n");
//return;
return env->NewStringUTF("ERROR: FAILED TO BEGIN");
}

//******************************************************
//Recognization Loop
//******************************************************
//enter main loop to recognize the input stream
//finish after whole input has been processed and input reaches end
ret = j_recognize_stream(g_recog);
if (ret == -1) //return -1;
return env->NewStringUTF("ERROR: RECOGNIZE ERROR");
}

return env->NewStringUTF((const char *) g_ret_str);
}


//******************************************************************************
// function :認識処理の終了(インスタンスの解放)
// parameter :env -JNIインタフェース構造体へのポインタ
// jObj -呼出し元JAVAクラスのインスタンス
// return :結果メッセージ(文字列)-未使用
//******************************************************************************
extern "C" JNIEXPORT jstring JNICALL
Java_com_teburarec_calljulius_JlibGate_jlibStop(
JNIEnv* env,
jobject jObj
)
{
if (g_recog != NULL) {
j_close_stream(g_recog);
j_recog_free(g_recog);
g_recog = NULL;
}

return env->NewStringUTF("");
}
---------------------------------------------------


path:app\src\main
Java側パッケージ名、クラス名変更に伴う修正
----AndroidManifest.xml----------------------------
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.teburarec.calljulius">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:extractNativeLibs="true">
<activity android:name="com.teburarec.calljulius.JlibGate">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
---------------------------------------------------


Java:calljuliusパッケージ
path:(プロジェクトフォルダ)\app\src\main\java\calljulius

JavaライブラリとしてのI/Fの整理
----JlibGate.java----------------------------------
package com.teburarec.calljulius;

import android.util.Log;
import java.lang.Thread;
import java.lang.Exception;
import android.content.Context;
import android.app.Activity;
import android.content.res.AssetManager;

import java.io.File; //"File" Object
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.IOException;

interface JlibRawSend {
int GetVoiceData();
}

interface JlibRecogNotify {
void NotifyRecognized(String word);
}


public class JlibGate {
private static final String TAG = "CallJulius";
private static final String ASSETSUBDIR = "jdata";
private static final int RECOG_READY = 1;
private static final int RECOG_IDLE = 0;
private static final int RECOG_COMPLETED = -1;

private static Activity myAct;
private static JlibGate myObj;
private static int recogState = RECOG_COMPLETED;
private static boolean fthread_live = false;


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

//### callback interface
private static JlibRawSend MySender = null;
private static JlibRecogNotify MyRequester = null;


//### constractor
public JlibGate(Activity act) {
myAct = act;
myObj = this;
initDataFiles();

//### start Jlib thread
Thread thread = new JlibCallThread();
thread.start();
}


//### setup the asset files
private void initDataFiles() {
AssetManager assetMgr = myAct.getResources().getAssets();
try {
byte[] buffer = new byte[4096];

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

//### Jlib call thread
static class JlibCallThread extends Thread {
public String myDataPath;

@Override
public void run() {
String rtmsg;
Log.i(TAG, "Start Jlib thread.");

synchronized (myObj) {
fthread_live = true;
File myDirObj = myAct.getFilesDir();
myDataPath = myDirObj.getPath();
Log.d(TAG, "My dir path: " + myDataPath);

rtmsg = jlibStart(myDataPath);
if (rtmsg.indexOf("ERROR:") == 0) {
Log.i(TAG, rtmsg);
return;
}
recogState = RECOG_IDLE;

while (recogState != RECOG_COMPLETED) {
while (recogState == RECOG_IDLE) {
try {
myObj.wait();
}
catch (Exception e) {
Log.e(TAG, "# Jlib thread: exception!", e);
return;
}
}
if (recogState != RECOG_COMPLETED) {
rtmsg = jlibRecognize();
if (rtmsg.indexOf("ERROR:") != 0) {
Log.d(TAG, rtmsg);
}
else {
Log.i(TAG, rtmsg);
MyRequester.NotifyRecognized(rtmsg);
}
recogState = RECOG_IDLE;
}
myObj.notifyAll();
}

jlibStop();
fthread_live = false;
} //end synchronized block
Log.i(TAG, "End Jlib thread.");
} //end run()
} //end inner class JlibCallThread



//### JNI C functions
public static native String jlibStart(String curpath);
public static native String jlibRecognize();
public static native String jlibStop();

public static native int jlibCopy(byte[] srcbuf);

//### start the JuliusLib
public boolean jlib_call_start(JlibRawSend callback) {
MySender = callback;
return true;
}

//### execute a recognizing
public boolean jlib_call_recognize(JlibRecogNotify callback) {
if (MySender == null) {
Log.e(TAG, "recognize: not started!");
return false;
}
MyRequester = callback;
synchronized (myObj) {
while (recogState == RECOG_READY) {
try {
myObj.wait();
}
catch (Exception e) {
Log.e(TAG, "recognize: exception!", e);
recogState = RECOG_COMPLETED;
myObj.notifyAll();
return false;
}
}

if (recogState != RECOG_COMPLETED) {
recogState = RECOG_READY;
}
myObj.notifyAll();
} //end synchronized block
return true;
}

//### callback gate
public synchronized int inputVoice() {
return MySender.GetVoiceData();
}

//### send the voice data
public int jlib_call_send(byte[] sendbuf) {
return jlibCopy(sendbuf);
}

//### end the JuliusLib
public void jlib_call_stop() {
try {
Thread.sleep(2);

synchronized (myObj) {
recogState = RECOG_COMPLETED;
myObj.notifyAll();
}
}
catch (Exception e) {
Log.e(TAG, "stop: exception!", e);
}
}


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


path:(プロジェクトフォルダ)\app
Javaライブラリ用に変更(.jaa作成用設定)
----build.gradle-----------------------------------
apply plugin: 'com.android.library'

android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
//applicationId "com.teburarec.call_julius"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {


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



最後のJavaコード上のI/Fを利用して、JuliusLibを動作させられるかまでは、確認できてません。
「ビルドが通って、.jaaファイルを作成できた。」レベルですので、悪しからず。

さて、ここまでで予定していた以下の段取り
Ⅰ.まずはJavaコードからC関数を呼び出すようなアプリの作り方を調べます。
Ⅱ.Javaコード(Cライブラリ含む)をReact Nativeで使えるパッケージ化方法を調べます。
Ⅲ.React Nativeにパッケージを取り込んで、Build&Go。
のⅡの途中ということになります。

「React Nativeで使えるパッケージ化」というワードで検索すると、npmパッケージの作り方とかが出てきます。
でも、考えたら一般に公開したいとかいう欲望はなく、「パッケージ化」という行為そのものが必要なの?と思えて来ました。
かなり面倒そうだし。
「基本(GradleとかManifestとか)を理解してないでパッケージ化もあるまい。」

それに、今回のJavaライブラリは、JSからは直接呼ばない感じで使われるようになりそう。
たぶん単に他のReactNative対応したJavaコードからのコールされるJavaのライブラリです。

そういったことを踏まえて、「AndroidStudio環境下でのJavaライブラリとして、実際の音声データを与えてのテスト(動作確認)」もスキップしようかと思います。

次回、以前に作っていたReactNativeのプロジェクトへのJava&C/C++ライブラリの取り込みを試行錯誤します。

では、また次回。ご機嫌よう。
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