スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

【同じタグを付けた記事の一覧】

Galatea Talk の音声合成部を C++ で使うサンプル

2011年12月31日(土)19時55分

【追記:2012/02/17】今回の内容に少しコードを書き足し、コマンドライン・ツールを作りました(「カナ読み上げツール KanateaTalk」)。

Galatea Talk

まず最初に、今回はプログラミング(C++)のお話で、ある程度「分かっている方」が対象となりますので、細かい説明(プロジェクトの作り方、文字コードの変換法等々)は端折り気味で進めていきます。
そして本題ですが、Galatea Talk というのはオープンソースで開発されている(た?)日本語音声合成システムで、「漢字かな交じり文」を音声データに変換するためのソフトです。
話者は標準で三人分(男性二人と女性一人)が用意されており、これらによって生成された音声データについては、商用利用も含めて二次使用が認められています。
また、Galatea Talk 本体の部分に関しても、使用許諾書を受諾し遵守する限り、例えば「自作ソフトに組み込んで配布する」というようなことも許可されているようです。

Galatea Talk のテキスト解析部

ただ、「自作ソフトに組み込んで配布したい」というような視点で見た場合、少し悩ましい点があります。
まず、Galatea Talk が漢字かな交じり文を音声データに変換する際、内部では「(漢字かな交じり文)→【テキスト解析部】→(音素データ)→【音声合成部】→(音声データ)」という変遷を経ます。
このうち、前半「テキスト解析部」の内訳は、入力した文章を「UniDic」という辞書ファイルを使い「茶筌(ChaSen)」という形態素解析システムで解析し、さらに「ChaOne」というツールで補正することで、音声合成に必要な情報(読みやアクセント)を得る、という流れになっています。
ここで考えなければならない点が、これらに使用される「UniDic」「ChaSen」「ChaOne」というのは、基本的には Galatea Talk とは別の存在で、それぞれに権利所有者やライセンスも違う、ということです。
特に、辞書である「UniDic」は最新版をダウンロードするために利用者登録が必要で、そして Galatea Talk に付属のものも再配布不可だったりします。

音声合成部のみ使いたい

つまり自作のプログラムなどで Galatea Talk を丸ごと使おうとすると、必要ファイル数やサイズが膨れ上がるだけでなく、権利関係とそれに伴う(配布等の)手間も複雑になるわけです。
ということで、音素データ(読みとアクセント)を手作業で用意することでテキスト解析を不要にし、「音声合成部のみを組み込んで利用する」という方法を模索してみました。
発声する内容はあらかじめほぼ決まっている、例えばゲームのセリフ等で一部分(主人公の名前等)のみ動的に変更したいとか、ユーザー設定により読み上げ速度を変更可能にしたい、という場合などでは、膨大な WAVE ファイルを用意するより、その場で合成する方が便利なこともあると思います。

一筋縄ではいかない

で、始めた当初、おそらくは入力された「漢字かな交じり文」が、「テキスト解析部」を通して、AquesTalk でいうところの「かな表記音声記号列」に変換され、それが「音声合成部」に渡されるのだろう、と勝手に想定していました。
つまり「ここは、日本です。」が「ココワ、ニッポ’ン/デス。」のように変換されて音声合成関数に渡されるのだろうと。
なので、この「音声合成関数」部分を見つければそれで終了だと軽く考えていたのですが、実際には「テキスト解析部」から「音声合成部」までが一連の流れのように連なっており、「この関数に音声記号列を渡せばOK!」というわけにはいかないようなのです。
ということで結局、今回作ったサンプルでは「テキスト解析部」が行っている品詞確定などの形態素解析作業を手作業に置き換えただけ、というような形になっています。
ただ、実際に試した感じでは、特に正確に品詞を指定せずとも、例えば全部「名詞-普通名詞-一般」でもそれなりに話してくれましたので、サンプルではこの方式で解析処理を簡略化しています。
この「簡易方式」ですと、「ココワ、ニッポ'ン/デス。」に近い形で使えますが、しかし、できることであれば正確に指定した方がより良い発音になるのではないかと思います。

方針

とりあえず開始前に、今回の方針についてですが、「なるべくそのまま」という方向で行いました。
Galatea Talk 付属のソースコード部分は、一切書き換えせずに流用しています(文字コードと改行コードの変換以外)。
ただ、コンソールでなく Windows アプリに組み込む場合は、少なくとも restart 関数呼び出し部分に関しては、別の処理に書き換えておかないとダメではないかなと思います。

Win32 コンソール アプリケーション

というわけで作業に入ります。
使用したのは Microsoft Visual C++ 2008 ですが、2010 や 2005 でも違いはないだろうと思います。
まず、新規で「Win32 コンソール アプリケーション」のからのプロジェクトを生成します。
名前は何でも構わないのですが、とりあえず「GalateaTest」としました。
以降、生成された GalateaTest フォルダを「プロジェクト・フォルダ」、その下に生成される GalateaTest フォルダを「ソース・フォルダ」、ビルド後に生成される Debug(または Release)フォルダを「実行ファイル・フォルダ」と呼びます。

必要ファイルのコピー

次に「Galatea Project(http://hil.t.u-tokyo.ac.jp/~galatea/index-jp.html)」のサイトを辿って「galatea4win-istc-2009-02.zip」をダウンロードしてきます。
解凍してできた「\engine\SSM\gtalk」フォルダから、以下37個のファイルを「ソース・フォルダ」にコピーします。

  • accent.c と accent.h
  • confpara.h
  • defaults.h
  • do_synthesis.c
  • hmmsynth.c と hmmsynth.h
  • make_aphrase.c
  • make_breath.c
  • make_duration.c
  • make_mora.c
  • make_parameter.c
  • make_phoneme.c
  • make_sentence.c
  • misc.c と misc.h
  • mlpg.c と mlpg.h
  • model.c と model.h
  • modify_parameter.c
  • morph.c
  • pos.h
  • pos_sjis.h
  • pronunciation.h
  • pronunciation_sjis.h
  • read_conf.c
  • slot.h
  • strings_sjis.h
  • synthesis.h
  • tag.c と tag.h
  • tree.c と tree.h
  • util.c
  • vocoder.c と vocoder.h

文字コードを変換してプロジェクトに追加

上記ソースコード群は、改行コードが「LF」で統一されており、文字コードは「EUC」と「シフト JIS」が混在している、という状態です。
さらに全てのファイルに「読み取り専用」属性がついています。
Windows で扱うならば、改行コード「CR + LF」、文字コード「シフト JIS」で統一されている方が便利ですので、コピーしてきた「ソース・フォルダ」内のファイルは「読み取り専用」属性を外し、文字コードを統一し、その後プロジェクトに追加します。

プロジェクトのプロパティを変更

文字は char で扱われていますので、プロジェクトのプロパティで「構成プロパティ → 全般 → 文字セット」を「マルチバイト文字セットを使用する」に替えておきます。
また、必須ではありませんが「構成プロパティ → C/C++ → 詳細 → 指定の警告を無効にする」に「4996」を追加しておかないと、セキュリティ関係の警告を大量に見ることになります。
それから忘れてはいけないのが「util.c」の設定で、「構成プロパティ → C/C++ → プリプロセッサ → プリプロセッサの定義」に「WORDS_LITTLEENDIAN」を記述しておく必要があります。

main 関数

main 関数の記述用として、プロジェクトに新規ファイル「GalateaTest.cpp」を追加します。
C でなく C++ で書かれていますので拡張子は「.cpp」です。
内容はこの記事の末尾に掲載していますのでコピー&ペーストで写してください。
SyntaxHighlighter で表示されていますので、ソース部分でダブルクリックすると全選択状態になり、コピーに便利です。

ビルドと依存外部ファイルの準備

以上でソースコード部の準備は完了ですが、いきなり実行せず、まずビルドして実行ファイルを生成してください。
そうしてできた「実行ファイル・フォルダ」に、「\engine\SSM\gtalk」フォルダから「mono.lst」と「ssm-win.conf」をコピーします。
またその親フォルダ(「\engine\SSM」フォルダ)にある「speakers」フォルダも、「実行ファイル・フォルダ」にコピーします。
その後、コピーしてきた「ssm-win.conf」を開き、後半の「../speakers/~」となっている部分を、全て「./speakers/~」に書き換えます(頭のピリオドを一つ削除する)。
これは話者情報フォルダの位置が、標準では「実行ファイルの"親"フォルダのspeakersフォルダ」と指定されているため、「実行ファイルのフォルダのspeakersフォルダ」に変更する処理です。

実行

以上で準備完了です。
生成された「GalateaTest.exe」を実行すれば、同じフォルダに「ここは、日本ですか?ここは日本です。」という内容の「GalateaTest.wav」が生成されると思います。
処理内容については、ソースコード中に相当量のコメントが記載してありますので、そちらをご覧ください。

文節と単語

なお、Galatea Talk では本来、読み上げ文を「文節」と「単語」に分け、「文節」を open_AP 関数で、「単語」を open_W2 関数で登録します。
例えば「ここは日本ですか」は、「文節」では「ここは/日本ですか」、「単語」では「ここ/は/日本/です/か」と分割できます。
これを、まず文節「ここは」を登録してからそこに含まれる二つの単語「ここ」「は」を登録し、続いて次の文節と含む単語を登録し…、といった手順で進めていくことになります。
そのうち、文中のアクセント(音程が上がる個所)を指定するのは open_AP 関数の方で、つまりアクセントは一文節あたり一つということになります。
もし「GalateaTest.cpp」において、「ここは、日本ですか?」という読み上げ文を正確に指定するなら、以下のようになるはずです。

	//AP(文節単位)要素。
	TAGOPTIONS TOAp[5];
	//属性ラベルはあらかじめ固定で指定しておき各語句毎に値のみ設定する。
	lstrcpyA(TOAp[0].attr,"orth");//表層文字列。
	lstrcpyA(TOAp[1].attr,"pron");//発音。カタカナで指定。
	lstrcpyA(TOAp[2].attr,"aType");//アクセント位置。
	lstrcpyA(TOAp[3].attr,"silence");//無音区間指定。「、=PAU」「。?=SILE」。
	lstrcpyA(TOAp[4].attr,"interrogative");//疑問文の語尾上げ。「YES」か「NO」かで通常は「NO」。
	//W2(単語単位)要素。
	TAGOPTIONS TOW2[7];
	//属性ラベルはあらかじめ固定で指定しておき各語句毎に値のみ設定する。
	lstrcpyA(TOW2[0].attr,"orth");//表層文字列。
	lstrcpyA(TOW2[1].attr,"pron");//発音。カタカナで指定。
	lstrcpyA(TOW2[2].attr,"pos");//品詞。
	lstrcpyA(TOW2[3].attr,"aType");//アクセント位置。
	lstrcpyA(TOW2[4].attr,"aConType");
	lstrcpyA(TOW2[5].attr,"cType");
	lstrcpyA(TOW2[6].attr,"cForm");
	//文頭の無音区間生成。
	make_sil_aphrase(SILB);
	//「ここは」。
	lstrcpyA(TOAp[0].val,"ここは");
	lstrcpyA(TOAp[1].val,"ココワ");
	lstrcpyA(TOAp[2].val,"2");
	lstrcpyA(TOAp[3].val,"NON");
	lstrcpyA(TOAp[4].val,"NO");
	open_AP(5,TOAp);
	lstrcpyA(TOW2[0].val,"ここ");
	lstrcpyA(TOW2[1].val,"ココ");
	lstrcpyA(TOW2[2].val,"代名詞");
	lstrcpyA(TOW2[3].val,"0");
	lstrcpyA(TOW2[4].val,"");
	OrthLen+=open_W2(5,TOW2);
	lstrcpyA(TOW2[0].val,"は");
	lstrcpyA(TOW2[1].val,"ワ");
	lstrcpyA(TOW2[2].val,"助詞-係助詞");
	lstrcpyA(TOW2[3].val,"");
	lstrcpyA(TOW2[4].val,"動詞%F2@0,名詞%F1,形容詞%F2@-1");
	OrthLen+=open_W2(5,TOW2);
	//「読点」。
	lstrcpyA(TOAp[0].val,"、");
	lstrcpyA(TOAp[1].val,"、");
	lstrcpyA(TOAp[2].val,"");
	lstrcpyA(TOAp[3].val,"PAU");
	lstrcpyA(TOAp[4].val,"NO");
	open_AP(5,TOAp);
	lstrcpyA(TOW2[0].val,"、");
	lstrcpyA(TOW2[1].val,"、");
	lstrcpyA(TOW2[2].val,"その他-読点");
	lstrcpyA(TOW2[3].val,"0");
	lstrcpyA(TOW2[4].val,"");
	OrthLen+=open_W2(5,TOW2);
	//「日本ですか」。
	lstrcpyA(TOAp[0].val,"日本ですか");
	lstrcpyA(TOAp[1].val,"ニッポンデスカ");
	lstrcpyA(TOAp[2].val,"3");
	lstrcpyA(TOAp[3].val,"NON");
	lstrcpyA(TOAp[4].val,"YES");
	open_AP(5,TOAp);
	lstrcpyA(TOW2[0].val,"日本");
	lstrcpyA(TOW2[1].val,"ニッポン");
	lstrcpyA(TOW2[2].val,"名詞-固有名詞-地名-国");
	lstrcpyA(TOW2[3].val,"3");
	lstrcpyA(TOW2[4].val,"");
	OrthLen+=open_W2(5,TOW2);
	lstrcpyA(TOW2[0].val,"です");
	lstrcpyA(TOW2[1].val,"デス");
	lstrcpyA(TOW2[2].val,"助動詞");
	lstrcpyA(TOW2[3].val,"");
	lstrcpyA(TOW2[4].val,"形容詞%F2@-1,動詞%F2@0,名詞%F2@1");
	lstrcpyA(TOW2[5].val,"助動詞-ジャ");
	lstrcpyA(TOW2[6].val,"基本形-一般");
	OrthLen+=open_W2(7,TOW2);
	lstrcpyA(TOW2[0].val,"か");
	lstrcpyA(TOW2[1].val,"カ");
	lstrcpyA(TOW2[2].val,"助詞-終助詞");
	lstrcpyA(TOW2[3].val,"");
	lstrcpyA(TOW2[4].val,"動詞%F2@0,形容詞%F2@-1,名詞%F1");
	OrthLen+=open_W2(5,TOW2);
	//「句点」。
	lstrcpyA(TOAp[0].val,"?");
	lstrcpyA(TOAp[1].val,"?");
	lstrcpyA(TOAp[2].val,"");
	lstrcpyA(TOAp[3].val,"SILE");
	lstrcpyA(TOAp[4].val,"NO");
	open_AP(5,TOAp);
	lstrcpyA(TOW2[0].val,"?");
	lstrcpyA(TOW2[1].val,"?");
	lstrcpyA(TOW2[2].val,"その他-句点");
	lstrcpyA(TOW2[3].val,"0");
	lstrcpyA(TOW2[4].val,"");
	OrthLen+=open_W2(5,TOW2);

これを読み上げると「GalateaSample.wav」のようになります。

「GalateaTest.cpp」

それでは以下に main 関数である「GalateaTest.cpp」を掲載します。
SyntaxHighlighter で表示されていますので、コピーの際はソース部分でダブルクリックしてください。

//Galatea Talkの制御実験用のコンソール・ツール。
//まず「\galatea4win\engine\SSM\gtalk」から以下のソースをプロジェクトに追加。
//「accent.c」と「accent.h」、「confpara.h」、「defaults.h」、「do_synthesis.c」、「hmmsynth.c」と「hmmsynth.h」、
//「make_aphrase.c」、「make_breath.c」、「make_duration.c」、「make_mora.c」、
//「make_parameter.c」、「make_phoneme.c」、「make_sentence.c」、「misc.c」と「misc.h」、「mlpg.c」と「mlpg.h」、
//「model.c」と「model.h」、「modify_parameter.c」、「morph.c」、「pos.h」、「pos_sjis.h」、
//「pronunciation.h」、「pronunciation_sjis.h」、「read_conf.c」、「slot.h」、「strings_sjis.h」、「synthesis.h」、
//「tag.c」と「tag.h」、「tree.c」と「tree.h」、「util.c」、「vocoder.c」と「vocoder.h」。
//各ソースは基本「読み取り専用」で文字コードや改行コードがそろっていないので注意。
//プロジェクトのプロパティで「構成プロパティ → 全般 → 文字セット」を「マルチバイト文字セットを使用する」に替えておく。
//また必須ではないが「構成プロパティ → C/C++ → 詳細 → 指定の警告を無効にする」に「4996」を追加しておかないと大量の警告が出る。
//
//・util.c
//Galatea TalkがBIG ENDIANを基本に作られているためWindowsではLITTLE ENDIANに切り替えるため
//「util.c」のプロパティで「構成プロパティ → C/C++ → プリプロセッサ → プリプロセッサの定義」に
//「WORDS_LITTLEENDIAN」を記述する必要がある。

#include <stdio.h>
#include <windows.h>
#include <tchar.h>
#include <locale.h>
#include <setjmp.h>

////////////////////
//「main.c」から。//
////////////////////
//命名規則の都合上Galateaのヘッダは全て「extern "C"」で使用。
extern "C"
{
#include "synthesis.h"
#include "confpara.h"
#include "misc.h"
#include "model.h"
#include "tree.h"
#include "defaults.h"
#include "hmmsynth.h"
#include "mlpg.h"
#include "tag.h"
#define INIT_SLOT_TABLE
#include "slot.h"
}

////////////////////
//「main.c」から。//
////////////////////
//各ヘッダファイル内でexternで宣言だけされている変数の実体を確保。
extern "C"
{
//■「synthesis.h」。
MORPH *mphead;
MORPH *mptail;
PHONEME *phhead;
PHONEME *phtail;
MORA *mrhead;
MORA *mrtail;
APHRASE *ahead;
APHRASE *atail;
BREATH *bhead;
BREATH *btail;
SENTENCE *shead;
SENTENCE *stail;
SENTENCE *sentence;
WAVE wave;
PARAM power;
PARAM f0;
PARAM alpha;
int s_mode = 0;
PROS prosBuf;//韻律データの一時格納用。
//■「confpara.h」。
char *phlist_file;
char *chasen_bin;
char *chasen_dll;
char *chaone_xsl;
char *chasen_rc;
char *chaone_bin;
char *read_number;
char *read_alphabet;
char *read_date;
char *read_time;
int n_speaker;//登録された話者数。
int spid;//現在の話者ID。
FILE *logfp;
SPEAKER speaker[MAX_SPEAKER];
char *dic_file;
char *conf_audiodev=NULL;//実行時に指定されるオーディオデバイス。
//■「hmmsynth.h」。
int nstate;
int pitchstream;
int mcepvsize;
ModelSet mset[MAX_SPEAKER];
double **mcep;//generated mel-cepstrum。
double **coeff;//mlsa filter coefficients。
int totalframe;
//■「mlpg.h」。
PStream pitchpst;
PStream mceppst;
Boolean *voiced;
//■「model.h」。
Model *mhead;
Model *mtail;
FILE *durmodel;
FILE *pitchmodel;
FILE *mcepmodel;
//■「tag.h」。
TAG *tag[MAX_TAG];
int n_tag;
//■「slot.h」。
SlotProp prop_Run;
SlotProp prop_ModuleVersion;
SlotProp prop_ProtocolVersion;
SlotProp prop_SpeakerSet;
SlotProp prop_Speaker;
SlotProp prop_SpeechFile;
SlotProp prop_ProsFile;
SlotProp prop_Text;
SlotProp prop_Text_text;
SlotProp prop_Text_pho;
SlotProp prop_Text_dur;
SlotProp prop_Speak;
SlotProp prop_Speak_text;
SlotProp prop_Speak_pho;
SlotProp prop_Speak_dur;
SlotProp prop_Speak_utt;
SlotProp prop_Speak_len;
SlotProp prop_Speak_stat;
SlotProp prop_Speak_syncinterval;
//■slots。
char slot_Run[20];
char slot_Speak_stat[20];
char input_text[MAX_TEXT_LEN];//入力されたテキスト(タグつき)。
char spoken_text[MAX_TEXT_LEN];//音声出力された発話のテキスト。
char slot_Log_file[256];
char slot_Err_file[256];
char slot_Speech_file[512];
char slot_Pros_file[512];
int slot_Auto_play;
int slot_Auto_play_delay;
int slot_n_phonemes;
int slot_total_dur;
int slot_Log_chasen;
int slot_Log_tag;
int slot_Log_phoneme;
int slot_Log_mora;
int slot_Log_morph;
int slot_Log_aphrase;
int slot_Log_breath;
int slot_Log_conf;
int slot_Log_text;
int slot_Log_arranged_text;
int slot_Log_sentence;
int slot_Speak_syncinterval;
}

////////////////////
//「main.c」から。//
////////////////////
//外部Cファイルで定義されている関数の宣言のうち必要なものを抜粋。
extern "C"
{
void init_conf();
void read_conf(char *);
void set_default_conf();
void init_text_analysis();
void init_hmmsynth();
void read_phonemes(char *);
void init_tag();
void init_mora();
void init_morph();
void init_aphrase();
void init_breath();
void init_phoneme();
void init_sentence();
void refresh_text_analysis();
void refresh_tag();
void refresh_mora();
void refresh_morph();
void refresh_aphrase();
void refresh_breath();
void refresh_phoneme();
void refresh_sentence();
void refresh_hmmsynth();
int RepMsg(char *, ...);
void init_parameter();
void make_duration();
void make_parameter();
void do_synthesis();
void modify_duration();
void make_cumul_time();
void modify_f0();
void modify_power();
void modify_voice();
}

/////////////////////////
//「do_output.c」から。//
/////////////////////////
extern "C"
{
//「do_synthesis.c」で必要。
int already_talked;
//「util.c」で必要。ただしサーバモードに関するもののようなので内容はカット。
int server_send(char *message){return 0;}
}

////////////////////
//「text.c」から。//
////////////////////
//テキスト解析は手動で行うため必要部分のみ抜粋。
extern "C"
{
int open_W2(int,TAGOPTIONS *);
void open_AP(int,TAGOPTIONS *);
void proc_JEITA_tag(char *,int,TAGOPTIONS *,int);
void make_sil_aphrase(SILENCE);
void make_breath();
void make_phoneme();
void make_sentence();
void make_mora();
//茶筌の初期化に必要な処理はカット。
void init_text_analysis(){input_text[0]=spoken_text[0]='\0';}	
void refresh_text_analysis(){input_text[0]=spoken_text[0]='\0';}	
}

////////////////////
//「main.c」から。//
////////////////////
extern "C"
{
//下の「void initialize()」に必要。
void SetRun( char *rel,char *val);

//下の「void initialize()」に必要。
void init_slot_prop()
{
	prop_Run=AutoOutput;
	prop_ModuleVersion=AutoOutput;
	prop_ProtocolVersion=AutoOutput;
	prop_SpeakerSet=AutoOutput;
	prop_Speaker=AutoOutput;
	prop_SpeechFile=AutoOutput;
	prop_ProsFile=AutoOutput;
	prop_Text=AutoOutput;
	prop_Text_text=AutoOutput;
	prop_Text_pho=AutoOutput;
	prop_Text_dur=AutoOutput;
	prop_Speak=AutoOutput;
	prop_Speak_text=AutoOutput;
	prop_Speak_pho=AutoOutput;
	prop_Speak_dur=AutoOutput;
	prop_Speak_utt=AutoOutput;
	prop_Speak_len=AutoOutput;
	prop_Speak_stat=AutoOutput;
	prop_Speak_syncinterval=AutoOutput;
}

/* 初期化: プログラム起動時に一度だけ実行 */
void initialize()
{
	init_slot_prop();
	init_text_analysis();
	init_hmmsynth();

	//外部ファイル「mono.lst」に記録された音素リストを読み込む処理。
	//ただし音素リストは組み込み用途ではほぼ不変と思われるので「make_phoneme.c」の
	//read_phonemes関数内にベタ書きしてしまえば必要外部ファイルを一つ減らせる。
	read_phonemes(phlist_file);

	init_tag();
	init_mora();
	init_morph();
	init_aphrase();
	init_breath();
	init_phoneme();
	init_sentence();
	strcpy(slot_Speak_stat,"IDLE");
	strcpy(slot_Log_file,"NO");
	logfp=NULL;
	slot_Log_chasen=slot_Log_tag=slot_Log_phoneme=0;
	slot_Log_mora=slot_Log_morph=slot_Log_aphrase=0;
	slot_Log_breath=slot_Log_sentence=0;
	strcpy(slot_Err_file,"CONSOLE");
	slot_Speech_file[0]='\0';
	slot_Pros_file[0]='\0';
	prosBuf.nPhoneme=0;
	slot_Speak_syncinterval=1000;
}

//下の「void refresh()」に必要。
void refresh_prosBuf()
{
	if(prosBuf.nPhoneme==0) return;
	free(prosBuf.ph_name);
	free(prosBuf.ph_dur);
	free(prosBuf.fr_power);
	free(prosBuf.fr_f0);
	prosBuf.nPhoneme=prosBuf.nFrame=0;
}

/* 初期化: 合成を行うたびに実行 */
void refresh()
{
	refresh_text_analysis();
	refresh_tag();
	refresh_mora();
	refresh_morph();
	refresh_aphrase();
	refresh_breath();
	refresh_phoneme();
	refresh_sentence();
	refresh_hmmsynth();
	refresh_prosBuf();
}

//呼び出した関数内でメモリ確保エラー等があると呼び出されlongjmp関数によりmain関数内の初期化(initialize)関数直後に戻される。
//おそらくWindows用アプリでは好ましい動作ではないため各関数内のrestart関数呼び出しを別の処理に替える必要があると思われる。
static jmp_buf ebuf;
void restart(int val)
{
	RepMsg("rep Speak.stat = ERROR\n");
	longjmp(ebuf,val);
	refresh();
}
}

int _tmain(int argc,TCHAR* argv[])
{
	//日本語の表示に必要な処理。
	_tsetlocale(LC_ALL,_T(""));

	//カレントを実行ファイル位置へ移動。「\」の扱いが面倒なのでUNICODEで処理。
	WCHAR ExePath[MAX_PATH];
	GetModuleFileNameW(NULL,ExePath,sizeof(WCHAR)*MAX_PATH);
	for(int Pos=lstrlenW(ExePath)-1;Pos>=0;Pos--){if(ExePath[Pos]==L'\\'){ExePath[Pos]=L'\0';break;}}
	SetCurrentDirectoryW(ExePath);

	////////////////////
	//各情報の初期化。//
	////////////////////
	//設定情報初期化。
	init_conf();

	//設定ファイル「ssm-win.conf」に記録された設定を読み込む処理。
	//ただし必要なのは「音素リストのファイルパス(=phlist_file)」「各話者のファイルパス集(=speaker[])」
	//「登録された話者数(=n_speaker)」ぐらいなので、ここをベタ書きに書き換えれば必要外部ファイルを一つ減らせる。
	read_conf("ssm-win.conf");

	//設定ファイルに記述がなかった項目の設定。
	set_default_conf();
	//初期化:プログラム起動時に一度だけ実行。
	initialize();
	//解析中にエラーが生じるとrestart関数が呼ばれここに戻る。
	int SetJmp=setjmp(ebuf);
	if(SetJmp>0)_tprintf(_T("Restart!\n"));

	////////////////////
	//テキスト解析部。//
	////////////////////
	//初期化:合成を行うたびに実行。
	refresh();

	//■以下「text.c」におけるテキスト解析処理を手動で実施。
	//※ただし品詞を全て「名詞-普通名詞-一般」した簡易版。
	//AP要素とW2要素の属性情報格納用。
	//本来AP(文節単位)要素とW2(単語単位)要素は別々に指定するが今回は「AP内にW2が一つ」のためまとめても問題ない。
	TAGOPTIONS TOApW2[6];
	//属性ラベルはあらかじめ固定で指定しておき各語句毎に値のみ設定する。
	lstrcpyA(TOApW2[0].attr,"orth");//表層文字列。
	lstrcpyA(TOApW2[1].attr,"pron");//発音。カタカナで指定。
	lstrcpyA(TOApW2[2].attr,"pos");//品詞。
	lstrcpyA(TOApW2[3].attr,"aType");//アクセント位置。
	lstrcpyA(TOApW2[4].attr,"silence");//無音区間指定。「、=PAU」「。?=SILE」。
	lstrcpyA(TOApW2[5].attr,"interrogative");//疑問文の語尾上げ。「YES」か「NO」かで通常は「NO」。
	//JEIDAタグ属性情報格納用。こちらはタグ毎に属性と値の両方を設定する必要あり。
	TAGOPTIONS TOJeida[1];
	//先頭から何文字(バイト)目かを記録(JEIDAタグの処理に使用)。
	int OrthLen=0;

	//////↓話す内容を変更する場合はここからを書き換える。↓//////

	//■第一文「ここは、日本ですか?」。
	//文頭の無音区間生成。
	make_sil_aphrase(SILB);
	//※タグ(必要に応じて)。話者を「male02」に変更。
	lstrcpyA(TOJeida[0].attr,"OPTIONAL");
	lstrcpyA(TOJeida[0].val,"male02");
	proc_JEITA_tag("VOICE",1,TOJeida,OrthLen);
	//※タグ(必要に応じて)。音程を少し低く(0.8)する。
	lstrcpyA(TOJeida[0].attr,"LEVEL");
	lstrcpyA(TOJeida[0].val,"0.8");
	proc_JEITA_tag("PITCH",1,TOJeida,OrthLen);
	//「ここは」。
	lstrcpyA(TOApW2[0].val,"ここは");
	lstrcpyA(TOApW2[1].val,"ココワ");
	lstrcpyA(TOApW2[2].val,"名詞-普通名詞-一般");
	lstrcpyA(TOApW2[3].val,"0");
	lstrcpyA(TOApW2[4].val,"NON");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//「読点」は考えることもないので一応正確に指定。
	lstrcpyA(TOApW2[0].val,"、");
	lstrcpyA(TOApW2[1].val,"、");
	lstrcpyA(TOApW2[2].val,"その他-読点");
	lstrcpyA(TOApW2[3].val,"0");
	lstrcpyA(TOApW2[4].val,"PAU");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//「日本ですか」ではなく「日本」「ですか」に分割する必要がある。
	lstrcpyA(TOApW2[0].val,"日本");
	lstrcpyA(TOApW2[1].val,"ニッポン");
	lstrcpyA(TOApW2[2].val,"名詞-普通名詞-一般");
	lstrcpyA(TOApW2[3].val,"3");
	lstrcpyA(TOApW2[4].val,"NON");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//「interrogative=YES」によって末尾の音程が上がる(=アクセント扱い)。
	//そのため「日本ですか」とまとめてしまうと「日本」に存在するはずのアクセントが末尾に奪われる。
	lstrcpyA(TOApW2[0].val,"ですか");
	lstrcpyA(TOApW2[1].val,"デスカ");
	lstrcpyA(TOApW2[2].val,"名詞-普通名詞-一般");
	lstrcpyA(TOApW2[3].val,"0");
	lstrcpyA(TOApW2[4].val,"NON");
	lstrcpyA(TOApW2[5].val,"YES");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//「句点」は考えることもないので一応正確に指定。
	lstrcpyA(TOApW2[0].val,"?");
	lstrcpyA(TOApW2[1].val,"?");
	lstrcpyA(TOApW2[2].val,"その他-句点");
	lstrcpyA(TOApW2[3].val,"0");
	lstrcpyA(TOApW2[4].val,"SILE");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//※閉じタグ(必要に応じて)。
	//開始タグが「VOICE→PITCH」の順であれば閉じタグは「PITCH→VOICE」の必要がある。
	//XMLをなぞるため「入れ子(<V><P>~</P></V>)」にはできても「互い違い(<V><P>~</V></P>)」にはできない。
	proc_JEITA_tag("/PITCH",0,TOJeida,OrthLen);
	proc_JEITA_tag("/VOICE",0,TOJeida,OrthLen);

	//■第二文「ここは、日本です。」。
	//文頭の無音区間生成。
	make_sil_aphrase(SILB);
	//※タグ(必要に応じて)。話者を「female01」に変更。
	lstrcpyA(TOJeida[0].attr,"OPTIONAL");
	lstrcpyA(TOJeida[0].val,"female01");
	proc_JEITA_tag("VOICE",1,TOJeida,OrthLen);
	//※タグ(必要に応じて)。速度を少し遅く(1.2)する。
	lstrcpyA(TOJeida[0].attr,"SPEED");
	lstrcpyA(TOJeida[0].val,"1.2");
	proc_JEITA_tag("RATE",1,TOJeida,OrthLen);
	//「ここは」。
	lstrcpyA(TOApW2[0].val,"ここは");
	lstrcpyA(TOApW2[1].val,"ココワ");
	lstrcpyA(TOApW2[2].val,"名詞-普通名詞-一般");
	lstrcpyA(TOApW2[3].val,"0");
	lstrcpyA(TOApW2[4].val,"NON");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//「読点」は考えることもないので一応正確に指定。
	lstrcpyA(TOApW2[0].val,"、");
	lstrcpyA(TOApW2[1].val,"、");
	lstrcpyA(TOApW2[2].val,"その他-読点");
	lstrcpyA(TOApW2[3].val,"0");
	lstrcpyA(TOApW2[4].val,"PAU");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//「日本です」よりも「日本」「です」と分割した方が自然に読み上げた。
	lstrcpyA(TOApW2[0].val,"日本");
	lstrcpyA(TOApW2[1].val,"ニッポン");
	lstrcpyA(TOApW2[2].val,"名詞-普通名詞-一般");
	lstrcpyA(TOApW2[3].val,"3");
	lstrcpyA(TOApW2[4].val,"NON");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//分割位置とアクセントは試行錯誤。
	lstrcpyA(TOApW2[0].val,"です");
	lstrcpyA(TOApW2[1].val,"デス");
	lstrcpyA(TOApW2[2].val,"名詞-普通名詞-一般");
	lstrcpyA(TOApW2[3].val,"1");
	lstrcpyA(TOApW2[4].val,"NON");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//「句点」は考えることもないので一応正確に指定。
	lstrcpyA(TOApW2[0].val,"。");
	lstrcpyA(TOApW2[1].val,"。");
	lstrcpyA(TOApW2[2].val,"その他-句点");
	lstrcpyA(TOApW2[3].val,"0");
	lstrcpyA(TOApW2[4].val,"SILE");
	lstrcpyA(TOApW2[5].val,"NO");
	open_AP(6,TOApW2);
	OrthLen+=open_W2(6,TOApW2);
	//※閉じタグ(必要に応じて)。「VOICE」の閉じタグ。
	proc_JEITA_tag("/RATE",0,TOJeida,OrthLen);
	proc_JEITA_tag("/VOICE",0,TOJeida,OrthLen);

	//////↑話す内容を変更する場合はここまでを書き換える。↑//////

	//必要があれば(「。」か「?」で終わってなければ)末尾の無音区間生成。
	if(mptail->silence!=SILE)make_sil_aphrase(SILE);

	//■以下、形態素解析済みデータから音素データを生成。
	//形態素をモーラに分解。
	make_mora();
	//呼気段落の作成。
	make_breath();
	//モーラ(カタカナ)を音素(ローマ字)に変換。
	make_phoneme();
	//ここまでの分析結果を文章(sentence)に再構築。
	make_sentence();

	////////////////
	//音声合成部。//
	////////////////
	//テキスト解析の結果から波形合成のためのパラメータを生成。
	//「main.c」で「parameter_generation();」としてまとめられているもの。
	//音声合成の初期設定。
	init_parameter();
	//音素継続長の決定。
	make_duration();
	//音素継続長の変更。
	modify_duration();
	make_cumul_time();
	modify_voice();
	//F0とMLSAフィルタ係数の生成。
	make_parameter();
	//F0とゲインb(0)にタグによる変化を反映させる。
	modify_f0();
	modify_power();
	//解析結果をもとに合成波形を生成。ここで「wave.data」に波形データが生成される。
	do_synthesis();

	////////////////////////
	//WAVEファイルに保存。//
	////////////////////////
	//■WAVEヘッダの準備。
	//生成形式は16kHz16bitモノラル。
	WAVEFORMATEX WaveFormatEx;memset(&WaveFormatEx,0,sizeof(WAVEFORMATEX));
	WaveFormatEx.wFormatTag=WAVE_FORMAT_PCM;
	WaveFormatEx.nChannels=1;
	WaveFormatEx.wBitsPerSample=16;
	WaveFormatEx.nSamplesPerSec=16000;
	WaveFormatEx.nBlockAlign=WaveFormatEx.nChannels*WaveFormatEx.wBitsPerSample/8;
	WaveFormatEx.nAvgBytesPerSec=WaveFormatEx.nSamplesPerSec*WaveFormatEx.nBlockAlign;
	//■WAVEファイルの生成。
	//新規ファイルを作る。
	HANDLE WaveFile=CreateFile(_T(".\\GalateaTest.wav"),GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
	if(WaveFile==INVALID_HANDLE_VALUE){_tprintf(_T("WAVEファイルが作れません。\n"));return EXIT_FAILURE;}
	//■WAVEヘッダ部分の書き込み。
	DWORD DataSize=wave.nsample*sizeof(SHORT);
	DWORD ChunkID=0x46464952;
	DWORD ChunkSize=12+sizeof(WAVEFORMATEX)+8+DataSize;
	DWORD ReadSize=0;
	DWORD WriteSize=0;
	//RIFF識別子の書き込み。
	WriteFile(WaveFile,&ChunkID,sizeof(DWORD),&WriteSize,NULL);
	if(WriteSize!=sizeof(DWORD)){_tprintf(_T("RIFF識別子の書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//RIFFサイズの書き込み。
	WriteFile(WaveFile,&ChunkSize,sizeof(DWORD),&WriteSize,NULL);
	if(WriteSize!=sizeof(DWORD)){_tprintf(_T("RIFFサイズの書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//WAVE識別子の書き込み。
	ChunkID=0x45564157;
	WriteFile(WaveFile,&ChunkID,sizeof(DWORD),&WriteSize,NULL);
	if(WriteSize!=sizeof(DWORD)){_tprintf(_T("WAVE識別子の書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//fmt識別子の書き込み。
	ChunkID=0x20746D66;
	WriteFile(WaveFile,&ChunkID,sizeof(DWORD),&WriteSize,NULL);
	if(WriteSize!=sizeof(DWORD)){_tprintf(_T("fmt識別子の書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//fmtサイズの書き込み。
	ChunkSize=sizeof(WAVEFORMATEX);
	WriteFile(WaveFile,&ChunkSize,sizeof(DWORD),&WriteSize,NULL);
	if(WriteSize!=sizeof(DWORD)){_tprintf(_T("fmtサイズの書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//fmt情報の書き込み。
	WriteFile(WaveFile,&WaveFormatEx,sizeof(WAVEFORMATEX),&WriteSize,NULL);
	if(WriteSize!=sizeof(WAVEFORMATEX)){_tprintf(_T("fmt情報の書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//data識別子の書き込み。
	ChunkID=0x61746164;
	WriteFile(WaveFile,&ChunkID,sizeof(DWORD),&WriteSize,NULL);
	if(WriteSize!=sizeof(DWORD)){_tprintf(_T("data識別子の書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//fmtサイズの書き込み。
	ChunkSize=DataSize;
	WriteFile(WaveFile,&ChunkSize,sizeof(DWORD),&WriteSize,NULL);
	if(WriteSize!=sizeof(DWORD)){_tprintf(_T("dataサイズの書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//■音声情報の書き込み。
	WriteFile(WaveFile,wave.data,DataSize,&WriteSize,NULL);
	if(DataSize!=WriteSize){_tprintf(_T("音声情報の書き込みに失敗しました。\n"));return EXIT_FAILURE;}
	//■後処理。
	CloseHandle(WaveFile);

	_tprintf(_T("WAVEファイル(計%d秒)生成。\n"),wave.nsample/16000);

	//////////
	//終了。//
	//////////
	//いきなり終了させないため一行読み捨て。
	TCHAR BuffText[64];memset(BuffText,0,sizeof(TCHAR)*64);
	DWORD BuffLen=0;
	ReadConsole(GetStdHandle(STD_INPUT_HANDLE),BuffText,64,&BuffLen,NULL);

	return EXIT_SUCCESS;
}
関連記事

【同じタグを付けた記事の一覧】
ソースコード プログラミング C++ 音声技術 GalateaProject 音声合成 GalateaTalk

スポンサーサイト

コメントの投稿

非公開コメント

Open JTalk

あけおめです~。

何故今頃、『Galatea Talk』の話題をとか、突っ込んでみたり。

Galatea Talkの開発メンバーが次に作ったのが、Open JTalkだそうです。
http://ja.nishimotz.com/open-jtalk 
NVDAの日本語版の開発者のお一人でもある西本さん
NVDAの日本語版が標準搭載している音声合成エンジンもこれが使われています。

でも、これも初心者が使うのは難しいんですけどね。
takayanさんが多少簡単にしてくれてはいますが。
http://mahoro-ba.net/e1569.html

Re: Open JTalk

あけましておめでとうございます、aさん。

まあホントまさに「今頃」なネタなんですけれど、
一度「やりたい」と思うと止められないもので。
あとGalatea Talkはもう開発が止まっているっぽいのも理由でした。
「ここまでやれば完成」という目途が付けられるというか。
Open JTalkは現在進行形のアクティブなプロジェクトで
今も頻繁にバージョンアップがなされているようですし、
それをずっと追い続けるほどの根性や覚悟はなかったのです…。

とはいえ、使ってみた感じでは
Galatea Talkを未完成のまま打ち捨ててOpen JTalkへ、という風でもなく、
これはこれで一つの完成形のように思えます。
何とかうまくアクセント情報を集められる仕組みを作れれば、
まだまだいいものにできそうな気がするのですが。

そのあたり、最近になってiPhoneやAndroidが「音声認識」の形で
音声データを大量に収集していますから、
次はその音声データを解析・反映させた「音声合成」が来るのではないかと
密かに期待していたりします。

それでは、今年もどうぞよろしくお願い申し上げます。
最新記事
最新コメント
Amazonおまかせリンク
カテゴリ
タグクラウド
Amazonお買い得ウィジェット
カレンダー
05 | 2017/06 | 07
- - - - 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 30 -
月別アーカイブ
プロフィール

電脳太助

Author:電脳太助
Website:電脳スピーチ web

RSSリンクの表示
メールフォーム

名前:
メール:
件名:
本文:

サイト内検索
Ads by Google
FC2アクセスランキング
Ads by Google
FC2拍手ランキング
ユーザータグ

音楽管理(66)
ポータブル(57)
ソフト紹介(44)
プログラミング(42)
音声技術(41)
自作ソフト(35)
サイト運営(32)
FC2(31)
ブログ(30)
iTunes(27)
Windows(25)
LISMO(24)
音声合成(23)
音声認識(22)
x-アプリ(22)
電子ブック(22)
eラーニング(20)
バックアップ(19)
語学学習(19)
foobar2000(18)
ソースコード(18)
WindowsLiveWriter(15)
画像管理(15)
C++(14)
アフィリエイト(10)
DnspTools(10)
fi-6130(9)
FLAC(9)
JavaScript(9)
ウォークマン(9)
英語音読学習計画(8)
Gracenote(8)
Prolog(8)
ベクター(8)
雑記(8)
CodeBlocks(7)
SyntaxHighlighter(7)
TraConv(7)
spcbght(7)
wxWidgets(7)
VirtualBox(6)
W63CA(6)
DCP-J552N(6)
WinRT(6)
WindowsLiveMesh(6)
iGoinLM(6)
英語発音矯正実験(6)
ExactAudioCopy(6)
MP3Gain(6)
LAME(5)
音楽技術(5)
Mery(5)
楽器演奏(5)
GalateaTalk(4)
nLite(4)
WindowsLiveSkyDrive(4)
ホームページ(4)
GalateaProject(4)
MIDI(4)
LLVM(4)
PC-98(3)
カウンター(3)
AACGain(3)
iTCDini(3)
OverCutChecker(3)
拍手(3)
PK-513L(3)
UniversalExtractor(3)
アクセスランキング(3)
ImageCompositeEditor(2)
アクセス解析(2)
OCR(2)
qtaacenc(2)
資格試験(1)
AquesTalk(1)
AquesCmdDl(1)

FC2アクセスランキング
最新トラックバック
アクセスランキング
[ジャンルランキング]
コンピュータ
106位
アクセスランキングを見る>>

[サブジャンルランキング]
ソフトウェア
11位
アクセスランキングを見る>>
FC2カウンター
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。