Compare commits

...

88 Commits

Author SHA1 Message Date
Joel Wetzell d82919e9f1 remove codecov badge 2026-04-13 22:59:44 -05:00
Joel Wetzell 4078efa090 update readme 2026-04-13 22:56:57 -05:00
Joel Wetzell 53864a35dc Merge pull request #20 from jwetzell/error-handling
add error handling to arg encoding and message/bundle ToBytes()
2026-04-13 22:53:07 -05:00
Joel Wetzell 2ec4be5426 more tests for bad arg parsing 2026-04-13 22:51:52 -05:00
Joel Wetzell 5f5c34c4ea error message formatting 2026-04-13 22:51:43 -05:00
Joel Wetzell 16093f2509 add support for OSC timetag arg 2026-04-13 22:51:29 -05:00
Joel Wetzell 191d74a20a error message formatting 2026-04-13 22:42:39 -05:00
Joel Wetzell 738b87b03f add tests for PacketFromBytes 2026-04-13 22:42:30 -05:00
Joel Wetzell 42b1f9cf8a add error tests for arg decoding in message 2026-04-13 22:42:15 -05:00
Joel Wetzell 16e1ad039e add test for nested bundle 2026-04-13 22:41:56 -05:00
Joel Wetzell eb539dd319 add test for bundle with bad contents 2026-04-13 22:41:43 -05:00
Joel Wetzell 1465663a63 add tests just for arg array to bytes 2026-04-13 22:22:57 -05:00
Joel Wetzell dd430dd380 add layout for testing bundle error cases 2026-04-13 22:22:44 -05:00
Joel Wetzell 9452268042 add layout for testing OSCBundle error cases 2026-04-13 19:11:11 -05:00
Joel Wetzell b621944dc5 error tests for arg values when encoding 2026-04-13 18:59:43 -05:00
Joel Wetzell 8fa8aa3a55 return errors instead of printing 2026-04-13 18:53:35 -05:00
Joel Wetzell 1d2684f446 cleanup test layout 2026-04-13 18:53:21 -05:00
Joel Wetzell 2548771fe3 add error handling for OSC address and bad arg type encoding 2026-04-13 18:48:50 -05:00
Joel Wetzell 4744321d71 add error handling to arg encoding and message/bundle ToBytes() 2026-04-13 18:38:25 -05:00
Joel Wetzell 1130adf046 Merge pull request #19 from jwetzell/message-tests
fix up some OSC string parsing issues and test for them
2026-04-13 18:21:56 -05:00
Joel Wetzell ad3c449149 fix up some OSC string parsing issues and test for them 2026-04-13 18:17:43 -05:00
Joel Wetzell fbd2bf3905 Merge pull request #18 from jwetzell/more-bad-tests
fill out some more error tests for messages
2026-04-13 16:24:11 -05:00
Joel Wetzell e1c8563522 fill out some more error tests for messages 2026-04-13 16:20:55 -05:00
Joel Wetzell e2a8daf013 Merge pull request #17 from jwetzell/coverage-test
cleanup message_test layout
2026-04-13 16:14:18 -05:00
Joel Wetzell baa140297f cleanup message_test layout 2026-04-13 16:12:19 -05:00
Joel Wetzell 8bb3f3fc97 add workflow for tests and code coverage report 2026-04-13 15:57:47 -05:00
Joel Wetzell e70934e705 Merge pull request #16 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.8.0
Bump github.com/urfave/cli/v3 from 3.7.0 to 3.8.0
2026-03-30 12:25:19 -05:00
dependabot[bot] 597b9ad053 Bump github.com/urfave/cli/v3 from 3.7.0 to 3.8.0
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 02:55:56 +00:00
Joel Wetzell 6f141eb742 Merge pull request #15 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.7.0
Bump github.com/urfave/cli/v3 from 3.6.2 to 3.7.0
2026-03-03 10:16:30 -06:00
Joel Wetzell 9bc51c787f add error checking to readstring method 2026-03-02 21:12:48 -06:00
dependabot[bot] 37cc551793 Bump github.com/urfave/cli/v3 from 3.6.2 to 3.7.0
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.6.2 to 3.7.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.6.2...v3.7.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 02:56:24 +00:00
Joel Wetzell 67a1cc083b remove fmt.println 2026-02-04 11:36:16 -06:00
Joel Wetzell 25c159013b fix variable name 2026-01-31 15:52:42 -06:00
Joel Wetzell 5ac60b0685 add support for SLIP and bundles 2026-01-31 15:52:08 -06:00
Joel Wetzell 6251acb852 use switch 2026-01-31 15:49:42 -06:00
Joel Wetzell 5600ef75f5 add method to parse bytes as packet 2026-01-31 15:49:07 -06:00
Joel Wetzell 77c92c5456 Merge pull request #14 from jwetzell/pointers
return pointers from byte parsing
2026-01-31 15:39:51 -06:00
Joel Wetzell 594c51d2a4 return pointers from byte parsing 2026-01-31 15:34:16 -06:00
Joel Wetzell d7c6da5c5b support OSC 1.0 TCP format if SLIP is not enabled 2026-01-28 14:37:23 -06:00
Joel Wetzell 6f3a65322b rework send string 2026-01-28 14:36:59 -06:00
Joel Wetzell e34a589c5f prevent parsing empty byte slice 2026-01-28 14:34:52 -06:00
Joel Wetzell 41112d583b Merge pull request #13 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.6.2
Bump github.com/urfave/cli/v3 from 3.6.1 to 3.6.2
2026-01-28 14:26:26 -06:00
dependabot[bot] 24a0501a4d Bump github.com/urfave/cli/v3 from 3.6.1 to 3.6.2
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.6.1 to 3.6.2.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.6.1...v3.6.2)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 03:08:19 +00:00
Joel Wetzell e06a0a2776 fix slip encoding 2025-12-24 18:46:39 -06:00
Joel Wetzell d1696baa31 Merge pull request #12 from jwetzell/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-12-11 15:19:19 -06:00
dependabot[bot] c0304bf7a6 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:31:39 +00:00
Joel Wetzell d62d65c386 Merge pull request #10 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.6.1
Bump github.com/urfave/cli/v3 from 3.6.0 to 3.6.1
2025-11-20 09:32:02 -06:00
dependabot[bot] f2888cc038 Bump github.com/urfave/cli/v3 from 3.6.0 to 3.6.1
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.6.0...v3.6.1)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 02:22:46 +00:00
jwetzell 24077a77d6 Merge pull request #9 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.6.0
Bump github.com/urfave/cli/v3 from 3.4.1 to 3.6.0
2025-11-14 14:36:32 -06:00
dependabot[bot] 9e515cc28e Bump github.com/urfave/cli/v3 from 3.4.1 to 3.6.0
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.4.1 to 3.6.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.4.1...v3.6.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 02:27:40 +00:00
jwetzell 1ad942e489 fix cmd name and description 2025-10-01 13:07:00 -05:00
jwetzell e748adfa1c Merge pull request #7 from jwetzell/json-support
add json output to receiveosc
2025-10-01 13:03:26 -05:00
jwetzell 74a62f6287 add support for json output to receiveosc 2025-09-30 12:54:38 -05:00
jwetzell a1a550b4bb add json struct tags 2025-09-30 12:54:13 -05:00
jwetzell c039245298 update names of workflows 2025-09-27 10:28:58 -05:00
jwetzell 085bd082b4 Merge pull request #4 from jwetzell/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-09-27 10:27:30 -05:00
jwetzell 0a4eff30b3 update go version 2025-09-27 10:22:45 -05:00
jwetzell 6482158fd6 Merge pull request #6 from jwetzell/urfave-cli
convert from cobra to urfave/cli
2025-09-27 10:20:21 -05:00
jwetzell 6808c36e88 remove cobra 2025-09-27 10:19:46 -05:00
jwetzell 257465d216 convert receiveosc to urfave/cli 2025-09-27 10:19:40 -05:00
jwetzell ce5e65e8f8 switch makeosc to urfave/cli 2025-09-27 09:55:09 -05:00
jwetzell e519f2d53c switch sendosc to urfave/cli 2025-09-27 09:54:58 -05:00
dependabot[bot] 048eb57d5a Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 03:36:57 +00:00
jwetzell 039d4e6f1a explicitly discard opening END when SLIP decoding 2025-05-19 09:23:57 -05:00
jwetzell 87853b8ccb SLIP encoding should be double END-ed 2025-05-19 09:23:06 -05:00
jwetzell d03f032e72 SLIP encoding should be double END-ed 2025-05-19 09:22:39 -05:00
jwetzell 2f4b90fc16 fix name of cmd in makeosc 2025-05-19 09:08:24 -05:00
jwetzell 278435e066 fix name of utility 2025-05-19 09:03:05 -05:00
jwetzell b50dc9969f add github actions to dependabot 2025-05-18 20:13:33 -05:00
jwetzell 30ffe3a965 Merge pull request #3 from jwetzell/dependabot/go_modules/github.com/spf13/cobra-1.9.1
Bump github.com/spf13/cobra from 1.8.1 to 1.9.1
2025-05-18 20:11:59 -05:00
jwetzell bdc1b45a9b add release workflow for receiveosc 2025-05-18 20:08:37 -05:00
jwetzell 518a083d04 add TCP to receiveosc 2025-05-18 20:06:19 -05:00
dependabot[bot] 06e8ca7da3 Bump github.com/spf13/cobra from 1.8.1 to 1.9.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.1 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 00:33:19 +00:00
jwetzell 48ec7908dd switch to using the actual osc library here 2025-05-18 19:31:56 -05:00
jwetzell 8b07ce0a44 Create dependabot.yml 2025-05-18 19:10:13 -05:00
jwetzell 5965c10942 fieldalignment tool 2024-12-26 14:57:40 -06:00
jwetzell 0928352a57 move package to root level 2024-12-26 14:57:19 -06:00
jwetzell 46ea24d25e Merge pull request #2 from jwetzell/feat/osc-lib
basic osc lib
2024-12-22 14:18:34 -06:00
jwetzell 676644a457 add osc bundle encoding with proper timetag 2024-12-22 14:17:14 -06:00
jwetzell b71ee95364 add osc timetag 2024-12-22 14:16:57 -06:00
jwetzell fea1d47085 cleanup comments 2024-12-22 13:38:48 -06:00
jwetzell fa3c29d0a0 bundle encoding 2024-12-22 13:38:32 -06:00
jwetzell 67920c5921 prep for bundle work 2024-12-22 13:37:56 -06:00
jwetzell 757eb8f4c1 split message functions out to separate file 2024-12-15 23:27:45 -06:00
jwetzell 3e1449f374 add decoding for all types except array 2024-12-15 22:52:06 -06:00
jwetzell a742219efa add encoding for all types except array 2024-12-15 22:51:56 -06:00
jwetzell bbdb193d65 split types out 2024-12-15 22:50:32 -06:00
jwetzell 94251d47d7 bring back the osc lib for go learning reasons 2024-12-10 17:56:03 +00:00
19 changed files with 2254 additions and 116 deletions
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
- package-ecosystem: gomod
directory: /
schedule:
interval: "weekly"
+3 -3
View File
@@ -1,4 +1,4 @@
name: "release go binaries for multiple os/arch"
name: "release go binaries for makeosc"
on:
push:
@@ -36,13 +36,13 @@ jobs:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "1.23.1"
goversion: "1.25.1"
project_path: "./cmd/makeosc"
binary_name: "makeosc"
asset_name: makeosc-${{ matrix.goos }}-${{ matrix.goarch }}
+49
View File
@@ -0,0 +1,49 @@
name: "release go binaries for receiveosc"
on:
push:
tags:
- 'receiveosc/*'
permissions:
contents: write
packages: write
jobs:
create-release:
name: Create receiveosc release
runs-on: ubuntu-latest
steps:
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
release-multi:
name: create binaries and upload
needs: create-release
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, windows, darwin]
goarch: ["386", amd64, arm64]
exclude:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v6
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "1.25.1"
project_path: "./cmd/receiveosc"
binary_name: "receiveosc"
asset_name: receiveosc-${{ matrix.goos }}-${{ matrix.goarch }}
release_name: ${{github.ref_name}}
+3 -3
View File
@@ -1,4 +1,4 @@
name: "release go binaries for multiple os/arch"
name: "release go binaries for sendosc"
on:
push:
@@ -36,13 +36,13 @@ jobs:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "1.23.1"
goversion: "1.25.1"
project_path: "./cmd/sendosc"
binary_name: "sendosc"
asset_name: sendosc-${{ matrix.goos }}-${{ matrix.goarch }}
+24
View File
@@ -0,0 +1,24 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run tests
run: go test -v -coverprofile=coverage.txt .
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.txt
+2 -1
View File
@@ -1 +1,2 @@
build
build
coverage*
+6 -4
View File
@@ -1,5 +1,7 @@
A collection of command line OSC utilities written in Go. Mainly an exercise in learning Go.
A mostly complete OSC implementation and collection of command line OSC utilities written in Go. Mainly an exercise in learning Go.
# `sendosc`
# `makeosc`
# `receiveosc`
## Utilities
### `sendosc`
### `makeosc`
### `receiveosc`
+110
View File
@@ -0,0 +1,110 @@
package osc
import (
"errors"
)
func (b *OSCBundle) ToBytes() ([]byte, error) {
bytes := stringToOSCBytes("#bundle")
timeTagBytes, err := timeTagToOSCBytes(b.TimeTag)
if err != nil {
return nil, err
}
bytes = append(bytes, timeTagBytes...)
for _, packet := range b.Contents {
packetBytes, err := packet.ToBytes()
if err != nil {
return nil, err
}
packetLength := len(packetBytes)
packetLengthBytes, err := int32ToOSCBytes(int32(packetLength))
if err != nil {
return nil, err
}
bytes = append(bytes, packetLengthBytes...)
bytes = append(bytes, packetBytes...)
}
return bytes, nil
}
func BundleFromBytes(bytes []byte) (*OSCBundle, []byte, error) {
if len(bytes) < 20 {
return nil, bytes, errors.New("OSC Bundle has to be at least 20 bytes")
}
if bytes[0] != 35 {
return nil, bytes, errors.New("OSC Bundle must start with a #")
}
bundleHeader, bytesAfterBundleHeader, err := readOSCString(bytes)
if err != nil {
return nil, bytes, err
}
if bundleHeader != "#bundle" {
return nil, bytesAfterBundleHeader, errors.New("OSC Bundle must start with #bundle string")
}
timeTag, bytesAfterTimeTag, err := readOSCTimeTag(bytesAfterBundleHeader)
if err != nil {
return nil, bytesAfterBundleHeader, err
}
bundleContents := []OSCPacket{}
endOfBundle := false
remainingBytes := bytesAfterTimeTag
for !endOfBundle {
contentSize, bytesAfterContentSize, err := readOSCInt32(remainingBytes)
if err != nil {
return nil, remainingBytes, err
}
remainingBytes = bytesAfterContentSize
if len(remainingBytes) < int(contentSize) {
return nil, 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 nil, remainingBytes, err
}
bundleContents = append(bundleContents, content)
} else if bundleContentBytes[0] == 47 {
content, err := MessageFromBytes(bundleContentBytes)
if err != nil {
return nil, remainingBytes, err
}
bundleContents = append(bundleContents, content)
} else {
return nil, 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
}
+212
View File
@@ -0,0 +1,212 @@
package osc
import (
"reflect"
"testing"
)
func TestGoodOSCBundleEncoding(t *testing.T) {
testCases := []struct {
name string
bundle *OSCBundle
expected []byte
}{
{
name: "simple contents single message",
bundle: &OSCBundle{
TimeTag: OSCTimeTag{
seconds: 32,
fractionalSeconds: 0,
},
Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}},
},
expected: []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 {
t.Run(testCase.name, func(t *testing.T) {
got, err := testCase.bundle.ToBytes()
if err != nil {
t.Fatalf("failed to encode properly: %s", err.Error())
}
if !reflect.DeepEqual(got, testCase.expected) {
t.Fatalf("failed to encode properly got '%v', expected '%v'", got, testCase.expected)
}
})
}
}
func TestBadOSCBundleEncoding(t *testing.T) {
testCases := []struct {
name string
bundle *OSCBundle
errorString string
}{
{
name: "bundle contains message with bad address",
bundle: &OSCBundle{
TimeTag: OSCTimeTag{
seconds: 32,
fractionalSeconds: 0,
},
Contents: []OSCPacket{&OSCMessage{Address: "hello", Args: []OSCArg{}}},
},
errorString: "OSC Message address must start with /",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got, err := testCase.bundle.ToBytes()
if err == nil {
t.Fatalf("OSCBundle.ToBytes() expected to fail but got: %+v", got)
}
if err.Error() != testCase.errorString {
t.Fatalf("OSCBundle.ToBytes() got error '%s', expected '%s'", err.Error(), testCase.errorString)
}
})
}
}
func TestGoodOSCBundleDecoding(t *testing.T) {
testCases := []struct {
name string
expected *OSCBundle
bytes []byte
}{
{
name: "simple contents single message",
expected: &OSCBundle{
TimeTag: OSCTimeTag{
seconds: 32,
fractionalSeconds: 0,
},
Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}},
},
bytes: []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},
},
{
name: "simple contents nested bundle",
expected: &OSCBundle{
TimeTag: OSCTimeTag{
seconds: 32,
fractionalSeconds: 0,
},
Contents: []OSCPacket{&OSCBundle{
TimeTag: OSCTimeTag{
seconds: 64,
fractionalSeconds: 0,
},
Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}},
}},
},
bytes: []byte{35, 98, 117, 110, 100, 108, 101, 0, // #bundle
0, 0, 0, 32, 0, 0, 0, 0, // time tag
0, 0, 0, 52, // content size
35, 98, 117, 110, 100, 108, 101, 0, // #bundle
0, 0, 0, 64, 0, 0, 0, 0, // time tag
0, 0, 0, 32, // content size
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 {
t.Run(testCase.name, func(t *testing.T) {
actual, remainingBytes, error := BundleFromBytes(testCase.bytes)
if error != nil {
t.Fatalf("failed to decode properly: %s", error.Error())
}
if len(remainingBytes) > 0 {
t.Fatalf("should not have any remaining bytes")
}
if !reflect.DeepEqual(actual, testCase.expected) {
t.Fatalf("failed to decode properly got '%v', expected '%v'", actual, testCase.expected)
}
})
}
}
func TestBadOSCBundleDecoding(t *testing.T) {
testCases := []struct {
name string
bytes []byte
errorString string
}{
{
name: "empty byte array",
bytes: []byte{},
errorString: "OSC Bundle has to be at least 20 bytes",
},
{
name: "does not start with #",
bytes: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
errorString: "OSC Bundle must start with a #",
},
{
name: "does not start with #bundle",
bytes: []byte{35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
errorString: "OSC Bundle must start with #bundle string",
},
{
name: "bundle header not properly null terminated",
bytes: []byte{
35, 98, 117, 110, 100, 108, 101,
35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35},
errorString: "OSC string must be null-terminated",
},
{
name: "bundle contains incorrect size",
bytes: []byte{
35, 98, 117, 110, 100, 108, 101, 0, // #bundle
0, 0, 0, 0, 0, 0, 0, 0, // time tag
0, 0, 0, 100, // content size of 100 but only 10 bytes of content
35, 35, 35, 35, 35, 35, 35, 35, 35, 35},
errorString: "bundle doesn't have enough bytes for the content size it specifies",
},
{
name: "bundle doesn't contain message or bundle",
bytes: []byte{
35, 98, 117, 110, 100, 108, 101, 0, // #bundle
0, 0, 0, 0, 0, 0, 0, 0, // time tag
0, 0, 0, 10,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
errorString: "bundle contents does not look a bundle or message",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got, _, err := BundleFromBytes(testCase.bytes)
if err == nil {
t.Fatalf("BundleFromBytes expected to fail but got: %+v", got)
}
if err.Error() != testCase.errorString {
t.Fatalf("BundleFromBytes got error '%s', expected '%s'", err.Error(), testCase.errorString)
}
})
}
}
+86 -30
View File
@@ -1,14 +1,14 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"os"
"strconv"
"github.com/hypebeast/go-osc/osc"
"github.com/spf13/cobra"
osc "github.com/jwetzell/osc-go"
"github.com/urfave/cli/v3"
)
func main() {
@@ -17,69 +17,123 @@ func main() {
var Types []string
var Slip bool
var rootCmd = &cobra.Command{
Use: "sendosc",
Run: func(cmd *cobra.Command, args []string) {
cmd := &cli.Command{
Name: "makeosc",
Usage: "make osc bytes",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Value: "",
Usage: "OSC address",
Destination: &Address,
Required: true,
},
&cli.StringSliceFlag{
Name: "arg",
Usage: "OSC args",
Destination: &Args,
},
&cli.StringSliceFlag{
Name: "type",
Usage: "OSC types",
Destination: &Types,
},
&cli.BoolFlag{
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
Destination: &Slip,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
make(Address, Args, Types, Slip)
return nil
},
}
rootCmd.Flags().StringVar(&Address, "address", "", "OSC address")
rootCmd.Flags().StringArrayVar(&Args, "arg", []string{}, "OSC args")
rootCmd.Flags().StringArrayVar(&Types, "type", []string{}, "OSC types")
rootCmd.Flags().BoolVar(&Slip, "slip", false, "whether to slip encode the OSC Message bytes")
rootCmd.MarkFlagRequired("address")
rootCmd.Execute()
if err := cmd.Run(context.Background(), os.Args); err != nil {
panic(err)
}
}
func argToTypedArg(rawArg string, oscType string) any {
func argToTypedArg(rawArg string, oscType string) osc.OSCArg {
switch oscType {
case "s":
return rawArg
return osc.OSCArg{
Value: rawArg,
Type: "s",
}
case "i":
number, err := strconv.ParseInt(rawArg, 10, 32)
if err != nil {
// ... handle error
panic(err)
}
return int32(number)
return osc.OSCArg{
Value: int32(number),
Type: "i",
}
case "f":
number, err := strconv.ParseFloat(rawArg, 32)
if err != nil {
// ... handle error
panic(err)
}
return float32(number)
return osc.OSCArg{
Value: float32(number),
Type: "f",
}
case "b":
data, err := hex.DecodeString(rawArg)
if err != nil {
// ... handle error
panic(err)
}
return data
return osc.OSCArg{
Value: data,
Type: "b",
}
case "h":
number, err := strconv.ParseInt(rawArg, 10, 64)
if err != nil {
// ... handle error
panic(err)
}
return int64(number)
return osc.OSCArg{
Value: int64(number),
Type: "h",
}
case "d":
number, err := strconv.ParseFloat(rawArg, 64)
if err != nil {
// ... handle error
panic(err)
}
return float64(number)
return osc.OSCArg{
Value: float64(number),
Type: "d",
}
case "T":
return true
return osc.OSCArg{
Value: true,
Type: "T",
}
case "F":
return false
return osc.OSCArg{
Value: false,
Type: "F",
}
case "N":
return nil
return osc.OSCArg{
Value: nil,
Type: "N",
}
default:
fmt.Print("unhandled osc type: ")
fmt.Printf("%s.\n", oscType)
return rawArg
// TODO(jwetzell): something better than this like actual nil, err thing
return osc.OSCArg{}
}
}
@@ -89,13 +143,13 @@ func slipEncode(bytes []byte) []byte {
ESC_END := byte(0xdc)
ESC_ESC := byte(0xdd)
var encodedBytes = []byte{}
var encodedBytes = []byte{END}
for _, byteToEncode := range bytes {
if byteToEncode == END {
encodedBytes = append(encodedBytes, ESC_END)
encodedBytes = append(encodedBytes, ESC, ESC_END)
} else if byteToEncode == ESC {
encodedBytes = append(encodedBytes, ESC_ESC)
encodedBytes = append(encodedBytes, ESC, ESC_ESC)
} else {
encodedBytes = append(encodedBytes, byteToEncode)
}
@@ -107,7 +161,10 @@ func slipEncode(bytes []byte) []byte {
func make(address string, args []string, types []string, slip bool) {
oscMessage := osc.NewMessage(address)
oscMessage := osc.OSCMessage{
Address: address,
Args: []osc.OSCArg{},
}
for index, arg := range args {
oscType := "s"
@@ -115,11 +172,10 @@ func make(address string, args []string, types []string, slip bool) {
oscType = types[index]
}
oscMessage.Append(argToTypedArg(arg, oscType))
oscMessage.Args = append(oscMessage.Args, argToTypedArg(arg, oscType))
}
oscMessageBuffer, err := oscMessage.MarshalBinary()
oscMessageBuffer, err := oscMessage.ToBytes()
if err != nil {
panic(err)
}
+196 -17
View File
@@ -1,30 +1,211 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"github.com/chabad360/go-osc/osc"
"github.com/spf13/cobra"
osc "github.com/jwetzell/osc-go"
"github.com/urfave/cli/v3"
)
func main() {
var Host string
var Port string
var IP string
var Port int32
var Protocol string
var Format string
var Slip bool
var rootCmd = &cobra.Command{
Use: "sendosc",
Run: func(cmd *cobra.Command, args []string) {
netAddress := Host + ":" + Port
listen(netAddress)
cmd := &cli.Command{
Name: "receiveosc",
Usage: "receive OSC messages via UDP or TCP",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "ip",
Usage: "ip to receive OSC messages on",
Value: "0.0.0.0",
Destination: &IP,
},
&cli.Int32Flag{
Name: "port",
Usage: "port to receive OSC messages on",
Destination: &Port,
Value: 8888,
},
&cli.StringFlag{
Name: "protocol",
Usage: "protocol to use to receive (tcp or udp)",
Value: "udp",
Destination: &Protocol,
Validator: func(flag string) error {
if flag != "udp" && flag != "tcp" {
return fmt.Errorf("protocol must be either 'udp' or 'tcp'")
}
return nil
},
},
&cli.StringFlag{
Name: "format",
Usage: "format for messages to be output in ('json')",
Value: "json",
Destination: &Format,
Validator: func(flag string) error {
if flag != "json" {
return fmt.Errorf("format must be 'json'")
}
return nil
},
},
&cli.BoolFlag{
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
Destination: &Slip,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
netAddress := fmt.Sprintf("%s:%d", IP, Port)
switch Protocol {
case "udp":
listenUDP(netAddress, Format)
case "tcp":
if !Slip {
return fmt.Errorf("OSC 1.0 over TCP is not supported yet")
}
listenTCP(netAddress, Slip, Format)
}
return nil
},
}
rootCmd.Flags().StringVar(&Host, "host", "127.0.0.1", "host to send OSC message to")
rootCmd.Flags().StringVar(&Port, "port", "8888", "port to send OSC message to")
rootCmd.Execute()
if err := cmd.Run(context.Background(), os.Args); err != nil {
panic(err)
}
}
func listen(netAddress string) {
func listenTCP(netAddress string, useSLIP bool, format string) {
socket, err := net.Listen("tcp4", netAddress)
if err != nil {
fmt.Println(err)
return
}
defer socket.Close()
for {
conn, err := socket.Accept()
if err != nil {
fmt.Println(err)
continue
}
go handleTCPConnection(conn, useSLIP, format)
}
}
type SLIP struct {
pendingBytes []byte
Packets chan osc.OSCPacket
}
func (s *SLIP) decode(bytes []byte) {
END := byte(0xc0)
ESC := byte(0xdb)
ESC_END := byte(0xdc)
ESC_ESC := byte(0xdd)
escapeNext := false
for _, packetByte := range bytes {
if packetByte == ESC {
escapeNext = true
continue
}
if escapeNext {
if packetByte == ESC_END {
s.pendingBytes = append(s.pendingBytes, END)
} else if packetByte == ESC_ESC {
s.pendingBytes = append(s.pendingBytes, ESC)
}
escapeNext = false
} else if packetByte == END {
if len(s.pendingBytes) == 0 {
// probably opening END byte, can discard
continue
} else {
oscPacket, _, err := osc.PacketFromBytes(s.pendingBytes)
if err != nil {
panic(err)
} else {
s.Packets <- oscPacket
}
}
s.pendingBytes = []byte{}
} else {
s.pendingBytes = append(s.pendingBytes, packetByte)
}
}
}
func handleSLIP(slip SLIP, format string) {
for message := range slip.Packets {
handlePacket(message, format)
}
}
func handleTCPConnection(conn net.Conn, useSLIP bool, format string) {
slip := SLIP{
pendingBytes: []byte{},
Packets: make(chan osc.OSCPacket),
}
go handleSLIP(slip, format)
defer conn.Close()
buffer := make([]byte, 1024)
for {
bytesRead, err := conn.Read(buffer)
if err != nil {
return
}
if useSLIP {
slip.decode(buffer[0:bytesRead])
} else {
// TODO(jwetzell): handle non-SLIP TCP messages properly
}
}
}
func handlePacket(message osc.OSCPacket, format string) {
if bundle, ok := message.(*osc.OSCBundle); ok {
handleBundle(bundle, format)
} else if msg, ok := message.(*osc.OSCMessage); ok {
handleMessage(msg, format)
} else {
fmt.Println("Received unknown OSC Packet type")
}
}
func handleMessage(message *osc.OSCMessage, format string) {
if format == "json" {
jsonData, _ := json.Marshal(message)
fmt.Println(string(jsonData))
} else {
fmt.Printf("%v\n", message)
}
}
func handleBundle(bundle *osc.OSCBundle, format string) {
for _, packet := range bundle.Contents {
handlePacket(packet, format)
}
}
func listenUDP(netAddress string, format string) {
s, err := net.ResolveUDPAddr("udp4", netAddress)
if err != nil {
@@ -38,8 +219,6 @@ func listen(netAddress string) {
return
}
fmt.Printf("listening on %s (udp)\n", netAddress)
defer connection.Close()
buffer := make([]byte, 1024)
@@ -50,11 +229,11 @@ func listen(netAddress string) {
panic(err)
}
oscMessage, err := osc.NewMessageFromData(buffer[0:bytesRead])
oscPacket, _, err := osc.PacketFromBytes(buffer[0:bytesRead])
if err != nil {
panic(err)
}
fmt.Println(oscMessage)
handlePacket(oscPacket, format)
}
}
+121 -34
View File
@@ -1,14 +1,17 @@
package main
import (
"context"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"strconv"
"github.com/hypebeast/go-osc/osc"
osc "github.com/jwetzell/osc-go"
"github.com/spf13/cobra"
"github.com/urfave/cli/v3"
)
func main() {
@@ -20,74 +23,148 @@ func main() {
var Types []string
var Slip bool
var rootCmd = &cobra.Command{
Use: "sendosc",
Run: func(cmd *cobra.Command, args []string) {
cmd := &cli.Command{
Name: "sendosc",
Usage: "send OSC messages via UDP or TCP",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "host",
Usage: "host to send OSC message to",
Destination: &Host,
Required: true,
},
&cli.Int32Flag{
Name: "port",
Usage: "port to send OSC message to",
Destination: &Port,
Required: true,
},
&cli.StringFlag{
Name: "protocol",
Usage: "protocol to use to send (tcp or udp)",
Value: "udp",
Destination: &Protocol,
Validator: func(flag string) error {
if flag != "udp" && flag != "tcp" {
return fmt.Errorf("protocol must be either 'udp' or 'tcp'")
}
return nil
},
},
&cli.StringFlag{
Name: "address",
Usage: "OSC address",
Destination: &Address,
Required: true,
},
&cli.StringSliceFlag{
Name: "arg",
Usage: "OSC args",
Value: []string{},
Destination: &Args,
},
&cli.StringSliceFlag{
Name: "type",
Usage: "OSC types",
Value: []string{},
Destination: &Types,
},
&cli.BoolFlag{
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
Destination: &Slip,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
send(Host, Port, Address, Args, Types, Protocol, Slip)
return nil
},
}
rootCmd.Flags().StringVar(&Host, "host", "", "host to send OSC message to")
rootCmd.Flags().Int32Var(&Port, "port", 9999, "port to send OSC message to")
rootCmd.Flags().StringVar(&Address, "address", "", "OSC address")
rootCmd.Flags().StringVar(&Protocol, "protocol", "udp", "protocol to use to send (tcp or udp)")
rootCmd.Flags().StringArrayVar(&Args, "arg", []string{}, "OSC args")
rootCmd.Flags().StringArrayVar(&Types, "type", []string{}, "OSC types")
rootCmd.Flags().BoolVar(&Slip, "slip", false, "whether to slip encode the OSC Message bytes")
rootCmd.MarkFlagRequired("host")
rootCmd.MarkFlagRequired("port")
rootCmd.MarkFlagRequired("address")
rootCmd.Execute()
if err := cmd.Run(context.Background(), os.Args); err != nil {
panic(err)
}
}
func argToTypedArg(rawArg string, oscType string) any {
func argToTypedArg(rawArg string, oscType string) osc.OSCArg {
switch oscType {
case "s":
return rawArg
return osc.OSCArg{
Value: rawArg,
Type: "s",
}
case "i":
number, err := strconv.ParseInt(rawArg, 10, 32)
if err != nil {
// ... handle error
panic(err)
}
return int32(number)
return osc.OSCArg{
Value: int32(number),
Type: "i",
}
case "f":
number, err := strconv.ParseFloat(rawArg, 32)
if err != nil {
// ... handle error
panic(err)
}
return float32(number)
return osc.OSCArg{
Value: float32(number),
Type: "f",
}
case "b":
data, err := hex.DecodeString(rawArg)
if err != nil {
// ... handle error
panic(err)
}
return data
return osc.OSCArg{
Value: data,
Type: "b",
}
case "h":
number, err := strconv.ParseInt(rawArg, 10, 64)
if err != nil {
// ... handle error
panic(err)
}
return int64(number)
return osc.OSCArg{
Value: int64(number),
Type: "h",
}
case "d":
number, err := strconv.ParseFloat(rawArg, 64)
if err != nil {
// ... handle error
panic(err)
}
return float64(number)
return osc.OSCArg{
Value: float64(number),
Type: "d",
}
case "T":
return true
return osc.OSCArg{
Value: true,
Type: "T",
}
case "F":
return false
return osc.OSCArg{
Value: false,
Type: "F",
}
case "N":
return nil
return osc.OSCArg{
Value: nil,
Type: "N",
}
default:
fmt.Print("unhandled osc type: ")
fmt.Printf("%s.\n", oscType)
return rawArg
// TODO(jwetzell): something better than this like actual nil, err thing
return osc.OSCArg{}
}
}
@@ -97,13 +174,13 @@ func slipEncode(bytes []byte) []byte {
ESC_END := byte(0xdc)
ESC_ESC := byte(0xdd)
var encodedBytes = []byte{}
var encodedBytes = []byte{END}
for _, byteToEncode := range bytes {
if byteToEncode == END {
encodedBytes = append(encodedBytes, ESC_END)
encodedBytes = append(encodedBytes, ESC, ESC_END)
} else if byteToEncode == ESC {
encodedBytes = append(encodedBytes, ESC_ESC)
encodedBytes = append(encodedBytes, ESC, ESC_ESC)
} else {
encodedBytes = append(encodedBytes, byteToEncode)
}
@@ -115,25 +192,35 @@ func slipEncode(bytes []byte) []byte {
func send(host string, port int32, address string, args []string, types []string, protocol string, slip bool) {
oscMessage := osc.NewMessage(address)
oscMessage := osc.OSCMessage{
Address: address,
Args: []osc.OSCArg{},
}
for index, arg := range args {
oscType := "s"
if len(types) > index {
oscType = types[index]
}
arg := argToTypedArg(arg, oscType)
oscMessage.Args = append(oscMessage.Args, arg)
oscMessage.Append(argToTypedArg(arg, oscType))
}
oscMessageBuffer, err := oscMessage.MarshalBinary()
oscMessageBuffer, err := oscMessage.ToBytes()
if err != nil {
panic(err)
}
if slip {
oscMessageBuffer = slipEncode(oscMessageBuffer)
} else if protocol == "tcp" {
// OSC 1.0 prepends a 4 byte size header for non-SLIP TCP messages
size := uint32(len(oscMessageBuffer))
sizeBytes := make([]byte, 4)
binary.BigEndian.PutUint32(sizeBytes, size)
oscMessageBuffer = append(sizeBytes, oscMessageBuffer...)
}
netAddress := fmt.Sprintf("%s:%d", host, port)
+2 -11
View File
@@ -1,14 +1,5 @@
module github.com/jwetzell/osc-go
go 1.23.1
go 1.25.1
require (
github.com/chabad360/go-osc v0.0.0-20220502185613-216f362cdf0a
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5
github.com/spf13/cobra v1.8.1
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
require github.com/urfave/cli/v3 v3.8.0
+9 -13
View File
@@ -1,14 +1,10 @@
github.com/chabad360/go-osc v0.0.0-20220502185613-216f362cdf0a h1:MTor/GgULww+hISVbeiNuC5vyT1mc2xa6/14ib0lvyc=
github.com/chabad360/go-osc v0.0.0-20220502185613-216f362cdf0a/go.mod h1:/aAn5LNn+s7zq33XItIcYLr6aOntVkWckHPAG40yeUc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ=
github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+85
View File
@@ -0,0 +1,85 @@
package osc
import (
"errors"
"strings"
)
func (m *OSCMessage) ToBytes() ([]byte, error) {
if len(m.Address) == 0 {
return nil, errors.New("OSC Message must have an address")
}
if m.Address[0] != '/' {
return nil, errors.New("OSC Message address must start with /")
}
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())...)
argsBuffer, err := argsToBuffer(m.Args)
if err != nil {
return nil, err
}
oscBuffer = append(oscBuffer, argsBuffer...)
return oscBuffer, nil
}
func MessageFromBytes(bytes []byte) (*OSCMessage, error) {
if len(bytes) == 0 {
return nil, errors.New("cannot create OSC Message from empty byte array")
}
if bytes[0] != 47 {
return nil, errors.New("OSC Message must start with /")
}
address, typeAndArgBytes, err := readOSCString(bytes)
if err != nil {
return nil, err
}
oscMessage := OSCMessage{
Address: address,
Args: []OSCArg{},
}
if len(typeAndArgBytes) == 0 {
// NOTE(jwetzell): no type string return early.
return &oscMessage, nil
}
typeString, argBytes, err := readOSCString(typeAndArgBytes)
if err != nil {
return nil, err
}
for index, oscType := range typeString {
if index == 0 {
if oscType != ',' {
return nil, errors.New("type string is malformed")
}
} else {
oscArg, remainingBytes, err := readOSCArg(argBytes, string(oscType))
if err != nil {
return nil, err
}
argBytes = remainingBytes
oscMessage.Args = append(oscMessage.Args, oscArg)
}
}
return &oscMessage, nil
}
+513
View File
@@ -0,0 +1,513 @@
package osc
import (
"math"
"reflect"
"testing"
)
func TestGoodOSCMessageEncoding(t *testing.T) {
testCases := []struct {
name string
message *OSCMessage
expected []byte
}{
{
name: "simple hello",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{},
},
expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 0, 0, 0},
},
{
name: "simple address string arg",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{
{
Type: "s",
Value: "arg1",
},
},
},
expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 115, 0, 0, 97, 114, 103, 49, 0, 0, 0, 0},
},
{
name: "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},
},
{
name: "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},
},
{
name: "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},
},
{
name: "simple address True arg",
message: &OSCMessage{Address: "/hello", Args: []OSCArg{{Type: "T", Value: true}}},
expected: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 84, 0, 0},
},
{
name: "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},
},
{
name: "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},
},
{
name: "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},
},
{
name: "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},
},
{
name: "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
// {
// name: "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,
// },
// },
{
name: "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,
},
},
{
name: "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 {
t.Run(testCase.name, func(t *testing.T) {
got, err := testCase.message.ToBytes()
if err != nil {
t.Fatalf("failed to encode properly: %s", err.Error())
}
if !reflect.DeepEqual(got, testCase.expected) {
t.Fatalf("failed to encode properly got '%v', expected '%v'", got, testCase.expected)
}
})
}
}
func TestBadOSCMessageEncoding(t *testing.T) {
testCases := []struct {
name string
message *OSCMessage
errorString string
}{
{
name: "empty message",
message: &OSCMessage{},
errorString: "OSC Message must have an address",
},
{
name: "address does not start with /",
message: &OSCMessage{Address: "hello"},
errorString: "OSC Message address must start with /",
},
{
name: "arg with unsupported type",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "x", Value: "unsupported"}},
},
errorString: "unsupported OSC argument type: x",
},
{
name: "string arg that is not a string",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "s", Value: 123}},
},
errorString: "OSC arg had string type but non-string value",
},
{
name: "int32 arg that is not a number",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "i", Value: "not an int"}},
},
errorString: "OSC arg had int32 type but non-number value",
},
{
name: "float32 arg that is not a number",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "f", Value: "not a float"}},
},
errorString: "OSC arg had float32 type but non-number value",
},
{
name: "int64 arg that is not a number",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "h", Value: "not an int"}},
},
errorString: "OSC arg had int64 type but non-number value",
},
{
name: "float64 arg that is not a number",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "d", Value: "not a float"}},
},
errorString: "OSC arg had float64 type but non-number value",
},
{
name: "blob arg that is not a byte array",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "b", Value: "not a blob"}},
},
errorString: "OSC arg had blob type but non-blob value",
},
{
name: "color arg that is not an OSCColor",
message: &OSCMessage{
Address: "/hello",
Args: []OSCArg{{Type: "r", Value: "not a color"}},
},
errorString: "OSC arg had color type but non-color value",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got, err := testCase.message.ToBytes()
if err == nil {
t.Fatalf("OSCMessage.ToBytes() expected to fail but got: %+v", got)
}
if err.Error() != testCase.errorString {
t.Fatalf("OSCMessage.ToBytes() got error '%s', expected '%s'", err.Error(), testCase.errorString)
}
})
}
}
func TestGoodOSCMessageDecoding(t *testing.T) {
testCases := []struct {
name string
bytes []byte
expected OSCMessage
}{
{
name: "simple address no args",
bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 0, 0, 0},
expected: OSCMessage{Address: "/hello", Args: []OSCArg{}},
},
{
name: "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"}}},
},
{
name: "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)}}},
},
{
name: "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)}}},
},
{
name: "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}}}},
},
{
name: "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}}},
},
{
name: "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}}},
},
{
name: "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}}}},
},
{
name: "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}}},
},
{
name: "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}}},
},
{
name: "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)}}},
},
{
name: "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
// {
// name: "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},
// },
// },
// },
// },
{
name: "simple address no type string",
bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0},
expected: OSCMessage{
Address: "/hello",
Args: []OSCArg{},
},
},
{
name: "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)}}},
},
{
name: "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 {
t.Run(testCase.name, func(t *testing.T) {
actual, err := MessageFromBytes(testCase.bytes)
if err != nil {
t.Fatalf("failed to encode properly: %s", err.Error())
}
if !reflect.DeepEqual(actual.Address, testCase.expected.Address) {
t.Fatalf("failed to encode address propertly got '%s', expected '%s'", actual.Address, testCase.expected.Address)
}
if !reflect.DeepEqual(actual.Args, testCase.expected.Args) {
t.Fatalf("failed to encode args properly got '%+v', expected '%+v'", actual.Args, testCase.expected.Args)
}
})
}
}
func TestBadOSCMessageDecoding(t *testing.T) {
testCases := []struct {
name string
bytes []byte
errorString string
}{
{
name: "empty byte array",
bytes: []byte{},
errorString: "cannot create OSC Message from empty byte array",
},
{
name: "does not start with /",
bytes: []byte{0, 104, 101, 108, 108, 111, 0, 0},
errorString: "OSC Message must start with /",
},
{
name: "address string not padded",
bytes: []byte{47, 104, 101, 108, 108, 111, 0},
errorString: "OSC string is not properly padded",
},
{
name: "type string not padded",
bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 0},
errorString: "OSC string is not properly padded",
},
{
name: "type string does not start with ,",
bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0, 45, 0, 0, 0},
errorString: "type string is malformed",
},
{
name: "string arg not null-terminated",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 115, 0, 0, 97, 114, 103, 49,
},
errorString: "OSC string must be null-terminated",
},
{
name: "string arg not padded",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 115, 0, 0, 104, 105, 0,
},
errorString: "OSC string is not properly padded",
},
{
name: "int32 arg not 4 bytes",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 105, 0, 0, 0,
},
errorString: "OSC int32 arg is not 4 bytes",
},
{
name: "int64 arg not 8 bytes",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 104, 0, 0, 0, 0, 0, 0,
},
errorString: "OSC int64 arg is not 8 bytes",
},
{
name: "float32 arg not 4 bytes",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 102, 0, 0, 66,
},
errorString: "OSC float32 arg is not 4 bytes",
},
{
name: "float64 arg not 8 bytes",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 100, 0, 0, 0,
},
errorString: "OSC float64 arg is not 8 bytes",
},
{
name: "blob arg size not valid",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 98, 0, 0, 0, 0, 0,
},
errorString: "OSC blob arg size not valid: OSC int32 arg is not 4 bytes",
},
{
name: "blob arg size mismatch",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 98, 0, 0, 0, 0, 0, 4, 98, 108, 111,
},
errorString: "OSC blob arg size not valid: size specified is larger than remaining bytes",
},
{
name: "color arg not 4 bytes",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 114, 0, 0, 20, 21,
},
errorString: "OSC color arg is not 4 bytes",
},
{
name: "time tag arg seconds not complete",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 116, 0, 0, 0,
},
errorString: "OSC time tag seconds are not valid: OSC int32 arg is not 4 bytes",
},
{
name: "time tag arg fractional seconds not complete",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 116, 0, 0, 0, 32, 0, 0, 0,
},
errorString: "OSC time tag fractional seconds are not valid: OSC int32 arg is not 4 bytes",
},
{
name: "unknown arg type",
bytes: []byte{
47, 104, 101, 108, 108, 111, 0, 0, 44, 120, 0, 0,
},
errorString: "unsupported OSC argument type: x",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got, err := MessageFromBytes(testCase.bytes)
if err == nil {
t.Fatalf("MessageFromBytes expected to fail but got: %+v", got)
}
if err.Error() != testCase.errorString {
t.Fatalf("MessageFromBytes got error '%s', expected '%s'", err.Error(), testCase.errorString)
}
})
}
}
+473
View File
@@ -0,0 +1,473 @@
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, error) {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func int64ToOSCBytes(number int64) ([]byte, error) {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func float32ToOSCBytes(number float32) ([]byte, error) {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func float64ToOSCBytes(number float64) ([]byte, error) {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func byteArrayToOSCBytes(bytes []byte) ([]byte, error) {
oscBytes := []byte{}
bytesSize := len(bytes)
bytesSizeBytes, err := int32ToOSCBytes(int32(bytesSize))
if err != nil {
return nil, err
}
oscBytes = append(oscBytes, bytesSizeBytes...)
oscBytes = append(oscBytes, bytes...)
padLength := 4 - (bytesSize % 4)
if padLength < 4 {
for i := 0; i < padLength; i++ {
oscBytes = append(oscBytes, 0)
}
}
return oscBytes, nil
}
func timeTagToOSCBytes(timeTag OSCTimeTag) ([]byte, error) {
timeTagBytes, err := int32ToOSCBytes(timeTag.seconds)
if err != nil {
return nil, err
}
fractionalSecondsBytes, err := int32ToOSCBytes(timeTag.fractionalSeconds)
if err != nil {
return nil, err
}
timeTagBytes = append(timeTagBytes, fractionalSecondsBytes...)
return timeTagBytes, nil
}
func argsToBuffer(args []OSCArg) ([]byte, error) {
//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 {
return nil, errors.New("OSC arg had string type but non-string value")
}
case "i":
if value, ok := arg.Value.(int); ok {
valueBytes, err := int32ToOSCBytes(int32(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
valueBytes, err := int32ToOSCBytes(int32(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else {
return nil, errors.New("OSC arg had int32 type but non-number value")
}
case "f":
if value, ok := arg.Value.(float32); ok {
valueBytes, err := float32ToOSCBytes(value)
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(float64); ok {
valueBytes, err := float32ToOSCBytes(float32(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int); ok {
valueBytes, err := float32ToOSCBytes(float32(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
valueBytes, err := float32ToOSCBytes(float32(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int64); ok {
valueBytes, err := float32ToOSCBytes(float32(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else {
return nil, errors.New("OSC arg had float32 type but non-number value")
}
case "b":
if value, ok := arg.Value.([]byte); ok {
valueBytes, err := byteArrayToOSCBytes(value)
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else {
return nil, errors.New("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 {
return nil, errors.New("OSC arg had color type but non-color value")
}
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 {
valueBytes, err := int64ToOSCBytes(int64(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
valueBytes, err := int64ToOSCBytes(int64(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int64); ok {
valueBytes, err := int64ToOSCBytes(value)
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else {
return nil, errors.New("OSC arg had int64 type but non-number value")
}
case "d":
if value, ok := arg.Value.(float32); ok {
valueBytes, err := float64ToOSCBytes(float64(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(float64); ok {
valueBytes, err := float64ToOSCBytes(value)
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int); ok {
valueBytes, err := float64ToOSCBytes(float64(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
valueBytes, err := float64ToOSCBytes(float64(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int64); ok {
valueBytes, err := float64ToOSCBytes(float64(value))
if err != nil {
return nil, err
}
argBuffers = append(argBuffers, valueBytes...)
} else {
return nil, errors.New("OSC arg had float64 type but non-number value")
}
default:
return nil, fmt.Errorf("unsupported OSC argument type: %s", oscType)
}
}
return argBuffers, nil
}
func readOSCString(bytes []byte) (string, []byte, error) {
oscString := ""
stringEndIndex := 0
nullByteFound := false
for index, byteIn := range bytes {
if byteIn == 0 {
nullByteFound = true
oscString = string(bytes[0:index])
stringEndIndex = index + 1
break
}
}
if !nullByteFound {
return "", bytes, errors.New("OSC string must be null-terminated")
}
stringPadding := 4 - (stringEndIndex % 4)
if stringPadding < 4 {
stringEndIndex = stringEndIndex + stringPadding
}
if stringEndIndex > len(bytes) {
return "", bytes, errors.New("OSC string is not properly padded")
}
remainingBytes := bytes[stringEndIndex:]
return oscString, remainingBytes, nil
}
func readOSCInt32(bytes []byte) (int32, []byte, error) {
if len(bytes) < 4 {
return 0, bytes, errors.New("OSC int32 arg is not 4 bytes")
}
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("OSC int64 arg is not 8 bytes")
}
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("OSC float32 arg is not 4 bytes")
}
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("OSC float64 arg is not 8 bytes")
}
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("OSC blob arg size not valid: " + err.Error())
}
if len(remainingBytes) < int(blobLength) {
return []byte{}, bytes, errors.New("OSC blob arg size not valid: size specified is larger than remaining bytes")
}
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("OSC color arg is not 4 bytes")
}
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, fmt.Errorf("OSC time tag seconds are not valid: %s", err)
}
fractionalSeconds, remainingBytes, err := readOSCInt32(bytesAfterSeconds)
if err != nil {
return OSCTimeTag{}, bytes, fmt.Errorf("OSC time tag fractional seconds are not valid: %s", 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, err := readOSCString(bytes)
if err != nil {
return OSCArg{}, bytes, err
}
oscArg.Value = argString
remainingBytes = bytesLeft
case "i":
argInt, bytesLeft, err := readOSCInt32(bytes)
if err != nil {
readArgError = err
}
oscArg.Value = argInt
remainingBytes = bytesLeft
case "f":
argFloat, bytesLeft, err := readOSCFloat32(bytes)
if err != nil {
readArgError = err
}
oscArg.Value = argFloat
remainingBytes = bytesLeft
case "b":
argBytes, bytesLeft, err := readOSCBlob(bytes)
if err != nil {
readArgError = err
}
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, err := readOSCColor(bytes)
if err != nil {
readArgError = err
}
oscArg.Value = argColor
remainingBytes = bytesLeft
case "h":
argInt, bytesLeft, err := readOSCInt64(bytes)
if err != nil {
readArgError = err
}
oscArg.Value = argInt
remainingBytes = bytesLeft
case "d":
argFloat, bytesLeft, err := readOSCFloat64(bytes)
if err != nil {
readArgError = err
}
oscArg.Value = argFloat
remainingBytes = bytesLeft
case "t":
argTimeTag, bytesLeft, err := readOSCTimeTag(bytes)
if err != nil {
readArgError = err
}
oscArg.Value = argTimeTag
remainingBytes = bytesLeft
default:
fmt.Printf("unsupported osc type: %s\n", oscType)
readArgError = errors.New("unsupported OSC argument type: " + oscType)
}
return oscArg, remainingBytes, readArgError
}
func PacketFromBytes(bytes []byte) (OSCPacket, []byte, error) {
if len(bytes) == 0 {
return nil, bytes, errors.New("cannot create OSC Packet from empty byte array")
}
switch bytes[0] {
case '#':
bundle, remainingBytes, err := BundleFromBytes(bytes)
if err != nil {
return nil, bytes, err
}
return bundle, remainingBytes, nil
case '/':
message, err := MessageFromBytes(bytes)
if err != nil {
return nil, bytes, err
}
return message, []byte{}, nil
default:
return nil, bytes, errors.New("OSC Packet must start with # for bundle or / for message")
}
}
+318
View File
@@ -0,0 +1,318 @@
package osc
import (
"reflect"
"testing"
)
func TestGoodOSCArgsToBuffer(t *testing.T) {
testCases := []struct {
name string
args []OSCArg
expected []byte
}{
{
name: "int arg",
args: []OSCArg{
{
Type: "i",
Value: int(123),
},
},
expected: []byte{0, 0, 0, 123},
},
{
name: "int32 arg",
args: []OSCArg{
{
Type: "i",
Value: int32(123),
},
},
expected: []byte{0, 0, 0, 123},
},
{
name: "float32 arg",
args: []OSCArg{
{
Type: "f",
Value: float32(123),
},
},
expected: []byte{66, 246, 0, 0},
},
{
name: "float32 arg with int value",
args: []OSCArg{
{
Type: "f",
Value: int(123),
},
},
expected: []byte{66, 246, 0, 0},
},
{
name: "float32 arg with int32 value",
args: []OSCArg{
{
Type: "f",
Value: int32(123),
},
},
expected: []byte{66, 246, 0, 0},
},
{
name: "float32 arg with int64 value",
args: []OSCArg{
{
Type: "f",
Value: int64(123),
},
},
expected: []byte{66, 246, 0, 0},
},
{
name: "float64 arg",
args: []OSCArg{
{
Type: "d",
Value: float64(123),
},
},
expected: []byte{64, 94, 192, 0, 0, 0, 0, 0},
},
{
name: "float64 arg with float32 value",
args: []OSCArg{
{
Type: "d",
Value: float32(123),
},
},
expected: []byte{64, 94, 192, 0, 0, 0, 0, 0},
},
{
name: "float64 arg with int value",
args: []OSCArg{
{
Type: "d",
Value: int(123),
},
},
expected: []byte{64, 94, 192, 0, 0, 0, 0, 0},
},
{
name: "float64 arg with int32 value",
args: []OSCArg{
{
Type: "d",
Value: int32(123),
},
},
expected: []byte{64, 94, 192, 0, 0, 0, 0, 0},
},
{
name: "float64 arg with int64 value",
args: []OSCArg{
{
Type: "d",
Value: int64(123),
},
},
expected: []byte{64, 94, 192, 0, 0, 0, 0, 0},
},
{
name: "int64 arg",
args: []OSCArg{
{
Type: "h",
Value: int64(123),
},
},
expected: []byte{0, 0, 0, 0, 0, 0, 0, 123},
},
{
name: "int64 arg with int32 value",
args: []OSCArg{
{
Type: "h",
Value: int32(123),
},
},
expected: []byte{0, 0, 0, 0, 0, 0, 0, 123},
},
{
name: "blob arg",
args: []OSCArg{
{
Type: "b",
Value: []byte{1, 2, 3},
},
},
expected: []byte{0, 0, 0, 3, 1, 2, 3, 0},
},
{
name: "true arg",
args: []OSCArg{
{
Type: "T",
Value: true,
},
},
expected: []byte{},
},
{
name: "false arg",
args: []OSCArg{
{
Type: "F",
Value: false,
},
},
expected: []byte{},
},
{
name: "nil arg",
args: []OSCArg{
{
Type: "N",
Value: nil,
},
},
expected: []byte{},
},
{
name: "inifinitum arg",
args: []OSCArg{
{
Type: "I",
Value: nil,
},
},
expected: []byte{},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got, err := argsToBuffer(testCase.args)
if err != nil {
t.Fatalf("failed to encode properly: %s", err.Error())
}
if !reflect.DeepEqual(got, testCase.expected) {
t.Fatalf("failed to encode properly got '%v', expected '%v'", got, testCase.expected)
}
})
}
}
func TestBadOSCArgsToBuffer(t *testing.T) {
testCases := []struct {
name string
args []OSCArg
errorString string
}{}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got, err := argsToBuffer(testCase.args)
if err == nil {
t.Fatalf("argsToBuffer expected to fail but got: %+v", got)
}
if err.Error() != testCase.errorString {
t.Fatalf("argsToBuffer got error '%s', expected '%s'", err.Error(), testCase.errorString)
}
})
}
}
func TestGoodPacketFromBytes(t *testing.T) {
testCases := []struct {
name string
expected OSCPacket
bytes []byte
}{
{
name: "message with no args",
expected: &OSCMessage{
Address: "/hello",
Args: []OSCArg{},
},
bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0},
},
{
name: "bundle with one message with no args",
expected: &OSCBundle{
TimeTag: OSCTimeTag{
seconds: 32,
fractionalSeconds: 0,
},
Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}},
},
bytes: []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 {
t.Run(testCase.name, func(t *testing.T) {
got, remainingBytes, err := PacketFromBytes(testCase.bytes)
if err != nil {
t.Fatalf("failed to decode properly: %s", err.Error())
}
if len(remainingBytes) != 0 {
t.Fatalf("failed to decode properly, expected no remaining bytes but got: %v", remainingBytes)
}
if !reflect.DeepEqual(got, testCase.expected) {
t.Fatalf("failed to decode properly got '%v', expected '%v'", got, testCase.expected)
}
})
}
}
func TestBadPacketFromBytes(t *testing.T) {
testCases := []struct {
name string
bytes []byte
errorString string
}{
{name: "empty bytes",
bytes: []byte{},
errorString: "cannot create OSC Packet from empty byte array",
},
{name: "packet that does not start with / or #",
bytes: []byte{0, 1, 2, 3},
errorString: "OSC Packet must start with # for bundle or / for message",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
got, _, err := PacketFromBytes(testCase.bytes)
if err == nil {
t.Fatalf("PacketFromBytes expected to fail but got: %+v", got)
}
if err.Error() != testCase.errorString {
t.Fatalf("PacketFromBytes got error '%s', expected '%s'", err.Error(), testCase.errorString)
}
})
}
}
+32
View File
@@ -0,0 +1,32 @@
package osc
type OSCPacket interface {
ToBytes() ([]byte, error)
}
type OSCBundle struct {
Contents []OSCPacket `json:"contents"`
TimeTag OSCTimeTag `json:"timeTag"`
}
type OSCArg struct {
Value any `json:"value"`
Type string `json:"type"`
}
type OSCMessage struct {
Address string `json:"address"`
Args []OSCArg `json:"args"`
}
type OSCColor struct {
r uint8
g uint8
b uint8
a uint8
}
type OSCTimeTag struct {
seconds int32
fractionalSeconds int32
}