mirror of
https://github.com/stryan/mumble-discord-bridge.git
synced 2024-12-28 17:15:40 -05:00
Merge pull request #21 from Stieneee/issue-20
Issue 20 - Mumble to Discord Buffer and Silence Frames
This commit is contained in:
commit
5bc118c97c
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,5 +3,7 @@ main
|
|||||||
mumble-discord-bridge
|
mumble-discord-bridge
|
||||||
dist
|
dist
|
||||||
bridge
|
bridge
|
||||||
.prof
|
*.prof
|
||||||
|
*.out
|
||||||
|
*.test
|
||||||
cert.pem
|
cert.pem
|
10
Makefile
10
Makefile
@ -1,4 +1,4 @@
|
|||||||
GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go
|
GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go sleepct.go
|
||||||
|
|
||||||
mumble-discord-bridge: $(GOFILES)
|
mumble-discord-bridge: $(GOFILES)
|
||||||
goreleaser build --skip-validate --rm-dist
|
goreleaser build --skip-validate --rm-dist
|
||||||
@ -7,11 +7,17 @@ dev: $(GOFILES)
|
|||||||
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge
|
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge
|
||||||
|
|
||||||
dev-race: $(GOFILES)
|
dev-race: $(GOFILES)
|
||||||
go run -race *.go
|
go run -race $(GOFILES)
|
||||||
|
|
||||||
dev-profile: $(GOFILES)
|
dev-profile: $(GOFILES)
|
||||||
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge -cpuprofile cpu.prof
|
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge -cpuprofile cpu.prof
|
||||||
|
|
||||||
|
test-chart: SHELL:=/bin/bash
|
||||||
|
test-chart:
|
||||||
|
go test &
|
||||||
|
until pidof mumble-discord-bridge.test; do continue; done;
|
||||||
|
psrecord --plot docs/test-cpu-memory.png $$(pidof mumble-discord-bridge.test)
|
||||||
|
|
||||||
docker-latest:
|
docker-latest:
|
||||||
docker build -t stieneee/mumble-discord-bridge:latest .
|
docker build -t stieneee/mumble-discord-bridge:latest .
|
||||||
|
|
||||||
|
39
README.md
39
README.md
@ -12,6 +12,10 @@ The binary will also attempt to load .env file located in the working directory.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
Usage of ./mumble-discord-bridge:
|
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 string
|
||||||
DISCORD_CID, discord cid, required
|
DISCORD_CID, discord cid, required
|
||||||
-discord-command string
|
-discord-command string
|
||||||
@ -26,14 +30,14 @@ Usage of ./mumble-discord-bridge:
|
|||||||
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 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 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
|
||||||
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, mumble insecure, optional
|
MUMBLE_INSECURE, mumble insecure, optional
|
||||||
-mumble-certificate
|
|
||||||
MUMBLE_CERTIFICATE, mumble client certificate, optional
|
|
||||||
-mumble-password string
|
-mumble-password string
|
||||||
MUMBLE_PASSWORD, mumble password, optional
|
MUMBLE_PASSWORD, mumble password, optional
|
||||||
-mumble-port int
|
-mumble-port int
|
||||||
@ -42,11 +46,14 @@ Usage of ./mumble-discord-bridge:
|
|||||||
MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord")
|
MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord")
|
||||||
-nice
|
-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)
|
||||||
-debug
|
-to-discord-buffer int
|
||||||
DEBUG_LEVEL, DISCORD debug level, optional (default: 1)
|
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:
|
The bridge can be run with the follow modes:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
auto
|
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.
|
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.
|
||||||
@ -62,13 +69,17 @@ In "auto" or "manual" modes, the bridge can be controlled in Discord with the fo
|
|||||||
```bash
|
```bash
|
||||||
!DISCORD_COMMAND link
|
!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
|
!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
|
!DISCORD_COMMAND refresh
|
||||||
Commands the bridge to unlink, then link again.
|
Commands the bridge to unlink, then link again.
|
||||||
|
|
||||||
!DISCORD_COMMAND auto
|
!DISCORD_COMMAND auto
|
||||||
Toggle between manual and auto mode
|
Toggle between manual and auto mode
|
||||||
```
|
```
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Creating a Discord Bot
|
### Creating a Discord Bot
|
||||||
@ -80,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.
|
Individual Discord servers need to invite the bot before it can connect.
|
||||||
The bot requires the following permissions:
|
The bot requires the following permissions:
|
||||||
|
|
||||||
* View Channels
|
* View Channels
|
||||||
* See Messages
|
* See Messages
|
||||||
* Read Message History
|
* Read Message History
|
||||||
@ -160,6 +172,21 @@ go build -o mumble-discord-bridge *.go
|
|||||||
make mumble-discord-bridge
|
make mumble-discord-bridge
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### OpenBSD Users
|
||||||
|
|
||||||
|
OpenBSD users should consider compiling a custom kernel to use 1000 ticks for the best possible performance.
|
||||||
|
See [issue 20](https://github.com/Stieneee/mumble-discord-bridge/issues/20) for the latest discussion about this topic.
|
||||||
|
|
||||||
|
## 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
|
## Known Issues
|
||||||
|
|
||||||
Currently there is an issue opening the discord voice channel.
|
Currently there is an issue opening the discord voice channel.
|
||||||
@ -185,5 +212,5 @@ Please consider opening an issue to discuss features and ideas.
|
|||||||
|
|
||||||
The project would not have been possible without:
|
The project would not have been possible without:
|
||||||
|
|
||||||
- [gumble](https://github.com/layeh/gumble)
|
* [gumble](https://github.com/layeh/gumble)
|
||||||
- [discordgo](https://github.com/bwmarrin/discordgo)
|
* [discordgo](https://github.com/bwmarrin/discordgo)
|
||||||
|
@ -26,10 +26,12 @@ type BridgeConfig struct {
|
|||||||
MumbleInsecure bool
|
MumbleInsecure bool
|
||||||
MumbleCertificate string
|
MumbleCertificate string
|
||||||
MumbleChannel []string
|
MumbleChannel []string
|
||||||
|
mumbleStartStreamCount int
|
||||||
MumbleDisableText bool
|
MumbleDisableText bool
|
||||||
Command string
|
Command string
|
||||||
GID string
|
GID string
|
||||||
CID string
|
CID string
|
||||||
|
DiscordStartStreamingCount int
|
||||||
DiscordDisableText bool
|
DiscordDisableText bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
143
discord.go
143
discord.go
@ -55,37 +55,25 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(20 * time.Millisecond)
|
// Generate Opus Silence Frame
|
||||||
|
opusSilence := []byte{0xf8, 0xff, 0xfe}
|
||||||
|
for i := 3; i < frameSize; i++ {
|
||||||
|
opusSilence = append(opusSilence, 0x00)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ticker := NewTickerCT(20 * time.Millisecond)
|
||||||
|
sleepTick := SleepCT{
|
||||||
|
d: 20 * time.Millisecond,
|
||||||
|
t: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
lastReady := true
|
lastReady := true
|
||||||
var readyTimeout *time.Timer
|
var readyTimeout *time.Timer
|
||||||
|
var speakingStart time.Time
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
for {
|
internalSend := func(opus []byte) {
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
wg.Done()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
<-ticker.C
|
|
||||||
if len(pcm) > 1 {
|
|
||||||
if !streaming {
|
|
||||||
dd.Bridge.DiscordVoice.Speaking(true)
|
|
||||||
streaming = true
|
|
||||||
}
|
|
||||||
|
|
||||||
r1 := <-pcm
|
|
||||||
r2 := <-pcm
|
|
||||||
|
|
||||||
// try encoding pcm frame with Opus
|
|
||||||
opus, err := opusEncoder.Encode(append(r1, r2...), frameSize, maxBytes)
|
|
||||||
if err != nil {
|
|
||||||
OnError("Encoding Error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dd.Bridge.DiscordVoice.RWMutex.RLock()
|
dd.Bridge.DiscordVoice.RWMutex.RLock()
|
||||||
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusSend == nil {
|
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusSend == nil {
|
||||||
if lastReady {
|
if lastReady {
|
||||||
@ -104,9 +92,55 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
|
|||||||
dd.Bridge.DiscordVoice.OpusSend <- opus
|
dd.Bridge.DiscordVoice.OpusSend <- opus
|
||||||
}
|
}
|
||||||
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
|
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
sleepTick.SleepNextTarget()
|
||||||
|
|
||||||
|
if (len(pcm) > 1 && streaming) || (len(pcm) > dd.Bridge.BridgeConfig.DiscordStartStreamingCount && !streaming) {
|
||||||
|
if !streaming {
|
||||||
|
speakingStart = time.Now()
|
||||||
|
dd.Bridge.DiscordVoice.Speaking(true)
|
||||||
|
streaming = true
|
||||||
|
}
|
||||||
|
|
||||||
|
r1 := <-pcm
|
||||||
|
r2 := <-pcm
|
||||||
|
|
||||||
|
// try encoding pcm frame with Opus
|
||||||
|
opus, err := opusEncoder.Encode(append(r1, r2...), frameSize, maxBytes)
|
||||||
|
if err != nil {
|
||||||
|
OnError("Encoding Error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
internalSend(opus)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if streaming {
|
if streaming {
|
||||||
|
// 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
|
||||||
|
if time.Since(speakingStart).Milliseconds() < 100 {
|
||||||
|
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)
|
||||||
|
sleepTick.SleepNextTarget()
|
||||||
|
}
|
||||||
|
|
||||||
dd.Bridge.DiscordVoice.Speaking(false)
|
dd.Bridge.DiscordVoice.Speaking(false)
|
||||||
streaming = false
|
streaming = false
|
||||||
}
|
}
|
||||||
@ -205,8 +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) {
|
func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGroup, toMumble chan<- gumble.AudioBuffer) {
|
||||||
ticker := time.NewTicker(10 * time.Millisecond)
|
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
|
sendAudio := false
|
||||||
|
toMumbleStreaming := false
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -214,9 +257,11 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
wg.Done()
|
wg.Done()
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sleepTick.SleepNextTarget()
|
||||||
|
|
||||||
dd.discordMutex.Lock()
|
dd.discordMutex.Lock()
|
||||||
|
|
||||||
sendAudio = false
|
sendAudio = false
|
||||||
@ -224,9 +269,16 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou
|
|||||||
|
|
||||||
// Work through each channel
|
// Work through each channel
|
||||||
for i := range dd.fromDiscordMap {
|
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
|
sendAudio = true
|
||||||
if !dd.fromDiscordMap[i].streaming {
|
|
||||||
|
if !isStreaming {
|
||||||
x := dd.fromDiscordMap[i]
|
x := dd.fromDiscordMap[i]
|
||||||
x.streaming = true
|
x.streaming = true
|
||||||
dd.fromDiscordMap[i] = x
|
dd.fromDiscordMap[i] = x
|
||||||
@ -245,6 +297,22 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou
|
|||||||
|
|
||||||
dd.discordMutex.Unlock()
|
dd.discordMutex.Unlock()
|
||||||
|
|
||||||
|
mumbleTimeoutSend := func(outBuf []int16) {
|
||||||
|
timeout := make(chan bool, 1)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
timeout <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case toMumble <- outBuf:
|
||||||
|
case <-timeout:
|
||||||
|
log.Println("toMumble timeout. Dropping packet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sendAudio {
|
||||||
|
// Regular send mixed audio
|
||||||
outBuf := make([]int16, 480)
|
outBuf := make([]int16, 480)
|
||||||
|
|
||||||
for i := 0; i < len(outBuf); i++ {
|
for i := 0; i < len(outBuf); i++ {
|
||||||
@ -253,13 +321,20 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sendAudio {
|
mumbleTimeoutSend(outBuf)
|
||||||
select {
|
} else if !sendAudio && toMumbleStreaming {
|
||||||
case toMumble <- outBuf:
|
// Send opus silence to mumble
|
||||||
default:
|
// See note above about jitter buffer warning
|
||||||
log.Println("toMumble buffer full. Dropping packet")
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
docs/audio-flow.drawio.png
Normal file
BIN
docs/audio-flow.drawio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 123 KiB |
BIN
docs/test-cpu-memory.png
Normal file
BIN
docs/test-cpu-memory.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require (
|
|||||||
github.com/golang/protobuf v1.4.3 // indirect
|
github.com/golang/protobuf v1.4.3 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
|
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
||||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
|
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
|
||||||
google.golang.org/protobuf v1.25.0 // indirect
|
google.golang.org/protobuf v1.25.0 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -46,6 +46,8 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
|||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9 h1:0l2H6Oj6JHMmkqm9xaBMQA5MOGhPT+Nn/thlTUcD9Iw=
|
||||||
|
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9/go.mod h1:54+oZlabriEpT52rPAjAeEWUFgYqv325LrS3hNxHGFE=
|
||||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
||||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
31
main.go
31
main.go
@ -4,6 +4,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
@ -41,10 +42,12 @@ func main() {
|
|||||||
mumbleInsecure := flag.Bool("mumble-insecure", lookupEnvOrBool("MUMBLE_INSECURE", false), " MUMBLE_INSECURE, mumble insecure, optional")
|
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")
|
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")
|
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)")
|
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")
|
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")
|
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")
|
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, 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)")
|
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)")
|
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)")
|
mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant)")
|
||||||
@ -95,6 +98,21 @@ func main() {
|
|||||||
defer pprof.StopCPUProfile()
|
defer pprof.StopCPUProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffer Math
|
||||||
|
if *discordSendBuffer < 10 {
|
||||||
|
*discordSendBuffer = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if *mumbleSendBuffer < 10 {
|
||||||
|
*mumbleSendBuffer = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
var discordStartStreamingCount int = int(math.Round(float64(*discordSendBuffer) / 10.0))
|
||||||
|
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
|
// BRIDGE SETUP
|
||||||
|
|
||||||
Bridge := &BridgeState{
|
Bridge := &BridgeState{
|
||||||
@ -104,10 +122,12 @@ func main() {
|
|||||||
MumbleInsecure: *mumbleInsecure,
|
MumbleInsecure: *mumbleInsecure,
|
||||||
MumbleCertificate: *mumbleCertificate,
|
MumbleCertificate: *mumbleCertificate,
|
||||||
MumbleChannel: strings.Split(*mumbleChannel, "/"),
|
MumbleChannel: strings.Split(*mumbleChannel, "/"),
|
||||||
|
mumbleStartStreamCount: mumbleStartStreamCount,
|
||||||
MumbleDisableText: *mumbleDisableText,
|
MumbleDisableText: *mumbleDisableText,
|
||||||
Command: *discordCommand,
|
Command: *discordCommand,
|
||||||
GID: *discordGID,
|
GID: *discordGID,
|
||||||
CID: *discordCID,
|
CID: *discordCID,
|
||||||
|
DiscordStartStreamingCount: discordStartStreamingCount,
|
||||||
DiscordDisableText: *discordDisableText,
|
DiscordDisableText: *discordDisableText,
|
||||||
},
|
},
|
||||||
Connected: false,
|
Connected: false,
|
||||||
@ -177,9 +197,16 @@ func main() {
|
|||||||
Bridge.Mode = bridgeModeConstant
|
Bridge.Mode = bridgeModeConstant
|
||||||
Bridge.DiscordChannelID = Bridge.BridgeConfig.CID
|
Bridge.DiscordChannelID = Bridge.BridgeConfig.CID
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Println("Bridge paniced", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
for {
|
for {
|
||||||
Bridge.startBridge()
|
Bridge.startBridge()
|
||||||
log.Println("Bridge died. Restarting")
|
log.Println("Bridge died")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
log.Println("Restarting")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
default:
|
default:
|
||||||
@ -191,7 +218,7 @@ func main() {
|
|||||||
|
|
||||||
// Shutdown on OS signal
|
// Shutdown on OS signal
|
||||||
sc := make(chan os.Signal, 1)
|
sc := make(chan os.Signal, 1)
|
||||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
|
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||||
<-sc
|
<-sc
|
||||||
|
|
||||||
log.Println("OS Signal. Bot shutting down")
|
log.Println("OS Signal. Bot shutting down")
|
||||||
|
26
mumble.go
26
mumble.go
@ -44,8 +44,13 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, toDiscord chan []int16) {
|
func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, toDiscord chan []int16) {
|
||||||
ticker := time.NewTicker(10 * time.Millisecond)
|
sleepTick := SleepCT{
|
||||||
|
d: 10 * time.Millisecond,
|
||||||
|
t: time.Now(),
|
||||||
|
}
|
||||||
sendAudio := false
|
sendAudio := false
|
||||||
|
bufferWarning := false
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -56,7 +61,7 @@ func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, t
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
<-ticker.C
|
sleepTick.SleepNextTarget()
|
||||||
|
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
|
|
||||||
@ -84,6 +89,8 @@ func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, t
|
|||||||
|
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
|
|
||||||
|
if sendAudio {
|
||||||
|
|
||||||
outBuf := make([]int16, 480)
|
outBuf := make([]int16, 480)
|
||||||
|
|
||||||
for i := 0; i < len(outBuf); i++ {
|
for i := 0; i < len(outBuf); i++ {
|
||||||
@ -92,11 +99,22 @@ func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sendAudio {
|
if len(toDiscord) > 20 {
|
||||||
|
if !bufferWarning {
|
||||||
|
log.Println("Warning: toDiscord buffer size")
|
||||||
|
bufferWarning = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if bufferWarning {
|
||||||
|
log.Println("Resolved: toDiscord buffer size")
|
||||||
|
bufferWarning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case toDiscord <- outBuf:
|
case toDiscord <- outBuf:
|
||||||
default:
|
default:
|
||||||
log.Println("toDiscord buffer full. Dropping packet")
|
log.Println("Error: toDiscord buffer full. Dropping packet")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
sleepct.go
Normal file
41
sleepct.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SleepCT - Sleep constant time step crates a sleep based ticker
|
||||||
|
// designed maintain a sleep/tick interval
|
||||||
|
type SleepCT struct {
|
||||||
|
sync.Mutex
|
||||||
|
d time.Duration // duration
|
||||||
|
t time.Time // last time target
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SleepCT) SleepNextTarget() {
|
||||||
|
s.Lock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
var last time.Time
|
||||||
|
if s.t.IsZero() {
|
||||||
|
fmt.Println("SleepCT reset")
|
||||||
|
last = now.Add(-s.d)
|
||||||
|
} else {
|
||||||
|
last = s.t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next Target
|
||||||
|
s.t = last.Add(s.d)
|
||||||
|
|
||||||
|
d := s.t.Sub(now)
|
||||||
|
|
||||||
|
time.Sleep(d)
|
||||||
|
|
||||||
|
// delta := now.Sub(s.t)
|
||||||
|
// fmt.Println("delta", delta, d, time.Since(s.t))
|
||||||
|
|
||||||
|
s.Unlock()
|
||||||
|
}
|
169
timing_test.go
Normal file
169
timing_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stieneee/tickerct"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testCount int64 = 10000
|
||||||
|
const maxSleepInterval time.Duration = 15 * time.Millisecond
|
||||||
|
const tickerInterval time.Duration = 10 * time.Millisecond
|
||||||
|
const testDuration time.Duration = time.Duration(testCount * 10 * int64(time.Millisecond))
|
||||||
|
|
||||||
|
func testTickerBaseCase(wg *sync.WaitGroup) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(interval time.Duration) {
|
||||||
|
now := time.Now()
|
||||||
|
start := now
|
||||||
|
// start the ticker
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
var i int64
|
||||||
|
for i = 0; i < testCount; i++ {
|
||||||
|
now = <-t.C
|
||||||
|
// fmt.Println(now)
|
||||||
|
}
|
||||||
|
t.Stop()
|
||||||
|
fmt.Println("Ticker (unloaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||||
|
wg.Done()
|
||||||
|
}(tickerInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTickerBaseCase(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
testTickerBaseCase(&wg)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTickerLoaded(wg *sync.WaitGroup) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(interval time.Duration) {
|
||||||
|
now := time.Now()
|
||||||
|
start := now
|
||||||
|
// start the ticker
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
var i int64
|
||||||
|
for i = 0; i < testCount; i++ {
|
||||||
|
if i+1 < testCount {
|
||||||
|
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
|
||||||
|
}
|
||||||
|
now = <-t.C
|
||||||
|
// fmt.Println(now)
|
||||||
|
}
|
||||||
|
t.Stop()
|
||||||
|
fmt.Println("Ticker (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||||
|
wg.Done()
|
||||||
|
}(tickerInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTicker(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
testTickerLoaded(&wg)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTickerCT(wg *sync.WaitGroup) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(interval time.Duration) {
|
||||||
|
now := time.Now()
|
||||||
|
start := now
|
||||||
|
// start the ticker
|
||||||
|
t := tickerct.NewTickerCT(interval)
|
||||||
|
var i int64
|
||||||
|
for i = 0; i < testCount; i++ {
|
||||||
|
if i+1 < testCount {
|
||||||
|
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
|
||||||
|
}
|
||||||
|
now = <-t.C
|
||||||
|
// fmt.Println(now)
|
||||||
|
}
|
||||||
|
t.Stop()
|
||||||
|
fmt.Println("TickerCT (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||||
|
wg.Done()
|
||||||
|
}(tickerInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTickerCT(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
testTickerCT(&wg)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSleepCT(wg *sync.WaitGroup) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(interval time.Duration) {
|
||||||
|
now := time.Now()
|
||||||
|
start := now
|
||||||
|
// start the ticker
|
||||||
|
s := SleepCT{
|
||||||
|
d: interval,
|
||||||
|
t: time.Now(),
|
||||||
|
}
|
||||||
|
var i int64
|
||||||
|
for i = 0; i < testCount; i++ {
|
||||||
|
if i+1 < testCount {
|
||||||
|
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
|
||||||
|
}
|
||||||
|
s.SleepNextTarget()
|
||||||
|
}
|
||||||
|
fmt.Println("SleepCT (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||||
|
wg.Done()
|
||||||
|
}(tickerInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSleepCT(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
testSleepCT(&wg)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdleJitter(t *testing.T) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
const testSize = 100000
|
||||||
|
const sleepTarget = time.Millisecond
|
||||||
|
|
||||||
|
res := make([]time.Duration, testSize)
|
||||||
|
|
||||||
|
for i := 0; i < testSize; i++ {
|
||||||
|
start := time.Now()
|
||||||
|
target := start.Add(sleepTarget)
|
||||||
|
|
||||||
|
time.Sleep(sleepTarget)
|
||||||
|
|
||||||
|
res[i] = time.Since(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(res, func(i, j int) bool {
|
||||||
|
return res[i] < res[j]
|
||||||
|
})
|
||||||
|
|
||||||
|
var total float64 = 0
|
||||||
|
for i := 0; i < testSize; i++ {
|
||||||
|
total += float64(res[i])
|
||||||
|
}
|
||||||
|
avg := time.Duration(total / testSize)
|
||||||
|
|
||||||
|
nineFive := int64(math.Round(testSize * 0.95))
|
||||||
|
nineNine := int64(math.Round(testSize * 0.99))
|
||||||
|
nineNineNine := int64(math.Round(testSize * 0.999))
|
||||||
|
|
||||||
|
fmt.Println("IdleJitter test", testSize, sleepTarget)
|
||||||
|
fmt.Println("IdleJitter results min/avg/95/99/99.9/max", res[0], avg, res[nineFive], res[nineNine], res[nineNineNine], res[testSize-1])
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user