add configurable buffer for mumble -> discord stream fixes #20

add silence to -> discord streams as suggested by disocrd docs closes #11
This commit is contained in:
Tyler Stiene 2021-04-10 15:00:13 -04:00
parent dcc3fd48b4
commit 026cdff797
4 changed files with 100 additions and 57 deletions

View File

@ -12,41 +12,46 @@ The binary will also attempt to load .env file located in the working directory.
```bash
Usage of ./mumble-discord-bridge:
-cpuprofile file
write cpu profile to file
-debug-level int
DEBUG_LEVEL, Discord debug level, optional, (default 1) (default 1)
-discord-cid string
DISCORD_CID, discord cid, required
DISCORD_CID, discord cid, required
-discord-command string
DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord) (default "mumble-discord")
DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord) (default "mumble-discord")
-discord-disable-text
DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)
DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)
-discord-gid string
DISCORD_GID, discord gid, required
DISCORD_GID, discord gid, required
-discord-token string
DISCORD_TOKEN, discord bot token, required
DISCORD_TOKEN, discord bot token, required
-mode string
MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant) (default "constant")
MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant) (default "constant")
-mumble-address string
MUMBLE_ADDRESS, mumble server address, example example.com, required
MUMBLE_ADDRESS, mumble server address, example example.com, required
-mumble-certificate string
MUMBLE_CERTIFICATE, client certificate to use when connecting to the Mumble server
-mumble-channel string
MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, optional
MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, optional
-mumble-disable-text
MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)
MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)
-mumble-insecure
MUMBLE_INSECURE, mumble insecure, optional
-mumble-certificate
MUMBLE_CERTIFICATE, mumble client certificate, optional
MUMBLE_INSECURE, mumble insecure, optional
-mumble-password string
MUMBLE_PASSWORD, mumble password, optional
MUMBLE_PASSWORD, mumble password, optional
-mumble-port int
MUMBLE_PORT, mumble port, (default 64738) (default 64738)
MUMBLE_PORT, mumble port, (default 64738) (default 64738)
-mumble-username string
MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord")
MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord")
-nice
NICE, whether the bridge should automatically try to 'nice' itself, (default false)
-debug
DEBUG_LEVEL, DISCORD debug level, optional (default: 1)
NICE, whether the bridge should automatically try to 'nice' itself, (default false)
-to-discord-buffer int
TO_DISCORD_BUFFER, Delay buffer from Mumble to Discord to absorb timing issues related to network and hardware quality. (Increments of 10ms) (default 50)
```
The bridge can be run with the follow modes:
```bash
auto
The bridge starts up but does not connect immediately. It can be either manually linked (see below) or will join the voice channels when there's at least one person on each side.

View File

@ -21,16 +21,17 @@ const (
//BridgeConfig holds configuration information set at startup
//It should not change during runtime
type BridgeConfig struct {
MumbleConfig *gumble.Config
MumbleAddr string
MumbleInsecure bool
MumbleCertificate string
MumbleChannel []string
MumbleDisableText bool
Command string
GID string
CID string
DiscordDisableText bool
MumbleConfig *gumble.Config
MumbleAddr string
MumbleInsecure bool
MumbleCertificate string
MumbleChannel []string
MumbleDisableText bool
Command string
GID string
CID string
DiscordStartStreamingCount int
DiscordDisableText bool
}
func lookupEnvOrString(key string, defaultVal string) string {

View File

@ -55,6 +55,12 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
panic(err)
}
// Generate Opus Silence Frame
opusSilence := []byte{0xf8, 0xff, 0xfe}
for i := 3; i < frameSize; i++ {
opusSilence = append(opusSilence, 0x00)
}
ticker := time.NewTicker(20 * time.Millisecond)
lastReady := true
@ -63,6 +69,27 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
wg.Add(1)
internalSend := func(opus []byte) {
dd.Bridge.DiscordVoice.RWMutex.RLock()
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusSend == nil {
if lastReady {
OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("set ready timeout")
cancel()
})
lastReady = false
}
} else if !lastReady {
fmt.Println("Discordgo ready to send opus packets")
lastReady = true
readyTimeout.Stop()
} else {
dd.Bridge.DiscordVoice.OpusSend <- opus
}
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
}
for {
select {
case <-ctx.Done():
@ -71,7 +98,7 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
default:
}
<-ticker.C
if len(pcm) > 1 {
if (len(pcm) > 1 && streaming) || (len(pcm) > dd.Bridge.BridgeConfig.DiscordStartStreamingCount && !streaming) {
if !streaming {
log.Println("Debug: Discord start speaking")
speakingStart = time.Now()
@ -89,28 +116,27 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
continue
}
dd.Bridge.DiscordVoice.RWMutex.RLock()
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusSend == nil {
if lastReady {
OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("set ready timeout")
cancel()
})
lastReady = false
}
} else if !lastReady {
fmt.Println("Discordgo ready to send opus packets")
lastReady = true
readyTimeout.Stop()
} else {
dd.Bridge.DiscordVoice.OpusSend <- opus
}
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
internalSend(opus)
} else {
if streaming {
log.Println("Debug: Discord stop speaking", time.Since(speakingStart).Milliseconds(), "ms")
// Check to see if there is a short speaking cycle.
// It is possible that short speaking cycle is the result of a short input to mumble (Not a problem). ie a quick tap of push to talk button.
// Or when timing delays are introduced via network, hardware or kernel delays (Problem).
// The problem delays result in choppy or stuttering sounds, especially when the silence frames are introduced into the opus frames below.
// Multiple short cycle delays can result in a Discrod rate limiter being trigger due to of multiple JSON speaking/not-speaking state changes
log.Println("Debug: Discord stop speaking", time.Since(speakingStart).Milliseconds(), "ms") // remove before merge
if time.Since(speakingStart).Milliseconds() < 100 {
log.Println("Warning: Short Mumble to Discord speaking cycle. Consider increaseing the size of the TO_DISCORD_BUFFER")
}
// Send silence as suggested by Discord Documentation.
// We want to do this after alerting the user of possible short speaking cycles
for i := 0; i < 5; i++ {
internalSend(opusSilence)
<-ticker.C
}
dd.Bridge.DiscordVoice.Speaking(false)
streaming = false
}

29
main.go
View File

@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"log"
"math"
"os"
"os/signal"
"runtime/pprof"
@ -45,6 +46,7 @@ func main() {
discordToken := flag.String("discord-token", lookupEnvOrString("DISCORD_TOKEN", ""), "DISCORD_TOKEN, discord bot token, required")
discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid, required")
discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid, required")
discordSendBuffer := flag.Int("to-discord-buffer", lookupEnvOrInt("TO_DISCORD_BUFFER", 50), "TO_DISCORD_BUFFER, Delay buffer from Mumble to Discord to absorb timing issues related to network and hardware quality. (Increments of 10ms)")
discordCommand := flag.String("discord-command", lookupEnvOrString("DISCORD_COMMAND", "mumble-discord"), "DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord)")
discordDisableText := flag.Bool("discord-disable-text", lookupEnvOrBool("DISCORD_DISABLE_TEXT", false), "DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)")
mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant)")
@ -95,20 +97,29 @@ func main() {
defer pprof.StopCPUProfile()
}
// Buffer Math
if *discordSendBuffer < 10 {
*discordSendBuffer = 10
}
var discordStartStreamingCount int = int(math.Round(float64(*discordSendBuffer) / 10.0))
log.Println("Discord Streaming Buffer: ", discordStartStreamingCount*10, " ms")
// BRIDGE SETUP
Bridge := &BridgeState{
BridgeConfig: &BridgeConfig{
// MumbleConfig: config,
MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort),
MumbleInsecure: *mumbleInsecure,
MumbleCertificate: *mumbleCertificate,
MumbleChannel: strings.Split(*mumbleChannel, "/"),
MumbleDisableText: *mumbleDisableText,
Command: *discordCommand,
GID: *discordGID,
CID: *discordCID,
DiscordDisableText: *discordDisableText,
MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort),
MumbleInsecure: *mumbleInsecure,
MumbleCertificate: *mumbleCertificate,
MumbleChannel: strings.Split(*mumbleChannel, "/"),
MumbleDisableText: *mumbleDisableText,
Command: *discordCommand,
GID: *discordGID,
CID: *discordCID,
DiscordStartStreamingCount: discordStartStreamingCount,
DiscordDisableText: *discordDisableText,
},
Connected: false,
DiscordUsers: make(map[string]discordUser),