mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-11-30 10:43:30 +00:00
examples: Add voice example
This commit is contained in:
parent
ae24217e34
commit
ec4cd6d661
19
0-examples/voice/go.mod
Normal file
19
0-examples/voice/go.mod
Normal 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
20
0-examples/voice/go.sum
Normal 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
127
0-examples/voice/main.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue