244 lines
6.0 KiB
Go
244 lines
6.0 KiB
Go
|
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
|
||
|
}
|
||
|
}
|
||
|
}
|