Merge pull request #21 from Stieneee/issue-20

Issue 20 - Mumble to Discord Buffer and Silence Frames
This commit is contained in:
Tyler Stiene 2021-04-24 13:23:09 -04:00 committed by GitHub
commit 5bc118c97c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 462 additions and 92 deletions

4
.gitignore vendored
View File

@ -3,5 +3,7 @@ main
mumble-discord-bridge
dist
bridge
.prof
*.prof
*.out
*.test
cert.pem

View File

@ -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)
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
dev-race: $(GOFILES)
go run -race *.go
go run -race $(GOFILES)
dev-profile: $(GOFILES)
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 build -t stieneee/mumble-discord-bridge:latest .

View File

@ -12,41 +12,48 @@ 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, 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:
```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.
@ -61,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
@ -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.
The bot requires the following permissions:
* View Channels
* See Messages
* Read Message History
@ -160,6 +172,21 @@ go build -o mumble-discord-bridge *.go
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
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:
- [gumble](https://github.com/layeh/gumble)
- [discordgo](https://github.com/bwmarrin/discordgo)
* [gumble](https://github.com/layeh/gumble)
* [discordgo](https://github.com/bwmarrin/discordgo)

View File

@ -21,16 +21,18 @@ 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
mumbleStartStreamCount int
MumbleDisableText bool
Command string
GID string
CID string
DiscordStartStreamingCount int
DiscordDisableText bool
}
func lookupEnvOrString(key string, defaultVal string) string {

View File

@ -55,13 +55,45 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
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
var readyTimeout *time.Timer
var speakingStart time.Time
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():
@ -69,9 +101,12 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup,
return
default:
}
<-ticker.C
if len(pcm) > 1 {
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
}
@ -86,27 +121,26 @@ 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 {
// 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)
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) {
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
toMumbleStreaming := false
wg.Add(1)
for {
@ -214,9 +257,11 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou
case <-ctx.Done():
wg.Done()
return
case <-ticker.C:
default:
}
sleepTick.SleepNextTarget()
dd.discordMutex.Lock()
sendAudio = false
@ -224,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
@ -245,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
}
}
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/golang/protobuf v1.4.3 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
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/sys v0.0.0-20210108172913-0df2131ae363 // indirect
google.golang.org/protobuf v1.25.0 // indirect

2
go.sum
View File

@ -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/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/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/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

49
main.go
View File

@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"log"
"math"
"os"
"os/signal"
"runtime/pprof"
@ -41,10 +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, 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)")
@ -95,20 +98,37 @@ func main() {
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 := &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, "/"),
mumbleStartStreamCount: mumbleStartStreamCount,
MumbleDisableText: *mumbleDisableText,
Command: *discordCommand,
GID: *discordGID,
CID: *discordCID,
DiscordStartStreamingCount: discordStartStreamingCount,
DiscordDisableText: *discordDisableText,
},
Connected: false,
DiscordUsers: make(map[string]discordUser),
@ -177,9 +197,16 @@ func main() {
Bridge.Mode = bridgeModeConstant
Bridge.DiscordChannelID = Bridge.BridgeConfig.CID
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Bridge paniced", r)
}
}()
for {
Bridge.startBridge()
log.Println("Bridge died. Restarting")
log.Println("Bridge died")
time.Sleep(5 * time.Second)
log.Println("Restarting")
}
}()
default:
@ -191,7 +218,7 @@ func main() {
// Shutdown on OS signal
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
log.Println("OS Signal. Bot shutting down")

View File

@ -44,8 +44,13 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
}
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
bufferWarning := false
wg.Add(1)
for {
@ -56,7 +61,7 @@ func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, t
default:
}
<-ticker.C
sleepTick.SleepNextTarget()
mutex.Lock()
@ -84,19 +89,32 @@ func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, t
mutex.Unlock()
outBuf := make([]int16, 480)
for i := 0; i < len(outBuf); i++ {
for j := 0; j < len(internalMixerArr); j++ {
outBuf[i] += (internalMixerArr[j])[i]
}
}
if sendAudio {
outBuf := make([]int16, 480)
for i := 0; i < len(outBuf); i++ {
for j := 0; j < len(internalMixerArr); j++ {
outBuf[i] += (internalMixerArr[j])[i]
}
}
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 {
case toDiscord <- outBuf:
default:
log.Println("toDiscord buffer full. Dropping packet")
log.Println("Error: toDiscord buffer full. Dropping packet")
}
}
}

41
sleepct.go Normal file
View 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
View 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()
}