examples: Add voice example

This commit is contained in:
diamondburned 2022-04-03 17:49:21 -07:00
parent ae24217e34
commit ec4cd6d661
No known key found for this signature in database
GPG Key ID: D78C4471CE776659
3 changed files with 166 additions and 0 deletions

19
0-examples/voice/go.mod Normal file
View File

@ -0,0 +1,19 @@
module github.com/diamondburned/arikawa/v3/0-examples/voice
go 1.17
replace github.com/diamondburned/arikawa/v3 => ../../
require (
github.com/diamondburned/arikawa/v3 v3.0.0-rc.6
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647
github.com/pkg/errors v0.9.1
)
require (
github.com/gorilla/schema v1.2.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/sys v0.0.0-20211001092434-39dca1131b70 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
)

20
0-examples/voice/go.sum Normal file
View File

@ -0,0 +1,20 @@
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647 h1:TJWvffl1cMLzSOvw8Wv3CQicuU9NaKDKXvBfh5T9W00=
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647/go.mod h1:xEJuvlmPx1wBKUWkx+MUp1ULSMQwSM9FS+bnFJhPQkk=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211001092434-39dca1131b70 h1:pGleJoyD1yA5HfvuaksHxD0404gsEkNDerKsQ0N0y1s=
golang.org/x/sys v0.0.0-20211001092434-39dca1131b70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

127
0-examples/voice/main.go Normal file
View File

@ -0,0 +1,127 @@
package main
import (
"context"
"flag"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"time"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/state"
"github.com/diamondburned/arikawa/v3/voice"
"github.com/diamondburned/arikawa/v3/voice/udp"
"github.com/diamondburned/oggreader"
"github.com/pkg/errors"
)
func main() {
flag.Parse()
file := flag.Arg(0)
if file == "" {
log.Fatalln("usage:", filepath.Base(os.Args[0]), "<audio file>")
}
voiceID, err := discord.ParseSnowflake(os.Getenv("VOICE_ID"))
if err != nil {
log.Fatalln("failed to parse $VOICE_ID:", err)
}
chID := discord.ChannelID(voiceID)
state := state.New("Bot " + os.Getenv("BOT_TOKEN"))
voice.AddIntents(state)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := state.Open(ctx); err != nil {
log.Fatalln("failed to open:", err)
}
defer state.Close()
if err := start(ctx, state, chID, file); err != nil {
// Ignore context canceled errors as they're often intentional.
if !errors.Is(err, context.Canceled) {
log.Fatalln(err)
}
}
}
// Optional constants to tweak the Opus stream.
const (
frameDuration = 60 // ms
timeIncrement = 2880
)
func start(ctx context.Context, s *state.State, id discord.ChannelID, file string) error {
v, err := voice.NewSession(s)
if err != nil {
return errors.Wrap(err, "cannot make new voice session")
}
// Optimize Opus frame duration. This step is optional, but it is
// recommended.
v.SetUDPDialer(udp.DialFuncWithFrequency(
frameDuration*time.Millisecond, // correspond to -frame_duration
timeIncrement,
))
ffmpeg := exec.CommandContext(ctx,
"ffmpeg", "-hide_banner", "-loglevel", "error",
// Streaming is slow, so a single thread is all we need.
"-threads", "1",
// Input file.
"-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.DialFuncWithFrequency.
"-frame_duration", strconv.Itoa(frameDuration),
// Disable variable bitrate to keep packet sizes consistent. This is
// optional.
"-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")
}
// Kickstart FFmpeg before we join. FFmpeg will wait until we start
// consuming the stream to process further.
if err := ffmpeg.Start(); err != nil {
return errors.Wrap(err, "failed to start ffmpeg")
}
// Join the voice channel.
if err := v.JoinChannelAndSpeak(ctx, id, false, true); err != nil {
return errors.Wrap(err, "failed to join channel")
}
defer v.Leave(ctx)
// Start decoding FFmpeg's OGG-container output and extract the raw Opus
// frames into the stream.
if err := oggreader.DecodeBuffered(v, stdout); err != nil {
return errors.Wrap(err, "failed to decode ogg")
}
// Wait until FFmpeg finishes writing entirely and leave.
if err := ffmpeg.Wait(); err != nil {
return errors.Wrap(err, "failed to finish ffmpeg")
}
return nil
}