『お帰り』は、イ○ジンのカバじゃなくて、マスコットに言ってもらいたい。

『お帰り』は、イ○ジンのカバじゃなくて、マスコットに言ってもらいたい。

若干壊れ気味のソフトウェアネタが続きます。よろしくお付き合いくださいませ。

前回は『話を聞く』をテーマにしました。今回は『話す』をテーマにしてみようと思います。最近では、日本のほこる素晴らしい技術でコンピュータでも自然に歌が歌えるようになってきました。コンピュータ侮り難し、キャラクターの人気も手伝って人間の歌手を凌駕する人気を記録したこともあるくらいです。

(VOCALOID) http://www.crypton.co.jp/vocaloid/

(IT Mediaの記事)http://www.itmedia.co.jp/news/articles/1005/25/news066.html

で、この歌姫が「しゃべる」こともできるようになったらしく、声優として使えるレベルなんだそうです。

(VOCALOID-flex)http://www.yamaha.co.jp/news/2010/10022501.html

こ、これは!早速、ミクミクにしてもらおうと思ったのですが、VOCALOID-flexは、一般向けに販売されるわけではないようです。

現段階では歌のVOCALOIDと同じように調整(調教?)が重要で、マスコットに載せてそのまま自然に話すのは無理なんでしょうけれども、いずれ近い将来にそういう時代になりそうです。ということで、そんな未来を夢見て今回は発話機能を実装してみることにしました。

と、前置きでだいぶ風呂敷を広げるのはいつものことですよね?
で、今回も VOCALOID とは無縁の『Windows 標準の発話機能』を使って実現することにします。

実はWindowsに発話機能は標準で搭載されてはいるものの、標準では日本語に対応していません。(たぶん、発話機能が標準で搭載されているのはXP以降かと)

イメージ

Office 2002~2003には日本語エンジンが搭載されているので、これをインストールすると日本語にも対応するのですが、その為だけには Office は高いなぁ。。。と思っていたら、最近になって Microsoft が新しい音声に関するプラットホームを公開していることが判明。日本語版も無料でリリースされているという気前の良さです。

動作環境に Windows XP が乗っていませんがちゃんと動作しました。

(Microsoft Speech Platform - Server Runtime v10.1)

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=674356C4-E742-4855-B3CC-FC4D5522C449&amp%3Bdisplaylang=en

(Microsoft Speech Platform - Server Runtime Languages v10.1)

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=f704cd64-1dbf-47a7-ba49-27c5843a12d5&displaylang=en

開発に必要なSDKも公開されています。

(Microsoft Speech Platform - Software Development Kit v10.1)

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=4d36908b-3264-49ef-b154-f23bf7f44ef4&displaylang=en

これをインストールすると、日本語も発話できるようになります。すばらしい限りです。

プログラムの流れとしては、発話する内容の文字列を音声合成エンジンに引き渡すだけです。 ただ、引き渡してから話し終わるまでタイムラグがあります(要するに話している時間です)。その間に別の発話要求があるとウマク動作しないので、スレッドをわけているのがポイントです。

なお、先ほどのSpeech Platform SDKのヘッダーとライブラリを参照パスの上位に登録しておいてください。(通常のSDK側が使われるとコンパイルはするものの、動作に失敗するようなので。)

あと、Speech系の API は Unicode しか受付けないので内部で変換しています。v5.1 の時からそうでした。マルチバイト使い泣かせですね。。。

ソースコード

#include <windows.h>
#include <stdio.h>
#include <sapi.h>
#include <sphelper.h>
#include <process.h>
#include "resource.h"

int speech_speakevent( char * );
static void speech_speakthread( void * );
static int flag = FALSE;

int main()
{
    char string[256];
    printf("文字を入力してください");
    //スピーク処理はユニコードが基本なのでユニコードで取得
    scanf( "%s" , &string );
    speech_speakevent( string );
    Sleep(-1);
    return TRUE;
}
//発話関数 呼び出し側
//発話が完了するまでに時間がかかるのでスレッド方式とする
//(通知タイプにしてすぐにリターンするようにもできるハズ)
int speech_speakevent( char *string )
{
    wchar_t *wcsbuf;
    int length;
    if(!flag)
    {
        //発話開始フラグを立てる
        flag = TRUE;
        //スレッド上で使うので文字列を確保したバッファーにコピーする
        //この際に、ついでだからユニコードに変換しておく。
        //この処理はいつやってもOK
        length = MultiByteToWideChar( CP_ACP, 0, string, -1, NULL, 0 );
        wcsbuf = (wchar_t*)malloc( sizeof(wchar_t)*(length+1) );
        memset( wcsbuf , 0 , sizeof(wchar_t)*(length+1) );
        MultiByteToWideChar( CP_ACP, 0, string, -1, wcsbuf, length+1 );
        _beginthread( speech_speakthread , 0 , wcsbuf );
    }
    return TRUE;
}
//発話関数 スレッド側
void speech_speakthread( void *lpvoid )
{
    ISpVoice *pVoice;
    ISpObjectToken *pToken;
    IEnumSpObjectTokens *pEnum;
    wchar_t *wcsbuf;
    //COMインターフェース初期化
    if(!SUCCEEDED( CoInitialize(NULL) ))
    {
        return;
    }
    // 文字データを受け取った
    wcsbuf = (wchar_t*)lpvoid;
    //日本語のエンジンを列挙
    // 日本語のエンジンを列挙
    // 411が日本語らしい レジストリみたらそうなっていた
    if(!SUCCEEDED(SpEnumTokens(SPCAT_VOICES, L"language = 411", NULL, &pEnum)))
    {
        return;
    }
    // 列挙したうち一番前のものを取得
    // 本来ならばリスト等で選べるとベスト
    if(!SUCCEEDED(pEnum->Next( 1, &pToken, NULL )))
    {
        return;
    }
    // ボイスのインスタンスを作成
    if(!SUCCEEDED(CoCreateInstance( CLSID_SpVoice , NULL , CLSCTX_ALL , IID_ISpVoice , (LPVOID*)&pVoice )))
    {
        return;
    }
    // さっき選択した日本語のエンジンを指定
    pVoice->SetVoice( pToken );
    // 発話する前処理
    pVoice->Speak( NULL , SPF_PURGEBEFORESPEAK , 0 );
    // 発話
    pVoice->Speak( wcsbuf , SPF_DEFAULT , NULL );
    // 話終わるまで待つ
    // この処理のためにスレッドにする必要がある。
    pVoice->WaitUntilDone(INFINITE);
    // 終了したので解放
    pVoice->Release();
    pToken->Release();
    pEnum->Release();
    //後始末
    CoUninitialize();
    // メモリを解放する
    free( wcsbuf );
    // フラグを落とす
    flag = FALSE;
    return;
}

さて、これでコマンドで話させることはできました。

イメージ

これをマスコットに組み込みます。

『Fy Mascot』 は、話始めに SYSTEM-STATUS 命令が各オプションプログラムに送られます。ここに話す内容のスクリプトの情報も入っているのでこれを使います。 SYSTEM-STATUS 命令からスクリプトの内容を取得、表示用のいらないタグとかをサクっと削除して、あとは先ほどのプログラムにその内容(話す内容)をいれます。

いつものことながら、実験用ということでかなりテキトーなコードですが、おおめにみてやってください。

ソースコード

スクリプトの内容の抜き出し
int gicp_systemstatus( char **in_buffer , int *in_length )
{
    char master[256];
    char follower[256];
    char witchcraft[256];
    char incantation[512];
    char *buffer;
    char *data;
    char *script;
    int length;
    char *p, *begin_p, *end_p;
    /* 各要素を作る */
    sprintf( master ,  "<place>SOMEONE</place><detail></detail>" );
    sprintf( follower ,  "<place>SOMEONE</place><detail></detail>" );
    sprintf( witchcraft , "<class>RETURN</class><order>RETURN</order>" );
    /* キーワードを抜き出します。 */
    /* かなりいい加減なXMLの処理なので、実際はもっと厳密に行ってください。 */
    p = strstr( *in_buffer , "<incantation>" );
    if(p)
    {
        begin_p = strstr( p , "<status>");
        end_p = strstr( p , "</status>" );
        /* 内容まではポインタを<data>分進めなければならない */
        begin_p = begin_p+strlen("<status>");
        /* 文字数にあわせてメモリを確保 */
        data = (char*)malloc( end_p-begin_p+1 );
        memset( data , 0 , end_p-begin_p+1 );
        memcpy( data , begin_p ,  end_p-begin_p );
        if(!strcmp(data,"START"))
        {
            begin_p = strstr( p , "<script>");
            end_p = strstr( p , "</script>" );
            /* 内容まではポインタを<data>分進めなければならない */
            begin_p = begin_p+strlen("<script>");
            script = (char*)malloc( end_p-begin_p+1 );
            memset( script , 0 , end_p-begin_p+1 );
            memcpy( script , begin_p ,  end_p-begin_p );
            sprintf( incantation ,  "<result>OK</result>" );
            script = script_decodescript( script );
            speech_speakevent( script );
            free( script );
        }
        else
        {
            sprintf( incantation ,  "<result>NOTSUPPORTED</result>" );
        }
        /* 忘れずにメモリの解放 */
        free( data );
    }
    else
    {
        /* 必要な内容がたりていない */
        sprintf( incantation ,  "<result>BADREQUEST</result>" );
    }
   
    /* 各データを元にGICP文字列を作る */
    buffer = gicp_setstring( master , follower , witchcraft , incantation );
    length = strlen(buffer)+1;
    /* 元のメモリ解放してから代入 */
    free( *in_buffer );
    *in_buffer = buffer;
    *in_length = length;
    return TRUE;
}

いらないタグの削除
static char *script_decodescript_internal( char *script , int *lp_result )
{
    int length;
    char *string;
    char *begin_p, *end_p;
    begin_p = script;
    end_p = strchr( begin_p , '\\' );
    if(!end_p)
    {
        //もうタグはない
        *lp_result = FALSE;
        return script;
    }
    //タグがあった。
    //スクリプトがすべてコピーできればストリングは間に合う。
    length = strlen(script);
    string = (char*)malloc( (length+1)*sizeof(char) );
    memset( string, 0, (length+1)*sizeof(char) );
    //まずタグを終端に変更
    *end_p = '';
    end_p=end_p+1;    //ずらす
    //pの次はなんの文字かによって場合わけ
    switch(*end_p)
    {
    case 'f':
    case 'w':
    case 'l':
    case 'c':
        {
            //[]カッコがある
            end_p = strchr( end_p , ']' );
            if(end_p)
            {
                end_p=end_p+1;
                sprintf( string , "%s%s" , begin_p , end_p );
                begin_p=end_p;
            }
        }
        break;
    case 'n':
    case 'e':
    case '\\':
    default:
        {
            //カッコなし
            end_p=end_p+1;
            sprintf( string , "%s%s" , begin_p , end_p );
            begin_p=end_p;
        }
        break;
    }
    //メモリ解放
    free( script );
    *lp_result = TRUE;
    return string;
}

以上、これを組み込みます。

イメージ

みての通り、画像だけだと発話しているかどうかサッパリわからないでしょうが、ちゃんと発話してくれています。ただし、まったく萌えません!!これは致命的な欠点ですw

Microsoftの音声合成エンジン、『haruka-(はるか)』とかいう名前がついていて、あれこれ妄想をふくらましてワクドキしながら聞いてみたところ、銀行の ATMの劣化版といったところ。。。早く、これに変わる萌える音声合成エンジンが登場しないものかと、楽しみにしているところなのです。