Last active
November 18, 2020 01:52
-
-
Save diamondburned/21a77fce9da542d33e19c9a1a6ca42cc to your computer and use it in GitHub Desktop.
Arikawa voice playback example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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