Skip to content

Instantly share code, notes, and snippets.

@diamondburned
Last active November 18, 2020 01:52
Show Gist options
  • Save diamondburned/21a77fce9da542d33e19c9a1a6ca42cc to your computer and use it in GitHub Desktop.
Save diamondburned/21a77fce9da542d33e19c9a1a6ca42cc to your computer and use it in GitHub Desktop.
Arikawa voice playback example
package main
import (
"context"
"log"
"os"
"os/exec"
"os/signal"
"time"
"github.com/diamondburned/arikawa/v2/discord"
"github.com/diamondburned/arikawa/v2/state"
"github.com/diamondburned/arikawa/v2/voice"
"github.com/diamondburned/arikawa/v2/voice/voicegateway"
"github.com/diamondburned/oggreader"
"github.com/pkg/errors"
)
func main() {
s, err := state.New("Bot " + os.Getenv("BOT_TOKEN"))
if err != nil {
log.Fatalln("failed to load:", err)
}
voiceID, err := discord.ParseSnowflake(os.Getenv("VOICE_ID"))
if err != nil {
log.Fatalln("failed to parse $VOICE_ID:", err)
}
v := voice.NewVoice(s)
if err := v.Open(); err != nil {
log.Fatalln("failed to open:", err)
}
defer v.Close()
if err := start(v, discord.ChannelID(voiceID), "centimeter.flac"); err != nil {
// Ignore context canceled errors as they're often intentional.
if !errors.Is(err, context.Canceled) {
log.Fatalln(err)
}
}
}
func start(v *voice.Voice, id discord.ChannelID, file string) error {
ch, err := v.Channel(id)
if err != nil {
return errors.Wrap(err, "failed to get channel")
}
voiceSession, err := v.JoinChannel(ch.GuildID, ch.ID, false, true)
if err != nil {
return errors.Wrap(err, "failed to join channel")
}
// This also disconnects.
defer v.RemoveSession(ch.GuildID)
// Use a context to cancel ffmpeg if needed.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Cancel ffmpeg if SIGINT is received.
go func() {
sig := make(chan os.Signal)
signal.Notify(sig, os.Interrupt)
select {
case <-sig:
cancel()
case <-ctx.Done():
return
}
}()
// Optimize Opus frame duration. This step is optional, but it is
// recommended.
udp := voiceSession.VoiceUDPConn()
udp.UseContext(ctx)
udp.ResetFrequency(60*time.Millisecond, 2880)
ffmpeg := exec.CommandContext(ctx,
"ffmpeg",
// Streaming is slow, so a single thread is all we need.
"-hide_banner", "-threads", "1", "-loglevel", "error",
// Input file. This should be changed.
"-i", file,
// Output format; leave as "libopus".
"-c:a", "libopus",
// Bitrate in kilobits. This doesn't matter, but I recommend 96k as the
// sweet spot.
"-b:a", "96k",
// Frame duration should be the same as what's given into
// udp.ResetFrequency.
"-frame_duration", "60",
// Disable variable bitrate to keep packet sizes consistent. This is
// optional, but it technically reduces stuttering.
"-vbr", "off",
// Output format, which is opus, so we need to unwrap the opus file.
"-f", "opus", "-",
)
ffmpeg.Stderr = os.Stderr
stdout, err := ffmpeg.StdoutPipe()
if err != nil {
return errors.Wrap(err, "failed to get stdout pipe")
}
if err := voiceSession.Speaking(voicegateway.Microphone); err != nil {
return errors.Wrap(err, "failed to send speaking")
}
if err := ffmpeg.Start(); err != nil {
return errors.Wrap(err, "failed to start ffmpeg")
}
if err := oggreader.DecodeBuffered(udp, stdout); err != nil {
return errors.Wrap(err, "failed to decode ogg")
}
if err := ffmpeg.Wait(); err != nil {
return errors.Wrap(err, "failed to finish ffmpeg")
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment