PCM の音声データを Discord Bot で再生する
Last Modified:
Discord Bot API が対応している音声コーデックは Opus です。なので、自分の Discord bot で再生するには、手元の音声データを Opus にエンコードする必要があります。
Go だと libopus をラップしたライブラリがあります。
https://github.com/hraban/opus
音声ファイルから PCM (波の高さを一定時間ごとに記録した一連のデータ) を取り出せさえすれば、あとはこれを使ってエンコードすればよいわけです。 といっても、libopus はある程度の Opus のことが分かっていないと使えないないライブラリです。僕も完全には分かっていないのですが、とりあえず動くものができたので記事にします。
// opus はフレームという単位に音声を切って非可逆圧縮を行います。
// その幅をここでは 20 ms とします。
// https://datatracker.ietf.org/doc/html/rfc6716#section-2.1.4
const fwMS = 20
// pcm を Opus に変換する
func pcmToOpus(pcm []int, sampleRate, numChannels int) ([][]byte, error) {
// fw が整数にならなければエラーとする (これでいいのかは知らない)
if sampleRate%(1000/fwMS) != 0 {
return nil, fmt.Errorf("sampling rate %% %d must be zero", 1000/fwMS)
}
// fwMS の間にあるサンプルの数
fw := sampleRate * fwMS / 1000
enc, err := opus.NewEncoder(sampleRate, numChannels, opus.AppVoIP)
if err != nil {
log.Printf("error loading opus file: %s", err)
}
// 全体が fw の倍数になるように調整する
// 最大で fwMS-1 ミリ秒無音の音声が末尾に追加されるが妥協する
for len(pcm)%fw != 0 {
pcm = append(pcm, 0)
}
// frame と呼ばれる単位でエンコードする
frames := make([][]byte, 0, len(pcm)/fw)
i16tmp := make([]int16, fw)
for i := 0; i < len(pcm); i += fw {
for j := 0; j < fw; j += 1 {
i16tmp[j] = int16(pcm[i+j])
}
fr := make([]byte, 1024)
n, err := enc.Encode(i16tmp, fr)
if err != nil {
return nil, fmt.Errorf("error encoding to opus: %w", err)
}
fr = fr[:n]
frames = append(frames, fr)
}
return frames, nil
}
discordgo で作った bot なら次のようにして喋らせます。
var s *discordgo.Session = ...
// 音声データから pcm を得る
var pcm []int = ...
// 音声データのサンプリングレート
sr := 44100
// 音声データのチャンネル数
// 1 以外ではテストしていません!
nc := 1
opus, err := pcmToOpus(pcm, sr, nc)
if err != nil {
log.Printf("error encoding to opus: %s", err)
return
}
conn, ok := s.VoiceConnections[guildID]
if !ok {
log.Printf("error voice connection not found on guild %s", guildID)
return
}
conn.Speaking(true)
for _, f := range opus {
conn.OpusSend <- f
}
conn.Speaking(false)