diff --git a/cmd/komputer/main.go b/cmd/komputer/main.go index cadaf38..3df9cca 100644 --- a/cmd/komputer/main.go +++ b/cmd/komputer/main.go @@ -61,7 +61,6 @@ func init() { }) } -// TODO Export registration commends to CLI tool func init() { checkEnvVariables("APPLICATION_ID", "SERVER_GUID") diff --git a/go.mod b/go.mod index caf6830..2f9de4b 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( ) require ( - github.com/bwmarrin/dgvoice v0.0.0-20210225172318-caaac756e02e // indirect github.com/golang/snappy v0.0.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect diff --git a/internal/assets/audio.go b/internal/assets/audio.go index bba0117..110dad3 100644 --- a/internal/assets/audio.go +++ b/internal/assets/audio.go @@ -24,7 +24,7 @@ func GetAudioPaths(filename string) ([]string, error) { return nil, err } - var paths = make([]string, len(ls)) + var paths []string for _, d := range ls { fName := d.Name() diff --git a/internal/command/spock.go b/internal/command/spock.go index 1a9b111..ff7b27a 100644 --- a/internal/command/spock.go +++ b/internal/command/spock.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/bwmarrin/dgvoice" "github.com/bwmarrin/discordgo" "github.com/wittano/komputer/internal/assets" "github.com/wittano/komputer/internal/interaction" @@ -58,9 +57,10 @@ func execSpookSpeak(ctx context.Context, s *discordgo.Session, i *discordgo.Inte ch := make(chan bool) SpockMusicStopCh[i.GuildID] = ch - // TODO Rewrite this function cause ffmpeg generate zombie process - // TODO Add multi-server playing same song - dgvoice.PlayAudioFile(voiceJoin, path[rand.Int()%len(path)], ch) + songPath := path[rand.Int()%len(path)] + if err = voice.PlayAudio(voiceJoin, songPath, ch); err != nil { + log.Error(ctx, fmt.Sprintf("Failed play '%s' audio", songPath), err) + } voiceJoin.Disconnect() }() diff --git a/internal/voice/audio.go b/internal/voice/audio.go new file mode 100644 index 0000000..7068348 --- /dev/null +++ b/internal/voice/audio.go @@ -0,0 +1,126 @@ +package voice + +import ( + "bufio" + "encoding/binary" + "errors" + "github.com/bwmarrin/discordgo" + "io" + "layeh.com/gopus" + "os/exec" + "strconv" + "syscall" +) + +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 = (frameSize * 2) * 2 // max size of opus data +) + +func PlayAudio(vc *discordgo.VoiceConnection, path string, stop <-chan bool) (err error) { + cmd := exec.Command("ffmpeg", "-i", path, "-f", "s16le", "-ar", strconv.Itoa(frameRate), "-ac", strconv.Itoa(channels), "pipe:1") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + + ffmpegout, err := cmd.StdoutPipe() + if err != nil { + return + } + + ffmpegbuf := bufio.NewReaderSize(ffmpegout, 16384) + + // Starts the ffmpeg command + err = cmd.Start() + if err != nil { + return + } + + defer cmd.Wait() + + //when stop is sent, kill ffmpeg + go func() { + <-stop + cmd.Process.Kill() + }() + + // Send "speaking" packet over the voice websocket + err = vc.Speaking(true) + if err != nil { + return + } + + // Send not "speaking" packet over the websocket when we finish + defer func() { + err := vc.Speaking(false) + if err != nil { + return + } + }() + + send := make(chan []int16, 2) + defer close(send) + + stopPlaying := make(chan bool) + go func() { + sendPCM(vc, send) + stopPlaying <- 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 { + return + } + + // Send received PCM to the sendPCM channel + select { + case send <- audiobuf: + case <-stopPlaying: + return + } + } +} + +func sendPCM(v *discordgo.VoiceConnection, pcm chan []int16) (err error) { + if pcm == nil { + return nil + } + + opusEncoder, err := gopus.NewEncoder(frameRate, channels, gopus.Audio) + + if err != nil { + return + } + + for { + + // read pcm from chan, exit if channel is closed. + recv, ok := <-pcm + if !ok { + return errors.New("PCM Channel closed") + } + + // try encoding pcm frame with Opus + opus, err := opusEncoder.Encode(recv, frameSize, maxBytes) + if err != nil { + return err + } + + if v.Ready == false || v.OpusSend == nil { + // Sending errors here might not be suited + return nil + } + // send encoded opus data to the sendOpus channel + v.OpusSend <- opus + } + + return +}