mirror of
https://github.com/mgerb/go-discord-bot
synced 2026-01-10 09:02:49 +00:00
switch opus library
This commit is contained in:
@@ -11,19 +11,17 @@ FROM golang:1.10.2-alpine3.7
|
|||||||
WORKDIR /go/src/github.com/mgerb/go-discord-bot/server
|
WORKDIR /go/src/github.com/mgerb/go-discord-bot/server
|
||||||
COPY --from=0 /home/dist /go/src/github.com/mgerb/go-discord-bot/dist
|
COPY --from=0 /home/dist /go/src/github.com/mgerb/go-discord-bot/dist
|
||||||
ADD ./server .
|
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/gobuffalo/packr/...
|
||||||
RUN go get -u github.com/golang/dep/cmd/dep
|
RUN go get -u github.com/golang/dep/cmd/dep
|
||||||
RUN dep ensure
|
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
|
RUN packr build -o /build/server
|
||||||
|
|
||||||
|
|
||||||
FROM wernight/youtube-dl
|
FROM wernight/youtube-dl
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
RUN apk add ca-certificates
|
RUN apk add ca-certificates opus-dev opusfile-dev
|
||||||
|
|
||||||
WORKDIR /bot
|
WORKDIR /bot
|
||||||
COPY --from=1 /build/server /
|
COPY --from=1 /build/server /
|
||||||
|
|||||||
4779
client/package-lock.json
generated
4779
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@
|
|||||||
"mini-css-extract-plugin": "^0.4.2",
|
"mini-css-extract-plugin": "^0.4.2",
|
||||||
"mobx": "^5.0.3",
|
"mobx": "^5.0.3",
|
||||||
"mobx-react": "^5.2.5",
|
"mobx-react": "^5.2.5",
|
||||||
"node-sass": "^4.9.0",
|
"node-sass": "^4.9.3",
|
||||||
"normalize.css": "^8.0.0",
|
"normalize.css": "^8.0.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"postcss-loader": "^2.1.5",
|
"postcss-loader": "^2.1.5",
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/bot
|
- ./:/bot
|
||||||
|
|||||||
2
makefile
2
makefile
@@ -1,5 +1,5 @@
|
|||||||
install:
|
install:
|
||||||
cd server && dep ensure
|
cd server && dep ensure && go get
|
||||||
cd client && npm install
|
cd client && npm install
|
||||||
|
|
||||||
build-server:
|
build-server:
|
||||||
|
|||||||
19
server/Gopkg.lock
generated
19
server/Gopkg.lock
generated
@@ -34,7 +34,7 @@
|
|||||||
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
|
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:9a20e959f4a4b52a79155282a67046fa49869993732ef6d5bc3c48b3e18692cc"
|
digest = "1:d5083934eb25e45d17f72ffa86cae3814f4a9d6c073c4f16b64147169b245606"
|
||||||
name = "github.com/gin-gonic/gin"
|
name = "github.com/gin-gonic/gin"
|
||||||
packages = [
|
packages = [
|
||||||
".",
|
".",
|
||||||
@@ -87,7 +87,15 @@
|
|||||||
version = "v1.4.0"
|
version = "v1.4.0"
|
||||||
|
|
||||||
[[projects]]
|
[[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"
|
name = "github.com/jinzhu/gorm"
|
||||||
packages = [
|
packages = [
|
||||||
".",
|
".",
|
||||||
@@ -211,7 +219,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:a6c91777916f37c288a9f2e352feb7567c3ff4c47a3880b391070740d3357e4f"
|
digest = "1:207d2e280e7a43845b12d07437538d22d02dbd4c274b49f3940bb3dce2ed70d7"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
packages = [
|
packages = [
|
||||||
"internal/subtle",
|
"internal/subtle",
|
||||||
@@ -225,7 +233,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:17a5cb5dbf1abd5193243c18a6d7edcad08fb01a3b75c96ed050623ce69efb16"
|
digest = "1:1a1ecfa7b54ca3f7a0115ab5c578d7d6a5d8b605839c549e80260468c42f8be7"
|
||||||
name = "golang.org/x/net"
|
name = "golang.org/x/net"
|
||||||
packages = [
|
packages = [
|
||||||
"html",
|
"html",
|
||||||
@@ -236,7 +244,7 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:19b2eb89a60dafcfe5c699fa21ef51143d9bc4825c155608f81e56e0a9a4bba4"
|
digest = "1:850d28ab022512e2cd3cf511a77f363c29e22689b4031f2050871f5de47ae4a0"
|
||||||
name = "golang.org/x/sys"
|
name = "golang.org/x/sys"
|
||||||
packages = [
|
packages = [
|
||||||
"unix",
|
"unix",
|
||||||
@@ -278,6 +286,7 @@
|
|||||||
"github.com/go-audio/audio",
|
"github.com/go-audio/audio",
|
||||||
"github.com/go-audio/wav",
|
"github.com/go-audio/wav",
|
||||||
"github.com/gobuffalo/packr",
|
"github.com/gobuffalo/packr",
|
||||||
|
"github.com/hraban/opus",
|
||||||
"github.com/jinzhu/gorm",
|
"github.com/jinzhu/gorm",
|
||||||
"github.com/jinzhu/gorm/dialects/sqlite",
|
"github.com/jinzhu/gorm/dialects/sqlite",
|
||||||
"github.com/rylio/ytdl",
|
"github.com/rylio/ytdl",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
# for detailed Gopkg.toml documentation.
|
# for detailed Gopkg.toml documentation.
|
||||||
#
|
#
|
||||||
# required = ["github.com/user/thing/cmd/thing"]
|
# required = ["github.com/user/thing/cmd/thing"]
|
||||||
ignored = ["layeh.com/gopus"]
|
|
||||||
#
|
#
|
||||||
# [[constraint]]
|
# [[constraint]]
|
||||||
# name = "github.com/user/project"
|
# name = "github.com/user/project"
|
||||||
|
|||||||
@@ -1,34 +1,25 @@
|
|||||||
package bothandlers
|
package bothandlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"layeh.com/gopus"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"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/config"
|
||||||
|
"github.com/mgerb/go-discord-bot/server/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
channels int = 2 // 1 for mono, 2 for stereo
|
channels int = 2 // 1 for mono, 2 for stereo
|
||||||
sampleRate int = 48000 // audio sampling rate - apparently a standard for opus
|
sampleRate int = 48000 // audio sampling rate - apparently a standard for opus
|
||||||
frameSize int = 960 // uint16 size of each audio frame
|
opusFrameSize int = 960 // at 48kHz the permitted values are 120, 240, 480, or 960
|
||||||
maxBytes int = (frameSize * 2) * 2 // max size of opus data
|
|
||||||
maxSoundQueue int = 10 // max amount of sounds that can be queued at one time
|
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
|
voiceClipQueuePacketSize int = 2000 // this packet size equates to roughly 40 seconds of audio
|
||||||
)
|
)
|
||||||
@@ -36,7 +27,6 @@ const (
|
|||||||
// ActiveConnections - current active bot connections
|
// ActiveConnections - current active bot connections
|
||||||
// store our connection objects in a map tied to a guild id
|
// store our connection objects in a map tied to a guild id
|
||||||
var ActiveConnections = make(map[string]*AudioConnection)
|
var ActiveConnections = make(map[string]*AudioConnection)
|
||||||
var speakers = make(map[uint32]*gopus.Decoder)
|
|
||||||
|
|
||||||
// AudioConnection -
|
// AudioConnection -
|
||||||
type AudioConnection struct {
|
type AudioConnection struct {
|
||||||
@@ -52,6 +42,7 @@ type AudioConnection struct {
|
|||||||
Mutex *sync.Mutex `json:"-"` // mutex for single audio connection
|
Mutex *sync.Mutex `json:"-"` // mutex for single audio connection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AudioClip -
|
||||||
type AudioClip struct {
|
type AudioClip struct {
|
||||||
Name string
|
Name string
|
||||||
Extension 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
|
// play a random sound clip
|
||||||
func (conn *AudioConnection) playRandomAudio(m *discordgo.MessageCreate) {
|
func (conn *AudioConnection) playRandomAudio(m *discordgo.MessageCreate) {
|
||||||
files, _ := ioutil.ReadDir(config.Config.SoundsPath)
|
files, _ := ioutil.ReadDir(config.Config.SoundsPath)
|
||||||
@@ -268,78 +256,25 @@ func (conn *AudioConnection) toggleSoundPlayingLock(playing bool) {
|
|||||||
// load audio file into memory
|
// load audio file into memory
|
||||||
func (conn *AudioConnection) loadFile(fileName string) error {
|
func (conn *AudioConnection) loadFile(fileName string) error {
|
||||||
|
|
||||||
// scan directory for file
|
extension, err := util.GetFileExtension(config.Config.SoundsPath, fileName)
|
||||||
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()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ffmpegbuf := bufio.NewReaderSize(ffmpegout, 16348)
|
opusData, err := util.GetFileOpusData(path.Join(config.Config.SoundsPath, fileName+extension), channels, opusFrameSize, sampleRate)
|
||||||
|
|
||||||
err = cmd.Start()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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{
|
conn.Sounds[fileName] = &AudioClip{
|
||||||
Content: make([][]byte, 0),
|
Content: opusData,
|
||||||
Name: fileName,
|
Name: fileName,
|
||||||
Extension: fextension,
|
Extension: extension,
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *AudioConnection) clipAudio(m *discordgo.MessageCreate) {
|
func (conn *AudioConnection) clipAudio(m *discordgo.MessageCreate) {
|
||||||
@@ -367,7 +302,13 @@ loop:
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case p := <-packets:
|
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:
|
default:
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
@@ -378,54 +319,11 @@ loop:
|
|||||||
timestamp := time.Now().UTC().Format("2006-01-02") + "-" + strconv.Itoa(int(time.Now().Unix()))
|
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"
|
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 {
|
if err != nil {
|
||||||
log.Error(err)
|
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)
|
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:
|
loop:
|
||||||
for {
|
for {
|
||||||
|
|
||||||
@@ -448,23 +360,6 @@ loop:
|
|||||||
continue
|
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 channel is full trim off from beginning
|
||||||
if len(conn.VoiceClipQueue) == cap(conn.VoiceClipQueue) {
|
if len(conn.VoiceClipQueue) == cap(conn.VoiceClipQueue) {
|
||||||
<-conn.VoiceClipQueue
|
<-conn.VoiceClipQueue
|
||||||
@@ -474,12 +369,9 @@ loop:
|
|||||||
conn.VoiceClipQueue <- opusChannel
|
conn.VoiceClipQueue <- opusChannel
|
||||||
|
|
||||||
// check if voice connection fails then break out of audio listener
|
// check if voice connection fails then break out of audio listener
|
||||||
default:
|
case <-exitChan:
|
||||||
if !conn.VoiceConnection.Ready {
|
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second * 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove lock upon exit
|
// remove lock upon exit
|
||||||
|
|||||||
173
server/util/audio.go
Normal file
173
server/util/audio.go
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user