package nliveroid.nlr.main; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import android.app.Activity; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; import android.os.AsyncTask; import android.util.Log; import aqkanji2koe.AqKanji2Koe; import aquestalk2.AquesTalk2; public class AquesSpeech implements Speechable{ private Activity act; private static AudioTrack audioTrack; private static AqKanji2Koe kanji2koe; private static AquesTalk2 aquestalk2; private String dic_dir; private int[] phontArray; private int phontIndex; private String packageName; private String typeName; private int planeSpeed; private int speed = 100; private boolean isInited; private boolean ENDFLAG = true; private String skip_word; private int maxBufferSize; private SpeechLoop loopThread; private String tempBeforeStr = ""; private ArrayList<String> readBuffer = new ArrayList<String>(10); private float vol = 0.5F; byte[] writeData = new byte[80000]; private int trackBufSize; private int playCount; final private int wavHeadPadding = 44; private byte[] wav; private boolean wait; private int oneReadLength; private byte[] phontDat; //TTSSpeechの影響でContextを別セット public AquesSpeech(String skipword,int maxBuffer,byte volp){ this.skip_word = skipword; this.maxBufferSize = maxBuffer; this.vol = (float)volp/10; kanji2koe = new AqKanji2Koe(); aquestalk2 = new AquesTalk2(); if(NLiveRoid.apiLevel >= 11){ trackBufSize = AudioTrack.getMinBufferSize(8000, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT); audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC,//ストリームタイプ 8000,//サンプルレート(kHz) AudioFormat.CHANNEL_CONFIGURATION_MONO,//オーディオチャネル AudioFormat.ENCODING_PCM_16BIT,//フォーマット trackBufSize, // 本来バッファサイズ 1秒が(8000*2)だけど、できない!!!!システムに管理されていて、それ以上のバッファ(最大値不明)を入れるとonMarkerReachedが呼ばれない(2回目は何故か呼ばれる) AudioTrack.MODE_STREAM);//STREAMモードかSTATICモードかいずれか audioTrack.setPlaybackPositionUpdateListener( new AudioTrack.OnPlaybackPositionUpdateListener() { public void onPeriodicNotification(AudioTrack track) { } // 再生完了時のコールバックで足りないバッファ分ループする public void onMarkerReached(AudioTrack track) { if(!ENDFLAG)return;//destroyによってこのクラスが破棄されていたら終了 playCount ++ ; Log.d("Log", "onMarkerReached ------- " + track.getPlaybackHeadPosition() + " " + trackBufSize*(playCount) + " " + oneReadLength); // 指定の長さの再生が終わっても、statusがPLAYINGのままなので、指定位置の再生後にコールバックを行う if(trackBufSize*playCount > oneReadLength){ Log.d("NLiveRoid", "STOP ONE READ ------- "); if(audioTrack.getPlayState()==AudioTrack.PLAYSTATE_PLAYING){ audioTrack.stop();// 発声中なら停止 audioTrack.flush(); } wait = false; return; } audioTrack.write(wav, trackBufSize*playCount+wavHeadPadding, trackBufSize); audioTrack.setNotificationMarkerPosition(trackBufSize/2);// 16bitデータなので、サンプル数は1/2。 audioTrack.play(); } } ); }else{ trackBufSize = 8000*2*10/2;//160000でもいけるかもしれんが、何故か連続してplayできない(だからgetMinBufferSizeだと足りない)+MODE_STATICじゃないとできない→(少なくともSC-02Bとx515mでは4.0以上と方式が違っている) audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC,//ストリームタイプ 8000,//サンプルレート(kHz) AudioFormat.CHANNEL_CONFIGURATION_MONO,//オーディオチャネル AudioFormat.ENCODING_PCM_16BIT,//フォーマット trackBufSize, // 本来バッファサイズ 1秒が(8000*2)だけど、できない!!!!システムに管理されていて、それ以上のバッファ(最大値不明)を入れるとonMarkerReachedが呼ばれない(2回目は何故か呼ばれる) AudioTrack.MODE_STATIC);//STREAMモードかSTATICモードかいずれか audioTrack.setPlaybackPositionUpdateListener( new AudioTrack.OnPlaybackPositionUpdateListener() { public void onPeriodicNotification(AudioTrack track) { } // 再生完了時のコールバックで足りないバッファ分ループする public void onMarkerReached(AudioTrack track) { if(!ENDFLAG)return;//destroyによってこのクラスが破棄されていたら終了 if(track.getPlayState()==AudioTrack.PLAYSTATE_PLAYING){ track.stop();// 発声中なら停止 track.flush(); } wait = false; } } ); } } @Override public void setContext(Activity act, int paramSpeed, int phontIndex) { this.act = act; this.planeSpeed = paramSpeed; this.speed = (int) ((paramSpeed+50)+(paramSpeed*31)); this.phontIndex = phontIndex; dic_dir = act.getFilesDir().toString() +"/aq_dic";// /data/data/<package name>/files packageName = act.getResources().getResourcePackageName(R.raw.aq_rm); typeName = act.getResources().getResourceTypeName(R.raw.aq_rm); phontArray = new int[10]; phontArray[0] = act.getResources().getIdentifier("aq_rm",typeName, packageName); phontArray[1] = act.getResources().getIdentifier("aq_f1c",typeName, packageName); phontArray[2] = act.getResources().getIdentifier("aq_f3a",typeName, packageName); phontArray[3] = act.getResources().getIdentifier("aq_rb2",typeName, packageName); phontArray[4] = act.getResources().getIdentifier("aq_rb3",typeName, packageName); phontArray[5] = act.getResources().getIdentifier("aq_robo",typeName, packageName); phontArray[6] = act.getResources().getIdentifier("aq_m4b",typeName, packageName); phontArray[7] = act.getResources().getIdentifier("aq_mf1",typeName, packageName); phontArray[8] = act.getResources().getIdentifier("aq_huskey",typeName, packageName); phontArray[9] = act.getResources().getIdentifier("aq_yukkuri",typeName, packageName); phontDat = loadPhont(); isInited = true; Log.d("NLiveRoid","AQUESTALK INIT ---------------- " ); //初期化前にアドされていたら読まれない loopThread = new SpeechLoop(); loopThread.execute(); } //native private String convert(String kanji){ if(act == null)return ""; String strKoe = kanji2koe.Convert(dic_dir, kanji); return strKoe; } public void destroy() { Log.d("NLiveRoid","AQUESTALK DESTROY ---------------- "); if(readBuffer != null)readBuffer.clear(); wait = false; ENDFLAG = false; if(loopThread != null&&loopThread.getStatus() != AsyncTask.Status.FINISHED)loopThread.cancel(true); loopThread = null; if(audioTrack != null)audioTrack.release(); } private void play(String koe, int speed) { if(audioTrack.getPlayState()==AudioTrack.PLAYSTATE_PLAYING){ audioTrack.stop();// 発声中なら停止 audioTrack.flush(); } // 音声合成 wav = aquestalk2.syntheWav(koe, speed, phontDat);//スピードを設定してwavが返ってくる // Log.d("SD","SYNCP"+readBuffer.size()); if(wav.length==1){//生成エラー時には,長さ1で、先頭にエラーコードが返される Log.d("NLiveRoid","AquesTalk2SyntheERROR:"+wav[0]); Log.d("NLiveRoid","koe: "+koe); /*エラーコード一覧 100 その他のエラー 101 メモリ不足 102 音声記号列に未定義の読み記号が指定された 103 韻律データの時間長がマイナスなっている 104 内部エラー(未定義の区切りコード検出) 105 音声記号列に未定義の読み記号が指定された 106 音声記号列のタグの指定が正しくない 107 タグの長さが制限を越えている(または[>]がみつからない) 108 タグ内の値の指定が正しくない 109 WAVE 再生ができない(サウンドドライバ関連の問題) 110 WAVE 再生ができない(サウンドドライバ関連の問題 非同期再生) 111 発声すべきデータがない -38 音声記号列が長すぎる -37 1つのフレーズ中の読み記号が多すぎる -36 音声記号列が長い(内部バッファオーバー1) -35 ヒープメモリ不足 -34 音声記号列が長い(内部バッファオーバー1) -16~-24 Phont データが正しくない */ try{ audioTrack.stop(); audioTrack.flush(); readBuffer.clear(); wait = false; act.runOnUiThread(new Runnable(){ @Override public void run() { MyToast.customToastShow(act, "読み上げのコンバートでこけました\nコメント欄を保存して制作に送信下さい"); } }); }catch(Exception e){ e.printStackTrace(); } return; }else { //コンバートエラーでないので再生する try{ if(NLiveRoid.apiLevel >= 11){ oneReadLength = wav.length-wavHeadPadding; Log.d("NLiveRoid","oneReadLength---------------- " + oneReadLength); playCount = 0; audioTrack.write(wav, wavHeadPadding, trackBufSize);//ここは無音区間を含めてAudioTrackのバッファを上書き // 指定の長さの再生が終わっても、statusがPLAYINGのままなので、指定位置の再生後にコールバックを行う audioTrack.setNotificationMarkerPosition(trackBufSize/2);// 16bitデータなので、サンプル数は1/2。 audioTrack.setStereoVolume(0, vol);//モノラルだから意味ない audioTrack.play(); }else{//APKLevel < 11 // データサイズ[byte] 先頭の44バイトはWAVヘッダでわからなくなったので除かない int len=wav.length-44; Log.d("NLiveRoid","AQ LOW LENGTH WHY--- " + len); //AudioTrackに指定したバッファサイズ(最大10秒)を超えるときは切り詰める if(len>trackBufSize){ len = trackBufSize; } // Tricky! audioTrackのバッファに残っているデータが最後に再生されるのを防ぐ // 波形データの後ろに無音区間を追加して、それを含めた長さをwrite()。 // setNotificationMarkerPosition()では、無音区間を含まない長さを指定。 // これで、setNotificationMarkerPosition()のコールバックでstop()が遅延しても、ゴミが出力されない。 // byte[] b = new byte[8000*2*10]; // Arrays.fill(writeData,0,80000,(byte)0);//データをクリアする // //先頭の44バイトはWAVヘッダなので除く // System.arraycopy(wav,44,writeData,0, len);//配列にwavデータを入れる audioTrack.reloadStaticData(); audioTrack.write(wav,44, len);//ここは無音区間を含めてAudioTrackのバッファを上書き // 指定の長さの再生が終わっても、statusがPLAYINGのままなので、指定位置の再生後にコールバックを行う audioTrack.setNotificationMarkerPosition(len/2);// 16bitデータなので、サンプル数は1/2。 audioTrack.setStereoVolume(0, vol);//モノラルだから意味ない audioTrack.play(); } }catch(Exception e){ Log.d("NLiveRoid","Failed Aques "); e.printStackTrace(); act.runOnUiThread(new Runnable(){ @Override public void run() { if(ENDFLAG){ MyToast.customToastShow(act, "読み上げがコケました:0001"); }else{ MyToast.customToastShow(act, "読み上げがキャンセルされました"); } } }); } } } // 指定のPhont名に等しいPhontデータをリソースからLoad private byte[] loadPhont() { try { InputStream in = act.getResources().openRawResource(phontArray[phontIndex]); int size = in.available(); // リソースのデータサイズ byte[] phontDat = new byte[size]; in.read(phontDat); return phontDat; }catch (IOException e) { return null; } } @Override public void addSpeech(final String str) throws InterruptedException {//これが非同期にならないみたいなのでここで非同期にする new AsyncTask<Void,Void,Void>(){ @Override protected Void doInBackground(Void... params) { if(isInited&& str.length() > 0) { if(NLiveRoid.isDebugMode)Log.d("NLiveRoid","AQ ADD " + str); readBuffer.add(str.replaceAll("・|<|>", "")); } return null; } }.execute(); } @Override public void setSpeed(int paramSpeed) { this.planeSpeed = paramSpeed; this.speed = (int) ((paramSpeed+50)+(paramSpeed*31));//50-300 } @Override public void setPich(int pich) {//Pichは調整できないので代わりにphontを設定する this.phontIndex = pich; phontDat = loadPhont(); } public void setVolume(byte param) {//Aquesのみ this.vol = (float)param/10; // if(audioTrack != null)audioTrack.setStereoVolume(0, vol); } @Override public Object[] getStatus() { // Log.d("NLiveRoid","AQ STATUS ---- " +planeSpeed+" "+phontIndex); return new Object[]{this.planeSpeed,5,false,(int)(this.vol*10),phontIndex}; } class SpeechLoop extends AsyncTask<Void,Void,Void>{ String tempStr = ""; @Override public void onCancelled(){ super.onCancelled(); Log.d("NLiveRoid","SpeechLoop canceled"); destroy(); } @Override protected Void doInBackground(Void... params) { try { while(ENDFLAG){ //常にADDされ続ける if(readBuffer.size() > 0){ wait = true; Log.d("SpeechLoop","AQreadBuffer "+readBuffer.size()); if(readBuffer.size() > maxBufferSize){ //無理だった場合クリアしてスキップワードを読み上げさせる readBuffer.clear(); if(skip_word != null&&!skip_word.equals("")){//ワードなしならクリアはしたのでそのまま継続 play(convert(skip_word),speed); } }else{//スキップする必要が無かったら読み上げる tempStr = readBuffer.get(0);//destroyでのclearのタイミングとかぶるとおかしくなるので一旦コピー readBuffer.remove(0); // Log.d("NLiveRoid","POLL---" + tempStr); if(tempStr != null && !tempStr.equals("")){ play(convert(tempStr),speed); } } //読み上げ完了するまで待つ while(wait){ try{ Thread.sleep(100); }catch(InterruptedException e){ Log.d("NLiveRoid","Speech Interrupted."); wait = false; break; } } Log.d("NLiveRoid","AQUES WAIT END"); } } } catch (IndexOutOfBoundsException e) {//途中キャンセル e.printStackTrace(); }catch(IllegalArgumentException e1){ e1.printStackTrace(); Log.d("NLiveRoid","IllegalArgumentException at SpeechLoop"); }catch (RuntimeException e) {//その他 e.printStackTrace(); } return null; } } @Override public boolean isInitalized() {//Testのみで参照(0.8.80) return isInited; } }