forked from bwmarrin/dgvoice
/
dgvoice.go
209 lines (171 loc) · 5.12 KB
/
dgvoice.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/*******************************************************************************
* This is very experimental code and probably a long way from perfect or
* ideal. Please provide feed back on areas that would improve performance
*
*/
// **********************************************************************
// NOTE :: This is getting closer to a Opus<->PCM layer for Discordgo and will
// probably eventually move into a sub-folder of the Discordgo package.
// **********************************************************************
// Package dgvoice provides opus encoding and audio file playback for the
// Discordgo package.
package dgvoice
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"os/exec"
"strconv"
"sync"
"github.com/bwmarrin/discordgo"
"github.com/layeh/gopus"
)
// NOTE: This API is not final and these are likely to change.
// Technically the below settings can be adjusted however that poses
// a lot of other problems that are not handled well at this time.
// These below values seem to provide the best overall performance
const (
channels int = 2 // 1 for mono, 2 for stereo
frameRate int = 48000 // audio sampling rate
frameSize int = 960 // uint16 size of each audio frame
maxBytes int = (frameSize * 2) * 2 // max size of opus data
)
var (
speakers map[uint32]*gopus.Decoder
opusEncoder *gopus.Encoder
run *exec.Cmd
sendpcm bool
recvpcm bool
recv chan *discordgo.Packet
send chan []int16
mu sync.Mutex
)
// SendPCM will receive on the provied channel encode
// received PCM data into Opus then send that to Discordgo
func SendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16) {
// make sure this only runs one instance at a time.
mu.Lock()
if sendpcm || pcm == nil {
mu.Unlock()
return
}
sendpcm = true
mu.Unlock()
defer func() { sendpcm = false }()
var err error
opusEncoder, err = gopus.NewEncoder(frameRate, channels, gopus.Audio)
if err != nil {
fmt.Println("NewEncoder Error:", err)
return
}
for {
// read pcm from chan, exit if channel is closed.
recv, ok := <-pcm
if !ok {
fmt.Println("PCM Channel closed.")
return
}
// try encoding pcm frame with Opus
opus, err := opusEncoder.Encode(recv, frameSize, maxBytes)
if err != nil {
fmt.Println("Encoding Error:", err)
return
}
if v.Ready == false || v.OpusSend == nil {
fmt.Printf("Discordgo not ready for opus packets. %+v : %+v", v.Ready, v.OpusSend)
return
}
// send encoded opus data to the sendOpus channel
v.OpusSend <- opus
}
}
// ReceivePCM will receive on the the Discordgo OpusRecv channel and decode
// the opus audio into PCM then send it on the provided channel.
func ReceivePCM(v *discordgo.VoiceConnection, c chan *discordgo.Packet) {
// make sure this only runs one instance at a time.
mu.Lock()
if recvpcm || c == nil {
mu.Unlock()
return
}
recvpcm = true
mu.Unlock()
defer func() { sendpcm = false }()
var err error
for {
if v.Ready == false || v.OpusRecv == nil {
fmt.Printf("Discordgo not ready to receive opus packets. %+v : %+v", v.Ready, v.OpusSend)
return
}
p, ok := <-v.OpusRecv
if !ok {
return
}
if speakers == nil {
speakers = make(map[uint32]*gopus.Decoder)
}
_, ok = speakers[p.SSRC]
if !ok {
speakers[p.SSRC], err = gopus.NewDecoder(48000, 2)
if err != nil {
fmt.Println("error creating opus decoder:", err)
continue
}
}
p.PCM, err = speakers[p.SSRC].Decode(p.Opus, 960, false)
if err != nil {
fmt.Println("Error decoding opus data: ", err)
continue
}
c <- p
}
}
// PlayAudioFile will play the given filename to the already connected
// Discord voice server/channel. voice websocket and udp socket
// must already be setup before this will work.
func PlayAudioFile(v *discordgo.VoiceConnection, filename string) {
// Create a shell command "object" to run.
run = exec.Command("ffmpeg", "-i", filename, "-f", "s16le", "-ar", strconv.Itoa(frameRate), "-ac", strconv.Itoa(channels), "pipe:1")
ffmpegout, err := run.StdoutPipe()
if err != nil {
fmt.Println("StdoutPipe Error:", err)
return
}
ffmpegbuf := bufio.NewReaderSize(ffmpegout, 16384)
// Starts the ffmpeg command
err = run.Start()
if err != nil {
fmt.Println("RunStart Error:", err)
return
}
// Send "speaking" packet over the voice websocket
v.Speaking(true)
// Send not "speaking" packet over the websocket when we finish
defer v.Speaking(false)
// will actually only spawn one instance, a bit hacky.
if send == nil {
send = make(chan []int16, 2)
}
go SendPCM(v, send)
for {
// read data from ffmpeg stdout
audiobuf := make([]int16, frameSize*channels)
err = binary.Read(ffmpegbuf, binary.LittleEndian, &audiobuf)
if err == io.EOF || err == io.ErrUnexpectedEOF {
return
}
if err != nil {
fmt.Println("error reading from ffmpeg stdout :", err)
return
}
// Send received PCM to the sendPCM channel
send <- audiobuf
}
}
// KillPlayer forces the player to stop by killing the ffmpeg cmd process
// this method may be removed later in favor of using chans or bools to
// request a stop.
func KillPlayer() {
run.Process.Kill()
}