1
0
mirror of https://github.com/mgerb/go-discord-bot synced 2026-01-09 08:32:48 +00:00

switch opus library

This commit is contained in:
2018-10-14 15:12:35 -05:00
parent 520ecb1ab7
commit 24db31ecd3
9 changed files with 2574 additions and 2600 deletions

View File

@@ -11,19 +11,17 @@ FROM golang:1.10.2-alpine3.7
WORKDIR /go/src/github.com/mgerb/go-discord-bot/server
COPY --from=0 /home/dist /go/src/github.com/mgerb/go-discord-bot/dist
ADD ./server .
RUN apk add --no-cache git alpine-sdk
RUN apk add --no-cache git alpine-sdk pkgconfig opus-dev opusfile-dev
RUN go get -u github.com/gobuffalo/packr/...
RUN go get -u github.com/golang/dep/cmd/dep
RUN dep ensure
# need to manually get this dependency because go dep doesn't work well with the C bindings
RUN go get layeh.com/gopus
RUN packr build -o /build/server
FROM wernight/youtube-dl
RUN apk update
RUN apk add ca-certificates
RUN apk add ca-certificates opus-dev opusfile-dev
WORKDIR /bot
COPY --from=1 /build/server /

4785
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@
"mini-css-extract-plugin": "^0.4.2",
"mobx": "^5.0.3",
"mobx-react": "^5.2.5",
"node-sass": "^4.9.0",
"node-sass": "^4.9.3",
"normalize.css": "^8.0.0",
"nprogress": "^0.2.0",
"postcss-loader": "^2.1.5",

View File

@@ -7,4 +7,4 @@ services:
ports:
- 8080:8080
volumes:
- ./data:/bot
- ./:/bot

View File

@@ -1,5 +1,5 @@
install:
cd server && dep ensure
cd server && dep ensure && go get
cd client && npm install
build-server:

19
server/Gopkg.lock generated
View File

@@ -34,7 +34,7 @@
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
[[projects]]
digest = "1:9a20e959f4a4b52a79155282a67046fa49869993732ef6d5bc3c48b3e18692cc"
digest = "1:d5083934eb25e45d17f72ffa86cae3814f4a9d6c073c4f16b64147169b245606"
name = "github.com/gin-gonic/gin"
packages = [
".",
@@ -87,7 +87,15 @@
version = "v1.4.0"
[[projects]]
digest = "1:111ff38c0376c091da851d34ad991fbce6a679a8b53fcc5ebd61b52f07ed197d"
branch = "v2"
digest = "1:92ac5796b86f454e9b8a9f0f2ceba033747aa8d8fe1fda89b9ec6d6176a431b6"
name = "github.com/hraban/opus"
packages = ["."]
pruneopts = "UT"
revision = "0f2e0b4fc6cd5710fddbb74ba2e5e02c1c1bc22b"
[[projects]]
digest = "1:2a21d36a5ab33e6e4ce82b3c7ba45c2330ca6e9af247382475e056b06395b8a9"
name = "github.com/jinzhu/gorm"
packages = [
".",
@@ -211,7 +219,7 @@
[[projects]]
branch = "master"
digest = "1:a6c91777916f37c288a9f2e352feb7567c3ff4c47a3880b391070740d3357e4f"
digest = "1:207d2e280e7a43845b12d07437538d22d02dbd4c274b49f3940bb3dce2ed70d7"
name = "golang.org/x/crypto"
packages = [
"internal/subtle",
@@ -225,7 +233,7 @@
[[projects]]
branch = "master"
digest = "1:17a5cb5dbf1abd5193243c18a6d7edcad08fb01a3b75c96ed050623ce69efb16"
digest = "1:1a1ecfa7b54ca3f7a0115ab5c578d7d6a5d8b605839c549e80260468c42f8be7"
name = "golang.org/x/net"
packages = [
"html",
@@ -236,7 +244,7 @@
[[projects]]
branch = "master"
digest = "1:19b2eb89a60dafcfe5c699fa21ef51143d9bc4825c155608f81e56e0a9a4bba4"
digest = "1:850d28ab022512e2cd3cf511a77f363c29e22689b4031f2050871f5de47ae4a0"
name = "golang.org/x/sys"
packages = [
"unix",
@@ -278,6 +286,7 @@
"github.com/go-audio/audio",
"github.com/go-audio/wav",
"github.com/gobuffalo/packr",
"github.com/hraban/opus",
"github.com/jinzhu/gorm",
"github.com/jinzhu/gorm/dialects/sqlite",
"github.com/rylio/ytdl",

View File

@@ -4,7 +4,6 @@
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
ignored = ["layeh.com/gopus"]
#
# [[constraint]]
# name = "github.com/user/project"

View File

@@ -1,42 +1,32 @@
package bothandlers
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"io"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
"time"
"layeh.com/gopus"
"github.com/bwmarrin/discordgo"
"github.com/go-audio/audio"
"github.com/go-audio/wav"
"github.com/mgerb/go-discord-bot/server/config"
"github.com/mgerb/go-discord-bot/server/util"
log "github.com/sirupsen/logrus"
)
const (
channels int = 2 // 1 for mono, 2 for stereo
sampleRate int = 48000 // audio sampling rate - apparently a standard for opus
frameSize int = 960 // uint16 size of each audio frame
maxBytes int = (frameSize * 2) * 2 // max size of opus data
maxSoundQueue int = 10 // max amount of sounds that can be queued at one time
voiceClipQueuePacketSize int = 2000 // this packet size equates to roughly 40 seconds of audio
channels int = 2 // 1 for mono, 2 for stereo
sampleRate int = 48000 // audio sampling rate - apparently a standard for opus
opusFrameSize int = 960 // at 48kHz the permitted values are 120, 240, 480, or 960
maxSoundQueue int = 10 // max amount of sounds that can be queued at one time
voiceClipQueuePacketSize int = 2000 // this packet size equates to roughly 40 seconds of audio
)
// ActiveConnections - current active bot connections
// store our connection objects in a map tied to a guild id
var ActiveConnections = make(map[string]*AudioConnection)
var speakers = make(map[uint32]*gopus.Decoder)
// AudioConnection -
type AudioConnection struct {
@@ -52,6 +42,7 @@ type AudioConnection struct {
Mutex *sync.Mutex `json:"-"` // mutex for single audio connection
}
// AudioClip -
type AudioClip struct {
Name string
Extension string
@@ -175,9 +166,6 @@ func (conn *AudioConnection) summon(m *discordgo.MessageCreate) {
}
}
func (conn *AudioConnection) queueAudio(soundName string) {
}
// play a random sound clip
func (conn *AudioConnection) playRandomAudio(m *discordgo.MessageCreate) {
files, _ := ioutil.ReadDir(config.Config.SoundsPath)
@@ -268,78 +256,25 @@ func (conn *AudioConnection) toggleSoundPlayingLock(playing bool) {
// load audio file into memory
func (conn *AudioConnection) loadFile(fileName string) error {
// scan directory for file
files, _ := ioutil.ReadDir(config.Config.SoundsPath)
var fextension string
var fname string
for _, f := range files {
fname = strings.Split(f.Name(), ".")[0]
fextension = "." + strings.Split(f.Name(), ".")[1]
if fname == fileName {
break
}
fname = ""
}
if fname == "" {
return errors.New("File not found")
}
log.Debug("Loading file: " + fname + fextension)
// use ffmpeg to convert file into a format we can use
cmd := exec.Command("ffmpeg", "-i", config.Config.SoundsPath+"/"+fname+fextension, "-f", "s16le", "-ar", strconv.Itoa(sampleRate), "-ac", strconv.Itoa(channels), "pipe:1")
ffmpegout, err := cmd.StdoutPipe()
extension, err := util.GetFileExtension(config.Config.SoundsPath, fileName)
if err != nil {
return err
}
ffmpegbuf := bufio.NewReaderSize(ffmpegout, 16348)
err = cmd.Start()
opusData, err := util.GetFileOpusData(path.Join(config.Config.SoundsPath, fileName+extension), channels, opusFrameSize, sampleRate)
if err != nil {
return err
}
// crate encoder to convert audio to opus codec
opusEncoder, err := gopus.NewEncoder(sampleRate, channels, gopus.Audio)
if err != nil {
return errors.New("NewEncoder error.")
}
conn.Sounds[fileName] = &AudioClip{
Content: make([][]byte, 0),
Content: opusData,
Name: fileName,
Extension: fextension,
}
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 nil
}
if err != nil {
return errors.New("Error reading from ffmpeg stdout.")
}
// convert audio to opus codec
opus, err := opusEncoder.Encode(audiobuf, frameSize, maxBytes)
if err != nil {
return errors.New("Encoding error.")
}
// append sound bytes to the content for this audio file
conn.Sounds[fileName].Content = append(conn.Sounds[fileName].Content, opus)
Extension: extension,
}
return nil
}
func (conn *AudioConnection) clipAudio(m *discordgo.MessageCreate) {
@@ -367,7 +302,13 @@ loop:
for {
select {
case p := <-packets:
pcmOut[p.SSRC] = append(pcmOut[p.SSRC], p.PCM...)
// convert opus to pcm
pcm, err := util.OpusToPCM(p.Opus, sampleRate, channels)
if err != nil {
log.Error(err)
return
}
pcmOut[p.SSRC] = append(pcmOut[p.SSRC], pcm...)
default:
break loop
}
@@ -378,54 +319,11 @@ loop:
timestamp := time.Now().UTC().Format("2006-01-02") + "-" + strconv.Itoa(int(time.Now().Unix()))
filename := config.Config.ClipsPath + "/" + timestamp + "-" + strconv.Itoa(int(key)) + "-" + username + ".wav"
out, err := os.Create(filename)
err := util.SavePCMToWavFile(pcmData, filename, sampleRate, channels)
if err != nil {
log.Error(err)
continue
}
// 8 kHz, 16 bit, 2 channel, WAV.
e := wav.NewEncoder(out, sampleRate, 16, channels, 1)
output := new(bytes.Buffer)
binary.Write(output, binary.LittleEndian, pcmData)
newReader := bytes.NewReader(output.Bytes())
// Create new audio.IntBuffer.
audioBuf, err := newAudioIntBuffer(newReader)
// Write buffer to output file. This writes a RIFF header and the PCM chunks from the audio.IntBuffer.
if err := e.Write(audioBuf); err != nil {
log.Error(err)
out.Close()
continue
}
if err := e.Close(); err != nil {
log.Error(err)
}
out.Close()
}
}
func newAudioIntBuffer(r io.Reader) (*audio.IntBuffer, error) {
buf := &audio.IntBuffer{
Format: &audio.Format{
NumChannels: 1,
SampleRate: 8000,
},
}
for {
var sample int16
err := binary.Read(r, binary.LittleEndian, &sample)
switch {
case err == io.EOF:
return buf, nil
case err != nil:
return nil, err
}
buf.Data = append(buf.Data, int(sample))
}
}
@@ -438,6 +336,20 @@ func (conn *AudioConnection) startAudioListener() {
conn.VoiceClipQueue = make(chan *discordgo.Packet, voiceClipQueuePacketSize)
}
// create new channel to watch for voice connection
// when voice connection is not ready the loop will exit
exitChan := make(chan bool)
go func() {
for {
if !conn.VoiceConnection.Ready {
exitChan <- true
break
}
time.Sleep(time.Second * 1)
}
}()
loop:
for {
@@ -448,23 +360,6 @@ loop:
continue
}
var err error
_, ok = speakers[opusChannel.SSRC]
if !ok {
speakers[opusChannel.SSRC], err = gopus.NewDecoder(sampleRate, channels)
if err != nil {
log.Error("error creating opus decoder", err)
continue
}
}
opusChannel.PCM, err = speakers[opusChannel.SSRC].Decode(opusChannel.Opus, frameSize, false)
if err != nil {
log.Error("Error decoding opus data", err)
continue
}
// if channel is full trim off from beginning
if len(conn.VoiceClipQueue) == cap(conn.VoiceClipQueue) {
<-conn.VoiceClipQueue
@@ -474,11 +369,8 @@ loop:
conn.VoiceClipQueue <- opusChannel
// check if voice connection fails then break out of audio listener
default:
if !conn.VoiceConnection.Ready {
break loop
}
time.Sleep(time.Second * 1)
case <-exitChan:
break loop
}
}

173
server/util/audio.go Normal file
View File

@@ -0,0 +1,173 @@
package util
import (
"bufio"
"bytes"
"encoding/binary"
"errors"
"io"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"github.com/go-audio/audio"
"github.com/go-audio/wav"
"github.com/hraban/opus"
)
// GetFileOpusData - uses ffmpeg to convert any audio
// file to opus data ready to send to discord
func GetFileOpusData(filePath string, channels, opusFrameSize, sampleRate int) ([][]byte, error) {
cmd := exec.Command("ffmpeg", "-i", filePath, "-f", "s16le", "-ar", strconv.Itoa(sampleRate), "-ac", strconv.Itoa(channels), "pipe:1")
cmdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
pcmdata := bufio.NewReader(cmdout)
err = cmd.Start()
if err != nil {
return nil, err
}
// crate encoder to convert audio to opus codec
opusEncoder, err := opus.NewEncoder(sampleRate, channels, opus.AppVoIP)
if err != nil {
return nil, errors.New("new opus encoder error")
}
opusOutput := make([][]byte, 0)
for {
// read pcm data from ffmpeg stdout
audiobuf := make([]int16, opusFrameSize*channels)
err = binary.Read(pcmdata, binary.LittleEndian, &audiobuf)
if err == io.EOF || err == io.ErrUnexpectedEOF {
return opusOutput, nil
}
if err != nil {
return nil, errors.New("error reading from ffmpeg stdout")
}
// convert raw pcm to opus
opus := make([]byte, 1000)
n, err := opusEncoder.Encode(audiobuf, opus)
if err != nil {
return nil, errors.New("encoding error")
}
// append bytes to output
opusOutput = append(opusOutput, opus[:n])
}
}
// GetFileExtension -
// scan directory for filename and return first extension found for that name
func GetFileExtension(path, fileName string) (string, error) {
files, _ := ioutil.ReadDir(path)
var fextension string
var fname string
for _, f := range files {
fname = strings.Split(f.Name(), ".")[0]
fextension = "." + strings.Split(f.Name(), ".")[1]
if fname == fileName {
return fextension, nil
}
}
return "", errors.New("file not found")
}
// cache the opusDecoder so we don't have to make a new one every time
// was causing audio issues creating a new instance of this every time
var opusDecoder *opus.Decoder
// OpusToPCM - convert opus to pcm
func OpusToPCM(data []byte, sampleRate, channels int) ([]int16, error) {
if opusDecoder == nil {
var err error
opusDecoder, err = opus.NewDecoder(sampleRate, channels)
if err != nil {
return []int16{}, err
}
}
// create pcm list with more than enough space
pcm := make([]int16, 10000)
n, err := opusDecoder.Decode(data, pcm)
if err != nil {
return []int16{}, err
}
// trim the remaining space
pcm = pcm[:n*channels]
return pcm, nil
}
// SavePCMToWavFile - save pcm data to wav file
func SavePCMToWavFile(data []int16, filename string, sampleRate, channels int) error {
out, err := os.Create(filename)
if err != nil {
return err
}
defer out.Close()
// 48 kHz, 16 bit, 2 channel, WAV.
e := wav.NewEncoder(out, sampleRate, 16, channels, 1)
output := new(bytes.Buffer)
binary.Write(output, binary.LittleEndian, data)
newReader := bytes.NewReader(output.Bytes())
// Create new audio.IntBuffer.
audioBuf, err := newAudioIntBuffer(newReader, sampleRate, channels)
if err != nil {
return err
}
// Write buffer to output file. This writes a RIFF header and the PCM chunks from the audio.IntBuffer.
if err := e.Write(audioBuf); err != nil {
return err
}
err = e.Close()
return err
}
func newAudioIntBuffer(r io.Reader, sampleRate, channels int) (*audio.IntBuffer, error) {
buf := &audio.IntBuffer{
Format: &audio.Format{
NumChannels: channels,
SampleRate: sampleRate,
},
}
for {
var sample int16
err := binary.Read(r, binary.LittleEndian, &sample)
switch {
case err == io.EOF:
return buf, nil
case err != nil:
return nil, err
}
buf.Data = append(buf.Data, int(sample))
}
}