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

audio clipping completed - back end

This commit is contained in:
2017-10-04 20:05:32 -05:00
parent b2d1e34a97
commit 442bcda4f9
3 changed files with 100 additions and 99 deletions

View File

@@ -4,7 +4,6 @@ import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
@@ -24,28 +23,30 @@ import (
const (
channels int = 2 // 1 for mono, 2 for stereo
frameRate int = 48000 // audio sampling rate
frameRate 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
// TODO: figure out why api isn't returning bitrate
bitRate int = 64000
sampleRate int = 96000
sampleRate int = 96000 // rate at which wav writer need to make audio up to speed
voiceClipQueuePacketSize int = 2000 // this packet size equates to roughly 40 seconds of audio
voiceClipDirectory string = "./clips"
)
// store our connection objects in a map tied to a guild id
var activeConnections = make(map[string]*audioConnection)
type audioConnection struct {
guild *discordgo.Guild
session *discordgo.Session
sounds map[string]*audioClip
soundQueue chan string
voiceConnection *discordgo.VoiceConnection
currentChannel *discordgo.Channel
soundPlayingLock bool
mutex *sync.Mutex // mutex for single audio connection
guild *discordgo.Guild
session *discordgo.Session
voiceConnection *discordgo.VoiceConnection
currentChannel *discordgo.Channel
sounds map[string]*audioClip
soundQueue chan string
voiceClipQueue chan *discordgo.Packet
soundPlayingLock bool
audioListenerLock bool
mutex *sync.Mutex // mutex for single audio connection
}
type audioClip struct {
@@ -61,7 +62,7 @@ func SoundsHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
c, err := s.State.Channel(m.ChannelID)
if err != nil {
// Could not find channel.
fmt.Println("Unable to find channel.")
log.Println("Unable to find channel.")
return
}
@@ -77,11 +78,12 @@ func SoundsHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
// create new connection instance
newInstance := &audioConnection{
guild: newGuild,
session: s,
sounds: make(map[string]*audioClip, 0),
soundQueue: make(chan string, maxSoundQueue),
mutex: &sync.Mutex{},
guild: newGuild,
session: s,
sounds: make(map[string]*audioClip, 0),
soundQueue: make(chan string, maxSoundQueue),
mutex: &sync.Mutex{},
audioListenerLock: false,
}
activeConnections[c.GuildID] = newInstance
@@ -110,7 +112,7 @@ func (conn *audioConnection) handleMessage(m *discordgo.MessageCreate) {
conn.dismiss()
case "clip":
conn.clipAudio()
conn.clipAudio(m)
default:
conn.playAudio(command, m)
@@ -118,22 +120,26 @@ func (conn *audioConnection) handleMessage(m *discordgo.MessageCreate) {
}
}
// dismiss bot from currnet channel if it's in one
func (conn *audioConnection) dismiss() {
if conn.voiceConnection != nil && !conn.soundPlayingLock && len(conn.soundQueue) == 0 {
conn.voiceConnection.Disconnect()
}
}
// summon bot to channel that user is currently in
func (conn *audioConnection) summon(m *discordgo.MessageCreate) {
// Join the channel the user issued the command from if not in it
if conn.voiceConnection == nil || conn.voiceConnection.ChannelID != m.ChannelID {
var err error
// Find the channel that the message came from.
c, err := conn.session.State.Channel(m.ChannelID)
if err != nil {
// Could not find channel.
fmt.Println("User channel not found.")
log.Println("User channel not found.")
return
}
@@ -144,7 +150,7 @@ func (conn *audioConnection) summon(m *discordgo.MessageCreate) {
return
}
// Look for the message sender in that guilds current voice states.
// Look for the message sender in that guilds current voice states
for _, vs := range g.VoiceStates {
if vs.UserID == m.Author.ID {
@@ -157,8 +163,11 @@ func (conn *audioConnection) summon(m *discordgo.MessageCreate) {
// set the current channel
conn.currentChannel = c
// start listening to audio after joining channel
go conn.startAudioListener()
// start listening to audio if not locked
if !conn.audioListenerLock {
go conn.startAudioListener()
conn.audioListenerLock = true
}
return
}
@@ -167,6 +176,7 @@ func (conn *audioConnection) summon(m *discordgo.MessageCreate) {
}
}
// play audio in channel that user is in
func (conn *audioConnection) playAudio(soundName string, m *discordgo.MessageCreate) {
// check if sound exists in memory
@@ -175,7 +185,7 @@ func (conn *audioConnection) playAudio(soundName string, m *discordgo.MessageCre
err := conn.loadFile(soundName)
if err != nil {
fmt.Println(err)
log.Println(err)
return
}
}
@@ -193,7 +203,7 @@ func (conn *audioConnection) playAudio(soundName string, m *discordgo.MessageCre
}
// load dca file into memory
// load audio file into memory
func (conn *audioConnection) loadFile(fileName string) error {
// scan directory for file
@@ -215,7 +225,7 @@ func (conn *audioConnection) loadFile(fileName string) error {
return errors.New("File not found")
}
fmt.Println("Loading file: " + fname + fextension)
log.Println("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(frameRate), "-ac", strconv.Itoa(channels), "pipe:1")
@@ -270,20 +280,70 @@ func (conn *audioConnection) loadFile(fileName string) error {
}
func (conn *audioConnection) clipAudio() {
func (conn *audioConnection) clipAudio(m *discordgo.MessageCreate) {
if len(conn.voiceClipQueue) < 10 {
conn.session.ChannelMessageSend(m.ChannelID, "Clip failed.")
} else {
writePacketsToFile(m.Author.Username, conn.voiceClipQueue)
conn.session.ChannelMessageSend(m.ChannelID, "Sound clipped!")
}
}
func writePacketsToFile(username string, packets chan *discordgo.Packet) {
// create clips folder if it does not exist
if _, err := os.Stat(voiceClipDirectory); os.IsNotExist(err) {
os.Mkdir(voiceClipDirectory, os.ModePerm)
}
// construct filename
timestamp := time.Now().UTC().Format("2006-01-02") + "-" + strconv.Itoa(int(time.Now().Unix()))
wavOut, err := os.Create(voiceClipDirectory + "/" + timestamp + "-" + username + ".wav")
checkErr(err)
defer wavOut.Close()
meta := wav.File{
Channels: 1,
SampleRate: uint32(sampleRate),
SignificantBits: 16,
}
writer, err := meta.NewWriter(wavOut)
checkErr(err)
defer writer.Close()
// grab everything from the voice packet channel and dump it to the file
// close when there is nothing left
loop:
for {
select {
case p := <-packets:
for _, pcm := range p.PCM {
err := writer.WriteInt32(int32(pcm))
checkErr(err)
}
default:
break loop
}
}
}
// start listening to the voice channel
func (conn *audioConnection) startAudioListener() {
if conn.voiceClipQueue == nil {
conn.voiceClipQueue = make(chan *discordgo.Packet, voiceClipQueuePacketSize)
}
speakers := make(map[uint32]*gopus.Decoder)
voicePackets := []*discordgo.Packet{}
var err error
loop:
for {
select {
// grab incomming audio
case opusChannel, ok := <-conn.voiceConnection.OpusRecv:
if !ok {
continue
@@ -305,83 +365,25 @@ loop:
continue
}
voicePackets = append(voicePackets, opusChannel)
// TODO:
fmt.Println(len(voicePackets))
fmt.Println(int(opusChannel.Timestamp) / bitRate)
if len(voicePackets) > bitRate*5 {
voicePackets = voicePackets[1:]
// if channel is full trim off from beginning
if len(conn.voiceClipQueue) == cap(conn.voiceClipQueue) {
<-conn.voiceClipQueue
}
// add current packet to channel queue
conn.voiceClipQueue <- opusChannel
// check if voice connection fails then break out of audio listener
default:
if !conn.voiceConnection.Ready {
break loop
}
// cleanOldPackets(voicePackets)
}
}
writeWavFile(voicePackets)
}
// // remove packets that are more than 1 minute old
// func cleanOldPackets(packets []*discordgo.Packet) {
// if len(packets) < 1 {
// return
// }
// // latest timestamp divided by bitrate
// currentTime := int(packets[len(packets)-1].Timestamp) / bitRate
// index := 0
// // grab the index of the first object less than a minute ago
// for i, p := range packets {
// timestamp := int(p.Timestamp) / bitRate
// if timestamp < currentTime-5 {
// index = i
// break
// } else if timestamp > currentTime-5 {
// break
// }
// }
// if index != 0 {
// log.Println("cleaning")
// log.Println(index)
// }
// packets = packets[index:]
// }
func writeWavFile(packets []*discordgo.Packet) {
wavOut, err := os.Create("test.wav")
checkErr(err)
defer wavOut.Close()
meta := wav.File{
Channels: 1,
SampleRate: uint32(sampleRate),
SignificantBits: 16,
}
writer, err := meta.NewWriter(wavOut)
checkErr(err)
defer writer.Close()
for _, p := range packets {
for _, pcm := range p.PCM {
err := writer.WriteInt32(int32(pcm))
checkErr(err)
}
}
// remove lock upon exit
conn.audioListenerLock = false
}
// playSounds - plays the current buffer to the provided channel.

View File

@@ -1,7 +1,6 @@
package handlers
import (
"fmt"
"io/ioutil"
"strings"
@@ -35,7 +34,6 @@ func SoundList(w http.ResponseWriter, r *http.Request) {
// PopulateSoundList -
func PopulateSoundList() error {
fmt.Println("Populating sound list.")
soundList = []sound{}