AquesTalkをPythonから(音声合成)

AquesTalkは発声用のコーパスを持たない非常に軽量な音声合成ライブラリです.営利,非営利かわらず無償で使えるのでいろいろなところで使ってみたいですね.AquesTalkの2つのDLLと以下のスクリプトを一緒に放り込むだけですぐに使える手軽なところも魅力的です.
AquesTalk - テキスト音声合成ミドルウェア

AquesTalkは、組み込み用途向けのテキスト情報を音声波形に変換出力するライブラリです。システムに負担をかけずに簡単に組み込めることを目指して開発しました。 AquesTalkを使えば、様々なアプリケーションやサービスに、任意の音声メッセージを動的に生成できます。
・超軽量
 他に類を見ない少ないリソースと処理量で高品質な音声を生成します。
・フリー(Win版のみ)
 営利、非営利にかかわらず無償で使用でき、製品に組み込んで販売することも可能です
・高い移植性
 ANSI準拠のCプログラムで記述されているので各種プラットフォームに容易に移植できます

PythonからWin32APIを使ってWindowを作成したい人にも参考にもなるかも.(移植性を考えるとwxWidgetsとかTkを使った方が良いかもしれないが配布時に重くなりそうなのでパス)

  • AquesTalk.py
# -*- coding:sjis -*-
import os, sys
import ctypes
from array import array
from ctypes import windll
from MessageEvents import MessageEvents

class AquesWrapper(object):
    """ AquesTalk音声合成ライブラリの薄いラッパ
    Python2.4以下の場合はctypesライブラリが必要(2.5は標準ライブラリ)
    Download:http://python.net/crew/theller/ctypes/
    """
    
    def __init__(self):
        self.talk_dll = windll.LoadLibrary("AquesTalkDa.dll")
        self.synt_dll = windll.LoadLibrary("AquesTalk.dll")
        
        # 同期発声
        self.aques_playsync = self.talk_dll.AquesTalkDa_PlaySync
        self.aques_playsync.argtypes = [ctypes.c_char_p, ctypes.c_int]
        self.aques_playsync.restype = ctypes.c_int
        
        # 非同期発声
        self.aques_play = self.talk_dll.AquesTalkDa_Play
        self.aques_play.argtypes = [ctypes.c_uint, ctypes.c_char_p,
            ctypes.c_int, ctypes.c_uint, ctypes.c_ulong, ctypes.c_ulong]
        self.aques_play.restype = ctypes.c_int
        
        # 音声合成エンジン生成
        self.aques_create = self.talk_dll.AquesTalkDa_Create
        self.aques_create.argtypes = []
        self.aques_create.restype = ctypes.c_uint
        
        # 音声合成エンジン解放
        self.aques_release = self.talk_dll.AquesTalkDa_Release
        self.aques_release.argtypes = [ctypes.c_uint]
        self.aques_release.restype = None
        
        # 発声ストップ
        self.aques_stop = self.talk_dll.AquesTalkDa_Stop
        self.aques_stop.argtypes = [ctypes.c_uint]
        self.aques_stop.restype = None
        
        # 発声チェック
        self.aques_isplay = self.talk_dll.AquesTalkDa_IsPlay
        self.aques_isplay.argtypes = [ctypes.c_uint]
        self.aques_isplay.restype = ctypes.c_int
        
        # 音声データ生成
        self.aques_synthe = self.synt_dll.AquesTalk_Synthe
        self.aques_synthe.argtypes = [ctypes.c_char_p, ctypes.c_int,
             ctypes.c_void_p]
        self.aques_synthe.restype = ctypes.c_void_p
        
        # 音声データ解放
        self.aques_free = self.synt_dll.AquesTalk_FreeWave
        self.aques_free.argtypes = [ctypes.c_void_p]
        self.aques_free.restype = None

class AquesTalk(AquesWrapper):
    """ 非同期再生のコールバックを使いやすくしたラッパ
    Python for Windows extensionsが必要
    Webサイト:http://starship.python.net/crew/mhammond/win32/Downloads.html
    Download :https://sourceforge.net/projects/pywin32/
    """
    
    # 非同期再生を同時に利用する際に識別できる最大数
    MAX_SYN_PLAY = 32
    
    # 次に非同期再生する際に付与する発声ID(0-MAX_SYNPLAY)
    count = 0
    
    # 現在再生中の発声ID
    play_list = []
    
    def __init__(self):
        AquesWrapper.__init__(self)
        
        # 音声合成エンジンの生成
        self.handle = self.Create()
        
        # このインスタンスで再生中の発声ID
        self.playing = None
    
    def PlaySync(self, text, speed):
        """ 発声が終了するまで戻らない同期タイプの音声合成
        text: 発声記号列
        speed: 50-300で指定(標準は100)
        """
        return self.aques_playsync(text, speed)
    
    def Create(self):
        """ 非同期再生用の音声合成エンジンのインスタンスを生成する """
        return self.aques_create()
        
    def Release(self):
        """ 音声合成エンジンのインスタンスを解放する """
        return self.aques_release(self.handle)
    
    def Stop(self):
        """ このインスタンスの非同期発声を停止する """
        return self.aques_stop(self.handle)
    
    def IsPlayNumber(self, number):
        """ 指定した番号の音声をどれかのインスタンスで再生中かどうか
        number: PlayASyncで再生したときの返り値を指定
        """
        if number in AquesTalk.play_list:
            return True
        return False
    
    def IsPlay(self):
        """ このインスタンスが持つ音声合成エンジンが再生中かどうか """
        return self.aques_isplay(self.handle)
    
    def PlayASync(self, text, speed, callback):
        """ 発生が終了する前に戻る非同期タイプの音声合成
        複数同時に発声させたい場合は複数のインスタンスを用いる
        
        callback: 発声終了時に呼ばれるコールバック関数
        (引数はhwnd, msg, wp, lp)
        """
        ret_code = AquesTalk.count
        AquesTalk.play_list.append(AquesTalk.count)
        self.playing = AquesTalk.count
        
        AquesTalk.count += 1
        if AquesTalk.count >= AquesTalk.MAX_SYN_PLAY:
            AquesTalk.count = 0
        
        def finish_wrapper(hwnd, msg, wparam, lparam):
            AquesTalk.play_list.remove(msg)
            self.playing = None
            return callback(hwnd, msg, wparam, lparam)
        
        msg = MessageEvents.set_callback(ret_code, finish_wrapper)
        hwnd = MessageEvents.get_hwnd()
        self.aques_play(self.handle, text, speed, hwnd, msg, 0)
        
        return ret_code
    
    def __synthe(self, text, speed):
        """ 音声記号列から音声波形を生成する """
        err_code = ctypes.c_int()
        ret = self.aques_synthe(text, speed, ctypes.byref(err_code))
        if ret == None:
            raise Exception, err_code.value
        return ret, err_code
    
    def SyntheData(self, text, speed):
        """ 音声データをarray(リスト形式)で返す """
        data, size = self.__synthe(text, speed)
        byte_ary = array('B')
        for i in range(size.value):
            byte = ctypes.c_ubyte.from_address(data+i).value
            byte_ary.append(byte)
        self.aques_free(data)
        return byte_ary
    
    def SyntheFile(self, text, speed, filename):
        """ 音声データをファイルに書き込む """
        byte_ary = self.SyntheData(text, speed)
        fp = open(filename, "wb")
        byte_ary.tofile(fp)
        fp.close()

if __name__ == '__main__':
    def talk_test():
        import time
        at = AquesTalk()
        at2 = AquesTalk()
        def finish(*args):
            print args
        at.PlayASync("こんにちわ", 100, finish)
        time.sleep(0.1)
        at2.PlayASync("おはようございます", 100, finish)
        time.sleep(0.5)
        at2.Stop()
        
        # 「こんにちわ」と「おはよう」を同時に発声
        # 「おはようございます」を途中でストップ

    def synthe_test():
        at = AquesTalk()
        at.SyntheFile("こんばんは", 100, "test.wav")
        at.Release()

    #synthe_test()
    talk_test()

AquesTalkのplay関数は非同期に発声を行ってその終了時に指定したウィンドウにメッセージを投げることでコールバックを実現していますが,できれば直接コールバック関数を指定して使いたいのでAquesTalk専用の見えないWindowを一つ作っています.ほかの言語から使う場合も同様の仕組みが必要ですが,.netについては以下のサイトに解説とソースがあります.

  • MessageEvents.py
# -*- coding: sjis -*-
import os, sys
import time
import threading
import win32api
import win32con
import win32gui

class Window(object):
    def __init__(self, lock):
        className = "win32gui_window_class"
        windowTitle = "win32gui Sample Window"
        style = win32con.WS_OVERLAPPEDWINDOW
        hIcon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
        
        self.lock = lock
        
        # Initialize instance
        win32gui.InitCommonControls()
        self.hinst = 0 # win32api.GetModuleHandle(None)
        
        # Register class
        wc = win32gui.WNDCLASS()
        wc.style = win32con.CS_HREDRAW | win32con.CS_VREDRAW
        wc.lpfnWndProc = self.WndProc
        wc.hCursor = win32gui.LoadCursor( 0, win32con.IDC_ARROW )
        wc.hbrBackground = win32con.COLOR_WINDOW + 1
        wc.hIcon = hIcon
        wc.lpszClassName = className
        wc.cbWndExtra = 0
        
        classAtom = win32gui.RegisterClass(wc)
        
        self.message_map = {}
        
        # Create window
        style = win32con.WS_OVERLAPPEDWINDOW
        self.hwnd = win32gui.CreateWindow(className,
                                          windowTitle,
                                          style,
                                          win32con.CW_USEDEFAULT,
                                          win32con.CW_USEDEFAULT,
                                          win32con.CW_USEDEFAULT,
                                          win32con.CW_USEDEFAULT,
                                          0,
                                          0,
                                          self.hinst,
                                          None)
        # Hook WndProc
        self.oldWndProc = win32gui.SetWindowLong(self.hwnd,
                                                 win32con.GWL_WNDPROC,
                                                 self.WndProc)
    
    def ShowWindow(self):
        win32gui.UpdateWindow(self.hwnd)
        win32gui.ShowWindow(self.hwnd, win32con.SW_SHOW)
    
    def WndProc(self, hwnd, message, wparam, lparam):
        self.lock.acquire()
        try:
            if message == win32con.WM_CLOSE:
                return win32gui.PostQuitMessage(0)
            elif message in self.message_map:
                return self.message_map[message](
                    hwnd, message - win32con.WM_USER,wparam, lparam)
        finally:
            self.lock.release()
        return win32gui.DefWindowProc(hwnd, message, wparam, lparam)
    
    def PumpMessages(self):
        return win32gui.PumpMessages()
    
    def PumpWaitingMessages(self):
        return win32gui.PumpWaitingMessages()

class WindowThread(threading.Thread):
    def __init__(self, lock):
        threading.Thread.__init__(self)
        self.hwnd = 0
        self.lock = lock
    
    def run(self):
        self.win = Window(self.lock)
        self.hwnd = self.win.hwnd
        self.win.PumpMessages()

class MessageEvents(object):
    # メッセージを処理するためのウィンドウを作成
    lock = threading.Lock()
    win = WindowThread(lock)
    win.setDaemon(True) # 親スレッド終了時に一緒に終了
    win.start()
    
    # ウィンドウができるまで待ち
    while win.hwnd == 0:
        time.sleep(0.1)
    message_map = {}
    
    @staticmethod
    def set_callback(msg_id, func):
        MessageEvents.lock.acquire()
        MessageEvents.message_map[msg_id] = func
        MessageEvents.win.win.message_map[win32con.WM_USER + msg_id] = func
        MessageEvents.lock.release()
        return win32con.WM_USER + msg_id
    
    @staticmethod
    def get_hwnd():
        return MessageEvents.win.hwnd

スレッドの知識が浅いので変なこと書いてるかも..