diff --git a/pkg/osc/bundle.go b/pkg/osc/bundle.go new file mode 100644 index 0000000..dba4ff4 --- /dev/null +++ b/pkg/osc/bundle.go @@ -0,0 +1,95 @@ +package osc + +import ( + "errors" +) + +func (b *OSCBundle) ToBytes() []byte { + + bytes := stringToOSCBytes("#bundle") + + bytes = append(bytes, timeTagToOSCBytes(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 +} + +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 new file mode 100644 index 0000000..5301973 --- /dev/null +++ b/pkg/osc/bundle_test.go @@ -0,0 +1,89 @@ +package osc + +import ( + "fmt" + "reflect" + "testing" +) + +func TestOSCBundleEncoding(t *testing.T) { + + testCases := []struct { + description string + bundle OSCBundle + expected []byte + }{ + { + "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 := testCase.bundle.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 + expected OSCBundle + bytes []byte + }{ + { + "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, remainingBytes, error := BundleFromBytes(testCase.bytes) + + if error != nil { + fmt.Println(error) + t.Errorf("Test '%s' failed to encode properly", testCase.description) + } + + if len(remainingBytes) > 0 { + t.Errorf("Test '%s' should not have any remaining bytes", testCase.description) + } + + 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) + } + + } +} diff --git a/pkg/osc/message.go b/pkg/osc/message.go new file mode 100644 index 0000000..403394d --- /dev/null +++ b/pkg/osc/message.go @@ -0,0 +1,58 @@ +package osc + +import ( + "errors" + "strings" +) + +func (m *OSCMessage) ToBytes() []byte { + //TODO(jwetzell): add error handling + oscBuffer := []byte{} + + oscBuffer = append(oscBuffer, stringToOSCBytes(m.Address)...) + + var sb strings.Builder + + sb.WriteString(",") + + for _, arg := range m.Args { + sb.WriteString(arg.Type) + } + + oscBuffer = append(oscBuffer, stringToOSCBytes(sb.String())...) + oscBuffer = append(oscBuffer, argsToBuffer(m.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/message_test.go b/pkg/osc/message_test.go new file mode 100644 index 0000000..053292e --- /dev/null +++ b/pkg/osc/message_test.go @@ -0,0 +1,285 @@ +package osc + +import ( + "fmt" + "math" + "reflect" + "testing" +) + +func TestOSCMessageEncoding(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{{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 { + + 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 TestOSCMessageDecoding(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 := 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/osc.go b/pkg/osc/osc.go new file mode 100644 index 0000000..c8618c7 --- /dev/null +++ b/pkg/osc/osc.go @@ -0,0 +1,356 @@ +package osc + +// TODO(jwetzell): split things up +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math" + "strings" +) + +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 int32ToOSCBytes(number int32) []byte { + var buf bytes.Buffer + err := binary.Write(&buf, binary.BigEndian, number) + if err != nil { + panic(err) + } + return buf.Bytes() +} + +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 { + panic(err) + } + return buf.Bytes() +} + +func byteArrayToOSCBytes(bytes []byte) []byte { + oscBytes := []byte{} + + bytesSize := len(bytes) + oscBytes = append(oscBytes, int32ToOSCBytes(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 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{} + + 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, int32ToOSCBytes(int32(value))...) + } else if value, ok := arg.Value.(int32); ok { + 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, float32ToOSCBytes(float32(value))...) + } else if value, ok := arg.Value.(float64); ok { + 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.") + } + 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.") + } + 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) + } + } + return argBuffers +} + +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 readOSCInt32(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 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") + } + bits := binary.BigEndian.Uint32(bytes[0:4]) + 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 := readOSCInt32(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 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 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 + + 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 := readOSCInt32(bytes) + if error != nil { + readArgError = error + } + oscArg.Value = argInt + remainingBytes = bytesLeft + case "f": + argFloat, bytesLeft, error := readOSCFloat32(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 + 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) + } + return oscArg, remainingBytes, readArgError +} diff --git a/pkg/osc/types.go b/pkg/osc/types.go new file mode 100644 index 0000000..7f91dc5 --- /dev/null +++ b/pkg/osc/types.go @@ -0,0 +1,32 @@ +package osc + +type OSCPacket interface { + ToBytes() []byte +} + +type OSCBundle struct { + TimeTag OSCTimeTag + Contents []OSCPacket +} + +type OSCArg struct { + Type string + Value any +} + +type OSCMessage struct { + Address string + Args []OSCArg +} + +type OSCColor struct { + r uint8 + g uint8 + b uint8 + a uint8 +} + +type OSCTimeTag struct { + seconds int32 + fractionalSeconds int32 +}