0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SPAJAMでAITalkを使ったのでメモ

Last updated at Posted at 2020-09-11

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

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

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

#AITalkとは?

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

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

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

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

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

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のリクエストを作成し、そのレスポンスをバイト配列で読み込んで保持しているだけです。

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通信はメインスレッドでできない為、コルーチンを使って呼び出しています。

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
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を使ってみたのでメモとして残しておきました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?