From 6c53cfa3834ad253d8d72a9d613f5d365d2e114d Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Sun, 3 Nov 2024 15:46:14 +0000 Subject: [PATCH] reorganise some of the internals of the package. functional options added. --- client.go | 48 +++++++++++++++++++++++++++++++++++++ errors.go | 9 +++++++ option.go | 35 +++++++++++++++++++++++++++ packet.go | 70 +++++++++++++++++++++++++++++++++++------------------- vbantxt.go | 59 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 24 deletions(-) create mode 100644 client.go create mode 100644 errors.go create mode 100644 option.go create mode 100644 vbantxt.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..60cda16 --- /dev/null +++ b/client.go @@ -0,0 +1,48 @@ +package vbantxt + +import ( + "fmt" + "net" + + log "github.com/sirupsen/logrus" +) + +// client represents the UDP client +type client struct { + conn *net.UDPConn +} + +// NewClient returns a UDP client +func newClient(host string, port int) (client, error) { + udpAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", host, port)) + if err != nil { + return client{}, err + } + conn, err := net.DialUDP("udp4", nil, udpAddr) + if err != nil { + return client{}, err + } + log.Infof("Outgoing address %s", conn.RemoteAddr()) + + return client{conn: conn}, nil +} + +// Write implements the io.WriteCloser interface +func (c client) Write(buf []byte) (int, error) { + n, err := c.conn.Write(buf) + if err != nil { + return 0, err + } + log.Debugf("Sending '%s' to: %s", string(buf), c.conn.RemoteAddr()) + + return n, nil +} + +// Close implements the io.WriteCloser interface +func (c client) Close() error { + err := c.conn.Close() + if err != nil { + return err + } + return nil +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..edb35d4 --- /dev/null +++ b/errors.go @@ -0,0 +1,9 @@ +package vbantxt + +// Error is used to define sentinel errors. +type Error string + +// Error implements the error interface. +func (r Error) Error() string { + return string(r) +} diff --git a/option.go b/option.go new file mode 100644 index 0000000..af999ba --- /dev/null +++ b/option.go @@ -0,0 +1,35 @@ +package vbantxt + +import ( + "time" + + log "github.com/sirupsen/logrus" +) + +// Option is a functional option type that allows us to configure the VbanTxt. +type Option func(*VbanTxt) + +// WithRateLimit is a functional option to set the ratelimit for requests +func WithRateLimit(ratelimit time.Duration) Option { + return func(vt *VbanTxt) { + vt.ratelimit = ratelimit + } +} + +// WithBPSOpt is a functional option to set the bps index for {VbanTx}.{Packet}.bpsIndex +func WithBPSOpt(bpsIndex int) Option { + return func(vt *VbanTxt) { + if bpsIndex < 0 || bpsIndex >= len(BpsOpts) { + log.Warnf("invalid bpsIndex %d, defaulting to 0", bpsIndex) + return + } + vt.packet.bpsIndex = bpsIndex + } +} + +// WithChannel is a functional option to set the bps index for {VbanTx}.{Packet}.channel +func WithChannel(channel int) Option { + return func(vt *VbanTxt) { + vt.packet.channel = channel + } +} diff --git a/packet.go b/packet.go index b25a54f..a395b18 100644 --- a/packet.go +++ b/packet.go @@ -1,50 +1,72 @@ -package main +package vbantxt -var r *requestHeader +import ( + "encoding/binary" -const VBAN_PROTOCOL_TXT = 0x40 + log "github.com/sirupsen/logrus" +) -// requestHeader represents a single request header -type requestHeader struct { +const ( + vbanProtocolTxt = 0x40 + streamNameSz = 16 + headerSz = 4 + 1 + 1 + 1 + 1 + 16 + 4 +) + +var BpsOpts = []int{0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250, + 38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600, + 1000000, 1500000, 2000000, 3000000} + +type packet struct { name string bpsIndex int channel int framecounter []byte } -// newRequestHeader returns a pointer to a requestHeader struct as a singleton -func newRequestHeader(streamname string, bpsI, channel int) *requestHeader { - if r != nil { - return r +// newPacket returns a packet struct with default values, framecounter at 0. +func newPacket(streamname string) packet { + return packet{ + name: streamname, + bpsIndex: 0, + channel: 0, + framecounter: make([]byte, 4), } - return &requestHeader{streamname, bpsI, channel, make([]byte, 4)} } // sr defines the samplerate for the request -func (r *requestHeader) sr() byte { - return byte(VBAN_PROTOCOL_TXT + r.bpsIndex) +func (p *packet) sr() byte { + return byte(vbanProtocolTxt + p.bpsIndex) } // nbc defines the channel of the request -func (r *requestHeader) nbc() byte { - return byte(r.channel) +func (p *packet) nbc() byte { + return byte(p.channel) } // streamname defines the stream name of the text request -func (r *requestHeader) streamname() []byte { - b := make([]byte, 16) - copy(b, r.name) +func (p *packet) streamname() []byte { + b := make([]byte, streamNameSz) + copy(b, p.name) return b } -// header returns a fully formed text request packet header -func (t *requestHeader) header() []byte { - h := []byte("VBAN") - h = append(h, t.sr()) +// header returns a fully formed packet header +func (p *packet) header() []byte { + h := make([]byte, 0, headerSz) + h = append(h, []byte("VBAN")...) + h = append(h, p.sr()) h = append(h, byte(0)) - h = append(h, t.nbc()) + h = append(h, p.nbc()) h = append(h, byte(0x10)) - h = append(h, t.streamname()...) - h = append(h, t.framecounter...) + h = append(h, p.streamname()...) + h = append(h, p.framecounter...) return h } + +// bumpFrameCounter increments the frame counter by 1 +func (p *packet) bumpFrameCounter() { + x := binary.LittleEndian.Uint32(p.framecounter) + binary.LittleEndian.PutUint32(p.framecounter, x+1) + + log.Tracef("framecounter: %d", x) +} diff --git a/vbantxt.go b/vbantxt.go new file mode 100644 index 0000000..845abce --- /dev/null +++ b/vbantxt.go @@ -0,0 +1,59 @@ +package vbantxt + +import ( + "fmt" + "io" + "time" +) + +// VbanTxt is used to send VBAN-TXT requests to a distant Voicemeeter/Matrix. +type VbanTxt struct { + client io.WriteCloser + packet packet + ratelimit time.Duration +} + +// New constructs a fully formed VbanTxt instance. This is the package's entry point. +// It sets default values for it's fields and then runs the option functions. +func New(host string, port int, streamname string, options ...Option) (*VbanTxt, error) { + client, err := newClient(host, port) + if err != nil { + return nil, fmt.Errorf("error creating UDP client for (%s:%d): %w", host, port, err) + } + + vt := &VbanTxt{ + client: client, + packet: newPacket(streamname), + ratelimit: time.Duration(20) * time.Millisecond, + } + + for _, o := range options { + o(vt) + } + + return vt, nil +} + +// Send is resonsible for firing each VBAN-TXT request. +// It waits for {vt.ratelimit} time before returning. +func (vt VbanTxt) Send(cmd string) error { + _, err := vt.client.Write(append(vt.packet.header(), []byte(cmd)...)) + if err != nil { + return fmt.Errorf("error sending command (%s): %w", cmd, err) + } + + vt.packet.bumpFrameCounter() + + time.Sleep(vt.ratelimit) + + return nil +} + +// Close is responsible for closing the UDP Client connection +func (vt VbanTxt) Close() error { + err := vt.client.Close() + if err != nil { + return fmt.Errorf("error attempting to close UDP Client: %w", err) + } + return nil +}