//////////////////////////////
// ECCSkelton
// CVOICEクラス定義ファイル
// Copyright (c) August, 2022
//        By Ysama
//////////////////////////////
#include <sapi.h>
#include <wchar.h>
#include <string>
#include <locale.h>
using namespace std;

#define	MAX_NUM	64							//音声トークンの最大数

class CVOICE {
private:
	//変数
	HRESULT m_hr;							//結果判定用変数
	ISpVoice* m_pVoice;						//音声オブジェクトポインター
	DWORD m_Num_of_Token;					//音声トークン（声の主）の数
	ISpObjectToken* m_pToken[MAX_NUM];		//音声トークンへのポインター（複数人用）
	ISpObjectTokenCategory* m_pCategory;	//トークンカテゴリーへのポインター
	IEnumSpObjectTokens* m_pTokens;			//列挙用トークン群へのポインター
	WCHAR m_TokenID[MAX_NUM][MAX_PATH];		//音声トークンｋKeyの文字列用
	WCHAR m_TokenName[MAX_NUM][32];			//音声トークンAttributeのName用
	WCHAR m_TokenGender[MAX_NUM][7];		//音声トークンAttributeのGender用（Male or Female）
	WCHAR m_TokenLanguage[MAX_NUM][5];		//音声トークンAttributeのLanguage用（411, 409 etc−4桁もある）
	wstring m_script;						//読上げるワイド文字列（音声id）へのポインター
	//関数
	WCHAR* GetAttribute(LPWSTR, WCHAR*);		//音声の指定Attributeを取得する

public:
	//関数
	CVOICE();								//コンストラクター
	~CVOICE();								//デストラクター
	bool GetVoices(HWND);					//利用可能な音声をリストボックスに登録する
	int GetCount()							//利用可能な音声数を返す
		{return m_Num_of_Token;}
	WCHAR* GetGender(DWORD);					//利用可能な特定の音声の性別を返す
	WCHAR* GetLanguage(DWORD);				//利用可能な特定の音声の言語を返す
	bool SetVoice(DWORD);					//利用可能な特定の音声を選択する
	HRESULT Speak(WCHAR*);					//スクリプトを読む
	HRESULT Speak(wstring);					//スクリプトを読む
	void Pause();							//読み上げの中断
	void Resume();							//読み上げの再開
};

//コンストラクター
CVOICE::CVOICE() {

	//ロケールの設定
	_wsetlocale(LC_CTYPE, L"JPN");			//ワイド文字使用のためロケールの初期化（PC環境）
	m_hr = CoInitialize(NULL);				//一回だけ呼び出すCOMの初期化
	if(FAILED(m_hr))
		MessageBoxW(0, L"Failed to initialize COM (Error 404)", L"COM Error", MB_OK | MB_ICONERROR);
	else {
		m_pVoice = NULL;					//音声オブジェクトポインター初期化
		for(int i =0; i < MAX_NUM; i++)		//音声オブジェクトトークンへのポインター初期化
			m_pToken[i] = NULL;
		m_hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&m_pVoice);
		if(SUCCEEDED(m_hr))
			m_hr = m_pVoice->GetVoice(&m_pToken[0]);	//デフォルトの音声を取得
		else
			MessageBoxW(0, L"Failed to create SAPI instance", L"SAPI Error", MB_OK | MB_ICONERROR);
	}
}

//デストラクター
CVOICE::~CVOICE() {

	m_pVoice->Release();
	m_pVoice = NULL;
	CoUninitialize();
}

//音声の指定Attributeを取得する
WCHAR* CVOICE::GetAttribute(LPWSTR ptVoiceID, WCHAR* entry) {

	static BYTE Buff[128];	//取得したentry(Age, Gender, Name等)の文字（REG_SZ）データの取得用
	WCHAR* VoiceID;			//ptVoiceIDコピー用文字列ポインター
	WCHAR* cp;				//ptVoiceID解析用ポインター
	VoiceID = new WCHAR[lstrlenW(ptVoiceID) + 12];	//ワイド文字数＋"\\Attributes"＋終端ヌル
	lstrcpyW(VoiceID, ptVoiceID);					//VoiceIDへptVoiceIDをコピーする
	for(cp = VoiceID; *cp && *cp != L'\\'; cp++);	//cpを使ってptVoiceIDの最初の「\」を検索する
	if(*cp)											//*cpがL'\0'でなければL'\\'である
		*cp++ = L'\0';								//最初の「\」をNULLとし、もう一つcpを進める
	else
		return NULL;								//NULL終端まで来たのでエラー
	//VoiceIDの頭がHKEY_LOCAL_MACHINEでなければNULLを返す
	if(lstrcmpW(VoiceID, L"HKEY_LOCAL_MACHINE"))
		return NULL;
	//VoiceIDのサブキーAttributesをKeyにしてhKeyを取得
	lstrcatW(cp, L"\\Attributes");
	HKEY hKey;
	DWORD dwResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, cp, 0, KEY_READ, &hKey);
	//VoiceIDのKeyが開けなければNULLを返す
	if(dwResult != ERROR_SUCCESS)
		return NULL;
	//Attributesのentryの文字データを取得
 	DWORD type;
	DWORD cbData = sizeof(Buff);
	dwResult = RegQueryValueExW(hKey, entry, NULL, &type, Buff, &cbData);
	//VoiceIDのAttributesのentryが読めなければNULLを返す
	if(dwResult != ERROR_SUCCESS)
		return NULL;
	RegCloseKey(hKey);
	delete [] VoiceID;	
	return (WCHAR*)Buff;
}

bool CVOICE::GetVoices(HWND listbox) {	//利用可能な音声をリストボックスに登録する

	if(FAILED(m_hr)) {	//インスタンスを作成後に呼ぶため、コンストラクターの結果をチェックする
		MessageBoxW(0, L"Failed to get Default Voice token", L"SAPI Error", MB_OK | MB_ICONERROR);
		return FALSE;
	}
	//１．デフォルト音声カテゴリーの取得
	bool RetSign1 = FALSE;
	m_hr = m_pToken[0]->GetCategory(&m_pCategory);
	if(SUCCEEDED(m_hr)) {
		m_hr = m_pCategory->EnumTokens(0, 0, &m_pTokens);	//利用可能トークン群の取得
		if(SUCCEEDED(m_hr)) {
			//トークン情報の列挙
			DWORD i;							//カウンター変数
			LPWSTR ptVoiceID;					//音声IDへのポインター
			m_hr = m_pTokens->GetCount(&m_Num_of_Token);	//トークン（音声）数の取得
			if(SUCCEEDED(m_hr)) {
				WCHAR Buff[MAX_PATH];
				for (i = 0; i < m_Num_of_Token; i++) {
					m_hr = m_pTokens->Item(i, &m_pToken[i]);
					m_hr = m_pToken[i]->GetId(&ptVoiceID);
					//ptVoiceIDをm_TokenID[i]へ収納（長さMAX_PATH）
					wcsncpy(m_TokenID[i], ptVoiceID, MAX_PATH);
					//m_TokenName[i]へ"Name"を収納
					lstrcpyW(m_TokenName[i], GetAttribute(ptVoiceID, L"Name"));
					//m_TokenGender[i]へ"Gender"を収納
					lstrcpyW(m_TokenGender[i], GetAttribute(ptVoiceID, L"Gender"));
					//m_TokenLanguage[i]へ"Language"を収納
					lstrcpyW(m_TokenLanguage[i], GetAttribute(ptVoiceID, L"Language"));
					wsprintfW(Buff, L"%s (a %s with the native language of %s)", m_TokenName[i], m_TokenGender[i], m_TokenLanguage[i]);
					SendMessageW(listbox, LB_INSERTSTRING, i, (LPARAM)Buff);
					CoTaskMemFree(ptVoiceID);
					RetSign1 = TRUE;
				}
			}
		}
	}
	//２．Speech_OneCoreの音声カテゴリーの取得
	bool RetSign2 = FALSE;
	ISpObjectTokenCategory* pCategory;
	IEnumSpObjectTokens* pTokens;
	HRESULT hr = CoCreateInstance(CLSID_SpObjectTokenCategory, NULL, CLSCTX_ALL, IID_ISpObjectTokenCategory, (void **)&pCategory);
	if(SUCCEEDED(hr)) {
		hr = pCategory->SetId(L"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices", FALSE);
		if(FAILED(hr))
			MessageBoxW(0, L"Can't set category id", L"OneCore - SetId", MB_OK | MB_ICONERROR);
		hr = pCategory->EnumTokens(0, 0, &pTokens);	//利用可能トークン群の取得
		if(SUCCEEDED(hr)) {
			//トークン情報の列挙
			DWORD n, i;							//音声数取得用とカウンター変数
			LPWSTR ptVoiceID;					//音声idへのポインター
			hr = pTokens->GetCount(&n);			//トークン（音声）数の取得
			if(SUCCEEDED(hr)) {
				WCHAR Buff[MAX_PATH];
				for (i = 0; i < n; i++) {
					hr = pTokens->Item(i, &m_pToken[m_Num_of_Token + i]);
					hr = m_pToken[m_Num_of_Token + i]->GetId(&ptVoiceID);
					//ptVoiceIDをm_TokenID[i]へ収納（長さMAX_PATH）
					wcsncpy(m_TokenID[m_Num_of_Token + i], ptVoiceID, MAX_PATH);
					//m_TokenName[i]へ"Name"を収納
					lstrcpyW(m_TokenName[m_Num_of_Token + i], GetAttribute(ptVoiceID, L"Name"));
					//m_TokenGender[i]へ"Gender"を収納
					lstrcpyW(m_TokenGender[m_Num_of_Token + i], GetAttribute(ptVoiceID, L"Gender"));
					//m_TokenLanguage[i]へ"Language"を収納
					lstrcpyW(m_TokenLanguage[m_Num_of_Token + i], GetAttribute(ptVoiceID, L"Language"));
					wsprintfW(Buff, L"(OneCore) %s (a %s with the native language of %s)", m_TokenName[m_Num_of_Token + i], m_TokenGender[m_Num_of_Token + i], m_TokenLanguage[m_Num_of_Token + i]);
					SendMessageW(listbox, LB_INSERTSTRING, m_Num_of_Token + i, (LPARAM)Buff);
					CoTaskMemFree(ptVoiceID);
					RetSign2 = TRUE;
				}
				m_Num_of_Token += n;
			}
		}
		pCategory->Release();
		pCategory = NULL;
	}
	return RetSign1 * RetSign2;
}

//利用可能な特定の音声の言語を返す
WCHAR* CVOICE::GetLanguage(DWORD num) {

	if(num > m_Num_of_Token)	//現存するISpObjectTokenポインター数を超える引数
		return NULL;
	else
		return m_TokenLanguage[num];
}

//利用可能な特定の音声の性別を返す
WCHAR* CVOICE::GetGender(DWORD num) {

	if(num > m_Num_of_Token)	//現存するISpObjectTokenポインター数を超える引数
		return NULL;
	else
		return m_TokenGender[num];
}

//利用可能な音声を選択する
bool CVOICE::SetVoice(DWORD num) {

	if(num > m_Num_of_Token)	//現存するISpObjectTokenポインター数を超える引数
		return FALSE;
	if(SUCCEEDED(m_pVoice->SetVoice(m_pToken[num])))
		return TRUE;
	else
		return FALSE;
}

//スクリプトを読む
HRESULT CVOICE::Speak(WCHAR* str) {

	SPVOICESTATUS status;
	m_pVoice->GetStatus(&status, NULL);
	if(!status.dwRunningState || status.dwRunningState == SPRS_IS_SPEAKING) {
		int ync = MessageBoxW(0, L"現在読み上げ中、または未読スクリプトが残っています。中断しますか？", L"TTS Busy", MB_YESNOCANCEL | MB_ICONQUESTION);
		if(ync == IDYES) {
			m_pVoice->Speak(L"<lang langid=\"411\">中断しました</lang><lang langid=\"409\">All pendeling scripts abandoned</lang>", SPF_PURGEBEFORESPEAK, NULL);
		}
		else if(ync == IDCANCEL)
			return -1;	//HRESULTのFAILEDの意味（32bit目が1）
	}
	m_script = str;
	return m_pVoice->Speak(m_script.c_str(), SVSFlagsAsync, NULL);	//非同期処理
//	return m_pVoice->Speak(m_script.c_str(), SVSFDefault, NULL);	//同期処理
}

//スクリプトを読む
HRESULT CVOICE::Speak(wstring script) {

	SPVOICESTATUS status;
	m_pVoice->GetStatus(&status, NULL);
	if(!status.dwRunningState || status.dwRunningState == SPRS_IS_SPEAKING) {
		int ync = MessageBoxW(0, L"現在読み上げ中、または未読スクリプトが残っています。中断しますか？", L"TTS Busy", MB_YESNOCANCEL | MB_ICONQUESTION);
		if(ync == IDYES) {
			m_pVoice->Speak(L"<lang langid=\"411\">中断しました</lang><lang langid=\"409\">All pendeling scripts abandoned</lang>", SPF_PURGEBEFORESPEAK, NULL);
		}
		else if(ync == IDCANCEL)
			return -1;	//HRESULTのFAILEDの意味（32bit目が1）
	}
	m_script = script;
	return m_pVoice->Speak(m_script.c_str(), SVSFlagsAsync, NULL);	//非同期処理
//	return m_pVoice->Speak(m_script.c_str(), SVSFDefault, NULL);	//同期処理
}

//読み上げの中断
void CVOICE::Pause() {

	m_pVoice->Pause();
}

//読み上げの再開
void CVOICE::Resume() {

	m_pVoice->Resume();
}
