-
Notifications
You must be signed in to change notification settings - Fork 39
/
Copy pathdgvoice.go
213 lines (177 loc) · 4.89 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
210
211
212
213
/*******************************************************************************
* This is very experimental code and probably a long way from perfect or
* ideal. Please provide feed back on areas that would improve performance
*
*/
// Package dgvoice provides opus encoding and audio file playback for the
// Discordgo package.
package dgvoice
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"sync"
"github.com/bwmarrin/discordgo"
"layeh.com/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
mu sync.Mutex
)
// OnError gets called by dgvoice when an error is encountered.
// By default logs to STDERR
var OnError = func(str string, err error) {
prefix := "dgVoice: " + str
if err != nil {
os.Stderr.WriteString(prefix + ": " + err.Error())
} else {
os.Stderr.WriteString(prefix)
}
}
// 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) {
if pcm == nil {
return
}
var err error
opusEncoder, err = gopus.NewEncoder(frameRate, channels, gopus.Audio)
if err != nil {
OnError("NewEncoder Error", err)
return
}
for {
// read pcm from chan, exit if channel is closed.
recv, ok := <-pcm
if !ok {
OnError("PCM Channel closed", nil)
return
}
// try encoding pcm frame with Opus
opus, err := opusEncoder.Encode(recv, frameSize, maxBytes)
if err != nil {
OnError("Encoding Error", err)
return
}
if v.Ready == false || v.OpusSend == nil {
// OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", v.Ready, v.OpusSend), nil)
// Sending errors here might not be suited
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) {
if c == nil {
return
}
var err error
for {
if v.Ready == false || v.OpusRecv == nil {
OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", v.Ready, v.OpusSend), nil)
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 {
OnError("error creating opus decoder", err)
continue
}
}
p.PCM, err = speakers[p.SSRC].Decode(p.Opus, 960, false)
if err != nil {
OnError("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, stop <-chan bool) {
// 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 {
OnError("StdoutPipe Error", err)
return
}
ffmpegbuf := bufio.NewReaderSize(ffmpegout, 16384)
// Starts the ffmpeg command
err = run.Start()
if err != nil {
OnError("RunStart Error", err)
return
}
// prevent memory leak from residual ffmpeg streams
defer run.Process.Kill()
//when stop is sent, kill ffmpeg
go func() {
<-stop
err = run.Process.Kill()
}()
// Send "speaking" packet over the voice websocket
err = v.Speaking(true)
if err != nil {
OnError("Couldn't set speaking", err)
}
// Send not "speaking" packet over the websocket when we finish
defer func() {
err := v.Speaking(false)
if err != nil {
OnError("Couldn't stop speaking", err)
}
}()
send := make(chan []int16, 2)
defer close(send)
close := make(chan bool)
go func() {
SendPCM(v, send)
close <- true
}()
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 {
OnError("error reading from ffmpeg stdout", err)
return
}
// Send received PCM to the sendPCM channel
select {
case send <- audiobuf:
case <-close:
return
}
}
}