SPAJAMでAITalkを使ったのでメモ

仕事が忙しかったり、顔にできたできものがすごく腫れて寝込んだりしたので、もっと早く書こうと思ってたけど、SPAJAMが終わって2週間ほどたってしまった^^;

さて、SPAJAMでは協賛企業さんがツールなどを予選や本選の2日間のみ利用できるように開放してくれます。
今回、AITalkを使ったのですが、Androidで利用するサンプルもなかったので、メモとして残しておきます。
来年も使うかもしれないしね。
雑なメモですが、誰かの参考なったら、これ幸いかと。

Kotlinで書いていますが、まだKotlin勉強中なので、変な書き方をしているかもしれません。
ご了承くださいm(_ _)m

#AITalkとは？

オフィシャルの[AITalkとは](https://www.ai-j.jp/about/)を読んでいただくのが一番なんですが、音声合成エンジンですね。
ボカロのしゃべらす版といったところでしょうか？
感情表現もできることが特徴です。
Web APIを呼び出して音声をダウンロードするような感じです。

#Androidで使う
AITalkを使うには、HTTP通信を行う必要があります。
SPAJAM用に公開されたURLはHTTPSではなくHTTPでした。
そのため、HTTPで通信できるよう、AndroidManifest.xmlに記述する必要があります・

まずは以下のパーミッションを追加します。

```xml:AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
```

そして、HTTPで通信するため、applicationタグに以下の属性を追加します。

```xml:AndroidManifest.xml
<application
・・・中略・・・
android:usesCleartextTraffic="true">
```

次に、AITalkのWeb APIの呼び出しになります。
AITalkのWebAPIには読み上げテキストや、音声データのフォーマットなどを含める必要があります。
今回は専用のWebサイトがあり、そこでパラメタを作成することができたので、パラメタについては割愛します。
SPAJAMで利用する場合、Web APIの仕様も公開されるのでそれを見てやればよいと思います。
SPAJAMでなくても、契約して利用する場合は仕様が公開されいると思いますので、そちらを参照しましょう。

で、Androidでどのように呼び出したかというと、以下のようなメソッドを作成して読み込みました。
今回は、読み込んだデータを起動時にキャッシュしておいて、後から再生する方法を取りましたので、読み込んだデータをHashMapに登録しています。
また、targetUrlには実際にAITalkのWeb APIのURLを指定します。
ハッカソン用に公開されたURLの為、適当に書いてあります。

やっていることは、単純に、GETのリクエストを作成し、そのレスポンスをバイト配列で読み込んで保持しているだけです。

```kotlin:Android
        private const val targetUrl = "http://host/webapi/xxxxx.php"
        private val talkMap : HashMap<String, ByteArray> = HashMap<String, ByteArray>()
        private var track : AudioTrack? = null

        /**
         * AITalkのデータを読み込んでキャッシュします
         * 以下の出力条件で作成したリクエストデータを引数に指定してください
         * HTTP通信はメインスレッドで呼び出せない為、このメソッドはコルーチンなどの非同期処理で呼び出してください
         */
        fun loadAITalkAudio(name : String, param : String){
            var urlText = "$targetUrl?$param"
            var con : HttpURLConnection? = null
            var baos : ByteArrayOutputStream? = null
            var url = URL(urlText)

            try{
                con = url.openConnection() as HttpURLConnection
                con.connectTimeout = 0
                con.readTimeout = 0
                con.requestMethod = "GET"
                con.useCaches = false
                con.doOutput = false
                con.doInput = true
                con.connect()

                if(HttpURLConnection.HTTP_OK ==  con.responseCode){
                    var bis = BufferedInputStream(con.inputStream)
                    var buff = ByteArray(1024)
                    baos = ByteArrayOutputStream()
                    var len = bis.read(buff)
                    while(len > 0){
                        baos.write(buff, 0, len)
                        len = bis.read(buff)
                    }
                    talkMap[name] = baos.toByteArray()
                }else{
                    var isr = InputStreamReader(con.errorStream)
                    var br = BufferedReader(isr)
                    var line = br.readLine()
                    val sb = StringBuilder()
                    while(line != null){
                        sb.append(line)
                        line = br.readLine()
                    }
                }
            }finally{
                baos?.close()
                con?.disconnect()
            }
        }
```

実際にこのメソッドを呼び出しているところは、HTTP通信はメインスレッドでできない為、コルーチンを使って呼び出しています。

```kotlin:Android
        runBlocking {
            GlobalScope.launch(Dispatchers.IO) {
                AITalkController.loadAITalkAudio(
                    "orderGuest",
                    "パラメータ"
                )
                AITalkController.loadAITalkAudio(
                    "orderGuest",
                    "パラメータ"
                )
                ・・・中略・・・
                AITalkController.loadAITalkAudio(
                    "orderGuest",
                    "パラメータ"
                )
            }.join()
        }
```

次に再生ですが、キャッシュしたバイト配列を読み込んで、AudioTrackクラスを利用して再生しています。
AudioTrack自体使い慣れておらず、setBufferSizeInBytes()メソッドに指定しているバッファサイズは自信なしです。
・・・今調べたらちゃんとサイズの計算方法ありましたorz
AudioTrack.getMinBufferSize()メソッドで求められるようです。

writeで46バイトを無視するように読み込んでいるのは、Wave形式の場合、ヘッダ情報が46バイトなので、そのようにしています。
というか、ここのコードはほぼ参考にしたサイトそのまま・・・

ちなみに、AITalkで作成した音声は以下のようなフォーマットになります。

|項目|値|
|:--|:--|
|音声形式|wav 8kHz 8bit|
|WEVEサンプルレート|8000|
|WEVEビットレート|8|
|WAVEチャンネル|2|

```kotlin:Android
        fun playTrack(name : String){
            if(!talkMap.containsKey(name)){
                return
            }

            if(track != null){
                track?.flush()
                track?.reloadStaticData()
            }

            track = AudioTrack.Builder()
                .setAudioAttributes(
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                        .build()
                )
                .setAudioFormat(
                    AudioFormat.Builder()
                        .setEncoding(AudioFormat.ENCODING_PCM_8BIT)
                        .setSampleRate(8000)
                        .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                        .build()

                )
                .setBufferSizeInBytes(8000 * 2)
                .setTransferMode(AudioTrack.MODE_STATIC)
                .build()

            track?.positionNotificationPeriod = 8000 / 10;
            track?.setPlaybackPositionUpdateListener(object : AudioTrack.OnPlaybackPositionUpdateListener{
                override fun onMarkerReached(p0: AudioTrack?) {
                    Log.i("BakeFish", "onMarkerReached")
                }

                override fun onPeriodicNotification(p0: AudioTrack?) {
                    Log.i("BakeFish", "onPeriodicNotification")
                    if(p0?.playState == AudioTrack.PLAYSTATE_STOPPED){
                        Log.i("BakeFish", "onPeriodicNotification End")
                    }
                }
            })

            track?.write( talkMap[name]!!, 46,  talkMap[name]!!.size - 46, AudioTrack.WRITE_BLOCKING)
            track?.play()
        }
    }
}
```

AudioTrackを理解していなかったのが問題なのですが、再生した音声の終わりにノイズが入ってしまっていました。
が、とりあえずこれで再生できました。
おそらく、パラメタの設定に問題があるんじゃないかと思っています。
この辺、改良したいので、AudioTrackを次回使うときにちゃんと勉強して使いたいと思います。

というわけで、ざっくりでかつ雑ですが、AITalkを使ってみたのでメモとして残しておきました。