package WinSAPI;

import java.io.File;

import jnr.ffi.LibraryLoader;
import jnr.ffi.annotations.Encoding;

//********************************************************
// Windows SAPI To Javaブリッジライブラリ
//------------------------------------------------------------------------
// CopyRight : MSLABO  Ver1.0
//********************************************************
// 大人の約束
// 本プログラムとドキュメント（以下、本APと呼称）の著作権はMSLABOにあります。
//本APは、以下のクリエィティブ・コモンズライセンスに従い提供されています。
//https://creativecommons.org/licenses/by-sa/4.0/
//
//利用にあたっては、必ず自己責任でお願い致します。
//
//本APの不具合については極力善処を行う予定ですが、これを保証するものではありません。
//また本APに関する情報はすべてMSLABOのHPから一方的に公開するものとし、原則として
//個別の問い合わせや要望は受け付けません。
//
//上記事項に同意頂ける方のみ、利用可能です。
//
// 情報掲載先：
// URL : http://mslabo.sakura.ne.jp/WordPress/
//
// 本プログラムが、みなさまのPROCESSING利用の一助になれば幸いです。
//
//***********************************************************

/**
 * Java To Windows SAPI ブリッジクラス
 *
 *
 * @see
 * <a href="https://creativecommons.org/licenses/by-sa/4.0/">
 * <img src="{@docRoot}/doc-files/by-sa.png" alt="クリエィティブ・コモンズライセンス">
 * </a><br><br>
 * <a href="http://mslabo.sakura.ne.jp/WordPress/">・関連情報公開元</a>
 *
 * @author MSLABO
 * @version 2017/04/30  1.0
 */
public class WinSAPI {
	private  MyLib  cpp = null;	//DLLライブラリポインタ
	SubThread	t1 = null;			//スレッド

	//---------------------------------------------------
	//DLL内にある関数一覧インタフェース
	//---------------------------------------------------
	protected  interface MyLib {
	    int createSynthesizer( @Encoding("UTF-16LE")String string );
	    void setSpeed( int speed );
	    void setVolume( int volume );
	    int setVoice( @Encoding("UTF-16LE")String string );
	    int getSpeed();
	    int getVolume();
	    String getVoice();
	    int getStat();
	    void speakMsg( @Encoding("UTF-16LE")String string );
	    void speakSsmlMsg( @Encoding("UTF-16LE")String string );
	    void speakAsyMsg( @Encoding("UTF-16LE")String string );
	    void speakAsySsmlMsg( @Encoding("UTF-16LE")String string );
	    void sapiDispose();
	}

/**
 * コンストラクタ：クラスを初期化し、シンセサイザーを生成します。<br><br>
 * WinSAPICPP.DLLを読み込み、発声に利用するシンセサイザーオブジェクトを生成します。<br>
 * WAVファイルに出力する場合、指定されたフォルダが存在しないとシンセイサイザーの生成に失敗します。<br>
 * またOSが許可していない場所や、ドライブ直下にWAVファイルを出力することはできません。
 *
 * @param fileName 発声データをWAVファイルに保存する場合に指定します。nullまたは空文字ならデフォルトデバイス（スピーカー）に出力します。
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
  *  WinSAPI  sapi = null;
  *  sapi = new WinSAPI(""); //生成（スピーカー出力）
  *  //sapi = new WinSAPI("C:\\Temp\\hoge.wav"); //生成（WAVファイル出力）
  *  sapi.speakMsg("Hello Processing" );
  *  sapi.dispose();
  * }
  * void draw(){
  * }
 * }</pre>
 */
	 public WinSAPI(String fileName )  {
		 //ライブラリのポインタを取得
		 cpp = LibraryLoader.create(MyLib.class).load("WinSAPICPP");

		 int ret = createSynthesizer( fileName );
		 if( ret != 0 ){
			 System.err.println( "createSynthesizer error\n" );
			 return;
		 }
		return;
	}

/**
 * シンセサイザー生成：シンセサイザーオブジェクトを生成します。<br><br>
 * 実際のシンセサイザーオブジェクトは、WinSAPICPP.DLL内部で保持されています。
 *
 * @param fileName 発声データをWAVファイルに保存する場合に指定します
 * @return <pre>{@code -99：シンセサイザー生成失敗;<br>0：成功;}</pre>
 */
	private int createSynthesizer( String fileName ){
		int ret = -99;

		if( cpp == null ){
			return( ret );
		}

		if( fileName != null && fileName.trim().length() > 0 ){
			//ファイルが指定された場合は、フォルダの存在を検査する
			String dirName = new File( fileName ).getParent();

			File file =null;
			if( dirName != null && dirName.length() > 0 ){
				file = new File( dirName );
				if( file.exists() == false ){
					//フォルダが存在しない
					System.err.printf( "%s folder is not found\n", dirName );
					return( ret );
				}
			}
		}

		//シンセイサイザーを生成する
		ret = cpp.createSynthesizer( fileName );
		return( ret );
	}

/**
 * シンセサイザー開放：リソースとシンセサイザーオブジェクトを開放します。<br><br>
 * 開放すると、強制的に発声は中断されます。非同期発声中は注意してください。<br>
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
  *  WinSAPI  sapi = null;
  *  sapi = new WinSAPI("");
  *  sapi.speakMsg("Hello Processing" );
  *  sapi.dispose();   //開放
  * }
  * void draw(){
  * }
 * }</pre>
 */
	public void dispose(){
		if( cpp != null ){
			cpp.sapiDispose();
			cpp = null;
		}
		return;
	}

/**
 * 読み上げ速度指定：読み上げ速度を変更します。<br><br>
 *
 * @param speed {@code +10 から -10。0なら等倍速で読み上げます}
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.speakMsg("Hello");
 *   sapi.setSpeed(7);  //[Processing]部分は速読する
 *   sapi.speakMsg("Processing");
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }</pre>
 */
	public void setSpeed( int speed ){
		if( cpp != null ){
			//パラメータの整合性はDLL側で実行
			cpp.setSpeed(speed);
		}
		return;
	}

/**
 * 読み上げ音量指定：読み上げ時の音量を変更します。<br><br>
 *
 * @param volume {@code 0 から 100。0なら無音、100ならMAX音量で読み上げます}
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.speakMsg("Hello");
 *   sapi.setVolume(0); //[Processing]部分は無音になる
 *   sapi.speakMsg("Processing");
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public void setVolume( int volume ){
		if( cpp != null ){
			//パラメータの整合性はDLL側で実行
			cpp.setVolume(volume);
		}
		return;
	}

/**
 * 音声名称指定：発声に利用する音声データ名称を指定します。<br><br>
 * デフォルトではOSの設定が利用されます。<br>
 * 指定された音声データが該当OSに未インストールの場合、変更に失敗します。
 *
 * @param voiceName {@code 音声データ名称。正式名称指定の他に、名称に含まれる部分文字列指定でも可能です}
 * @return <pre>{@code -99：変更失敗;
 *  0：成功;}</pre>
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.setVoice("Microsoft Zira Desktop");  //Windows10 標準英語音声データを指定
 *   sapi.speakMsg("Zira is english voice data");  //英語で読み上げます
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public int setVoice(String voiceName ){
		int ret = -99;
		if( cpp != null ){
			ret = cpp.setVoice( voiceName );
		}
		return( ret );
	}

/**
 * 読み上げ速度取得：現在の読み上げ速度を取得します。<br><br>
 *
 * @return {@code +10 から -10。0なら等倍速です}
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   println( sapi.getSpeed() );  // 0 が表示される
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public int getSpeed(){
		int ret = -99;
		if( cpp != null ){
			ret = cpp.getSpeed( );
		}
		return( ret );
	}

/**
 * 音声名称取得：現在選択されている音声データ名称を取得します。<br><br>
 * 音声データの正式名称が戻されます。
 *
 * @return {@code 音声データ正式名称 }
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   println( sapi.getVoice() );  //[Microsoft Haruka Desktop]が表示される
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public String getVoice(){
		String voice = null;
		if( cpp != null ){
			voice = cpp.getVoice( );
		}
		return( voice );
	}

/**
 * 読み上げ音量取得：現在の音量を取得します。<br><br>
 *
 * @return {@code 0 から 100 。0なら無音。100ならMAX音量}
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   println( sapi.getVolume() );  //100 が表示される
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public int getVolume(){
		int ret = -99;
		if( cpp != null ){
			ret = cpp.getVolume( );
		}
		return( ret );
	}

/**
 * 同期読み上げ：与えられた文章を同期モードで音声化します。<br><br>
 * 出力はデフォルトデバイス（スピーカー）か、WAVファイルに行われます。<br>
 * 読み上げが終わるまで、制御が上位に戻りません。<br>
 * 設定されている音声データが、該当文章を読み上げるのに適切でない場合、期待した読み上げ結果にならない事があります（例：英語音声データで日本語文章を読ませるなど）。<br>
 * 事前に、読み上げたい文章のロケールに応じた音声データをOSにインストールし、必要に応じて{@link #setVoice} で
 * 音声データを指定しておく必要があります。
 *
 * @param msg {@code 読み上げる文章 }
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.setVoice("Microsoft Haruka Desktop");
 *   sapi.speakMsg("貴社の記者が汽車で帰社する");  //読み上げます
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
	 */
	public void speakMsg( String msg ){
		if( cpp != null ){
			cpp.speakMsg( msg );
		}
		return;
	}

	/**
	 * 発声スレッド：非同期読み上げ時に利用します。<br><br>
	 * 内部クラスであり、非公開です。
	 */
	private class SubThread extends Thread{
		private String 	msg;				//読み上げる文字列
		private boolean ssml_sw;		//true : 通常   false : SSML

		//コンストラクタ
		SubThread( String msg, boolean mode ){
			this.msg = msg;
			this.ssml_sw = mode;
		}
		//スレッド
		public void run(){
			if( this.ssml_sw ){
				//通常読み上げ
				cpp.speakMsg( this.msg );
			} else {
				//SSML読み上げ
				cpp.speakSsmlMsg( this.msg );
			}
			return;
		}
	}

	/**
	 * 発声スレッド終了待ち合わせ：発声スレッドの終了を待ち合わせます。<br><br>
	 * cpp.speakAsyMsg() などを使うなら本処理は必要ありません。<br>
	 * ただしDLL側で非同期処理させると、発声が終わったことを検知できな
	 * いため、あえて自力でスレッド制御しています。
	 */
	private void ThreadExitWait(){
		//非同期読み上げ（スレッドがある）場合のみ、待ち合わせる
		if( t1 != null ){
			//スレッドが終了するまで待機する
			while( true ){
				if( isAsySpeak() == false ){
					//スレッドが終了した
					break;
				} else {
					//生きているなら100ms待機
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO 自動生成された catch ブロック
						e.printStackTrace();
						break;
					}
				}
			}
		}
		return;
	}

/**
 * 非同期発声終了検査：非同期読み上げの状態を返却します。<br><br>
 * 本メソッドは、同期読み上げ時には意味を持ちません。
 *
 * @return <pre>{@code false：読み上げていない;
 * true：読み上げ中;}</pre>
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.setVoice("Microsoft Haruka Desktop");
 *   sapi.speakAsyMsg("貴社の記者が汽車で帰社する");
 *   while( sapi.isAsySpeak()){  //非同期読み上げの終了を待つ
 *     delay(100);
 *   }
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public boolean isAsySpeak(){
		boolean alive = false;
		if( t1 != null ){
			if( t1.getState() == Thread.State.NEW ||
				t1.getState() == Thread.State.TERMINATED ){
				//スレッドが終了した
				alive = false;
				t1 = null;
			} else {
				//スレッドが生きている
				alive = true;
			}
		}
		return( alive );
	}

/**
 * 非同期読み上げ：与えられた文章を非同期モードで音声化します。<br><br>
 * 出力はデフォルトデバイス（スピーカー）か、WAVファイルに行われます。<br>
 * 読み上げ開始後、直ちに制御が上位に戻ります。<br>
 * 読み上げ完了前に {@link WinSAPI#dispose } すると、強制的に発声が停止します。読み上げ中か否かは  {@link WinSAPI#isAsySpeak } で知ることが可能です。<br>
 * 設定されている音声データが、該当文章を読み上げるのに適切でない場合、期待した読み上げ結果にならない事があります（例：英語音声データで日本語文章を読ませるなど）。<br>
 * 事前に、読み上げたい文章のロケールに応じた音声データをOSにインストールし、必要に応じて{@link #setVoice} で
 * 音声データを指定しておく必要があります。
 *
 * @param msg {@code 読み上げる文章 }
 * <br>
 * <br>
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.setVoice("Microsoft Haruka Desktop");
 *   sapi.speakAsyMsg("貴社の記者が汽車で帰社する");  //非同期読み上げ
 *   while( sapi.isAsySpeak()){
 *     delay(100);
 *   }
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public void speakAsyMsg( String msg ){
		if( cpp != null ){
			//前回のスレッド終了を待ち合わせる
			ThreadExitWait();
			//非同期読み上げ開始
			t1 = new SubThread( msg , true );
			t1.start();
		}
		return;

		//		cpp.speakAsyMsg( string );
		//		return;
	}

/**
 * SSML非同期読み上げ：与えられた文章を非同期モードで音声化します。<br><br>
 * 与えられた文章はSSML形式の文章とみなします。出力はデフォルトデバイス（スピーカー）か、WAVファイルに行われます。<br>
 * 先頭の speak タグは省略可能です。省略した場合の音声ローケルには、選択中の音声データのものが採用されます。<br>
 * 読み上げ開始後、直ちに制御が上位に戻ります。<br>
 * 読み上げ完了前に {@link WinSAPI#dispose } すると、強制的に発声が停止します。読み上げ中か否かは  {@link WinSAPI#isAsySpeak } で知ることが可能です。<br>
 * 本メソッドに与える文章は SSML 1.0 形式である必要があります。<br>
 * 設定されている音声データが、該当文章を読み上げるのに適切でない場合、期待した読み上げ結果にならない事があります（例：英語音声データで日本語文章を読ませるなど）。<br>
 * 事前に、読み上げたい文章のロケールに応じた音声データをOSにインストールし、必要に応じて{@link WinSAPI#setVoice} で
 * 音声データを指定しておく必要があります。
 *
 * @see <a href="https://www.w3.org/TR/speech-synthesis/">w3.org  Speech Synthesis Markup Language (SSML) Version 1.0</a>
 * @param msg {@code 読み上げる文章 }
 * <br>
 * 
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.setVoice("Microsoft Haruka Desktop");
 *   sapi.speakAsySsmlMsg("<speak version=\"1.0\" xml:lang=\"ja-JP\"><sub alias=\"ピカチュウ\">光宙</sub></speak>);
 *   while( sapi.isAsySpeak()){
 *     delay(100);
 *   }
 *   sapi.dispose();
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public void speakAsySsmlMsg( String msg ){
		if( cpp != null ){
			//前回のスレッド終了を待ち合わせる
			ThreadExitWait();
			//非同期SSML読み上げ開始
			t1 = new SubThread( msg , false );
			t1.start();
		}
		return;

		//cpp.speakAsySsmlMsg(string);
		//return;
	}

/**
 * SSML同期読み上げ：与えられた文章を同期モードで音声化します。<br><br>
 * 与えられた文章はSSML形式の文章とみなします。出力はデフォルトデバイス（スピーカー）か、WAVファイルに行われます。<br>
 * 先頭の speak タグは省略可能です。省略した場合の音声ローケルには、選択中の音声データのものが採用されます。<br>
 * 読み上げが終わるまで、制御が上位に戻りません。<br>
 * 本メソッドに与える文章は SSML 1.0 形式である必要があります。<br>
 * 設定されている音声データが、該当文章を読み上げるのに適切でない場合、期待した読み上げ結果にならない事があります（例：英語音声データで日本語文章を読ませるなど）。<br>
 * 事前に、読み上げたい文章のロケールに応じた音声データをOSにインストールし、必要に応じて{@link WinSAPI#setVoice} で
 * 音声データを指定しておく必要があります。
 * 
 * @see <a href="https://www.w3.org/TR/speech-synthesis/">w3.org  Speech Synthesis Markup Language (SSML) Version 1.0</a>
 * @param msg {@code 読み上げる文章 }
 * <br>
 * 
 * <pre>
 * {@code
 * import WinSAPI.WinSAPI;
 * void setup(){
 *   WinSAPI  sapi = null;
 *   sapi = new WinSAPI("");
 *   sapi.setVoice("Microsoft Haruka Desktop");
 *   sapi.speakSsmlMsg("<speak version=\"1.0\" xml:lang=\"ja-JP\">今日は<say-as interpret-as=\"date\" format=\"ymd\">2017/04/30</say-as>です。</speak>");
 * }
 * void draw(){
 * }
 * }
 * </pre>
 */
	public void speakSsmlMsg( String msg ){
		if( cpp != null ){
			cpp.speakSsmlMsg( msg );
		}
		return;
	}

}
