FC2ブログ

スマフォのアプリを作りたい(20):音声認識と同時に録音③

   プログラミング [2020/04/06]
前回の続きです。

録音込みの音声認識を作りました。
でも、なんだか録音処理を追加する前より認識率が悪くなったような気がします。
もぞもぞ話しても、ほとんど文字にしてくれません。
調子のいいときは、いいんだけど・・・。

録音してみて分かったのですが、PCのマイク入力を経由してAndroidエミュレータに渡ってくる音声の音質なんだかよくない。
ザラザラした感じの音で、プツプツした雑音も混ざってます。
それはそれで仕方ないことかもしれないけど、何とかならんのかな?

試しにサンプルレートを上げてみることにしました。

前回の記事に改造react-natove-google-speech-apiのAndroid向け修正ソースをまるっと乗せたので、コードの全容は前回記事をご参照ください。


◆サンプルレートを変えて見る
ベースの音声入力クラス(VoiceRecoder.java)でどうやってサンプルレートを決めているか見てみました。
以下のような配列が定義されています。
private static final int[] SAMPLE_RATE_CANDIDATES = new int[]{16000, 11025, 22050, 44100};
それで、初期化時に以下のような処理関数を動かしています。
----VoiceRecoder.java------------------------------

private AudioRecord createAudioRecord() {
for (int sampleRate : SAMPLE_RATE_CANDIDATES) {
final int sizeInBytes = AudioRecord.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
);
if (sizeInBytes == AudioRecord.ERROR_BAD_VALUE) {
//Log.d(TAG, "invalid samplrate="+sampleRate);
continue;
}
//Log.d(TAG, "valid samplrate="+sampleRate);
final AudioRecord audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
sizeInBytes
);
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
buffer = new byte[sizeInBytes];
return audioRecord;
} else {
audioRecord.release();
}
}
return null;
}

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

つまり、SAMPLE_RATE_CANDIDATES配列にセットされているサンプルレート毎にAudioRecordクラスをnewしてみて、そのステートが正常であればループを抜けています。
サンプリングレートの値は低目の値から並んでいるので、一番最初の16000でステートがOKならば、その値で確定してしまいます。
なんで、16000が先頭なんだ?
サンプルレートって高い方が「いい音」なのでは?
と思ったので、配列の順番を高い数値順に変えてみました。
private static final int[] SAMPLE_RATE_CANDIDATES = new int[]{44100, 22050, 16000, 11025};

結果から言うと、44100が選択されるけど認識率は変わりませんでした。

サンプルレートは、「音質」というものに直結するわけではない?

Google Cloudの説明に解説がありました。
参考:https://cloud.google.com/speech-to-text/docs/enccoding?hl=ja

高くすると「周波数の高い音を再現できるようになる」ということだそうで。
人間の耳で聞こえる周波数~20,000Hzを再現するのに必要なのが40000Hz以上(44100Hz)なのだそうだ。
さらに、その下の「非圧縮音声」のところを見ると、人の声として拾うなら16000Hzが適していると書いてます。

だからSAMPLE_RATE_CANDIDATES配列の先頭に16000があったのね。
よって、配列の初期化値の変更は不採用です。


◆見つけた不具合
上記の変更を試している最中に不具合を一つ見つけました。

音声入力時のサンプルレートを録音クラス(VoiceEncoder.java)に渡す処理(start()メソッド)のコール位置がよろしくありませんでした。
GoogleSpeechApiModuleクラスのstart()メソッド中にあったのですが、onServiceConnected()コールバックのVoiceRecorderクラスのstart()メソッドのコール後に移動しました。修正版のコード(GoogleSpeechApiModule.java)を全部載せます。

気になる方は、続きをご覧ください。


朱書き部分はオリジナルコードからの変更点で、太字は今回のバグ修正箇所です。
----GoogleSpeechApiModule.java---------------------
package com.reactlibrary;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;
import java.lang.Thread.UncaughtExceptionHandler;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.reactlibrary.speech_service.SpeechEvent;
import com.reactlibrary.speech_service.SpeechService;
import com.reactlibrary.voice_recorder.VoiceEvent;
import com.reactlibrary.voice_recorder.VoiceRecorder;
import com.reactlibrary.voice_encoder.VoiceEncoder;

import io.reactivex.disposables.CompositeDisposable;

public class GoogleSpeechApiModule extends ReactContextBaseJavaModule {

private static final String TAG = "GoogleSpeechApi";
private static final String KEY_TEXT = "text";
private static final String KEY_IS_FINAL = "isFinal";
private static final String KEY_REASON_CODE = "reasonCode";
private static final String KEY_MESSAGE = "message";
private static final String ON_SPEECH_RECOGNIZED = "onSpeechRecognized";
private static final String ON_SPEECH_RECOGNIZED_ERROR = "onSpeechRecognizedError";

private VoiceRecorder voiceRecorder = new VoiceRecorder();
private VoiceEncoder voiceEnc = new VoiceEncoder();
private boolean Encoding = false;

private SpeechService speechService;
private CompositeDisposable compositeDisposable;
private String apiKey;
private String langCode;
private int maxVoiceBlank;
private String EncFilePath;
private String speechText;
private String tempText;


private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
speechService = SpeechService.from(service);

compositeDisposable.add(
speechService.getSpeechEventObservable()
.subscribe(GoogleSpeechApiModule.this::handleSpeechEvent)
);
compositeDisposable.add(
speechService.getSpeechErrorEventObservable()
.doAfterNext(event -> stop())
.subscribe(GoogleSpeechApiModule.this::handleErrorEvent)
);
compositeDisposable.add(
voiceRecorder.getVoiceEventObservable()
.subscribe(GoogleSpeechApiModule.this::handleVoiceEvent)
);
compositeDisposable.add(
voiceRecorder.getVoiceErrorEventObservable()
.doAfterNext(event -> stop())
.subscribe(GoogleSpeechApiModule.this::handleErrorEvent)
);

voiceRecorder.start(maxVoiceBlank);
//# Start the encoder.
if (!Encoding) {
if (voiceEnc.start(voiceRecorder.getSampleRate(), 128000, EncFilePath) != VoiceEncoder.RETURN_ABEND) {
Encoding = true;
}
}

}

@Override
public void onServiceDisconnected(ComponentName name) {
speechService = null;
}
};

public GoogleSpeechApiModule(ReactApplicationContext reactContext) {
super(reactContext);

//# FOR DEBUG
//# When an exception occurred at unknown point, enable the following statements.
final UncaughtExceptionHandler savedUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
Log.d(TAG, "UncaughtException", ex);
savedUncaughtExceptionHandler.uncaughtException(thread, ex);
}
});

}

@ReactMethod
public void start(String encFilePath) {
Log.i(TAG, "start");
EncFilePath = encFilePath;
if (apiKey == null) {
sendJSErrorEvent("call setApiKey() with valid access token before calling start()", 0);
}
if (compositeDisposable != null) {
compositeDisposable.dispose();
}
compositeDisposable = new CompositeDisposable();
if (speechService == null) {
Intent serviceIntent = new Intent(getReactApplicationContext(), SpeechService.class);
getReactApplicationContext().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
speechText = "";
tempText = "";

} else {
sendJSErrorEvent("Another instance of SpeechService is already running", 0);
}
}

@ReactMethod
public void stop() {
Log.i(TAG, "stop");
voiceRecorder.stop();
compositeDisposable.dispose();
getReactApplicationContext().unbindService(serviceConnection);
speechService = null;
Log.d(TAG, "stoped.");
}

@ReactMethod
public void setApiKey(String apiKey) {
Log.i(TAG, "setApiKey");
this.apiKey = apiKey;
}

@ReactMethod
public void setLangCode(String langCode) {
Log.i(TAG, "setLangCode");
this.langCode = langCode;
}
@ReactMethod
public void setMaxVoiceBlank(int maxVoiceBlank) {
Log.i(TAG, "setMaxVoiceBlank");
this.maxVoiceBlank = maxVoiceBlank;
}


@Override
public String getName() {
return TAG;
}

private void handleVoiceEvent(VoiceEvent event) {
switch (event.getState()) {
case START:
onVoiceStart();
break;
case VOICE:
onVoice(event.getData(), event.getSize());
break;
case END:
onVoiceEnd(event.getReason());
break;
}
}

private void onVoiceStart() {
Log.i(TAG, "onVoiceStart");
if (speechService != null) {
speechService.startRecognizing(voiceRecorder.getSampleRate(), apiKey, langCode);
}
}

private void onVoice(byte[] data, int size) {
Log.i(TAG, "onVoice");
if (speechService != null) {
speechService.recognize(data, size);
}
//# Send a voice data buffer to the encoder.
if (Encoding && voiceEnc.send(data, size) == VoiceEncoder.RETURN_ABEND) {
Encoding = false;
}

}

private void onVoiceEnd(int reason) {
Log.i(TAG, "onVoiceEnd("+reason+")");
if (speechService != null) {
speechService.finishRecognizing();
}
if (reason == VoiceRecorder.STOP_REASON_BLOCK) {
//# Stop the encoder.
if (Encoding) {
if (voiceEnc.send(null, VoiceEncoder.STOP_STREAM) != VoiceEncoder.RETURN_ABEND) {
Log.d(TAG, "onVoiceEnd sended end of stream");
voiceEnc.stop();
}
Encoding = false;
}
//# The calling of stop() on here has risk that produce an exception.
//# Therefore, another error event is issued by VoiceRecorder after this event.
//stop();
Log.d(TAG, "onVoiceEnd push text="+speechText+"temp="+tempText);
speechText = speechText + tempText + ".";
WritableMap params = Arguments.createMap();
params.putString(KEY_TEXT, speechText);
params.putBoolean(KEY_IS_FINAL, true);
sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED, params);
}
Log.d(TAG, "onVoiceEnd exit");

}

private void handleSpeechEvent(SpeechEvent speechEvent) {
Log.i(TAG, speechEvent.getText() + " " + speechEvent.isFinal());
WritableMap params = Arguments.createMap();

//# I changed the specification here.
//# Even if the "isFinal" was returned from the SpeechService, the voices inputting and the speech
//# regonition is continued. The text of "isFinal" is merged with previouse texts.

//if (!TextUtils.isEmpty(speechEvent.getText())) {
// params.putString(KEY_TEXT, speechEvent.getText());
//} else {
// params.putString(KEY_TEXT, "");
//}
//params.putBoolean(KEY_IS_FINAL, speechEvent.isFinal());
//sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED, params);
//if (speechEvent.isFinal()) {
// stop();
//}

boolean isfinal = speechEvent.isFinal();
String currenttext = speechEvent.getText();
if (TextUtils.isEmpty(currenttext)) {
currenttext = "";
}
if (isfinal) {
speechText = speechText + currenttext + ".";
tempText = "";
params.putString(KEY_TEXT, speechText);
}
else {
tempText = currenttext;
params.putString(KEY_TEXT, (speechText + tempText + "."));
}
params.putBoolean(KEY_IS_FINAL, false);
sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED, params);

}

//# In serviceConnection, the calling this method be set to execute stop() after here.
private void handleErrorEvent(Throwable throwable) {
sendJSErrorEvent(throwable.getMessage(), 0);
}

private void sendJSErrorEvent(String message, int reasonCode){
Log.d(TAG, "ssendJSErrorEvent message="+message+", reasonCode="+reasonCode);
//# If the event from VoiceRecoder was "VR_STOP_MSG", this event is not notified to App.
//# In this case, the event is issued only to call stop() safely.
//# Its event is issued when voice blank reached the timeout.
if (!message.equals(VoiceRecorder.VR_STOP_MSG)) {
WritableMap params = Arguments.createMap();
params.putString(KEY_MESSAGE, message);
params.putInt(KEY_REASON_CODE, reasonCode);
sendJSEvent(getReactApplicationContext(), ON_SPEECH_RECOGNIZED_ERROR, params);
}

}

private void sendJSEvent(ReactContext reactContext,
String eventName,
WritableMap params) {
Log.d(TAG, "sendJSEvent params="+params);
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
Log.d(TAG, "sendJSEvent exit");
}
}
---------------------------------------------------



このサブタイトル(音声認識と同時に録音)はこれで一旦終わりにします。
もうちょっと、認識率が良くなる手立てがあるかもしれないのですが、どうも音源的によろしくない気がしてます。
さるは育った地域の訛りが抜けてないので、標準語の発音/イントネーションには程遠いし。
エミュレータで撮れる音も雑音があるし。
何より、家でパソコンに向かってブツブツ話してると周りから「うるさい!」と言われるし、環境がよくない。

なので、エミュレータから実機に乗せ換えてから、改めて評価してみたいと思います。

ああ、それと
前回から載せたコードでエミュレータ環境に作成された音声ファイル(.m4a)の取り出しは以下でできます。
-コマンドプロンプトを起動します。
-adbの含まれるフォルダにカレントを移動します。(Android SDKの「platform-tools」というフォルダ)
-以下のコマンドを実行します。
 adb pull /data/user/0/com.(プロジェクト名)/files/speechN.m4a C:\(任意のフォルダ)
 ※Nは、「認識開始」ボタンを押した回数を表します。2回目に押したときの音声はspeech2.m4aになります。

ついでに、
adbで取り出す(pull)前に入っているか確認するには「adb shell」コマンドが使えます。
シェルを起動するので、lsとかcdとかコマンドが使えました。
「cd /data/users/0/com.vtchar/files」とかやって「ls」とやれば、作成された音声ファイル名が見られます。
終わらせたいときは「exit」でいいみたいです。


次回からは、録音した音声ファイル(.m4a)の再生のさせ方を調べて見ます。

それではこの辺で、ごきげんよう。
(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