From 1bd68fc0ba4aaa157b38265e2c8065adaad598f0 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Fri, 17 Mar 2023 03:40:27 +0100 Subject: [PATCH] Add auto-restart-voicemeeter tool. --- icedreammusic/auto-restart-voicemeeter/go.mod | 10 + icedreammusic/auto-restart-voicemeeter/go.sum | 6 + .../auto-restart-voicemeeter/main.go | 243 ++++++++++++++++++ icedreammusic/compile-win.sh | 2 +- 4 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 icedreammusic/auto-restart-voicemeeter/go.mod create mode 100644 icedreammusic/auto-restart-voicemeeter/go.sum create mode 100644 icedreammusic/auto-restart-voicemeeter/main.go diff --git a/icedreammusic/auto-restart-voicemeeter/go.mod b/icedreammusic/auto-restart-voicemeeter/go.mod new file mode 100644 index 0000000..43376c5 --- /dev/null +++ b/icedreammusic/auto-restart-voicemeeter/go.mod @@ -0,0 +1,10 @@ +module git.icedream.tech/icedream/auto-restart-voicemeeter + +go 1.20 + +require ( + github.com/JamesDunne/go-asio v0.0.0-20150322061733-6c4b099ca927 + github.com/davecgh/go-spew v1.1.1 +) + +require github.com/cenkalti/backoff/v4 v4.2.0 // indirect diff --git a/icedreammusic/auto-restart-voicemeeter/go.sum b/icedreammusic/auto-restart-voicemeeter/go.sum new file mode 100644 index 0000000..a940dc7 --- /dev/null +++ b/icedreammusic/auto-restart-voicemeeter/go.sum @@ -0,0 +1,6 @@ +github.com/JamesDunne/go-asio v0.0.0-20150322061733-6c4b099ca927 h1:qcTT3RNLm21eamlM+G/AJrh1KG9VcyW8AwOsbzVRW04= +github.com/JamesDunne/go-asio v0.0.0-20150322061733-6c4b099ca927/go.mod h1:dRYieepeZSO8hlYXKnpnn6WqR7Myv+kb+Y+w14NxfFw= +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/icedreammusic/auto-restart-voicemeeter/main.go b/icedreammusic/auto-restart-voicemeeter/main.go new file mode 100644 index 0000000..4f0311a --- /dev/null +++ b/icedreammusic/auto-restart-voicemeeter/main.go @@ -0,0 +1,243 @@ +package main + +import ( + "errors" + "fmt" + "log" + "math" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "time" + + asio "github.com/JamesDunne/go-asio" + "github.com/cenkalti/backoff/v4" +) + +func reverse(s string) string { + r := []rune(s) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return string(r) +} + +func main() { + // TODO - handle interrupt signal here rather than in sample loop + + bo := backoff.NewConstantBackOff(time.Second) + err := backoff.Retry(run, bo) + if err != nil { + log.Fatal(err) + } +} + +func run() error { + fmt.Printf("CoInitialize(0)\n") + asio.CoInitialize(0) + defer fmt.Printf("CoUninitialize()\n") + defer asio.CoUninitialize() + + drivers, err := asio.ListDrivers() + if err != nil { + return backoff.Permanent(err) + } + + var mainOutDriver *asio.ASIODriver + for _, driver := range drivers { + if driver.Name != "Voicemeeter Virtual ASIO" { + continue + } + mainOutDriver = driver + break + } + + if mainOutDriver == nil { + return backoff.Permanent(errors.New("could not find main Voicemeeter ASIO output")) + } + + log.Println("ASIO driver:", mainOutDriver.GUID, mainOutDriver.CLSID, mainOutDriver.Name) + + if err := mainOutDriver.Open(); err != nil { + return err + } + defer mainOutDriver.Close() + + drv := mainOutDriver.ASIO + fmt.Printf("getDriverName(): '%s'\n", drv.GetDriverName()) + fmt.Printf("getDriverVersion(): %d\n", drv.GetDriverVersion()) + + // mainOutUnknown := drv.AsIUnknown() + // mainOutUnknown.AddRef() + // defer mainOutUnknown.Release() + + // getChannels + in, out, err := drv.GetChannels() + if err != nil { + return err + } + fmt.Printf("getChannels(): %d, %d\n", in, out) + + // getBufferSize + minSize, maxSize, preferredSize, granularity, err := drv.GetBufferSize() + if err != nil { + return err + } + fmt.Printf("getBufferSize(): %d, %d, %d, %d\n", minSize, maxSize, preferredSize, granularity) + + // getSampleRate + srate, err := drv.GetSampleRate() + if err != nil { + return err + } + fmt.Printf("getSampleRate(): %v\n", srate) + + // canSampleRate + var sampleRate float64 + // for _, canSampleRate := range []int{ + // 48000, + // 44100, + // } { + // sampleRateF64 := float64(canSampleRate) + // err = drv.CanSampleRate(sampleRateF64) + // fmt.Printf("canSampleRate(%q): %v\n", sampleRateF64, err) + // if err != nil { + // continue + // } + // sampleRate = canSampleRate + // break + // } + // if sampleRate == 0 { + // // log.Fatal("Could not negotiate a compatible samplerate") + // // return + // fmt.Println("WARNING: Defaulting to 48000 Hz") + // sampleRate = 48000 + // } + sampleRate = srate + // sampleRate = 48000 + + // SetSampleRate + err = drv.SetSampleRate(sampleRate) + fmt.Printf("setSampleRate(%v): %v\n", float64(sampleRate), err) + if err != nil { + fmt.Println("WARNING: setSampleRate failed, ignoring:", err) + } + + // outputReady + fmt.Printf("outputReady(): %v\n", drv.OutputReady()) + + // open control panel: + // drv.ControlPanel() + + bufferDescriptors := make([]asio.BufferInfo, 0, in+out) + for i := 0; i < in; i++ { + bufferDescriptors = append(bufferDescriptors, asio.BufferInfo{ + Channel: i, + IsInput: true, + }) + cinfo, err := drv.GetChannelInfo(i, true) + if err != nil { + log.Fatal(err) + continue + } + fmt.Printf(" IN%-2d: active=%v, group=%d, type=%d, name=%s\n", i+1, cinfo.IsActive, cinfo.ChannelGroup, cinfo.SampleType, cinfo.Name) + } + for i := 0; i < out; i++ { + bufferDescriptors = append(bufferDescriptors, asio.BufferInfo{ + Channel: i, + IsInput: false, + }) + cinfo, err := drv.GetChannelInfo(i, false) + if err != nil { + log.Fatal(err) + continue + } + fmt.Printf("OUT%-2d: active=%v, group=%d, type=%d, name=%s\n", i+1, cinfo.IsActive, cinfo.ChannelGroup, cinfo.SampleType, cinfo.Name) + } + + err = drv.CreateBuffers(bufferDescriptors, 512, asio.Callbacks{ + Message: func(selector, value int32, message uintptr, opt *float64) int32 { + log.Println("Message:", selector, value, message, opt) + return 0 + }, + BufferSwitch: func(doubleBufferIndex int, directProcess bool) { + log.Println("Buffer switch:", doubleBufferIndex, directProcess) + }, + BufferSwitchTimeInfo: func(params *asio.ASIOTime, doubleBufferIndex int32, directProcess bool) *asio.ASIOTime { + log.Println("Buffer switch time info:", params, doubleBufferIndex, directProcess) + return params + }, + SampleRateDidChange: func(rate float64) { + log.Println("Sample rate did change:", rate) + }, + }) + if err != nil { + return err + } + defer fmt.Printf("disposeBuffers()\n") + defer drv.DisposeBuffers() + fmt.Printf("createBuffers()\n") + + // getLatencies + latin, latout, err := drv.GetLatencies() + if err != nil { + return err + } + fmt.Printf("getLatencies(): %d, %d\n", latin, latout) + + err = drv.Start() + if err != nil { + return err + } + defer drv.Stop() + + c := make(chan os.Signal, 1) + go signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + output := new(strings.Builder) + ladder := " ▁▂▃▄▅▆▇█" + chars := []rune(reverse(ladder) + ladder) + grace := 5 * time.Second + lastSignalTime := time.Now() + for { + select { + case <-time.After(33 * time.Millisecond): + output.Reset() + for _, desc := range bufferDescriptors { + for _, buf := range desc.Buffers { + output.WriteString("|") + if buf == nil { + output.WriteString("?") + continue + } + if *buf != 0 { + lastSignalTime = time.Now() + } + output.WriteString(fmt.Sprintf("%c", + chars[len(chars)/2+ + int(float64(len(chars))/2* + float64(*buf)/float64(math.MaxInt32))])) + } + } + os.Stdout.WriteString(output.String() + "\r") + if time.Now().Sub(lastSignalTime) > time.Second { + os.Stdout.WriteString("Silence!\r") + } + if time.Now().Sub(lastSignalTime) > grace { + os.Stdout.WriteString("\n") + log.Println("Restarting audio engine...") + cmd := exec.Command(`C:\Program Files (x86)\VB\Voicemeeter\voicemeeterpro.exe`, "-r") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + log.Printf("Restart audio engine result: %v", cmd.Run()) + // time.Sleep(3 * time.Second) + // lastSignalTime = time.Now() + return errors.New("audio engine restarted, retry") + } + case <-c: + return nil + } + } +} diff --git a/icedreammusic/compile-win.sh b/icedreammusic/compile-win.sh index fa79999..e40773e 100644 --- a/icedreammusic/compile-win.sh +++ b/icedreammusic/compile-win.sh @@ -26,7 +26,7 @@ then rm winfsp.zip fi -for bin in foobar2000 tunadish tunaposter prime4 +for bin in auto-restart-voicemeeter foobar2000 tunadish tunaposter prime4 do cd "$SCRIPT_DIR/$bin" go build -ldflags "-s -w" -o "$GOBIN/$bin$ext" -v