From 94251d47d73ae0491541fe9dbcf9c33c5a4244df Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Tue, 10 Dec 2024 17:56:03 +0000 Subject: [PATCH 01/10] bring back the osc lib for go learning reasons --- pkg/osc/osc.go | 267 ++++++++++++++++++++++++++++++++++++++++++++ pkg/osc/osc_test.go | 67 +++++++++++ 2 files changed, 334 insertions(+) create mode 100644 pkg/osc/osc.go create mode 100644 pkg/osc/osc_test.go diff --git a/pkg/osc/osc.go b/pkg/osc/osc.go new file mode 100644 index 0000000..3475554 --- /dev/null +++ b/pkg/osc/osc.go @@ -0,0 +1,267 @@ +package osc + +// TODO(jwetzell): split things up +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math" + "strings" +) + +type OSCArg struct { + Type string + Value any +} + +type OSCMessage struct { + Address string + Args []OSCArg +} + +func stringToOSCBytes(rawString string) []byte { + var sb strings.Builder + + sb.WriteString(rawString) + sb.WriteString("\u0000") + + padLength := 4 - (len(sb.String()) % 4) + if padLength < 4 { + for i := 0; i < padLength; i++ { + sb.WriteString("\u0000") + } + } + + return []byte(sb.String()) +} + +func integerToOSCBytes(number int32) []byte { + var buf bytes.Buffer + err := binary.Write(&buf, binary.BigEndian, number) + if err != nil { + panic(err) + } + return buf.Bytes() +} + +func floatToOSCBytes(number float32) []byte { + var buf bytes.Buffer + err := binary.Write(&buf, binary.BigEndian, number) + if err != nil { + panic(err) + } + return buf.Bytes() +} + +func byteArrayToOSCBytes(bytes []byte) []byte { + oscBytes := []byte{} + + bytesSize := len(bytes) + oscBytes = append(oscBytes, integerToOSCBytes(int32(bytesSize))...) + oscBytes = append(oscBytes, bytes...) + + padLength := 4 - (bytesSize % 4) + if padLength < 4 { + for i := 0; i < padLength; i++ { + oscBytes = append(oscBytes, 0) + } + } + + return oscBytes +} + +func argsToBuffer(args []OSCArg) []byte { + //TODO(jwetzell): add error handling + var argBuffers = []byte{} + + for _, arg := range args { + switch oscType := arg.Type; oscType { + case "s": + if value, ok := arg.Value.(string); ok { + argBuffers = append(argBuffers, stringToOSCBytes(value)...) + } else { + fmt.Println("OSC arg had string type but non-string value.") + } + case "i": + if value, ok := arg.Value.(int); ok { + argBuffers = append(argBuffers, integerToOSCBytes(int32(value))...) + } else if value, ok := arg.Value.(int32); ok { + argBuffers = append(argBuffers, integerToOSCBytes(value)...) + } else { + fmt.Println("OSC arg had integer type but non-integer value.") + } + case "f": + if value, ok := arg.Value.(float32); ok { + argBuffers = append(argBuffers, floatToOSCBytes(float32(value))...) + } else if value, ok := arg.Value.(float64); ok { + argBuffers = append(argBuffers, floatToOSCBytes(float32(value))...) + } else { + fmt.Println("OSC arg had float type but non-float value.") + } + case "b": + if value, ok := arg.Value.([]byte); ok { + argBuffers = append(argBuffers, byteArrayToOSCBytes(value)...) + } else { + fmt.Println("OSC arg had blob type but non-blob value.") + } + default: + fmt.Printf("unhandled osc type: %s.\n", oscType) + } + } + return argBuffers +} + +func ToBytes(message OSCMessage) []byte { + //TODO(jwetzell): add error handling + oscBuffer := []byte{} + + oscBuffer = append(oscBuffer, stringToOSCBytes(message.Address)...) + + var sb strings.Builder + + sb.WriteString(",") + + for _, arg := range message.Args { + sb.WriteString(arg.Type) + } + + oscBuffer = append(oscBuffer, stringToOSCBytes(sb.String())...) + oscBuffer = append(oscBuffer, argsToBuffer(message.Args)...) + + return oscBuffer +} + +func readOSCString(bytes []byte) (string, []byte) { + //TODO(jwetzell): add error handling + oscString := "" + stringFinished := false + stringEndIndex := 0 + remainingBytes := []byte{} + + for index, byteIn := range bytes { + if !stringFinished { + if byteIn == 0 { + oscString = string(bytes[0:index]) + stringEndIndex = index + 1 + break + } + } + } + + stringPadding := 4 - (stringEndIndex % 4) + + if stringPadding < 4 { + stringEndIndex = stringEndIndex + stringPadding + } + + remainingBytes = bytes[stringEndIndex:] + + return oscString, remainingBytes +} + +func readOSCInt(bytes []byte) (int32, []byte, error) { + if len(bytes) < 4 { + return 0, bytes, errors.New("int data must be at least 4 bytes large") + } + bits := binary.BigEndian.Uint32(bytes[0:4]) + return int32(bits), bytes[4:], nil +} + +func readOSCFloat(bytes []byte) (float32, []byte, error) { + if len(bytes) < 4 { + return 0, bytes, errors.New("float data must be at least 4 bytes large") + } + bits := binary.BigEndian.Uint32(bytes[0:4]) + return math.Float32frombits(bits), bytes[4:], nil +} + +func readOSCBlob(bytes []byte) ([]byte, []byte, error) { + blobLength, remainingBytes, err := readOSCInt(bytes) + + if err != nil { + return []byte{}, bytes, errors.New("problem reading blob data size") + } + + if len(remainingBytes) < int(blobLength) { + return []byte{}, bytes, errors.New("blob data specified a size larger than the remaining message data") + } + + blobLengthPadding := 4 - (blobLength % 4) + blobEnd := 4 + blobLength + + if blobLengthPadding < 4 { + blobEnd = blobEnd + blobLengthPadding + } + return bytes[4 : 4+blobLength], bytes[blobEnd:], nil +} + +func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) { + var readArgError error + + oscArg := OSCArg{} + oscArg.Type = oscType + + remainingBytes := []byte{} + //TODO(jwetzell): add error handling + switch oscType { + case "s": + argString, bytesLeft := readOSCString(bytes) + oscArg.Value = argString + remainingBytes = bytesLeft + case "i": + argInt, bytesLeft, error := readOSCInt(bytes) + if error != nil { + readArgError = error + } + oscArg.Value = argInt + remainingBytes = bytesLeft + case "f": + argFloat, bytesLeft, error := readOSCFloat(bytes) + if error != nil { + readArgError = error + } + oscArg.Value = argFloat + remainingBytes = bytesLeft + case "b": + argBytes, bytesLeft, error := readOSCBlob(bytes) + if error != nil { + readArgError = error + } + oscArg.Value = argBytes + remainingBytes = bytesLeft + default: + fmt.Printf("unsupported osc type: %s\n", oscType) + readArgError = errors.New("unsupported osc type: " + oscType) + } + return oscArg, remainingBytes, readArgError +} + +func FromBytes(bytes []byte) (OSCMessage, error) { + //TODO(jwetzell): add Message and Bundle support + address, typeAndArgBytes := readOSCString(bytes) + + oscMessage := OSCMessage{ + Address: address, + Args: []OSCArg{}, + } + + typeString, argBytes := readOSCString(typeAndArgBytes) + + for index, oscType := range typeString { + if index == 0 { + if oscType != ',' { + return OSCMessage{}, errors.New("type string is malformed") + } + } else { + oscArg, remainingBytes, error := readOSCArg(argBytes, string(oscType)) + if error != nil { + return oscMessage, error + } + argBytes = remainingBytes + oscMessage.Args = append(oscMessage.Args, oscArg) + } + } + + return oscMessage, nil +} diff --git a/pkg/osc/osc_test.go b/pkg/osc/osc_test.go new file mode 100644 index 0000000..d5d42fa --- /dev/null +++ b/pkg/osc/osc_test.go @@ -0,0 +1,67 @@ +package osc + +import ( + "fmt" + "reflect" + "testing" +) + +// TestHelloName calls greetings.Hello with a name, checking +// for a valid return value. +func TestOSCEncoding(t *testing.T) { + + testCases := []struct { + description string + message OSCMessage + expected []byte + }{ + { + "simple hello", + OSCMessage{ + Address: "/hello", + Args: []OSCArg{}, + }, + []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 0, 0, 0}, + }, + { + "simple address string arg", + OSCMessage{ + Address: "/hello", + Args: []OSCArg{ + { + Type: "s", + Value: "arg1", + }, + }, + }, + []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 115, 0, 0, 97, 114, 103, 49, 0, 0, 0, 0}, + }, + { + description: "simple address integer arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "i", Value: 35}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 105, 0, 0, 0, 0, 0, 35}, + }, + { + description: "simple address float arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "f", Value: 34.5}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 102, 0, 0, 66, 10, 0, 0}, + }, + { + description: "simple address blob arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{OSCArg{Type: "b", Value: []byte{98, 108, 111, 98}}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 98, 0, 0, 0, 0, 0, 4, 98, 108, 111, 98}, + }, + } + + for _, testCase := range testCases { + + actual := ToBytes(testCase.message) + + if !reflect.DeepEqual(actual, testCase.expected) { + t.Errorf("Test '%s' failed to encode properly", testCase.description) + fmt.Printf("expected: %v\n", testCase.expected) + fmt.Printf("actual: %v\n", actual) + } + } + +} From bbdb193d65f5bace4652d73b345e504f9eea081a Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 15 Dec 2024 22:50:32 -0600 Subject: [PATCH 02/10] split types out --- pkg/osc/osc.go | 10 ---------- pkg/osc/types.go | 11 +++++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 pkg/osc/types.go diff --git a/pkg/osc/osc.go b/pkg/osc/osc.go index 3475554..5a88063 100644 --- a/pkg/osc/osc.go +++ b/pkg/osc/osc.go @@ -10,16 +10,6 @@ import ( "strings" ) -type OSCArg struct { - Type string - Value any -} - -type OSCMessage struct { - Address string - Args []OSCArg -} - func stringToOSCBytes(rawString string) []byte { var sb strings.Builder diff --git a/pkg/osc/types.go b/pkg/osc/types.go new file mode 100644 index 0000000..9a80952 --- /dev/null +++ b/pkg/osc/types.go @@ -0,0 +1,11 @@ +package osc + +type OSCArg struct { + Type string + Value any +} + +type OSCMessage struct { + Address string + Args []OSCArg +} From a742219efa1333895466eecf40880663f84e4990 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 15 Dec 2024 22:51:56 -0600 Subject: [PATCH 03/10] add encoding for all types except array --- pkg/osc/osc.go | 76 +++++++++++++++++++++++++++++++++++++++----- pkg/osc/osc_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++- pkg/osc/types.go | 7 +++++ 3 files changed, 152 insertions(+), 8 deletions(-) diff --git a/pkg/osc/osc.go b/pkg/osc/osc.go index 5a88063..99bd21b 100644 --- a/pkg/osc/osc.go +++ b/pkg/osc/osc.go @@ -26,7 +26,7 @@ func stringToOSCBytes(rawString string) []byte { return []byte(sb.String()) } -func integerToOSCBytes(number int32) []byte { +func int32ToOSCBytes(number int32) []byte { var buf bytes.Buffer err := binary.Write(&buf, binary.BigEndian, number) if err != nil { @@ -35,7 +35,25 @@ func integerToOSCBytes(number int32) []byte { return buf.Bytes() } -func floatToOSCBytes(number float32) []byte { +func int64ToOSCBytes(number int64) []byte { + var buf bytes.Buffer + err := binary.Write(&buf, binary.BigEndian, number) + if err != nil { + panic(err) + } + return buf.Bytes() +} + +func float32ToOSCBytes(number float32) []byte { + var buf bytes.Buffer + err := binary.Write(&buf, binary.BigEndian, number) + if err != nil { + panic(err) + } + return buf.Bytes() +} + +func float64ToOSCBytes(number float64) []byte { var buf bytes.Buffer err := binary.Write(&buf, binary.BigEndian, number) if err != nil { @@ -48,7 +66,7 @@ func byteArrayToOSCBytes(bytes []byte) []byte { oscBytes := []byte{} bytesSize := len(bytes) - oscBytes = append(oscBytes, integerToOSCBytes(int32(bytesSize))...) + oscBytes = append(oscBytes, int32ToOSCBytes(int32(bytesSize))...) oscBytes = append(oscBytes, bytes...) padLength := 4 - (bytesSize % 4) @@ -75,17 +93,23 @@ func argsToBuffer(args []OSCArg) []byte { } case "i": if value, ok := arg.Value.(int); ok { - argBuffers = append(argBuffers, integerToOSCBytes(int32(value))...) + argBuffers = append(argBuffers, int32ToOSCBytes(int32(value))...) } else if value, ok := arg.Value.(int32); ok { - argBuffers = append(argBuffers, integerToOSCBytes(value)...) + argBuffers = append(argBuffers, int32ToOSCBytes(value)...) } else { fmt.Println("OSC arg had integer type but non-integer value.") } case "f": if value, ok := arg.Value.(float32); ok { - argBuffers = append(argBuffers, floatToOSCBytes(float32(value))...) + argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) } else if value, ok := arg.Value.(float64); ok { - argBuffers = append(argBuffers, floatToOSCBytes(float32(value))...) + argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + } else if value, ok := arg.Value.(int); ok { + argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + } else if value, ok := arg.Value.(int32); ok { + argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + } else if value, ok := arg.Value.(int64); ok { + argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) } else { fmt.Println("OSC arg had float type but non-float value.") } @@ -95,6 +119,44 @@ func argsToBuffer(args []OSCArg) []byte { } else { fmt.Println("OSC arg had blob type but non-blob value.") } + case "T": + argBuffers = append(argBuffers, make([]byte, 0)...) + case "F": + argBuffers = append(argBuffers, make([]byte, 0)...) + case "N": + argBuffers = append(argBuffers, make([]byte, 0)...) + case "I": + argBuffers = append(argBuffers, make([]byte, 0)...) + case "r": + color, ok := arg.Value.(OSCColor) + if ok { + colorBytes := []byte{color.r, color.g, color.b, color.a} + argBuffers = append(argBuffers, colorBytes...) + } + case "h": + if value, ok := arg.Value.(int); ok { + argBuffers = append(argBuffers, int64ToOSCBytes(int64(value))...) + } else if value, ok := arg.Value.(int32); ok { + argBuffers = append(argBuffers, int64ToOSCBytes(int64(value))...) + } else if value, ok := arg.Value.(int64); ok { + argBuffers = append(argBuffers, int64ToOSCBytes(value)...) + } else { + fmt.Println("OSC arg had integer type but non-integer value.") + } + case "d": + if value, ok := arg.Value.(float32); ok { + argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + } else if value, ok := arg.Value.(float64); ok { + argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + } else if value, ok := arg.Value.(int); ok { + argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + } else if value, ok := arg.Value.(int32); ok { + argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + } else if value, ok := arg.Value.(int64); ok { + argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + } else { + fmt.Println("OSC arg had float type but non-float value.") + } default: fmt.Printf("unhandled osc type: %s.\n", oscType) } diff --git a/pkg/osc/osc_test.go b/pkg/osc/osc_test.go index d5d42fa..6b42c32 100644 --- a/pkg/osc/osc_test.go +++ b/pkg/osc/osc_test.go @@ -48,9 +48,84 @@ func TestOSCEncoding(t *testing.T) { }, { description: "simple address blob arg", - message: OSCMessage{Address: "/hello", Args: []OSCArg{OSCArg{Type: "b", Value: []byte{98, 108, 111, 98}}}}, + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "b", Value: []byte{98, 108, 111, 98}}}}, expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 98, 0, 0, 0, 0, 0, 4, 98, 108, 111, 98}, }, + { + description: "simple address True arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{OSCArg{Type: "T", Value: true}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 84, 0, 0}, + }, + { + description: "simple address False arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "F", Value: false}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 70, 0, 0}, + }, + { + description: "simple address color arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "r", Value: OSCColor{r: 20, g: 21, b: 22, a: 10}}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 114, 0, 0, 20, 21, 22, 10}, + }, + { + description: "simple address nil arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "N", Value: nil}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 78, 0, 0}, + }, + { + description: "simple address int64 arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "h", Value: 281474976710655}}}, + expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 104, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255}, + }, + { + description: "simple address float64 arg", + message: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "d", Value: 12.7654763}}}, + expected: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 100, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6, + }, + }, + // TODO(jwetzell): get array args working working + // { + // description: "simple address array arg", + // message: OSCMessage{ + // Address: "/hello", + // Args: []OSCArg{ + // []OSCArg{ + // {Type: "d", Value: 12.7654763}, + // {Type: "i", Value: 1000}, + // }, + // }, + // }, + // expected: []byte{ + // 47, 104, 101, 108, 108, 111, 0, 0, 44, 91, 100, 105, 93, 0, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6, + // 0, 0, 3, 232, + // }, + // }, + { + description: "osc 1.0 spec example 1", + message: OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: 440}}}, + expected: []byte{ + 47, 111, 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, 44, + 102, 0, 0, 67, 220, 0, 0, + }, + }, + { + description: "osc 1.0 spec example 2", + message: OSCMessage{ + Address: "/foo", + Args: []OSCArg{ + {Type: "i", Value: 1000}, + {Type: "i", Value: -1}, + {Type: "s", Value: "hello"}, + // thanks IEEE 754 + {Type: "f", Value: 1.2339999675750732421875}, + {Type: "f", Value: 5.677999973297119140625}, + }, + }, + expected: []byte{ + 47, 102, 111, 111, 0, 0, 0, 0, 44, 105, 105, 115, 102, 102, 0, 0, 0, 0, 3, 232, 255, 255, 255, 255, 104, 101, 108, + 108, 111, 0, 0, 0, 63, 157, 243, 182, 64, 181, 178, 45, + }, + }, } for _, testCase := range testCases { diff --git a/pkg/osc/types.go b/pkg/osc/types.go index 9a80952..22e7fa8 100644 --- a/pkg/osc/types.go +++ b/pkg/osc/types.go @@ -9,3 +9,10 @@ type OSCMessage struct { Address string Args []OSCArg } + +type OSCColor struct { + r uint8 + g uint8 + b uint8 + a uint8 +} From 3e1449f374040524ad8a6ded6cb28b877264061a Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 15 Dec 2024 22:52:06 -0600 Subject: [PATCH 04/10] add decoding for all types except array --- pkg/osc/osc.go | 72 ++++++++++++++++++++-- pkg/osc/osc_test.go | 145 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 5 deletions(-) diff --git a/pkg/osc/osc.go b/pkg/osc/osc.go index 99bd21b..7ecd500 100644 --- a/pkg/osc/osc.go +++ b/pkg/osc/osc.go @@ -212,7 +212,7 @@ func readOSCString(bytes []byte) (string, []byte) { return oscString, remainingBytes } -func readOSCInt(bytes []byte) (int32, []byte, error) { +func readOSCInt32(bytes []byte) (int32, []byte, error) { if len(bytes) < 4 { return 0, bytes, errors.New("int data must be at least 4 bytes large") } @@ -220,7 +220,15 @@ func readOSCInt(bytes []byte) (int32, []byte, error) { return int32(bits), bytes[4:], nil } -func readOSCFloat(bytes []byte) (float32, []byte, error) { +func readOSCInt64(bytes []byte) (int64, []byte, error) { + if len(bytes) < 8 { + return 0, bytes, errors.New("int data must be at least 4 bytes large") + } + bits := binary.BigEndian.Uint64(bytes[0:8]) + return int64(bits), bytes[8:], nil +} + +func readOSCFloat32(bytes []byte) (float32, []byte, error) { if len(bytes) < 4 { return 0, bytes, errors.New("float data must be at least 4 bytes large") } @@ -228,8 +236,16 @@ func readOSCFloat(bytes []byte) (float32, []byte, error) { return math.Float32frombits(bits), bytes[4:], nil } +func readOSCFloat64(bytes []byte) (float64, []byte, error) { + if len(bytes) < 4 { + return 0, bytes, errors.New("float data must be at least 4 bytes large") + } + bits := binary.BigEndian.Uint64(bytes[0:8]) + return math.Float64frombits(bits), bytes[8:], nil +} + func readOSCBlob(bytes []byte) ([]byte, []byte, error) { - blobLength, remainingBytes, err := readOSCInt(bytes) + blobLength, remainingBytes, err := readOSCInt32(bytes) if err != nil { return []byte{}, bytes, errors.New("problem reading blob data size") @@ -248,6 +264,19 @@ func readOSCBlob(bytes []byte) ([]byte, []byte, error) { return bytes[4 : 4+blobLength], bytes[blobEnd:], nil } +func readOSCColor(bytes []byte) (OSCColor, []byte, error) { + if len(bytes) < 4 { + return OSCColor{0, 0, 0, 0}, bytes, errors.New("color data must be at least 4 bytes large") + } + oscColor := OSCColor{ + r: bytes[0], + g: bytes[1], + b: bytes[2], + a: bytes[3], + } + return oscColor, bytes[4:], nil +} + func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) { var readArgError error @@ -262,14 +291,14 @@ func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) { oscArg.Value = argString remainingBytes = bytesLeft case "i": - argInt, bytesLeft, error := readOSCInt(bytes) + argInt, bytesLeft, error := readOSCInt32(bytes) if error != nil { readArgError = error } oscArg.Value = argInt remainingBytes = bytesLeft case "f": - argFloat, bytesLeft, error := readOSCFloat(bytes) + argFloat, bytesLeft, error := readOSCFloat32(bytes) if error != nil { readArgError = error } @@ -282,6 +311,39 @@ func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) { } oscArg.Value = argBytes remainingBytes = bytesLeft + case "T": + oscArg.Value = true + remainingBytes = bytes + case "F": + oscArg.Value = false + remainingBytes = bytes + case "N": + oscArg.Value = nil + remainingBytes = bytes + case "I": + oscArg.Value = math.MaxInt32 + remainingBytes = bytes + case "r": + argColor, bytesLeft, error := readOSCColor(bytes) + if error != nil { + readArgError = error + } + oscArg.Value = argColor + remainingBytes = bytesLeft + case "h": + argInt, bytesLeft, error := readOSCInt64(bytes) + if error != nil { + readArgError = error + } + oscArg.Value = argInt + remainingBytes = bytesLeft + case "d": + argFloat, bytesLeft, error := readOSCFloat64(bytes) + if error != nil { + readArgError = error + } + oscArg.Value = argFloat + remainingBytes = bytesLeft default: fmt.Printf("unsupported osc type: %s\n", oscType) readArgError = errors.New("unsupported osc type: " + oscType) diff --git a/pkg/osc/osc_test.go b/pkg/osc/osc_test.go index 6b42c32..c6da803 100644 --- a/pkg/osc/osc_test.go +++ b/pkg/osc/osc_test.go @@ -2,6 +2,7 @@ package osc import ( "fmt" + "math" "reflect" "testing" ) @@ -140,3 +141,147 @@ func TestOSCEncoding(t *testing.T) { } } + +func TestOSCDecoding(t *testing.T) { + testCases := []struct { + description string + bytes []byte + expected OSCMessage + }{ + { + description: "simple address no args", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 0, 0, 0}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{}}, + }, + { + description: "simple address string arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 115, 0, 0, 97, 114, 103, 49, 0, 0, 0, 0}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "s", Value: "arg1"}}}, + }, + { + description: "simple address integer arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 105, 0, 0, 0, 0, 0, 35}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "i", Value: int32(35)}}}, + }, + { + description: "simple address float arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 102, 0, 0, 66, 10, 0, 0}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "f", Value: float32(34.5)}}}, + }, + { + description: "simple address blob arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 98, 0, 0, 0, 0, 0, 4, 98, 108, 111, 98}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "b", Value: []byte{98, 108, 111, 98}}}}, + }, + { + description: "simple address True arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 84, 0, 0}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "T", Value: true}}}, + }, + { + description: "simple address False arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 70, 0, 0}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "F", Value: false}}}, + }, + { + description: "simple address color arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 114, 0, 0, 20, 21, 22, 10}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "r", Value: OSCColor{r: 20, g: 21, b: 22, a: 10}}}}, + }, + { + description: "simple address nil arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 78, 0, 0}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "N", Value: nil}}}, + }, + { + description: "simple address Inifinitum arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 73, 0, 0}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "I", Value: math.MaxInt32}}}, + }, + { + description: "simple address int64 arg", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 104, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255}, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "h", Value: int64(281474976710655)}}}, + }, + { + description: "simple address float64 arg", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 100, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6, + }, + expected: OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "d", Value: float64(12.7654763)}}}, + }, + // TODO(jwetzell): support OSC array + // { + // description: "simple address array arg", + // bytes: []byte{ + // 47, 104, 101, 108, 108, 111, 0, 0, 44, 91, 100, 105, 93, 0, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6, + // 0, 0, 3, 232, + // }, + // expected: OSCMessage{ + // Address: "/hello", + // Args: []OSCArg{ + // []OSCArg{ + // {Type: "d", Value: 12.7654763}, + // {Type: "i", Value: 1000}, + // }, + // }, + // }, + // }, + { + description: "simple address no type string", + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0}, + expected: OSCMessage{ + Address: "/hello", + Args: []OSCArg{}, + }, + }, + { + description: "osc 1.0 spec example 1", + bytes: []byte{ + 47, 111, 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, 44, + 102, 0, 0, 67, 220, 0, 0, + }, + expected: OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}, + }, + { + description: "osc 1.0 spec example 2", + bytes: []byte{ + 47, 102, 111, 111, 0, 0, 0, 0, 44, 105, 105, 115, 102, 102, 0, 0, 0, 0, 3, 232, 255, 255, 255, 255, 104, 101, 108, + 108, 111, 0, 0, 0, 63, 157, 243, 182, 64, 181, 178, 45, + }, + expected: OSCMessage{ + Address: "/foo", + Args: []OSCArg{ + {Type: "i", Value: int32(1000)}, + {Type: "i", Value: int32(-1)}, + {Type: "s", Value: "hello"}, + // thanks IEEE 754 + {Type: "f", Value: float32(1.2339999675750732421875)}, + {Type: "f", Value: float32(5.677999973297119140625)}, + }, + }, + }, + } + + for _, testCase := range testCases { + + actual, error := FromBytes(testCase.bytes) + + if error != nil { + fmt.Println(error) + t.Errorf("Test '%s' failed to encode properly", testCase.description) + } + + if !reflect.DeepEqual(actual.Address, testCase.expected.Address) { + t.Errorf("Test '%s' failed to encode address properly", testCase.description) + fmt.Printf("expected: %v\n", testCase.expected.Address) + fmt.Printf("actual: %v\n", actual.Address) + } + + if !reflect.DeepEqual(actual.Args, testCase.expected.Args) { + t.Errorf("Test '%s' failed to encode args properly", testCase.description) + fmt.Printf("expected: %v\n", testCase.expected.Args) + fmt.Printf("actual: %v\n", actual.Args) + } + } +} From 757eb8f4c13012e9ab0a099e88aba01aae24663e Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 15 Dec 2024 23:27:45 -0600 Subject: [PATCH 05/10] split message functions out to separate file --- pkg/osc/message.go | 58 ++++++++++++++++++++++++ pkg/osc/{osc_test.go => message_test.go} | 8 ++-- pkg/osc/osc.go | 49 -------------------- 3 files changed, 62 insertions(+), 53 deletions(-) create mode 100644 pkg/osc/message.go rename pkg/osc/{osc_test.go => message_test.go} (98%) diff --git a/pkg/osc/message.go b/pkg/osc/message.go new file mode 100644 index 0000000..a590d10 --- /dev/null +++ b/pkg/osc/message.go @@ -0,0 +1,58 @@ +package osc + +import ( + "errors" + "strings" +) + +func MessageToBytes(message OSCMessage) []byte { + //TODO(jwetzell): add error handling + oscBuffer := []byte{} + + oscBuffer = append(oscBuffer, stringToOSCBytes(message.Address)...) + + var sb strings.Builder + + sb.WriteString(",") + + for _, arg := range message.Args { + sb.WriteString(arg.Type) + } + + oscBuffer = append(oscBuffer, stringToOSCBytes(sb.String())...) + oscBuffer = append(oscBuffer, argsToBuffer(message.Args)...) + + return oscBuffer +} + +func MessageFromBytes(bytes []byte) (OSCMessage, error) { + address, typeAndArgBytes := readOSCString(bytes) + + if address[0] != 47 { + return OSCMessage{}, errors.New("OSC Message address must start with /") + } + + oscMessage := OSCMessage{ + Address: address, + Args: []OSCArg{}, + } + + typeString, argBytes := readOSCString(typeAndArgBytes) + + for index, oscType := range typeString { + if index == 0 { + if oscType != ',' { + return OSCMessage{}, errors.New("type string is malformed") + } + } else { + oscArg, remainingBytes, error := readOSCArg(argBytes, string(oscType)) + if error != nil { + return oscMessage, error + } + argBytes = remainingBytes + oscMessage.Args = append(oscMessage.Args, oscArg) + } + } + + return oscMessage, nil +} diff --git a/pkg/osc/osc_test.go b/pkg/osc/message_test.go similarity index 98% rename from pkg/osc/osc_test.go rename to pkg/osc/message_test.go index c6da803..c4e6352 100644 --- a/pkg/osc/osc_test.go +++ b/pkg/osc/message_test.go @@ -9,7 +9,7 @@ import ( // TestHelloName calls greetings.Hello with a name, checking // for a valid return value. -func TestOSCEncoding(t *testing.T) { +func TestOSCMessageEncoding(t *testing.T) { testCases := []struct { description string @@ -131,7 +131,7 @@ func TestOSCEncoding(t *testing.T) { for _, testCase := range testCases { - actual := ToBytes(testCase.message) + actual := MessageToBytes(testCase.message) if !reflect.DeepEqual(actual, testCase.expected) { t.Errorf("Test '%s' failed to encode properly", testCase.description) @@ -142,7 +142,7 @@ func TestOSCEncoding(t *testing.T) { } -func TestOSCDecoding(t *testing.T) { +func TestOSCMessageDecoding(t *testing.T) { testCases := []struct { description string bytes []byte @@ -265,7 +265,7 @@ func TestOSCDecoding(t *testing.T) { for _, testCase := range testCases { - actual, error := FromBytes(testCase.bytes) + actual, error := MessageFromBytes(testCase.bytes) if error != nil { fmt.Println(error) diff --git a/pkg/osc/osc.go b/pkg/osc/osc.go index 7ecd500..c655a15 100644 --- a/pkg/osc/osc.go +++ b/pkg/osc/osc.go @@ -164,26 +164,6 @@ func argsToBuffer(args []OSCArg) []byte { return argBuffers } -func ToBytes(message OSCMessage) []byte { - //TODO(jwetzell): add error handling - oscBuffer := []byte{} - - oscBuffer = append(oscBuffer, stringToOSCBytes(message.Address)...) - - var sb strings.Builder - - sb.WriteString(",") - - for _, arg := range message.Args { - sb.WriteString(arg.Type) - } - - oscBuffer = append(oscBuffer, stringToOSCBytes(sb.String())...) - oscBuffer = append(oscBuffer, argsToBuffer(message.Args)...) - - return oscBuffer -} - func readOSCString(bytes []byte) (string, []byte) { //TODO(jwetzell): add error handling oscString := "" @@ -350,32 +330,3 @@ func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) { } return oscArg, remainingBytes, readArgError } - -func FromBytes(bytes []byte) (OSCMessage, error) { - //TODO(jwetzell): add Message and Bundle support - address, typeAndArgBytes := readOSCString(bytes) - - oscMessage := OSCMessage{ - Address: address, - Args: []OSCArg{}, - } - - typeString, argBytes := readOSCString(typeAndArgBytes) - - for index, oscType := range typeString { - if index == 0 { - if oscType != ',' { - return OSCMessage{}, errors.New("type string is malformed") - } - } else { - oscArg, remainingBytes, error := readOSCArg(argBytes, string(oscType)) - if error != nil { - return oscMessage, error - } - argBytes = remainingBytes - oscMessage.Args = append(oscMessage.Args, oscArg) - } - } - - return oscMessage, nil -} From 67920c59214e2b9c28c0e94c0bb0500b739940ec Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 22 Dec 2024 13:37:56 -0600 Subject: [PATCH 06/10] prep for bundle work --- pkg/osc/message.go | 8 ++++---- pkg/osc/message_test.go | 2 +- pkg/osc/types.go | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/osc/message.go b/pkg/osc/message.go index a590d10..403394d 100644 --- a/pkg/osc/message.go +++ b/pkg/osc/message.go @@ -5,22 +5,22 @@ import ( "strings" ) -func MessageToBytes(message OSCMessage) []byte { +func (m *OSCMessage) ToBytes() []byte { //TODO(jwetzell): add error handling oscBuffer := []byte{} - oscBuffer = append(oscBuffer, stringToOSCBytes(message.Address)...) + oscBuffer = append(oscBuffer, stringToOSCBytes(m.Address)...) var sb strings.Builder sb.WriteString(",") - for _, arg := range message.Args { + for _, arg := range m.Args { sb.WriteString(arg.Type) } oscBuffer = append(oscBuffer, stringToOSCBytes(sb.String())...) - oscBuffer = append(oscBuffer, argsToBuffer(message.Args)...) + oscBuffer = append(oscBuffer, argsToBuffer(m.Args)...) return oscBuffer } diff --git a/pkg/osc/message_test.go b/pkg/osc/message_test.go index c4e6352..df56295 100644 --- a/pkg/osc/message_test.go +++ b/pkg/osc/message_test.go @@ -131,7 +131,7 @@ func TestOSCMessageEncoding(t *testing.T) { for _, testCase := range testCases { - actual := MessageToBytes(testCase.message) + actual := testCase.message.ToBytes() if !reflect.DeepEqual(actual, testCase.expected) { t.Errorf("Test '%s' failed to encode properly", testCase.description) diff --git a/pkg/osc/types.go b/pkg/osc/types.go index 22e7fa8..0cdd205 100644 --- a/pkg/osc/types.go +++ b/pkg/osc/types.go @@ -1,5 +1,10 @@ package osc +type OSCPacket interface { + ToBytes() []byte +} + + type OSCArg struct { Type string Value any From fa3c29d0a07f854dda72f3ef6b25e81cb59cb67c Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 22 Dec 2024 13:38:32 -0600 Subject: [PATCH 07/10] bundle encoding --- pkg/osc/bundle.go | 20 ++++++++++++ pkg/osc/bundle_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++ pkg/osc/types.go | 4 +++ 3 files changed, 95 insertions(+) create mode 100644 pkg/osc/bundle.go create mode 100644 pkg/osc/bundle_test.go diff --git a/pkg/osc/bundle.go b/pkg/osc/bundle.go new file mode 100644 index 0000000..326751d --- /dev/null +++ b/pkg/osc/bundle.go @@ -0,0 +1,20 @@ +package osc + +import "encoding/binary" + +func (b *OSCBundle) ToBytes() []byte { + + bytes := stringToOSCBytes("#bundle") + + bytes = binary.BigEndian.AppendUint64(bytes, b.TimeTag) + + for _, packet := range b.Contents { + packetBytes := packet.ToBytes() + packetLength := len(packet.ToBytes()) + + bytes = append(bytes, int32ToOSCBytes(int32(packetLength))...) + bytes = append(bytes, packetBytes...) + } + + return bytes +} diff --git a/pkg/osc/bundle_test.go b/pkg/osc/bundle_test.go new file mode 100644 index 0000000..44fbdd4 --- /dev/null +++ b/pkg/osc/bundle_test.go @@ -0,0 +1,71 @@ +package osc + +import ( + "fmt" + "reflect" + "testing" +) + +func TestOSCBundleEncoding(t *testing.T) { + + testCases := []struct { + description string + message OSCBundle + expected []byte + }{ + { + "simple contents single message", + OSCBundle{ + TimeTag: 0x0000002000000000, + Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}}, + }, + []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 32, 47, 111, + 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, + 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, + 44, 102, 0, 0, 67, 220, 0, 0}, + }, + } + + for _, testCase := range testCases { + + actual := testCase.message.ToBytes() + + if !reflect.DeepEqual(actual, testCase.expected) { + t.Errorf("Test '%s' failed to encode properly", testCase.description) + fmt.Printf("expected: %v\n", testCase.expected) + fmt.Printf("actual: %v\n", actual) + } + } + +} + +func TestOSCBundleDecoding(t *testing.T) { + testCases := []struct { + description string + bytes []byte + expected OSCMessage + }{} + + for _, testCase := range testCases { + + actual, error := MessageFromBytes(testCase.bytes) + + if error != nil { + fmt.Println(error) + t.Errorf("Test '%s' failed to encode properly", testCase.description) + } + + if !reflect.DeepEqual(actual.Address, testCase.expected.Address) { + t.Errorf("Test '%s' failed to encode address properly", testCase.description) + fmt.Printf("expected: %v\n", testCase.expected.Address) + fmt.Printf("actual: %v\n", actual.Address) + } + + if !reflect.DeepEqual(actual.Args, testCase.expected.Args) { + t.Errorf("Test '%s' failed to encode args properly", testCase.description) + fmt.Printf("expected: %v\n", testCase.expected.Args) + fmt.Printf("actual: %v\n", actual.Args) + } + } +} diff --git a/pkg/osc/types.go b/pkg/osc/types.go index 0cdd205..f72ca9a 100644 --- a/pkg/osc/types.go +++ b/pkg/osc/types.go @@ -4,6 +4,10 @@ type OSCPacket interface { ToBytes() []byte } +type OSCBundle struct { + TimeTag uint64 + Contents []OSCPacket +} type OSCArg struct { Type string From fea1d47085934eced92486c3faf9db41ddd7f359 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 22 Dec 2024 13:38:48 -0600 Subject: [PATCH 08/10] cleanup comments --- pkg/osc/message_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/osc/message_test.go b/pkg/osc/message_test.go index df56295..053292e 100644 --- a/pkg/osc/message_test.go +++ b/pkg/osc/message_test.go @@ -7,8 +7,6 @@ import ( "testing" ) -// TestHelloName calls greetings.Hello with a name, checking -// for a valid return value. func TestOSCMessageEncoding(t *testing.T) { testCases := []struct { From b71ee95364ad86d7fe6d829a00ed6947f8545f7b Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 22 Dec 2024 14:16:57 -0600 Subject: [PATCH 09/10] add osc timetag --- pkg/osc/osc.go | 24 ++++++++++++++++++++++++ pkg/osc/types.go | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/osc/osc.go b/pkg/osc/osc.go index c655a15..c8618c7 100644 --- a/pkg/osc/osc.go +++ b/pkg/osc/osc.go @@ -79,6 +79,13 @@ func byteArrayToOSCBytes(bytes []byte) []byte { return oscBytes } +func timeTagToOSCBytes(timeTag OSCTimeTag) []byte { + timeTagBytes := int32ToOSCBytes(timeTag.seconds) + timeTagBytes = append(timeTagBytes, int32ToOSCBytes(timeTag.fractionalSeconds)...) + + return timeTagBytes +} + func argsToBuffer(args []OSCArg) []byte { //TODO(jwetzell): add error handling var argBuffers = []byte{} @@ -256,6 +263,23 @@ func readOSCColor(bytes []byte) (OSCColor, []byte, error) { } return oscColor, bytes[4:], nil } +func readOSCTimeTag(bytes []byte) (OSCTimeTag, []byte, error) { + seconds, bytesAfterSeconds, err := readOSCInt32(bytes) + if err != nil { + return OSCTimeTag{}, bytes, err + } + fractionalSeconds, remainingBytes, err := readOSCInt32(bytesAfterSeconds) + if err != nil { + return OSCTimeTag{}, bytes, err + } + + return OSCTimeTag{ + seconds: seconds, + fractionalSeconds: fractionalSeconds, + }, + remainingBytes, + nil +} func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) { var readArgError error diff --git a/pkg/osc/types.go b/pkg/osc/types.go index f72ca9a..7f91dc5 100644 --- a/pkg/osc/types.go +++ b/pkg/osc/types.go @@ -5,7 +5,7 @@ type OSCPacket interface { } type OSCBundle struct { - TimeTag uint64 + TimeTag OSCTimeTag Contents []OSCPacket } @@ -25,3 +25,8 @@ type OSCColor struct { b uint8 a uint8 } + +type OSCTimeTag struct { + seconds int32 + fractionalSeconds int32 +} From 676644a4576480bb040685de6022564a3b3c3759 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 22 Dec 2024 14:17:14 -0600 Subject: [PATCH 10/10] add osc bundle encoding with proper timetag --- pkg/osc/bundle.go | 79 ++++++++++++++++++++++++++++++++++++++++-- pkg/osc/bundle_test.go | 46 ++++++++++++++++-------- 2 files changed, 109 insertions(+), 16 deletions(-) diff --git a/pkg/osc/bundle.go b/pkg/osc/bundle.go index 326751d..dba4ff4 100644 --- a/pkg/osc/bundle.go +++ b/pkg/osc/bundle.go @@ -1,12 +1,14 @@ package osc -import "encoding/binary" +import ( + "errors" +) func (b *OSCBundle) ToBytes() []byte { bytes := stringToOSCBytes("#bundle") - bytes = binary.BigEndian.AppendUint64(bytes, b.TimeTag) + bytes = append(bytes, timeTagToOSCBytes(b.TimeTag)...) for _, packet := range b.Contents { packetBytes := packet.ToBytes() @@ -18,3 +20,76 @@ func (b *OSCBundle) ToBytes() []byte { return bytes } + +func BundleFromBytes(bytes []byte) (OSCBundle, []byte, error) { + if len(bytes) < 20 { + return OSCBundle{}, bytes, errors.New("bundle has to be at least 20 bytes") + } + + if bytes[0] != 35 { + return OSCBundle{}, bytes, errors.New("bundle must start with a #") + } + + bundleHeader, bytesAfterBundleHeader := readOSCString(bytes) + + if bundleHeader != "#bundle" { + return OSCBundle{}, bytesAfterBundleHeader, errors.New("bundle must start with #bundle string") + } + + timeTag, bytesAfterTimeTag, err := readOSCTimeTag(bytesAfterBundleHeader) + + if err != nil { + return OSCBundle{}, bytesAfterBundleHeader, err + } + + bundleContents := []OSCPacket{} + + endOfBundle := false + + remainingBytes := bytesAfterTimeTag + + for !endOfBundle { + contentSize, bytesAfterContentSize, err := readOSCInt32(remainingBytes) + + if err != nil { + return OSCBundle{}, remainingBytes, err + } + + remainingBytes = bytesAfterContentSize + + if len(remainingBytes) < int(contentSize) { + return OSCBundle{}, remainingBytes, errors.New("bundle doesn't have enough bytes for the content size it specifies") + } + + bundleContentBytes := remainingBytes[0:contentSize] + + if bundleContentBytes[0] == 35 { + content, _, err := BundleFromBytes(bundleContentBytes) + if err != nil { + return OSCBundle{}, remainingBytes, err + } + bundleContents = append(bundleContents, &content) + } else if bundleContentBytes[0] == 47 { + content, err := MessageFromBytes(bundleContentBytes) + if err != nil { + return OSCBundle{}, remainingBytes, err + } + bundleContents = append(bundleContents, &content) + } else { + return OSCBundle{}, remainingBytes, errors.New("bundle contents does not look a bundle or message") + } + remainingBytes = bytesAfterContentSize[contentSize:] + if len(remainingBytes) == 0 { + endOfBundle = true + } + + } + + return OSCBundle{ + TimeTag: timeTag, + Contents: bundleContents, + }, + remainingBytes, + nil + +} diff --git a/pkg/osc/bundle_test.go b/pkg/osc/bundle_test.go index 44fbdd4..5301973 100644 --- a/pkg/osc/bundle_test.go +++ b/pkg/osc/bundle_test.go @@ -10,13 +10,16 @@ func TestOSCBundleEncoding(t *testing.T) { testCases := []struct { description string - message OSCBundle + bundle OSCBundle expected []byte }{ { "simple contents single message", OSCBundle{ - TimeTag: 0x0000002000000000, + TimeTag: OSCTimeTag{ + seconds: 32, + fractionalSeconds: 0, + }, Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}}, }, []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, @@ -29,7 +32,7 @@ func TestOSCBundleEncoding(t *testing.T) { for _, testCase := range testCases { - actual := testCase.message.ToBytes() + actual := testCase.bundle.ToBytes() if !reflect.DeepEqual(actual, testCase.expected) { t.Errorf("Test '%s' failed to encode properly", testCase.description) @@ -43,29 +46,44 @@ func TestOSCBundleEncoding(t *testing.T) { func TestOSCBundleDecoding(t *testing.T) { testCases := []struct { description string + expected OSCBundle bytes []byte - expected OSCMessage - }{} + }{ + { + "simple contents single message", + OSCBundle{ + TimeTag: OSCTimeTag{ + seconds: 32, + fractionalSeconds: 0, + }, + Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}}, + }, + []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 32, 47, 111, + 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, + 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, + 44, 102, 0, 0, 67, 220, 0, 0}, + }, + } for _, testCase := range testCases { - actual, error := MessageFromBytes(testCase.bytes) + actual, remainingBytes, error := BundleFromBytes(testCase.bytes) if error != nil { fmt.Println(error) t.Errorf("Test '%s' failed to encode properly", testCase.description) } - if !reflect.DeepEqual(actual.Address, testCase.expected.Address) { - t.Errorf("Test '%s' failed to encode address properly", testCase.description) - fmt.Printf("expected: %v\n", testCase.expected.Address) - fmt.Printf("actual: %v\n", actual.Address) + if len(remainingBytes) > 0 { + t.Errorf("Test '%s' should not have any remaining bytes", testCase.description) } - if !reflect.DeepEqual(actual.Args, testCase.expected.Args) { - t.Errorf("Test '%s' failed to encode args properly", testCase.description) - fmt.Printf("expected: %v\n", testCase.expected.Args) - fmt.Printf("actual: %v\n", actual.Args) + if !reflect.DeepEqual(actual, testCase.expected) { + t.Errorf("Test '%s' failed to encode bundle properly", testCase.description) + fmt.Printf("expected: %v\n", testCase.expected) + fmt.Printf("actual: %v\n", actual) } + } }