diff --git a/Makefile b/Makefile index e7cabea..80a6b48 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go tickerct.go +GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go sleepct.go mumble-discord-bridge: $(GOFILES) goreleaser build --skip-validate --rm-dist diff --git a/README.md b/README.md index 5882071..0db32c1 100644 --- a/README.md +++ b/README.md @@ -13,41 +13,43 @@ 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 + write cpu profile to file -debug-level int - DEBUG_LEVEL, Discord debug level, optional, (default 1) (default 1) + 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_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_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) + 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) + TO_DISCORD_BUFFER, Jitter buffer from Mumble to Discord to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms) (default 50) + -to-mumble-buffer int + TO_MUMBLE_BUFFER, Jitter buffer from Discord to Mumble to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms) (default 50) ``` The bridge can be run with the follow modes: @@ -66,14 +68,18 @@ In "auto" or "manual" modes, the bridge can be controlled in Discord with the fo ```bash !DISCORD_COMMAND link - Commands the bridge to join the Discord channel the user is in and the Mumble server + Commands the bridge to join the Discord channel the user is in and the Mumble server + !DISCORD_COMMAND unlink - Commands the bridge to leave the Discord channel the user is in and the Mumble server + Commands the bridge to leave the Discord channel the user is in and the Mumble server + !DISCORD_COMMAND refresh - Commands the bridge to unlink, then link again. + Commands the bridge to unlink, then link again. + !DISCORD_COMMAND auto - Toggle between manual and auto mode + Toggle between manual and auto mode ``` + ## Setup ### Creating a Discord Bot @@ -85,6 +91,7 @@ The guide below provides information on how to setup a Discord bot. Individual Discord servers need to invite the bot before it can connect. The bot requires the following permissions: + * View Channels * See Messages * Read Message History @@ -165,6 +172,16 @@ go build -o mumble-discord-bridge *.go make mumble-discord-bridge ``` +## Jitter Buffer + +The bridge implements simple jitter buffers that attempt to compensate for network, OS and hardware related jitter. +These jitter buffers are configurable in both directions. +A jitter buffer will slightly the delay the transmission of audio in order to have audio packets buffered for the next time step. +The Mumble client itself includes a jitter buffer for similar reasons. +A default jitter of 50ms should be adequate for most scenarios. +A warning will be logged if short burst or audio are seen. +A single warning can be ignored multiple warnings in short time spans would suggest the need for a larger jitter buffer. + ## Known Issues Currently there is an issue opening the discord voice channel. @@ -187,5 +204,5 @@ Please consider opening an issue to discuss features and ideas. The project would not have been possible without: -- [gumble](https://github.com/layeh/gumble) -- [discordgo](https://github.com/bwmarrin/discordgo) +* [gumble](https://github.com/layeh/gumble) +* [discordgo](https://github.com/bwmarrin/discordgo) diff --git a/config.go b/config.go index 149254e..bcf4539 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,7 @@ type BridgeConfig struct { MumbleInsecure bool MumbleCertificate string MumbleChannel []string + mumbleStartStreamCount int MumbleDisableText bool Command string GID string diff --git a/discord.go b/discord.go index ca463d0..23fe141 100644 --- a/discord.go +++ b/discord.go @@ -102,7 +102,6 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup, default: } - // <-ticker.C sleepTick.SleepNextTarget() if (len(pcm) > 1 && streaming) || (len(pcm) > dd.Bridge.BridgeConfig.DiscordStartStreamingCount && !streaming) { @@ -132,14 +131,13 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup, // 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 if time.Since(speakingStart).Milliseconds() < 100 { - log.Println("Warning: Short Mumble to Discord speaking cycle. Consider increaseing the size of the TO_DISCORD_BUFFER") + log.Println("Warning: Short Mumble to Discord speaking cycle. Consider increaseing the size of the to Discord jitter 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 sleepTick.SleepNextTarget() } @@ -241,11 +239,17 @@ func (dd *DiscordDuplex) discordReceivePCM(ctx context.Context, wg *sync.WaitGro } func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGroup, toMumble chan<- gumble.AudioBuffer) { + mumbleSilence := gumble.AudioBuffer{} + for i := 3; i < 480; i++ { + mumbleSilence = append(mumbleSilence, 0x00) + } + var speakingStart time.Time sleepTick := SleepCT{ d: 10 * time.Millisecond, t: time.Now(), } sendAudio := false + toMumbleStreaming := false wg.Add(1) for { @@ -265,9 +269,16 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou // Work through each channel for i := range dd.fromDiscordMap { - if len(dd.fromDiscordMap[i].pcm) > 0 { + bufferLength := len(dd.fromDiscordMap[i].pcm) + isStreaming := dd.fromDiscordMap[i].streaming + if (bufferLength > 0 && isStreaming) || (bufferLength > dd.Bridge.BridgeConfig.mumbleStartStreamCount && !isStreaming) { + if !toMumbleStreaming { + speakingStart = time.Now() + toMumbleStreaming = true + } sendAudio = true - if !dd.fromDiscordMap[i].streaming { + + if !isStreaming { x := dd.fromDiscordMap[i] x.streaming = true dd.fromDiscordMap[i] = x @@ -286,21 +297,44 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou dd.discordMutex.Unlock() - outBuf := make([]int16, 480) + mumbleTimeoutSend := func(outBuf []int16) { + timeout := make(chan bool, 1) + go func() { + time.Sleep(10 * time.Millisecond) + timeout <- true + }() - for i := 0; i < len(outBuf); i++ { - for j := 0; j < len(internalMixerArr); j++ { - outBuf[i] += (internalMixerArr[j])[i] + select { + case toMumble <- outBuf: + case <-timeout: + log.Println("toMumble timeout. Dropping packet") } } if sendAudio { - select { - case toMumble <- outBuf: - default: - log.Println("toMumble buffer full. Dropping packet") + // Regular send mixed audio + outBuf := make([]int16, 480) + + for i := 0; i < len(outBuf); i++ { + for j := 0; j < len(internalMixerArr); j++ { + outBuf[i] += (internalMixerArr[j])[i] + } } + mumbleTimeoutSend(outBuf) + } else if !sendAudio && toMumbleStreaming { + // Send opus silence to mumble + // See note above about jitter buffer warning + if time.Since(speakingStart).Milliseconds() < 100 { + log.Println("Warning: Short Discord to Mumble speaking cycle. Consider increaseing the size of the to Mumble jitter buffer.") + } + + for i := 0; i < 5; i++ { + mumbleTimeoutSend(mumbleSilence) + sleepTick.SleepNextTarget() + } + + toMumbleStreaming = false } } } diff --git a/main.go b/main.go index 7b9ec15..813c5c9 100644 --- a/main.go +++ b/main.go @@ -42,11 +42,12 @@ func main() { mumbleInsecure := flag.Bool("mumble-insecure", lookupEnvOrBool("MUMBLE_INSECURE", false), " MUMBLE_INSECURE, mumble insecure, optional") mumbleCertificate := flag.String("mumble-certificate", lookupEnvOrString("MUMBLE_CERTIFICATE", ""), "MUMBLE_CERTIFICATE, client certificate to use when connecting to the Mumble server") mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, optional") + mumbleSendBuffer := flag.Int("to-mumble-buffer", lookupEnvOrInt("TO_MUMBLE_BUFFER", 50), "TO_MUMBLE_BUFFER, Jitter buffer from Discord to Mumble to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms)") mumbleDisableText := flag.Bool("mumble-disable-text", lookupEnvOrBool("MUMBLE_DISABLE_TEXT", false), "MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)") 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)") + discordSendBuffer := flag.Int("to-discord-buffer", lookupEnvOrInt("TO_DISCORD_BUFFER", 50), "TO_DISCORD_BUFFER, Jitter buffer from Mumble to Discord to absorb timing issues related to network, OS 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)") @@ -102,8 +103,15 @@ func main() { *discordSendBuffer = 10 } + if *mumbleSendBuffer < 10 { + *mumbleSendBuffer = 10 + } + var discordStartStreamingCount int = int(math.Round(float64(*discordSendBuffer) / 10.0)) - log.Println("Discord Streaming Buffer: ", discordStartStreamingCount*10, " ms") + log.Println("To Discord Jitter Buffer: ", discordStartStreamingCount*10, " ms") + + var mumbleStartStreamCount int = int(math.Round(float64(*mumbleSendBuffer) / 10.0)) + log.Println("To Mumble Jitter Buffer: ", mumbleStartStreamCount*10, " ms") // BRIDGE SETUP @@ -114,6 +122,7 @@ func main() { MumbleInsecure: *mumbleInsecure, MumbleCertificate: *mumbleCertificate, MumbleChannel: strings.Split(*mumbleChannel, "/"), + mumbleStartStreamCount: mumbleStartStreamCount, MumbleDisableText: *mumbleDisableText, Command: *discordCommand, GID: *discordGID,