FC2ブログ

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

   プログラミング [2020/08/07]
前回、ちょっと横道に逸れてしまいましたが、元の流れに戻します。

とは言え、ReactNativeからは話が、2か月以上離れてしまってます。
ちょっとこれまでの経緯を説明します。

アプリを作るメインの環境/言語はReactNative(JavaScript:JSX)です。
作りたいアプリでは2種類の音声認識を使いたいと思ってます。

最初の短い単語で、なんらかの機能を選択します。
例えば、「メモ」と言ってから話す内容は音声をテキスト化して残すとか、「チャット」とか言ったらその後の内容をチャットするとか。
短い発話(キーワード)は、スマフォ本体のみで音声認識(Julius)し、長い発話はクラウドサービス(Google Speech)を使います。

そんなことを構想しながらアプリを作り始めて、主題の「スマフォのアプリを作りたい」ももう30回を超えてしまいました。

現在の副題「スマフォだけで動作する音声認識」は、JuliusをAndroidスマフォ上で動作させるところまで来ました。
これまで、以下の段取りで試行錯誤しています。

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

前回までで、Ⅰは終わったかなという感じです。
今回からⅡになります。


パッケージ化の手順もそうですが、その前にReactNativeから利用できるライブラリとしてのI/Fを決めて、JuliusLibを使ってそのI/F・音声認識を実現させないといけない。
つまりⅡとして書いた段取りが、Ⅰ~Ⅲの中で一番重要かつたぶん大変かもしれません。
なぜ大変か。それは、さるがJuliusLibの本当の仕様、I/F、アルゴリズム?・・・何にも理解してないからです。

あちこちの書き込みの背景/前提をろくに理解しないで試すから、無駄、間違いも多いです。
なので、過去記事も含めて、ここの記述事例を「参考に」などと読む方がいらっしゃる場合は、その点を覚悟して読んでください。

またまた、前置きが長いですね。
では、前回からの続きです。


◆JuliusLibに対する機能変更について
だいぶ前に、react-native-google-speech-apiというパッケージをダウンロードしてきて、マイクから入力した音声をGoogle Speechに送り込んで認識させて見ました。(「:音声認識させたい」「:音声認識させたい④」

で、さらにそのパッケージに手を加えて、認識と同時に録音するようにもしてみました。(「:音声認識と同時に録音」「:音声認識と同時に録音③」

今回、単語認識を取り入れるとしたら、手を加える箇所はマイク入力音声を制御しているreact-native-google-speech-api(さるカスタマイズ版)だと思います。
Google Speechに渡す音声を録音処理に回したように、単語認識のタイミングならGoogle Speechに音声を渡さずにJuliusLibに渡すように作り変えます。

なので、JuliusLibに対して、バッファで音声を受け取れるI/Fを追加します。

-ちょっと余談:AndroudStudioのTABの間隔変更-
JuliusLibに含まれるソースの書き方が、ネストが深くなる毎に2カラムのインデントを追加しているようです。たまーにSPACEコードではなくTABコードが使われているのですが、それがどうも8カラムになっているみたい。それに対して、AndroidStudioのデフォルトは4カラムになっているみたいで、AndoidStudio上でソースを参照するとヒジョーに読みにくい。

なので、AndroisStudioでのTAB間隔の変更方法です。
-[File]-[Settings...]メニューを選択
-「Settings」ダイアログ左側で、「Editor」-「Code Style」-「C/C++」の順で展開/選択
-右側表示の「Scheme」で「Project」を選択し、その下の「Tabs and Indents」タブを選択
-タブ内の上から3番目「Tab size」欄に8をしてして画面右下の[OK]をクリック

でも、ソースを追っかける場合は、AndroidStudioじゃなくて、慣れてるテキストエディタを使うけどね。



まずは、JuliusLibが外部に公開しているI/F関数と、音声入力デバイス制御用の関数の関係をGrep機能(sakuraエディタ)でざっと調べてみました。
だいたい以下の感じ・・・かな。

外部I/F
(julius-simple.c使用関数)
入力デバイス関数
(ポインタ名)
マイク入力時関数機能概要
j_adin_inita->ad_standbyadin_mic_standbyデバイスの初期化
j_open_streama->ad_beginadin_mic_begin音の取り込みの開始
j_recognize_streama->ad_endadin_mic_end音の取り込み終了
a->ad_readadin_mic_read音声入力
〃 | j_get_current_filenamea->ad_input_namadin_mic_input_name入力デバイス名取得
〃 | j_request_pausea->ad_pauseadin_mic_pause認識処理の一時停止時に呼出し
j_request_terminatea->ad_terminateadin_mic_terminate認識処理の即時停止時に呼出し
j_request_terminatea->ad_resumeadin_mic_resume一時停止している認識処理の再開時に呼出し


adin_xxx_...関数は、使用する入力デバイス毎に関数(ソースファイル)が準備されていて、起動パラメタの「-input xxx」で使い分けされるようになっている。

音声データの入力部分(adin_xxx_read)で、バッファ渡しで音声を受け取る仕掛けを作り込めばできそうです。

ただ、「-input」に新しいオプションを追加して、まるっと疑似デバイスの仕掛けを準備するのは、修正がかなり広範囲に及ぶ懸念がある。
関連のパラメタとかも理解するのに骨が折れそう。
なので、既存の「-input stdin」を利用して、かつ対応するコードが含まれているソース(adin_file.c)に手を加えることで済ませたい。

だいたいの修正の内容は以下の感じになるかな。

・JuliusLibへの入り口のソースファイル名は、julius-entry.cppに変更。
・ビルド結果もjuliusword.soに変更。

・インタフェースは、jlibStart、jlibRecognize、jlibStopの3種類。
・jlib_recognize時にJava側コールバック関数を呼び出し、バッファ渡しで音声データを取得する。
・コールバック関数の指定は、jlibStart()コール時に行う。

・パッケージ名は、これまで通りcall_juliusのまま。calljuliusに変更。
・Java側のメソッドにjlib_call_start、jlib_call_recognize、jlib_call_stopの3種類を作ってそれぞれjulius-entry.cppの関数を呼び出す。


まずは、前回までのjulius_callプロジェクトをフォルダ丸ごとバックアップし、ともかくやり始めます。

続きます。長いです。


◆C++エントリーソース名とライブラリ名の変更
これまで、Java→C/C++で動作させるときのエントリー関数のソースファイルは、AndroidStudioのテンプレートのままのnative-lib.cpp。
JuliusLibを取り込んだライブラリ名は、native-lib.soになってました。
それを native-lib.cpp → julius-entry.cpplibnative-lib.so → libjuliusword.so に変更します。

1)native-lib.cppをコピーしてjulius-entry.cppにリネームして、(プロジェクトフォルダ)\app\src\main\cppに格納します。
2)native-lib.cppと同じフォルダにあるCMakelist.txtの内容を変更します。
 ターゲット名:「native-lib」→「juliusword」
 ソース名  :「native-lib.cpp」→「julius-entry.cpp」
3)呼び出し側MainActivity.java内のライブラリのロード部分の指定(System.loadLibrary())を「native-lib」→「juliusword」に変更します。

この時点で、再ビルドして実行して見ます。
正常に(お試し時の)動作をしたので、たぶんこれでいいのでしょう。


◆C++側インタフェース関数の仮コーディング
julius-entry.cpp内に、jlibStart()、jlibRecognize()、jlibStop()の3種類のインタフェース関数を作ります。
呼び出し元で、上記のネーミングの関数を使うとすれば、実際関数名の先頭には「Java_com_teburarec_call_1julius_MainActivity_」と付きます。
JNIの仕様です。

※当初「jlib_xxxx()」という関数名にしようとしたのですが、「_」アンダーバーを使うと「jlib_1xxxx」と変換されて定義されるみたいです。
Java/C++のどっちで、どの名前を記述するべきなのかが紛らわしいのでアンダーバーは使わないことにした。

これまで、julius-simple.cのmain()関数でやってた処理をほぼそのままもってきて1つのI/F関数にしていましたが、それを3分割しました。
I/F関数名と使っているJuliusLib関数名です。
・jlibStart :j_config_load_args_new()、j_create_instance_from_jconf()、
 callback_add()、j_adin_init()、j_recog_info()
・jlibRecognize :j_open_stream()※、j_recognize_stream()
・jlibStop :j_close_stream()、j_recog_free()
※この時点で、j_open_stream()が複数回呼ばれるのに対して、j_close_stream()が最後の1回だけな点が正しいシーケンスかはまだグレーです。
 ともかく、このI/Fを仮として、MainActivity.Java側のライブラリコール部分を以下のようにしました。


----MainActivity.java------------------------------
【変更前】

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


【変更後】

String rtmsg;
rtmsg = jlibStart(this.myDataPath);
while (rtmsg.length() == 0) {
rtmsg = jlibRecognize();
}
jlibStop();

tv.setText(rtmsg);
}

/**
* A native method that is implemented by the 'juliusword' native library,
* which is packaged with this application.
*/
public native String jlibStart(String curpath);
public native String jlibRecognize();
public native String jlibStop();


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

※各I/Fのリターン値は、String型にしてあります。文字列がEmptyの場合を正常終了としています。

ここで、もう一回Build&Debugしてみました。普通に動作しました。
この時点で、未だ音声入力は.wavファイルで、その複数の.wavファイルをファイルリストとしてJuliusLibに食わせています。



認識結果は、これまで標準出力(logcat)にメッセージ出力するのみで、データとしてコール元には返していません。
どういうふうにJuliusLib側からリターンされてくるのか、メッセージ出力箇所(julius-simple.c由来のoutput_result())の内容とJuliusBookの解説を見てみました。

認識結果は、Recog構造体(インスタンス)のprocess_list要素の配下に格納されるようです。(詳細はjulius-simple.c等をご参照ください。)

JuliusBookの解説には、JuliusLibの動作パラメタの-n-outputの使い方が書かれています。認識結果候補を何パターン用意して、その内何パターンを返すかを指定するパラメタのようです。仮に両方を1とした場合、もっとも信頼できる認識結果が得られるとは限らないと読めた。

認識結果は「sentenceN」とタイトルされているメッセージ、「wseqN」は「sentenceN」内の単語単位の読みを表し、「cmscoreN」は単語毎の確からしさを表す数値のようです。
つまり、「-n 3 -output 3」と指定すれば、sentence1~3の認識結果が得られて、sentence1~3内に含まれる単語単位の読みと確からしさの数値がwseq1~3とcmscore1~3に含まれて来るということ・・・なのかな?

試しに.jconfファイルで「-n 3 -output 3」を指定してみます。
これまで誤認識されていた発話の認識結果で、正解の単語が2番目以降に来る場合があるのは確認できました。ただし、スコアが高い順番になっているので、1番目(誤認識)のcmscore値が最大です。なので、複数の認識結果を返したとしてもcmscore値だけでは正解を判断できないことが分かりました。(そもそも、話者が訛っていることが根本原因なのは間違いないですが・・・)

JuliusBookの8.2、8.3節には他にも色んな説明がされていましたが理解不能でした。(飛ばし読みなもので・・・ビーム?ペナルティ?)

ともかく、認識結果をJuliusLib内で1パターンに絞ってしまうと、誤認識の救済方法がなくなってしまうので「-n 4 -output 2」に設定することにします。
認識結果を受け取った側でなんらかの方法で正解/不正解を絞り込めるのであれば、その方法を使います。

取り出したかった内容は、認識結果の「単語」と「読み」と「スコア」です。
ちょっとベタな感じですが、「単語」「読み」「スコア」をワンセットとして、TABコードで区切った文字列で返します。2セットがリターンされるということになります。
ただし、「読み」に関しては、julius-simple.cではメッセージ出力していません。
どこをどう辿れば出てくるのか皆目見当がつきませんでした。
なので、リターンデータの枠だけ作って当面空欄にします。

これまでは、エラー時にはメッセージ「ERROR:・・・」と返していたので、正常時には「RETURN:(認識結果タブ区切り文字列)」と返して、先頭の文字列で区別しようかと思います。

リターン文字列を作るのは、元々julius-simple.cから持ってきたoutput_result()で行います。
文字列を返すために、未使用だった第2引数に文字列を格納するバッファを渡すことにします。


output_result()への付け足しの内容はこんな感じ。

----julius-entry.cpp:output_result()--------------

#define MY_RETURN_MAX 240
char* g_ret_str[MY_RETURN_MAX+16]; //認識結果リターン文字列



//******************************************************************************
// function :Callback to output final recognition result.
// This function will be called just after recognition of an input ends
// parameter :
// return :
//******************************************************************************
static void
output_result(
Recog* recog,
void* vp_ret
)
{
int i,
j;
int len;
WORD_INFO* winfo;
WORD_ID* seq;



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;


//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<seqnum;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;i<seqnum;i++) printf(" %s", winfo->wname[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<seqnum; 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


} //end for process_list

//flush output buffer
fflush(stdout);
}
---------------------------------------------------

注)上記コードは、決して誉められた内容ではありません。リターン文字列のバッファ超えチェックが雑です。少しくらい超えてもいいように余裕を持たせて済ませてありますが。sprintfもsprintf_sを使えと警告されます。

Java側へのインタフェース関数は、以下の感じ。
※元々のメインの制御内容を3分割して全体的に弄ってあるので、変更部分のマークは省略します。
----julius-entry.cpp:外部I/F----------------------


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


//******************************************************************************
// function :動作パラメタの読込と解析、認識インスタンスの初期化
// parameter :
// return :
//******************************************************************************
extern "C" JNIEXPORT jstring JNICALL
Java_com_teburarec_call_1julius_MainActivity_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;

//.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);

//音声入力デバイスの初期化
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 :
// return :
//******************************************************************************
extern "C" JNIEXPORT jstring JNICALL
Java_com_teburarec_call_1julius_MainActivity_jlibRecognize(
JNIEnv *env,
jobject jObj
)
{
int ret;

//音声入力と認識
//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 :
// return :
//******************************************************************************
extern "C" JNIEXPORT jstring JNICALL
Java_com_teburarec_call_1julius_MainActivity_jlibStop(
JNIEnv *env,
jobject jObj
)
{
if (!g_recog) {
j_close_stream(g_recog);
j_recog_free(g_recog);
}

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


Java側のリターンの判定&ループ条件も変更します。
----MainActivity.java------------------------------

String rtmsg;
rtmsg = jlibStart(this.myDataPath);
//while (rtmsg.length() == 0) {
while (rtmsg.indexOf("ERROR:") != 0) {

rtmsg = jlibRecognize();
}
jlibStop();
tv.setText(rtmsg);
}

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

ここで、もう一回Build&Debugして予定動作することをデバッガで確認しました。


◆音声入力用のコールバック関数の指定
残ったのが、音声入力用のコールバック関数の指定の仕方と動作確認です。

コールバック関数はJava側での準備になるので、C/C++側からJavaの関数を呼び出す方法をぐぐりました。
参考:https://symfoware.blog.fc2.com/blog-entry-829.html
書いてあるようにやってみますが、「なぜそれをするのか?」理解できてません。

1)Java側にコールバックされる関数を作る
まずは、Java側にコールバック関数を追加でコーディングしてみます。
----MainActivity.java-----------------------------


public synchronized int inputVoice(byte[] buffer, int size) {
Log.d(TAG, "buffer: "+ buffer + ", size: " + size);
return 1111;
}


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



2)Java側コードをビルド
参考サイトに「ビルドする」とあるのでこれまで同様のAndroidStudioのプロジェクトでビルドしてみます。


3)ヘッダファイルを生成する???
※これで、結構ハマった。でもって、結果的にはヘッダは使用しなくてもコンパイルできたので、この手順は不要です

javahの解説(https://docs.oracle.com/javase/jp/8/docs/technotes/tools/windows/javah.html)では、「javahコマンドは、ネイティブ・メソッドを実装するために必要なCヘッダーおよびソース・ファイルを生成します。作成されたヘッダーとソース・ファイルは、ネイティブ・ソース・コードからオブジェクトのインスタンス変数を参照するためにCプログラムによって使用されます。.hファイルは、対応するクラスと一致する配置を持つstruct定義を含みます。structのフィールドは、クラスのインスタンス変数に対応します。・・・」
ヘッダを生成することができるようです。「ふーん」です。

ところでそれはどこにある?
(AndroidStudioインストールフォルダ)\jre\jre\bin」にありました。

ヘッダ生成のための入力は・・・「クラス・ファイル」とある。
「.class」ファイルでいいんだよね?・・・でもjavahの解説には「.jar」みたいなことも書いてある。
「.jar」ファイルは開発中のプロジェクト中では該当するようなものが見当たらず、[MainActivity.class」というファイルなら以下にあった。
「(プロジェクトフォルダ)\app\build\intermediates\javac\debug\classes\com\teburarec\call_julius」

さっそく、コマンドプロンプトを起動して、カレントを上記のフォルダに変更して、
"(AndroidStudioインストールフォルダ)\jre\bin\javah.exe" -classpath . com.teburarec.call_julius.MainActivity
とやってみた。「-classpath .」はカレントディレクトリの意味です。

でも、「クラス・ファイルが見つかりませんでした。」と出る。なんで?

さんざん、「パッケージ名&クラス名指定の仕方がまずいのか?」とか試行錯誤したり、調べたりした挙句、
パッケージ名のパスを含んだ親ディレクトリをカレントにしないといけなさそうなことが分かった。
つまり、パッケージ名が「com.teburarec.call_julius」なら、配下に「com\teburarec\call_julius」を含む位置をカレントにしないといけないらしい。
今回の場合は、「(プロジェクトフォルダ)\app\build\intermediates\javac\debug\classes」。

"(AndroidStudioインストールフォルダ)\jre\bin\javah.exe" -classpath . com.teburarec.call_julius.MainActivity
したら、今度は、「エラー: androidx.appcompat.app.AppCompatActivityにアクセスできません ・・・」と出ます。

上記の(基底)クラスのパスも指定が必要なようです。
しかも今度は.jarというファイルを指定するみたい。
※jarって何?:「JRE - Java Runtime Environmentプログラミング環境で使用されるZIPパッケージに圧縮されたアーカイブのファイルです。 JARパッケージには、メタデータファイルと、テキスト、画像、その他の種類のデータなど、Java環境で使用できる単一のファイルにまとめられたリソースを含むファイルが含まれています。」だそうです。

ググった類似事例から「-bootclasspath (Android SDKインストールフォルダ)\platforms\(使用androidバージョン)/android.jar」とかパラメタを追加して再実行してみましたが、現象は解消しませんでした。.jar違いなようです。

どこに含まれてるんだろう?
探す方法を見つけました。

-AndroidStudioでプロジェクトを開いた状態で、左側表示をProjectペインにします。
-「External Libraries」を展開し、android.jar配下から、見つからないと言われたandroidx.appcompat.app.AppCompatActivityを探します。
結果、androidx.appcompat/app/AppCompatActivityというパスで見つかりました。
20200806_1.jpg
-「AppCompatActivity」をダブルクリックします。ダイアログが表示されます。
20200806_2.jpg
-上記ダイアログ上の「Sources」欄に記載された.jarがそれのようです。パスはコピーできます。
-その.jarのパスをjavahの-classpathに指定して実行します。

----コマンドプロンプト-----------------------------
C:\WINDOWS\system32>cd /d D:\AppMake\julius_base\AndroidStudio\call_julius\app\build\intermediates\javac\debug\classes

・・・\classes>"D:\AppMake\Android\Android Studio\jre\bin\javah.exe" -classpath C:\Users\sarumosunaru\.gradle\caches\modules-2\files-2.1\androidx.appcompat\appcompat\1.1.0\9865019bbd2d95e41dede3d8ebf964aa93f97766\appcompat-1.1.0-sources.jar;. com.teburarec.call_julius.MainActivity

・・・\classes>dir


2020/07/28 16:57 com
2020/08/06 11:22 1,064 com_teburarec_call_julius_MainActivity.h
1 個のファイル 1,064 バイト
3 個のディレクトリ 947,774,443,520 バイトの空き領域

・・・\classes>
---------------------------------------------------

作成されたヘッダの中身はこんなでした。
----com_teburarec_call_julius_MainActivity.h-------
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_teburarec_call_julius_MainActivity */

#ifndef _Included_com_teburarec_call_julius_MainActivity
#define _Included_com_teburarec_call_julius_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_teburarec_call_julius_MainActivity
* Method: jlibStart
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_teburarec_call_1julius_MainActivity_jlibStart
(JNIEnv *, jobject, jstring);

/*
* Class: com_teburarec_call_julius_MainActivity
* Method: jlibRecognize
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_teburarec_call_1julius_MainActivity_jlibRecognize
(JNIEnv *, jobject);

/*
* Class: com_teburarec_call_julius_MainActivity
* Method: jlibStop
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_teburarec_call_1julius_MainActivity_jlibStop
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
---------------------------------------------------

あれ?
コールバック関数の定義とかってどれ?
参考サイトの例も見ましたが、同じ感じです。
Javaのコールバック関数を定義している感じではない。
このヘッダは必要だったのかな。いいえ、さるのビルド環境では必要ありませんでした。


4)コールバックするコードをC++側に入れてみる
大雑把に言うとJNIのGetMethodID()CallXxxMethod()と言う関数を使ってコールバックするらしい。

・メソッドID = env->GetMethodID(jclass, "コールバック・メソッド名", "メソッドのシグニチャ");
・メソッドのリターン値 = env->CallXxxMethod(jObj, メソッドID, コールバックへの引数...);

CallXxxMethod()のXxxは、コールされるメソッドのリターン型で変化する部分だそうです。
envとjObjは、JNIでC++関数がコールされるときの引数です。
GetMethodID()で、メソッドIDを取得して、そのIDを使ってCallXxxMethod()でコールバックする感じ。
GetMethodID()にjclassという型の引数が必要ですが、それは「env->GetObjectClass(jObj)」で取り出せそう。

ちょっと?なのがメソッドのシグニチャというやつです。
javap.exeというツールで、クラス・ファイルから調べられると、どこぞに書いてありました。
(クラスファイル内に書かれているらしいので、Java側にコールバック関数を仕込んだら、一度ビルドしないといけない。)

----コマンドプロンプト-----------------------------
C:\WINDOWS\system32>cd /d D:\AppMake\julius_base\AndroidStudio\call_julius\app\build\intermediates\javac\debug\classes\com\teburarec\call_julius

・・・\call_julius>"D:\AppMake\Android\Android Studio\jre\bin\javap.exe" -s MainActivity.class
Compiled from "MainActivity.java"
public class com.teburarec.call_julius.MainActivity extends androidx.appcompat.app.AppCompatActivity {
public java.lang.String myDataPath;
descriptor: Ljava/lang/String;
public com.teburarec.call_julius.MainActivity();
descriptor: ()V

protected void onCreate(android.os.Bundle);
descriptor: (Landroid/os/Bundle;)V

public native java.lang.String jlibStart(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;

public native java.lang.String jlibRecognize();
descriptor: ()Ljava/lang/String;

public native java.lang.String jlibStop();
descriptor: ()Ljava/lang/String;

public synchronized int inputVoice(byte[], int);
descriptor: ([BI)I

static {};
descriptor: ()V
}

・・・\call_julius>
---------------------------------------------------

「descriptor:」と書いてある後ろの文字列がそれだそうです。
なので、inputVoice()の場合、「([BI)I」がシグニチャです。

以下の感じでC++側コードを書いてみました。
----C++側お試しコード-----------------------------

jclass javagate = env->GetObjectClass(jObj);
jmethodID inVoiceCallback = env->GetMethodID(javagate, "inputVoice", "([BI)I");
if (!inVoiceCallback) {
return env->NewStringUTF("ERROR: FAILED TO GET CALLBACK");
}

jbyteArray testbuf = env->NewByteArray(128);
jint bufsize = 128;
try {
jint jret_lng = env->CallIntMethod(jObj, inVoiceCallback, testbuf, bufsize);
int ret_lng = (int) jret_lng;
}
catch (std::exception &e) {
return env->NewStringUTF("ERROR: EXCEPTION AT CALLBACK");
}

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

ビルドして実行してみました。

Logcat内に「08-06 20:58:59.053 com.teburarec.call_julius D/Call_Julius: buffer: [B@9152021, size: 128」と出てました。
また、C++側のコールバックからのリターン後のret_lenを見てみたら、「1111」と入っていたので、一応両者でパラメタとリターン値受け渡しができていることが確認できました。

ちなみに、CallIntMethod()を呼び出すときの引数の型が、Javaの定義側と違ってたりすると、デバッグ自体が異常終了するので要注意です。
なので、try~catchを仕込んでみましたが、異常状態をcatchできるかは未確認です。

もう一つ、AndroidStudioで、(プロジェクトフォルダ)\app\build\intermediates\javac\debug\classes以下をエクスプローラで開いていたりするとビルドできない現象がありました。詳細原因は不明ですが、comフォルダをデリートしに行ってアクセス拒否される状態になってた。




あーまた長くなりました。一旦ここで切ります。

このあと実際に使える状態にコードを書き直して、JuliusLib側にも必要最小限の変更を加えようと思います。
データの受け渡しが、Cコード同士ならポインタの受け渡しで簡単なんだけど、Javaとの間でデータコピー回数を最小限にする方法を調べないといけないので、またちょっと調べが長くなりそうです。

次回で、を終わらせたいとは思うんだけど・・・終わるのかなー。

では、この辺で。ご機嫌よう。
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