仕事が忙しかったり、顔にできたできものがすごく腫れて寝込んだりしたので、もっと早く書こうと思ってたけど、SPAJAMが終わって2週間ほどたってしまった^^;
さて、SPAJAMでは協賛企業さんがツールなどを予選や本選の2日間のみ利用できるように開放してくれます。
今回、AITalkを使ったのですが、Androidで利用するサンプルもなかったので、メモとして残しておきます。
来年も使うかもしれないしね。
雑なメモですが、誰かの参考なったら、これ幸いかと。
Kotlinで書いていますが、まだKotlin勉強中なので、変な書き方をしているかもしれません。
ご了承くださいm(_ _)m
#AITalkとは?
オフィシャルのAITalkとはを読んでいただくのが一番なんですが、音声合成エンジンですね。
ボカロのしゃべらす版といったところでしょうか?
感情表現もできることが特徴です。
Web APIを呼び出して音声をダウンロードするような感じです。
#Androidで使う
AITalkを使うには、HTTP通信を行う必要があります。
SPAJAM用に公開されたURLはHTTPSではなくHTTPでした。
そのため、HTTPで通信できるよう、AndroidManifest.xmlに記述する必要があります・
まずは以下のパーミッションを追加します。
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
そして、HTTPで通信するため、applicationタグに以下の属性を追加します。
<application
・・・中略・・・
android:usesCleartextTraffic="true">
次に、AITalkのWeb APIの呼び出しになります。
AITalkのWebAPIには読み上げテキストや、音声データのフォーマットなどを含める必要があります。
今回は専用のWebサイトがあり、そこでパラメタを作成することができたので、パラメタについては割愛します。
SPAJAMで利用する場合、Web APIの仕様も公開されるのでそれを見てやればよいと思います。
SPAJAMでなくても、契約して利用する場合は仕様が公開されいると思いますので、そちらを参照しましょう。
で、Androidでどのように呼び出したかというと、以下のようなメソッドを作成して読み込みました。
今回は、読み込んだデータを起動時にキャッシュしておいて、後から再生する方法を取りましたので、読み込んだデータをHashMapに登録しています。
また、targetUrlには実際にAITalkのWeb APIのURLを指定します。
ハッカソン用に公開されたURLの為、適当に書いてあります。
やっていることは、単純に、GETのリクエストを作成し、そのレスポンスをバイト配列で読み込んで保持しているだけです。
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通信はメインスレッドでできない為、コルーチンを使って呼び出しています。
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 |
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を使ってみたのでメモとして残しておきました。