Goでゆっくりしていってね!

GoからAquesTalkを使い「ゆっくりの声」再生するパッケージを作りました。

この記事は Go Advent Calendar 2020 6日目の参加記事です。

TL;DR

  • koron-go/aquestalk を作った: Goで「ゆっくり」を喋らせることができる
  • WindowsとLinuxでDLL(SO)を動的に読み込む方法を紹介
  • macOSでFrameworkを利用する方法を紹介

koron-go/aquesktalk 紹介

koron-go/aquesktalk は株式会社アクエストさまが販売している 規則音声合成エンジン AquesTalk をGo言語で利用するためのラッパーパッケージです。 AquesTalk はもはや説明の必要もないであろう「ゆっくりの声」のエンジンです。 つまりGoで「ゆっくり」を喋らせることができます。

注意! ラッパーパッケージは評価ライセンスを用いて開発しました。 ご自身のプログラムに組み込んで使う場合には、適切なライセンスを購入 ・入手してください。

注意その2! koron-go/aquesktalk の使い方などの質問は issue などで私へお問い合わせください。株式会社アクエストさまに問い合わせて迷惑をかけることがないようにしてください。

Getting started: 試してみよう

では実際に試してみましょう。

まずはAquesTalkの評価版をダウンロードしましょう。 WindowsとLinuxはAquesTalk1のVersion 1.7.1をダウンロードしてください。 macOSではAquesTalk10の1.1.0をダウンロードしてください。

次に以下のコマンドで koron-go/aquesktalk をダウンロードして、プロジェクトディレクトリに移動してください。

$ go get github.com/koron-go/aquestalk
$ cd ~/go/src/github.com/koron-go/aquestalk

次にダウンロードした評価版の zip から必要なファイルを koron-go/aquestalk のディレクトリにコピーします。Windowsでは x64/f1/AquesTalk.dll をコピーしてください。Linuxでは lib64/f1/libAquesTalk.so をコピーしてください。macOSだけ若干特殊で AquesTalk.framework ディレクトリを /Library/Frameworks/ の下にコピーしてください。

それぞれコピーのコマンドは以下のようになるかと思います。

# Windows
> copy aqtk1-win-eva/x64/f1/AquesTalk.dll .

# Linux
$ cp aqtk1-lnx-eva/lib64/f1/libAquesTalk.so .

# macOS
$ sudo cp -Rp aqtk10_mac/AquesTalk.framework /Library/Frameworks/

あとはコンパイルして実行するだけなのですが、Linuxだけ一手間かかる場合があります。 ubuntuでは以下のコマンドで libasound2-dev パッケージをインストールしてください。

$ sudo apt install -y libasound2-dev

さぁこれで準備は完了です。go run でサンプルプログラムを実行すると「こんぬちはGopher」とゆっくり声で喋ってくれます。 「に」が「ぬ」になっているのは評価版のAquesTalkの制限事項です。 試用版以外を用いればではちゃんと「に」になるはずです。

$ go run ./examples/01_hello/main.go

WindowsにおけるDLLの動的読み込み

WindowsでDLLを動的に読み込む方法はいくつかあるのですが、今回は syscall.LoadDLL を用いました。コードは以下の通りです。

dll, err := syscall.LoadDLL(DLLName)
if err != nil {
	dllErr = err
	return
}
pSynthe, err := dll.FindProc("AquesTalk_Synthe_Utf8")
if err != nil {
	dll.Release()
	dllErr = err
	return
}
pFreeWave, err := dll.FindProc("AquesTalk_FreeWave")
if err != nil {
	dll.Release()
	dllErr = err
	return
}

https://github.com/koron-go/aquestalk/blob/535919413be0481a58945536fcc7d8b38feebff3/aquestalk_windows.go#L56-L72 より

Win32 プログラムに慣れている人にはお馴染みのコードになっています。ただシステムのDLLを読み込む場合は、 DLLのプリロード攻撃を防ぐために x/sys/windows の NewLazySystemDLL を使ったほうが良いです。

LinuxにおけるDLLの動的読み込み

Linuxで動的読み込みをするには libdl の dlopen を使えば良いのですが、 CGOとGoとの境界をどこに引くのかでバリエーションが考えられます。 今回はシンボル=関数ポインタの解決までGoで行い、 CGOはその関数を呼び出す薄いラッパーをCGOで書きました。

関数ポインタの解決は以下のようになっています。

h := C.dlopen(C.CString(DLLName), C.RTLD_LAZY)
if h == nil {
        return nil, fmt.Errorf("failed to load %s", DLLName)
}
pSynthe := C.dlsym(h, C.CString("AquesTalk_Synthe_Utf8"))
if pSynthe == nil {
        return nil, errors.New("not found symbol: AquesTalk_Synthe_Utf8")
}
pFree := C.dlsym(h, C.CString("AquesTalk_FreeWave"))
if pFree == nil {
        return nil, errors.New("not found symbol: AquesTalk_FreeWave")
}

(https://github.com/koron-go/aquestalk/blob/535919413be0481a58945536fcc7d8b38feebff3/aquestalk_linux.go#L30-L41 より)

Goで C.dlopen が使えるのは以下のCGO部分に由来しています。

/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>

...(snip)...

*/
import "C"

呼び出し時は以下のGoとCGOのコードが働きます。

var size C.int
r := C.callSynthe(pSynthe, C.CString(koe), C.int(speed), &size)
if r == nil {
	return nil, errno(size)
}
unsigned char* callSynthe(void *p, const char *koe, int speed, int *size) {
	unsigned char* (*synthe)(const char*, int, int*) = p;
	return synthe(koe, speed, size);
}

LinuxでCGOを使うなら静的読み込みという方法もありそちらのが楽なのですが、 今回は動的読み込みをやってみたかったということでこのようになっています。 またちょっと手抜きをしてて、 現時点では複数回鳴らせないか鳴らせてもメモリがリークするかもしれません。

macOSにおけるFrameworkの利用

macOSでのFrameworkの動的読み込みはちょっと調査時間が足らず、 また久しぶりにmacOSを使ったということで見通しも経たなかったので、 まずは静的リンクからということにしてみました。

macOS版のキモはCGOに渡している3つのフラグで、 コンパイラとリンカに /Library/Frameworks/ をフレームワークの検索先として教えてあげ、 リンカにはさらに AquesTalk Framework を使うことを教えてあげれば完了です。 (ここに行きつくまで四苦八苦したがそれは割愛)

/*
#cgo CFLAGS: -F/Library/Frameworks
#cgo LDFLAGS: -F/Library/Frameworks
#cgo LDFLAGS: -framework AquesTalk
#import <AquesTalk/AquesTalk.h>
*/
import "C"

あとはLinux版とほぼ一緒で、CGOとしての書き方の問題になりました。

余談ですが、この動作確認には M1 macmini と Go 1.15.5 を使いました。 ということは生成された x64 バイナリを Rosetta2 で aarch64 に変換して動かしていたというわけ。 もうなにがなんだか。 仮にこれで動的読み込みでも動いたらそれこそ凄すぎるなって感じがしますね。

終わりに

ここまで見てきたように syscall だけで動的にDLLを読み込んだり、 CGOを組み合わせてSOを動的に読み込んだり、 静的に読み込んでみたりしてみました。 GoからでもCで書かれた(?)プロプライエタリなライブラリを扱いうる、 ということが見て取れるでしょう。

2020年は実感のないまま残りわずかとなってしまいましたが… Goでゆっくりしていってね!