www.aatomu.work
Jump to Discord Server Jump to Twitter Jump to Github

Discord VC の Opus で遊ぶ

DiscordのVCはOpusでpacketやり取りしているので、
そこをうまくつついて遊んだっていう記録。
開発環境としてはdiscordgo v1.28.1です。

まず最初にオウム返し

BotをVCに接続させるとsession.OpusRecvにRTPの整形済みデータが来るので、
そこからOpus Packetだけを抜き取ってsession.OpusSend<-することでそのままオウム返しできました。

func onReady(discord *discordgo.Session, r *discordgo.Ready) {
	log.Println("Discord bot on ready")

	vc, _ := discord.ChannelVoiceJoin("***", "***", false, false)
	go func() {
		for {
			p := <-vc.OpusRecv
			vc.OpusSend <- p.Opus
		}
	}()
}

オウム返しの次は.wavで保存

vc.OpusRecvを RTP packetに変換し それをffmpegに流すことで wavで保存しました

func onReady(discord *discordgo.Session, r *discordgo.Ready) {
	log.Println("Discord bot on ready")

	vc_to, decTo := NewVCSession(discord, "***", "***")

	es := NewMemEncodeStream(vc_to, EncodeOpts{}, make(chan error))
	go func() {
		for {
			pcm := decTo.pcm

			if es.pipe == nil {
				continue
			}
			n, err := es.pipe.Write(pcm)
			if n > 0 && err != nil {
				log.Println("Write", n, err)
			}
		}
	}()
}

func NewVCSession(discord *discordgo.Session, guild, channel string) (vc *discordgo.VoiceConnection, d *decoder) {
	vc, err := discord.ChannelVoiceJoin(guild, channel, false, false)
	if err != nil {
		panic(err)
	}

	d = NewDecoder()
	go func() {
		for {
			s := <-vc.OpusRecv
			d.Decode(s)
		}
	}()
	return
}

type decoder struct {
	ffmpeg       *exec.Cmd
	writer       *oggwriter.OggWriter
	ffmpegOutput io.ReadCloser
	pcm          []byte
}

func NewDecoder() (d *decoder) {
	rawCommand := strings.Split("ffmpeg -loglevel debug -i pipe:0 -c:a pcm_s16le -ac 2 -ar 48000 -b:a 1024k -f wav example.wav", " ")
	ffmpeg := exec.Command(rawCommand[0], rawCommand[1:]...)
	ffmpegOutput, _ := ffmpeg.StderrPipe()
	stdIn, _ := ffmpeg.StdinPipe()
	stdOut, _ := ffmpeg.StdoutPipe()

	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := ffmpegOutput.Read(buf)
			if err != nil {
				fmt.Println("ffmpeg Error", err)
				return
			}
			fmt.Print(string(buf[:n]))
		}
	}()

	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := stdOut.Read(buf)
			if err != nil {
				fmt.Println("Opus Encode Error", err)
				return
			}
			d.pcm = buf[:n]
		}
	}()

	ffmpeg.Start()

	writer, _ := oggwriter.NewWith(stdIn, 48000, 2)
	return &decoder{
		ffmpeg:       ffmpeg,
		ffmpegOutput: ffmpegOutput,
		writer:       writer,
	}
}

func (d *decoder) Decode(p *discordgo.Packet) {
	rtp := &rtp.Packet{
		Header: rtp.Header{
			Version: 2,
			// Taken from Discord voice docs
			PayloadType:    0x78,
			SequenceNumber: p.Sequence,
			Timestamp:      p.Timestamp,
			SSRC:           p.SSRC,
		},
		Payload: p.Opus,
	}
	d.writer.WriteRTP(rtp)
}

Opus=>PCM=>Opus

Opus(.ogg)=>PCM(.wav)は上のまんまでそっからOpusに変換するのにくじけました。
雑にコードを書くとffmpeg -i pipe:0 -c:a pcm_s16le pipe:1 | ffmpeg -i pipe:0 -c:a libopus output.oggをしたが
謎のハウリングを起こしたのであきらめた。

あとがき

PCMに変換することはできたのでPCM同士の合成後 ファイル(pipeではなく)からOpusに変換することで、
うまく複数:1のVC 接続できるのでは?と思った。