2

I am writing code to control external LEDs over UDP, and need to keep a frame rate (e.g. 60Hz) that is as constant as possible. Too much jitter will look bad. I've written a simple test using time.NewTicker and the results are not ideal. I'm wondering if there is a different way to execute code on a more accurate interval. This test was run on macOS, but needs to run on Windows and Linux too. For what it's worth, it needs to sync to audio, so maybe there is an audio ticker API on each OS it could potentially sync with?

package main

import (
    "encoding/csv"
    "fmt"
    "log"
    "os"
    "strconv"
    "time"
)

var f *os.File
var testTimeSeconds = 30

func appendToCsv(t time.Time) {

    w := csv.NewWriter(f)
    defer w.Flush()

    records := []string{strconv.FormatInt(t.UnixMicro(), 10)}
    w.Write(records)
}

func init() {
    var err error
    f, err = os.Create("newTicker.csv")
    if err != nil {
        log.Fatal(err)
    }

    w := csv.NewWriter(f)
    defer w.Flush()

    records := []string{"timestamps"}
    w.Write(records)
}

func main() {
    usPerFrame := 16666
    ticker := time.NewTicker(time.Duration(usPerFrame) * time.Microsecond)
    defer ticker.Stop()
    done := make(chan bool)
    go func() {
        time.Sleep(time.Duration(testTimeSeconds) * time.Second)
        done <- true
    }()
    for {
        select {
        case <-done:
            fmt.Println("Done!")
            return
        case t := <-ticker.C:
            appendToCsv(t)
        }
    }
}

enter image description here

UPDATE: I ran another test comparing the first method with the method in @jochen's answer, still not very accurate.

sleeptimer

JBaczuk
  • 13,886
  • 10
  • 58
  • 86
  • 2
    Although `time.Ticker` does not guarantee high precision timing, likely the inaccuracy comes from the precision / granularity of the system time. That on Windows is around 10 ms! See: [Why does time.Now().UnixNano() returns the same result after an IO operation?](https://stackoverflow.com/questions/57285292/why-does-time-now-unixnano-returns-the-same-result-after-an-io-operation?noredirect=1&lq=1) – icza Jan 05 '22 at 14:57
  • So is this type of timing just not possible on these platforms, regardless of the language? – JBaczuk Jan 05 '22 at 15:21
  • I believe it's possible, just not with tools that rely / use the system time. – icza Jan 05 '22 at 15:29
  • Your use of `csv.NewWriter()` in `appendToCsv` seems unnecessary. Wouldn't `fmt.Fprintf(f, "%d\n", t.UnixMicro())` do everything you need? – jochen Jan 06 '22 at 15:50

1 Answers1

1

One idea would be to just use time.Sleep() instead of a ticker. This takes the channel send/receive out of the loop and may lead to more accurate timing. To do this, you could run a function like the following in a separate goroutine:

func ticker(step time.Duration, done <-chan struct{}) {
        next := time.Now().Add(step)
        for {
                time.Sleep(time.Until(next))
                appendToCsv(time.Now())

                select { // check whether `done` was closed
                case <-done:
                        return
                default:
                        // pass
                }

                next = next.Add(step)
        }
}
jochen
  • 3,728
  • 2
  • 39
  • 49
  • I tried this, and ran the same exact test, it seems to improve accuracy somewhat, but doesn't cut out outliers, which are really the problem for this specific application. – JBaczuk Jan 07 '22 at 03:53
  • Thanks for your answer btw. See updated question. – JBaczuk Jan 07 '22 at 04:00