From 442bcda4f90ad05b29a05cfcb67659799155ed2b Mon Sep 17 00:00:00 2001 From: Mitchell Gerber Date: Wed, 4 Oct 2017 20:05:32 -0500 Subject: [PATCH] audio clipping completed - back end --- .gitignore | 1 + server/bothandlers/sounds.go | 196 +++++++++++++------------ server/webserver/handlers/soundlist.go | 2 - 3 files changed, 100 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 07f7dd0..364b22f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ yarn-error* vendor bot sounds +clips debug youtube go-discord-bot diff --git a/server/bothandlers/sounds.go b/server/bothandlers/sounds.go index 87c8027..368c09a 100644 --- a/server/bothandlers/sounds.go +++ b/server/bothandlers/sounds.go @@ -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. diff --git a/server/webserver/handlers/soundlist.go b/server/webserver/handlers/soundlist.go index beb46fe..80a2521 100644 --- a/server/webserver/handlers/soundlist.go +++ b/server/webserver/handlers/soundlist.go @@ -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{}