├── .babelrc
├── .editorconfig
├── .esdoc.json
├── .eslintrc
├── .github
└── workflows
│ ├── docs.yaml
│ └── tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── atomic.js
├── atomic
│ ├── blob.js
│ ├── constant.js
│ ├── float32.js
│ ├── float64.js
│ ├── int32.js
│ ├── int64.js
│ ├── string.js
│ ├── timetag.js
│ └── uint64.js
├── bundle.js
├── common
│ ├── helpers.js
│ └── utils.js
├── events.js
├── external
│ ├── dgram.js
│ └── ws.js
├── message.js
├── osc.js
├── packet.js
└── plugin
│ ├── bridge.js
│ ├── dgram.js
│ ├── plugin.js
│ ├── wsclient.js
│ └── wsserver.js
├── test
├── atomic.spec.js
├── atomic
│ ├── blob.spec.js
│ ├── float32.spec.js
│ ├── int32.spec.js
│ ├── int64.spec.js
│ ├── string.spec.js
│ ├── timetag.spec.js
│ └── uint64.spec.js
├── bundle.spec.js
├── common
│ ├── helpers.spec.js
│ └── utils.spec.js
├── events.spec.js
├── message.spec.js
├── osc.spec.js
├── packet.spec.js
└── plugin
│ ├── bridge.spec.js
│ ├── dgram.spec.js
│ ├── ws.spec.js
│ ├── wsclient.spec.js
│ └── wsserver.spec.js
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.esdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./src",
3 | "destination": "./docs",
4 | "plugins": [
5 | {
6 | "name": "esdoc-standard-plugin",
7 | "option": {
8 | "test": {
9 | "source": "./test"
10 | }
11 | }
12 | },
13 | {
14 | "name": "esdoc-ecmascript-proposal-plugin",
15 | "option": {
16 | "all": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true,
4 | "node": true
5 | },
6 | "parser": "@babel/eslint-parser",
7 | "extends": "airbnb-base",
8 | "globals": {
9 | "BigInt": true,
10 | "WebSocket": true,
11 | "MozWebSocket": true
12 | },
13 | "rules": {
14 | "max-classes-per-file": "off",
15 | "class-methods-use-this": "off",
16 | "comma-dangle": [
17 | "error",
18 | "always-multiline"
19 | ],
20 | "function-paren-newline": [
21 | "error",
22 | "consistent"
23 | ],
24 | "no-bitwise": "off",
25 | "no-else-return": "off",
26 | "no-unused-expressions": "off",
27 | "semi": [
28 | "error",
29 | "never"
30 | ],
31 | "strict": [
32 | "error",
33 | "global"
34 | ],
35 | "prefer-destructuring": [
36 | "error",
37 | {
38 | "object": true,
39 | "array": false
40 | }
41 | ],
42 | "import/no-extraneous-dependencies": [
43 | "error",
44 | {
45 | "optionalDependencies": false,
46 | "peerDependencies": false
47 | }
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy documentation via Github Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | name: Deploy
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: '12'
19 | - run: npm ci
20 | - run: npm run docs
21 | - uses: peaceiris/actions-gh-pages@v3
22 | with:
23 | github_token: ${{ secrets.GITHUB_TOKEN }}
24 | publish_dir: ./docs
25 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | name: Test and build library
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v2
14 | with:
15 | node-version: "12"
16 | - run: npm ci
17 | - run: npm run build
18 | - run: npm test
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | docs
2 | lib
3 | node_modules
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2016 adzialocha (Andreas Dzialocha)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | osc-js
2 | ======
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | osc-js is an [Open Sound Control](http://opensoundcontrol.org/) library for JavaScript applications (UMD module for Node, Browser etc.) with address pattern matching and timetag handling. Sends messages via *UDP*, *WebSocket* or both (bridge mode) and offers a customizable Plugin API for network protocols.
20 |
21 | [Wiki](https://github.com/adzialocha/osc-js/wiki) | [Basic Usage](https://github.com/adzialocha/osc-js/wiki/Basic-Usage) | [Documentation](https://adzialocha.github.io/osc-js) | [Plugin API](https://github.com/adzialocha/osc-js/wiki/Plugin-API)
22 |
23 | ## Features
24 |
25 | - UMD Module running in Node.js, Electron, Chrome Apps, browser or any other JS environment
26 | - Can be used with Webpack and Browserify
27 | - TypeScript definitions
28 | - No dependencies (except of `ws` in Node.js or similar environments)
29 | - Receive sender information from incoming messages
30 | - Built-in *UDP*, *WebSocket* networking support as plugins
31 | - Special bridge plugin for easy communication between *UDP*- and *WebSocket* clients
32 | - Plugin API for custom network protocols
33 | - Featuring all [OSC 1.0 specifications](http://opensoundcontrol.org/spec-1_0)
34 | - OSC Address pattern matching
35 | - Time-critical OSC Bundles with Timetags
36 | - Extended (nonstandard) argument types
37 |
38 | ## Documentation
39 |
40 | Read more about osc-js and how to use it in the [Wiki](https://github.com/adzialocha/osc-js/wiki) and [Documentation](https://adzialocha.github.io/osc-js).
41 |
42 | ## Example
43 |
44 | ```js
45 | const osc = new OSC()
46 |
47 | osc.on('/param/density', (message, rinfo) => {
48 | console.log(message.args)
49 | console.log(rinfo)
50 | })
51 |
52 | osc.on('*', message => {
53 | console.log(message.args)
54 | })
55 |
56 | osc.on('/{foo,bar}/*/param', message => {
57 | console.log(message.args)
58 | })
59 |
60 | osc.on('open', () => {
61 | const message = new OSC.Message('/test', 12.221, 'hello')
62 | osc.send(message)
63 | })
64 |
65 | osc.open({ port: 9000 })
66 | ```
67 |
68 | ## Installation and Usage
69 |
70 | Recommended installation via npm: `npm i osc-js` or `yarn add osc-js`.
71 |
72 | Import the library `const OSC = require('osc-js')` or add the script `lib/osc.js` or `lib/osc.min.js` (minified version) for usage in a browser.
73 |
74 | ## Plugins
75 |
76 | osc-js offers a plugin architecture for extending it's networking capabilities. The library comes with four built-in plugins. This is propably all you need for an OSC application:
77 |
78 | - `WebsocketClientPlugin` (default)
79 | - `WebsocketServerPlugin`
80 | - `DatagramPlugin` for UDP network messaging
81 | - `BridgePlugin` useful Bridge between WebSocket- and UDP Clients
82 |
83 | Configuration and examples of every plugin can be read here: [Wiki](https://github.com/adzialocha/osc-js/wiki).
84 |
85 | ### Example: WebSocket Server
86 |
87 | Register the plugin when creating the OSC instance:
88 |
89 | ```js
90 | const osc = new OSC({ plugin: new OSC.WebsocketServerPlugin() })
91 | osc.open() // listening on 'ws://localhost:8080'
92 | ```
93 |
94 | ### Example: OSC between MaxMSP/PD/SC etc. and your browser
95 |
96 | 1. Write a simple webpage. The library will use a WebSocket client
97 | by default.
98 |
99 | ```html
100 |
101 |
102 |
111 | ```
112 |
113 | 2. Write a Node app (the "bridge" between your UDP and WebSocket clients).
114 |
115 | ```js
116 | const OSC = require('osc-js')
117 |
118 | const config = { udpClient: { port: 9129 } }
119 | const osc = new OSC({ plugin: new OSC.BridgePlugin(config) })
120 |
121 | osc.open() // start a WebSocket server on port 8080
122 | ```
123 |
124 | 3. Create your Max/MSP patch (or PD, SuperCollider etc.).
125 |
126 | ```
127 | [udpreceive 9129] // incoming '/test/random' messages with random number
128 | ```
129 |
130 | ### Custom solutions with Plugin API
131 |
132 | It is possible to write more sophisticated solutions for OSC applications without loosing the osc-js interface (including its message handling etc.). Read the [Plugin API documentation](https://github.com/adzialocha/osc-js/wiki/Plugin-API) for further information.
133 |
134 | ```js
135 | class MyCustomPlugin {
136 | // ... read docs for implementation details
137 | }
138 |
139 | const osc = new OSC({ plugin: MyCustomPlugin() })
140 | osc.open()
141 |
142 | osc.on('/test', message => {
143 | // use event listener with your plugin
144 | })
145 | ```
146 |
147 | ### Usage without plugins
148 |
149 | The library can be used without the mentioned features in case you need to write and read binary OSC data. See this example below for using the [Low-Level API](https://github.com/adzialocha/osc-js/wiki/Low-Level-API) (even though the library already has a solution for handling UDP like in this example):
150 |
151 | ```js
152 | const dgram = require('dgram')
153 | const OSC = require('osc-js')
154 |
155 | const socket = dgram.createSocket('udp4')
156 |
157 | // send a messsage via udp
158 | const message = new OSC.Message('/some/path', 21)
159 | const binary = message.pack()
160 | socket.send(new Buffer(binary), 0, binary.byteLength, 41234, 'localhost')
161 |
162 | // receive a message via UDP
163 | socket.on('message', data => {
164 | const msg = new OSC.Message()
165 | msg.unpack(data)
166 | console.log(msg.args)
167 | })
168 | ```
169 |
170 | ## Development
171 |
172 | osc-js uses [Babel](http://babeljs.io) for ES6 support, [ESDoc](https://esdoc.org) for documentation, [Mocha](https://mochajs.org/) + [Chai](http://chaijs.com/) for testing and [Rollup](https://rollupjs.org) for generating the UMD module.
173 |
174 | Clone the repository and install all dependencies:
175 |
176 | ```
177 | git clone git@github.com:adzialocha/osc-js.git
178 | cd osc-js
179 | npm install
180 | ```
181 |
182 | ### Testing
183 |
184 | `npm run test` for running the tests.
185 | `npm run test:watch` for running specs during development. Check code style with `npm run lint`.
186 |
187 | ### Deployment
188 |
189 | `npm run build` for exporting UMD module in `lib` folder.
190 |
191 | ### Contributors
192 |
193 | * [@adzialocha](https://github.com/adzialocha)
194 | * [@davidgranstrom](https://github.com/davidgranstrom)
195 | * [@elgiano](https://github.com/elgiano)
196 | * [@eliot-akira](https://github.com/eliot-akira)
197 | * [@JacobMuchow](https://github.com/JacobMuchow)
198 | * [@PeterKey](https://github.com/PeterKey)
199 | * [@yaxu](https://github.com/yaxu)
200 | * [@yojeek](https://github.com/yojeek)
201 |
202 | ### ESDocs
203 |
204 | `npm run docs` for generating a `docs` folder with HTML files documenting the library. Read them online here: [https://adzialocha.github.io/osc-js](https://adzialocha.github.io/osc-js)
205 |
206 | ## License
207 |
208 | MIT License `MIT`
209 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "osc-js",
3 | "version": "2.4.1",
4 | "description": "OSC library for Node.js and the browser, with customizable Plugin API for WebSocket, UDP or bridge networking",
5 | "main": "lib/osc.js",
6 | "browser": "lib/osc.min.js",
7 | "types": "lib/osc.d.ts",
8 | "scripts": {
9 | "build": "rollup -c",
10 | "docs": "esdoc",
11 | "lint": "eslint rollup.config.js src/** test/**",
12 | "test": "mocha test/** --require @babel/register --exit",
13 | "test:watch": "mocha test/** --require @babel/register --reporter min --watch test/** src/**"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+ssh://git@github.com/adzialocha/osc-js.git"
18 | },
19 | "keywords": [
20 | "osc",
21 | "data",
22 | "open",
23 | "sound",
24 | "control",
25 | "websocket",
26 | "udp",
27 | "datagram",
28 | "network"
29 | ],
30 | "author": "Andreas Dzialocha",
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/adzialocha/osc-js/issues"
34 | },
35 | "files": [
36 | "lib"
37 | ],
38 | "homepage": "https://github.com/adzialocha/osc-js#readme",
39 | "devDependencies": {
40 | "@babel/core": "7.24.4",
41 | "@babel/eslint-parser": "7.24.1",
42 | "@babel/preset-env": "7.24.4",
43 | "@babel/register": "7.23.7",
44 | "@rollup/plugin-alias": "^3.1.9",
45 | "@rollup/plugin-babel": "^5.3.1",
46 | "@types/node": "^18.8.3",
47 | "@types/ws": "^8.5.10",
48 | "chai": "4.3.6",
49 | "chai-spies-next": "0.9.3",
50 | "esdoc": "1.1.0",
51 | "esdoc-ecmascript-proposal-plugin": "^1.0.0",
52 | "esdoc-standard-plugin": "1.0.0",
53 | "eslint": "8.24.0",
54 | "eslint-config-airbnb-base": "15.0.0",
55 | "eslint-plugin-import": "2.29.1",
56 | "mocha": "10.4.0",
57 | "rollup": "2.79.1",
58 | "rollup-plugin-cleanup": "3.2.1",
59 | "rollup-plugin-dts": "^4.2.2",
60 | "rollup-plugin-terser": "7.0.2",
61 | "typescript": "^4.8.4"
62 | },
63 | "dependencies": {
64 | "ws": "^8.16.0"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import alias from '@rollup/plugin-alias'
2 | import babel from '@rollup/plugin-babel'
3 | import cleanup from 'rollup-plugin-cleanup'
4 | import dts from 'rollup-plugin-dts'
5 | import { terser } from 'rollup-plugin-terser'
6 |
7 | function rollupPlugins({ isBrowser = false } = {}) {
8 | const plugins = [
9 | babel({
10 | babelHelpers: 'bundled',
11 | exclude: 'node_modules/**',
12 | }),
13 | cleanup(),
14 | ]
15 |
16 | return isBrowser ? [alias({
17 | entries: [
18 | { find: 'ws', replacement: 'src/external/ws.js' },
19 | { find: 'dgram', replacement: 'src/external/dgram.js' },
20 | ],
21 | }), ...plugins, terser()] : plugins
22 | }
23 |
24 | function buildOptions(customOptions = {}) {
25 | const { file, isBrowser } = customOptions
26 |
27 | const defaultOptions = {
28 | input: 'src/osc.js',
29 | external: isBrowser ? [] : ['ws', 'dgram'],
30 | plugins: rollupPlugins({ isBrowser }),
31 | output: {
32 | globals: isBrowser ? {} : {
33 | ws: 'ws',
34 | dgram: 'dgram',
35 | },
36 | file: file || 'lib/osc.js',
37 | name: 'OSC',
38 | format: 'umd',
39 | sourcemap: isBrowser || false,
40 | },
41 | }
42 |
43 | return defaultOptions
44 | }
45 |
46 | export default [
47 | buildOptions(),
48 | buildOptions({
49 | file: 'lib/osc.min.js',
50 | isBrowser: true,
51 | }),
52 | {
53 | input: './src/osc.js',
54 | output: [{ file: 'lib/osc.d.ts', format: 'es' }],
55 | plugins: [dts()],
56 | external: ['http', 'https'],
57 | },
58 | ]
59 |
--------------------------------------------------------------------------------
/src/atomic.js:
--------------------------------------------------------------------------------
1 | import { isUndefined } from './common/utils'
2 |
3 | /**
4 | * Base class for OSC Atomic Data Types
5 | */
6 | export default class Atomic {
7 | /**
8 | * Create an Atomic instance
9 | * @param {*} [value] Initial value of any type
10 | */
11 | constructor(value) {
12 | /** @type {*} value */
13 | this.value = value
14 | /** @type {number} offset */
15 | this.offset = 0
16 | }
17 |
18 | /**
19 | * Interpret the given value of this entity as packed binary data
20 | * @param {string} method The DataView method to write to the ArrayBuffer
21 | * @param {number} byteLength Size of array in bytes
22 | * @return {Uint8Array} Packed binary data
23 | */
24 | pack(method, byteLength) {
25 | if (!(method && byteLength)) {
26 | throw new Error('OSC Atomic cant\'t be packed without given method or byteLength')
27 | }
28 |
29 | const data = new Uint8Array(byteLength)
30 | const dataView = new DataView(data.buffer)
31 |
32 | if (isUndefined(this.value)) {
33 | throw new Error('OSC Atomic cant\'t be encoded with empty value')
34 | }
35 |
36 | // use DataView to write to ArrayBuffer
37 | dataView[method](this.offset, this.value, false)
38 |
39 | // always return binary Uint8Array after packing
40 | return data
41 | }
42 |
43 | /**
44 | * Unpack binary data from DataView according to the given format
45 | * @param {DataView} dataView The DataView holding the binary representation of the value
46 | * @param {string} method The DataView method to read the format from the ArrayBuffer
47 | * @param {number} byteLength Size of array in bytes
48 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
49 | * @return {number} Offset after unpacking
50 | */
51 | unpackWithMethod(dataView, method, byteLength, initialOffset = 0) {
52 | if (!(dataView && method && byteLength)) {
53 | throw new Error('OSC Atomic cant\'t be unpacked without given dataView, method or byteLength')
54 | }
55 |
56 | if (!(dataView instanceof DataView)) {
57 | throw new Error('OSC Atomic expects an instance of type DataView')
58 | }
59 |
60 | // use DataView to read from ArrayBuffer and add offset
61 | this.value = dataView[method](initialOffset, false)
62 | this.offset = initialOffset + byteLength
63 |
64 | // always return offset number after unpacking
65 | return this.offset
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/atomic/blob.js:
--------------------------------------------------------------------------------
1 | import {
2 | isBlob,
3 | isUndefined,
4 | pad,
5 | } from '../common/utils'
6 |
7 | import Atomic from '../atomic'
8 |
9 | /**
10 | * 8-bit bytes of arbitrary binary data OSC Atomic Data Type
11 | */
12 | export default class AtomicBlob extends Atomic {
13 | /**
14 | * Create an AtomicBlob instance
15 | * @param {Uint8Array} [value] Binary data
16 | */
17 | constructor(value) {
18 | if (value && !isBlob(value)) {
19 | throw new Error('OSC AtomicBlob constructor expects value of type Uint8Array')
20 | }
21 |
22 | super(value)
23 | }
24 |
25 | /**
26 | * Interpret the given blob as packed binary data
27 | * @return {Uint8Array} Packed binary data
28 | */
29 | pack() {
30 | if (isUndefined(this.value)) {
31 | throw new Error('OSC AtomicBlob can not be encoded with empty value')
32 | }
33 |
34 | const byteLength = pad(this.value.byteLength)
35 | const data = new Uint8Array(byteLength + 4)
36 | const dataView = new DataView(data.buffer)
37 |
38 | // an int32 size count
39 | dataView.setInt32(0, this.value.byteLength, false)
40 | // followed by 8-bit bytes of arbitrary binary data
41 | data.set(this.value, 4)
42 |
43 | return data
44 | }
45 |
46 | /**
47 | * Unpack binary data from DataView and read a blob
48 | * @param {DataView} dataView The DataView holding the binary representation of the blob
49 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
50 | * @return {number} Offset after unpacking
51 | */
52 | unpack(dataView, initialOffset = 0) {
53 | if (!(dataView instanceof DataView)) {
54 | throw new Error('OSC AtomicBlob expects an instance of type DataView')
55 | }
56 |
57 | const byteLength = dataView.getInt32(initialOffset, false)
58 |
59 | /** @type {Uint8Array} value */
60 | this.value = new Uint8Array(dataView.buffer, initialOffset + 4, byteLength)
61 | /** @type {number} offset */
62 | this.offset = pad(initialOffset + 4 + byteLength)
63 |
64 | return this.offset
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/atomic/constant.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Extended boolean type without data representing "True"
3 | */
4 | export const VALUE_TRUE = true
5 |
6 | /**
7 | * Extended boolean type without data representing "False"
8 | */
9 | export const VALUE_FALSE = false
10 |
11 | /**
12 | * Extended type without data representing "None"
13 | * @type {null}
14 | */
15 | export const VALUE_NONE = null
16 |
17 | /**
18 | * Extended type without data representing "Infinity"
19 | */
20 | export const VALUE_INFINITY = Infinity
21 |
--------------------------------------------------------------------------------
/src/atomic/float32.js:
--------------------------------------------------------------------------------
1 | import { isNumber } from '../common/utils'
2 |
3 | import Atomic from '../atomic'
4 |
5 | /**
6 | * 32-bit big-endian IEEE 754 floating point number OSC Atomic Data Type
7 | */
8 | export default class AtomicFloat32 extends Atomic {
9 | /**
10 | * Create an AtomicFloat32 instance
11 | * @param {number} [value] Float number
12 | */
13 | constructor(value) {
14 | if (value && !isNumber(value)) {
15 | throw new Error('OSC AtomicFloat32 constructor expects value of type float')
16 | }
17 |
18 | super(value)
19 | }
20 |
21 | /**
22 | * Interpret the given number as packed binary data
23 | * @return {Uint8Array} Packed binary data
24 | */
25 | pack() {
26 | return super.pack('setFloat32', 4)
27 | }
28 |
29 | /**
30 | * Unpack binary data from DataView and read a Float32 number
31 | * @param {DataView} dataView The DataView holding the binary representation of the value
32 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
33 | * @return {number} Offset after unpacking
34 | */
35 | unpack(dataView, initialOffset = 0) {
36 | return super.unpackWithMethod(dataView, 'getFloat32', 4, initialOffset)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/atomic/float64.js:
--------------------------------------------------------------------------------
1 | import { isNumber } from '../common/utils'
2 |
3 | import Atomic from '../atomic'
4 |
5 | /**
6 | * 64-bit big-endian IEEE 754 floating point number OSC Atomic Data Type
7 | */
8 | export default class AtomicFloat64 extends Atomic {
9 | /**
10 | * Create an AtomicFloat64 instance
11 | * @param {number} [value] Float number
12 | */
13 | constructor(value) {
14 | if (value && !isNumber(value)) {
15 | throw new Error('OSC AtomicFloat64 constructor expects value of type float')
16 | }
17 |
18 | super(value)
19 | }
20 |
21 | /**
22 | * Interpret the given number as packed binary data
23 | * @return {Uint8Array} Packed binary data
24 | */
25 | pack() {
26 | return super.pack('setFloat64', 8)
27 | }
28 |
29 | /**
30 | * Unpack binary data from DataView and read a Float64 number
31 | * @param {DataView} dataView The DataView holding the binary representation of the value
32 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
33 | * @return {number} Offset after unpacking
34 | */
35 | unpack(dataView, initialOffset = 0) {
36 | return super.unpackWithMethod(dataView, 'getFloat64', 8, initialOffset)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/atomic/int32.js:
--------------------------------------------------------------------------------
1 | import { isInt } from '../common/utils'
2 |
3 | import Atomic from '../atomic'
4 |
5 | /**
6 | * 32-bit big-endian two's complement integer OSC Atomic Data Type
7 | */
8 | export default class AtomicInt32 extends Atomic {
9 | /**
10 | * Create an AtomicInt32 instance
11 | * @param {number} [value] Initial integer value
12 | */
13 | constructor(value) {
14 | if (value && !isInt(value)) {
15 | throw new Error('OSC AtomicInt32 constructor expects value of type number')
16 | }
17 |
18 | super(value)
19 | }
20 |
21 | /**
22 | * Interpret the given number as packed binary data
23 | * @return {Uint8Array} Packed binary data
24 | */
25 | pack() {
26 | return super.pack('setInt32', 4)
27 | }
28 |
29 | /**
30 | * Unpack binary data from DataView and read a Int32 number
31 | * @param {DataView} dataView The DataView holding the binary representation of the value
32 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
33 | * @return {number} Offset after unpacking
34 | */
35 | unpack(dataView, initialOffset = 0) {
36 | return super.unpackWithMethod(dataView, 'getInt32', 4, initialOffset)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/atomic/int64.js:
--------------------------------------------------------------------------------
1 | import Atomic from '../atomic'
2 |
3 | const MAX_INT64 = BigInt('9223372036854775807')
4 | const MIN_INT64 = BigInt('-9223372036854775808')
5 |
6 | /**
7 | * 64-bit big-endian two's complement integer OSC Atomic Data Type
8 | */
9 | export default class AtomicInt64 extends Atomic {
10 | /**
11 | * Create an AtomicInt64 instance
12 | * @param {number} [value] Initial integer value
13 | */
14 | constructor(value) {
15 | if (value && typeof value !== 'bigint') {
16 | throw new Error('OSC AtomicInt64 constructor expects value of type BigInt')
17 | }
18 |
19 | if (value && (value < MIN_INT64 || value > MAX_INT64)) {
20 | throw new Error('OSC AtomicInt64 value is out of bounds')
21 | }
22 |
23 | let tmp
24 | if (value) {
25 | tmp = BigInt.asIntN(64, value)
26 | }
27 |
28 | super(tmp)
29 | }
30 |
31 | /**
32 | * Interpret the given number as packed binary data
33 | * @return {Uint8Array} Packed binary data
34 | */
35 | pack() {
36 | return super.pack('setBigInt64', 8)
37 | }
38 |
39 | /**
40 | * Unpack binary data from DataView and read a Int64 number
41 | * @param {DataView} dataView The DataView holding the binary representation of the value
42 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
43 | * @return {number} Offset after unpacking
44 | */
45 | unpack(dataView, initialOffset = 0) {
46 | return super.unpackWithMethod(dataView, 'getBigInt64', 8, initialOffset)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/atomic/string.js:
--------------------------------------------------------------------------------
1 | import {
2 | hasProperty,
3 | isString,
4 | isUndefined,
5 | pad,
6 | } from '../common/utils'
7 |
8 | import Atomic from '../atomic'
9 |
10 | /** Slice size of large strings for fallback method */
11 | const STR_SLICE_SIZE = 65537
12 |
13 | /** Text encoding format */
14 | const STR_ENCODING = 'utf-8'
15 |
16 | /**
17 | * Helper method to decode a string using different methods depending on environment
18 | * @param {number[]} charCodes Array of char codes
19 | * @return {string} Decoded string
20 | */
21 | function charCodesToString(charCodes) {
22 | // Use these methods to be able to convert large strings
23 | if (hasProperty('Buffer')) {
24 | return Buffer.from(charCodes).toString(STR_ENCODING)
25 | } else if (hasProperty('TextDecoder')) {
26 | return new TextDecoder(STR_ENCODING) // eslint-disable-line no-undef
27 | .decode(new Int8Array(charCodes))
28 | }
29 |
30 | // Fallback method
31 | let str = ''
32 |
33 | for (let i = 0; i < charCodes.length; i += STR_SLICE_SIZE) {
34 | str += String.fromCharCode.apply(
35 | null,
36 | charCodes.slice(i, i + STR_SLICE_SIZE),
37 | )
38 | }
39 |
40 | return str
41 | }
42 |
43 | /**
44 | * A sequence of non-null ASCII characters OSC Atomic Data Type
45 | */
46 | export default class AtomicString extends Atomic {
47 | /**
48 | * Create an AtomicString instance
49 | * @param {string} [value] Initial string value
50 | */
51 | constructor(value) {
52 | if (value && !isString(value)) {
53 | throw new Error('OSC AtomicString constructor expects value of type string')
54 | }
55 |
56 | super(value)
57 | }
58 |
59 | /**
60 | * Interpret the given string as packed binary data
61 | * @return {Uint8Array} Packed binary data
62 | */
63 | pack() {
64 | if (isUndefined(this.value)) {
65 | throw new Error('OSC AtomicString can not be encoded with empty value')
66 | }
67 |
68 | // add 0-3 null characters for total number of bits a multiple of 32
69 | const terminated = `${this.value}\u0000`
70 | const byteLength = pad(terminated.length)
71 |
72 | const buffer = new Uint8Array(byteLength)
73 |
74 | for (let i = 0; i < terminated.length; i += 1) {
75 | buffer[i] = terminated.charCodeAt(i)
76 | }
77 |
78 | return buffer
79 | }
80 |
81 | /**
82 | * Unpack binary data from DataView and read a string
83 | * @param {DataView} dataView The DataView holding the binary representation of the string
84 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
85 | * @return {number} Offset after unpacking
86 | */
87 | unpack(dataView, initialOffset = 0) {
88 | if (!(dataView instanceof DataView)) {
89 | throw new Error('OSC AtomicString expects an instance of type DataView')
90 | }
91 |
92 | let offset = initialOffset
93 | let charcode
94 | const charCodes = []
95 |
96 | for (; offset < dataView.byteLength; offset += 1) {
97 | charcode = dataView.getUint8(offset)
98 |
99 | // check for terminating null character
100 | if (charcode !== 0) {
101 | charCodes.push(charcode)
102 | } else {
103 | offset += 1
104 | break
105 | }
106 | }
107 |
108 | if (offset === dataView.length) {
109 | throw new Error('OSC AtomicString found a malformed OSC string')
110 | }
111 |
112 | /** @type {number} offset */
113 | this.offset = pad(offset)
114 | /** @type {string} value */
115 | this.value = charCodesToString(charCodes)
116 |
117 | return this.offset
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/atomic/timetag.js:
--------------------------------------------------------------------------------
1 | import {
2 | isDate,
3 | isInt,
4 | isUndefined,
5 | } from '../common/utils'
6 |
7 | import Atomic from '../atomic'
8 |
9 | /** 70 years in seconds */
10 | export const SECONDS_70_YEARS = 2208988800
11 | /** 2^32 */
12 | export const TWO_POWER_32 = 4294967296
13 |
14 | /**
15 | * Timetag helper class for representing NTP timestamps
16 | * and conversion between them and javascript representation
17 | */
18 | export class Timetag {
19 | /**
20 | * Create a Timetag instance
21 | * @param {number} [seconds=0] Initial NTP *seconds* value
22 | * @param {number} [fractions=0] Initial NTP *fractions* value
23 | */
24 | constructor(seconds = 0, fractions = 0) {
25 | if (!(isInt(seconds) && isInt(fractions))) {
26 | throw new Error('OSC Timetag constructor expects values of type integer number')
27 | }
28 |
29 | /** @type {number} seconds */
30 | this.seconds = seconds
31 | /** @type {number} fractions */
32 | this.fractions = fractions
33 | }
34 |
35 | /**
36 | * Converts from NTP to JS representation and back
37 | * @param {number} [milliseconds] Converts from JS milliseconds to NTP.
38 | * Leave empty for converting from NTP to JavaScript representation
39 | * @return {number} Javascript timestamp
40 | */
41 | timestamp(milliseconds) {
42 | let seconds
43 |
44 | if (typeof milliseconds === 'number') {
45 | seconds = milliseconds / 1000
46 | const rounded = Math.floor(seconds)
47 |
48 | this.seconds = rounded + SECONDS_70_YEARS
49 | this.fractions = Math.round(TWO_POWER_32 * (seconds - rounded))
50 |
51 | return milliseconds
52 | }
53 |
54 | seconds = this.seconds - SECONDS_70_YEARS
55 | return (seconds + Math.round(this.fractions / TWO_POWER_32)) * 1000
56 | }
57 | }
58 |
59 | /**
60 | * 64-bit big-endian fixed-point time tag, semantics
61 | * defined below OSC Atomic Data Type
62 | */
63 | export default class AtomicTimetag extends Atomic {
64 | /**
65 | * Create a AtomicTimetag instance
66 | * @param {number|Timetag|Date} [value] Initial date, leave empty if
67 | * you want it to be the current date
68 | */
69 | constructor(value = Date.now()) {
70 | let timetag = new Timetag()
71 |
72 | if (value instanceof Timetag) {
73 | timetag = value
74 | } else if (isInt(value)) {
75 | timetag.timestamp(value)
76 | } else if (isDate(value)) {
77 | timetag.timestamp(value.getTime())
78 | }
79 |
80 | super(timetag)
81 | }
82 |
83 | /**
84 | * Interpret the given timetag as packed binary data
85 | * @return {Uint8Array} Packed binary data
86 | */
87 | pack() {
88 | if (isUndefined(this.value)) {
89 | throw new Error('OSC AtomicTimetag can not be encoded with empty value')
90 | }
91 |
92 | const { seconds, fractions } = this.value
93 | const data = new Uint8Array(8)
94 | const dataView = new DataView(data.buffer)
95 |
96 | dataView.setInt32(0, seconds, false)
97 | dataView.setInt32(4, fractions, false)
98 |
99 | return data
100 | }
101 |
102 | /**
103 | * Unpack binary data from DataView and read a timetag
104 | * @param {DataView} dataView The DataView holding the binary representation of the timetag
105 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
106 | * @return {number} Offset after unpacking
107 | */
108 | unpack(dataView, initialOffset = 0) {
109 | if (!(dataView instanceof DataView)) {
110 | throw new Error('OSC AtomicTimetag expects an instance of type DataView')
111 | }
112 |
113 | const seconds = dataView.getUint32(initialOffset, false)
114 | const fractions = dataView.getUint32(initialOffset + 4, false)
115 |
116 | /** @type {Timetag} value */
117 | this.value = new Timetag(seconds, fractions)
118 | /** @type {number} offset */
119 | this.offset = initialOffset + 8
120 |
121 | return this.offset
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/atomic/uint64.js:
--------------------------------------------------------------------------------
1 | import Atomic from '../atomic'
2 |
3 | const MAX_UINT64 = BigInt('18446744073709551615')
4 |
5 | /**
6 | * Unsigned 64-bit big-endian two's complement integer OSC Atomic Data Type
7 | */
8 | export default class AtomicUInt64 extends Atomic {
9 | /**
10 | * Create an AtomicUInt64 instance
11 | * @param {number} [value] Initial integer value
12 | */
13 | constructor(value) {
14 | if (value && typeof value !== 'bigint') {
15 | throw new Error('OSC AtomicUInt64 constructor expects value of type BigInt')
16 | }
17 |
18 | if (value && (value < 0 || value > MAX_UINT64)) {
19 | throw new Error('OSC AtomicUInt64 value is out of bounds')
20 | }
21 |
22 | let tmp
23 | if (value) {
24 | tmp = BigInt.asUintN(64, value)
25 | }
26 |
27 | super(tmp)
28 | }
29 |
30 | /**
31 | * Interpret the given number as packed binary data
32 | * @return {Uint8Array} Packed binary data
33 | */
34 | pack() {
35 | return super.pack('setBigUint64', 8)
36 | }
37 |
38 | /**
39 | * Unpack binary data from DataView and read a UInt64 number
40 | * @param {DataView} dataView The DataView holding the binary representation of the value
41 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
42 | * @return {number} Offset after unpacking
43 | */
44 | unpack(dataView, initialOffset = 0) {
45 | return super.unpackWithMethod(dataView, 'getBigUint64', 8, initialOffset)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/bundle.js:
--------------------------------------------------------------------------------
1 | import EncodeHelper from './common/helpers'
2 | import { isArray, isInt } from './common/utils'
3 |
4 | import AtomicInt32 from './atomic/int32'
5 | import AtomicString from './atomic/string'
6 | import AtomicTimetag from './atomic/timetag'
7 | import Message from './message'
8 |
9 | /** OSC Bundle string */
10 | export const BUNDLE_TAG = '#bundle'
11 |
12 | /**
13 | * An OSC Bundle consist of a Timetag and one or many Bundle Elements.
14 | * The elements are either OSC Messages or more OSC Bundles
15 | */
16 | export default class Bundle {
17 | /**
18 | * Create a Bundle instance
19 | * @param {...*} args Timetag and elements. See examples for options
20 | *
21 | * @example
22 | * const bundle = new Bundle(new Date() + 500)
23 | *
24 | * @example
25 | * const message = new Message('/test/path', 51.2)
26 | * const anotherBundle = new Bundle([message], Date.now() + 1500)
27 | *
28 | * @example
29 | * const message = new Message('/test/path', 51.2)
30 | * const anotherMessage = new Message('/test/message', 'test', 12)
31 | * const anotherBundle = new Bundle(message, anotherMessage)
32 | */
33 | constructor(...args) {
34 | /**
35 | * @type {number} offset
36 | * @private
37 | */
38 | this.offset = 0
39 | /** @type {AtomicTimetag} timetag */
40 | this.timetag = new AtomicTimetag()
41 | /** @type {array} bundleElements */
42 | this.bundleElements = []
43 |
44 | if (args.length > 0) {
45 | // first argument is an Date or js timestamp (number)
46 | if (args[0] instanceof Date || isInt(args[0])) {
47 | this.timetag = new AtomicTimetag(args[0])
48 | } else if (isArray(args[0])) {
49 | // first argument is an Array of Bundle elements
50 | args[0].forEach((item) => {
51 | this.add(item)
52 | })
53 |
54 | // second argument is an Date or js timestamp (number)
55 | if (args.length > 1 && (args[1] instanceof Date || isInt(args[1]))) {
56 | this.timetag = new AtomicTimetag(args[1])
57 | }
58 | } else {
59 | // take all arguments as Bundle elements
60 | args.forEach((item) => {
61 | this.add(item)
62 | })
63 | }
64 | }
65 | }
66 |
67 | /**
68 | * Take a JavaScript timestamp to set the Bundle's timetag
69 | * @param {number} ms JS timestamp in milliseconds
70 | *
71 | * @example
72 | * const bundle = new Bundle()
73 | * bundle.timestamp(Date.now() + 5000) // in 5 seconds
74 | */
75 | timestamp(ms) {
76 | if (!isInt(ms)) {
77 | throw new Error('OSC Bundle needs an integer for setting the timestamp')
78 | }
79 |
80 | this.timetag = new AtomicTimetag(ms)
81 | }
82 |
83 | /**
84 | * Add a Message or Bundle to the list of elements
85 | * @param {Bundle|Message} item
86 | */
87 | add(item) {
88 | if (!(item instanceof Message || item instanceof Bundle)) {
89 | throw new Error('OSC Bundle contains only Messages and Bundles')
90 | }
91 |
92 | this.bundleElements.push(item)
93 | }
94 |
95 | /**
96 | * Interpret the Bundle as packed binary data
97 | * @return {Uint8Array} Packed binary data
98 | */
99 | pack() {
100 | const encoder = new EncodeHelper()
101 |
102 | // an OSC Bundle consists of the OSC-string "#bundle"
103 | encoder.add(new AtomicString(BUNDLE_TAG))
104 |
105 | // followed by an OSC Time Tag
106 | if (!this.timetag) {
107 | this.timetag = new AtomicTimetag()
108 | }
109 |
110 | encoder.add(this.timetag)
111 |
112 | // followed by zero or more OSC Bundle Elements
113 | this.bundleElements.forEach((item) => {
114 | encoder.add(new AtomicInt32(item.pack().byteLength))
115 | encoder.add(item)
116 | })
117 |
118 | return encoder.merge()
119 | }
120 |
121 | /**
122 | * Unpack binary data to read a Bundle
123 | * @param {DataView} dataView The DataView holding the binary representation of a Bundle
124 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
125 | * @return {number} Offset after unpacking
126 | */
127 | unpack(dataView, initialOffset = 0) {
128 | if (!(dataView instanceof DataView)) {
129 | throw new Error('OSC Bundle expects an instance of type DataView')
130 | }
131 |
132 | // read the beginning bundle string
133 | const parentHead = new AtomicString()
134 | parentHead.unpack(dataView, initialOffset)
135 |
136 | if (parentHead.value !== BUNDLE_TAG) {
137 | throw new Error('OSC Bundle does not contain a valid #bundle head')
138 | }
139 |
140 | // read the timetag
141 | const timetag = new AtomicTimetag()
142 | let offset = timetag.unpack(dataView, parentHead.offset)
143 |
144 | // read the bundle elements
145 | this.bundleElements = []
146 |
147 | while (offset < dataView.byteLength) {
148 | const head = new AtomicString()
149 | const size = new AtomicInt32()
150 |
151 | offset = size.unpack(dataView, offset)
152 |
153 | // check if Packet is a Bundle or a Message
154 | let item
155 | head.unpack(dataView, offset)
156 |
157 | if (head.value === BUNDLE_TAG) {
158 | item = new Bundle()
159 | } else {
160 | item = new Message()
161 | }
162 |
163 | offset = item.unpack(dataView, offset)
164 |
165 | this.bundleElements.push(item)
166 | }
167 |
168 | this.offset = offset
169 | this.timetag = timetag
170 |
171 | return this.offset
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/common/helpers.js:
--------------------------------------------------------------------------------
1 | import {
2 | isArray,
3 | isBlob,
4 | isBoolean,
5 | isFloat,
6 | isInfinity,
7 | isInt,
8 | isNull,
9 | isString,
10 | } from './utils'
11 |
12 | /**
13 | * Checks type of given object and returns the regarding OSC
14 | * Type tag character
15 | * @param {*} item Any object
16 | * @return {string} OSC Type tag character
17 | */
18 | export function typeTag(item) {
19 | if (isInt(item)) {
20 | return 'i'
21 | } else if (isFloat(item)) {
22 | return 'f'
23 | } else if (isString(item)) {
24 | return 's'
25 | } else if (isBlob(item)) {
26 | return 'b'
27 | } else if (isBoolean(item)) {
28 | return item ? 'T' : 'F'
29 | } else if (isNull(item)) {
30 | return 'N'
31 | } else if (isInfinity(item)) {
32 | return 'I'
33 | }
34 |
35 | throw new Error('OSC typeTag() found unknown value type')
36 | }
37 |
38 | /**
39 | * Sanitizes an OSC-ready Address Pattern
40 | * @param {string[]|string} obj Address as string or array of strings
41 | * @return {string} Corrected address string
42 | *
43 | * @example
44 | * // all calls return '/test/path' string:
45 | * prepareAddress('test/path')
46 | * prepareAddress('/test/path/')
47 | * prepareAddress([test, path])
48 | */
49 | export function prepareAddress(obj) {
50 | let address = ''
51 |
52 | if (isArray(obj)) {
53 | return `/${obj.join('/')}`
54 | } else if (isString(obj)) {
55 | address = obj
56 |
57 | // remove slash at ending of address
58 | if (address.length > 1 && address[address.length - 1] === '/') {
59 | address = address.slice(0, address.length - 1)
60 | }
61 |
62 | // add slash at beginning of address
63 | if (address.length > 1 && address[0] !== '/') {
64 | address = `/${address}`
65 | }
66 |
67 | return address
68 | }
69 |
70 | throw new Error('OSC prepareAddress() needs addresses of type array or string')
71 | }
72 |
73 | /**
74 | * Make an OSC address pattern javascript-regex-ready
75 | * @param {string} str OSC address pattern
76 | * @return {string} Javascript RegEx string
77 | */
78 | export function prepareRegExPattern(str) {
79 | let pattern
80 |
81 | if (!(isString(str))) {
82 | throw new Error('OSC prepareRegExPattern() needs strings')
83 | }
84 |
85 | pattern = str.replace(/\./g, '\\.')
86 | pattern = pattern.replace(/\(/g, '\\(')
87 | pattern = pattern.replace(/\)/g, '\\)')
88 |
89 | pattern = pattern.replace(/\{/g, '(')
90 | pattern = pattern.replace(/\}/g, ')')
91 | pattern = pattern.replace(/,/g, '|')
92 |
93 | pattern = pattern.replace(/\[!/g, '[^')
94 |
95 | pattern = pattern.replace(/\?/g, '.')
96 | pattern = pattern.replace(/\*/g, '.*')
97 |
98 | return pattern
99 | }
100 |
101 | /**
102 | * Holds a list of items and helps to merge them
103 | * into a single array of packed binary data
104 | */
105 | export default class EncodeHelper {
106 | /**
107 | * Create a new EncodeHelper instance
108 | */
109 | constructor() {
110 | /** @type {array} data */
111 | this.data = []
112 | /** @type {number} byteLength */
113 | this.byteLength = 0
114 | }
115 |
116 | /**
117 | * Packs an item and adds it to the list
118 | * @param {*} item Any object
119 | * @return {EncodeHelper}
120 | */
121 | add(item) {
122 | // Skip encoding items which do not need a payload as they are constants
123 | if (isBoolean(item) || isInfinity(item) || isNull(item)) {
124 | return this
125 | }
126 |
127 | const buffer = item.pack()
128 | this.byteLength += buffer.byteLength
129 | this.data.push(buffer)
130 |
131 | return this
132 | }
133 |
134 | /**
135 | * Merge all added items into one Uint8Array
136 | * @return {Uint8Array} Merged binary data array of all items
137 | */
138 | merge() {
139 | const result = new Uint8Array(this.byteLength)
140 | let offset = 0
141 |
142 | this.data.forEach((data) => {
143 | result.set(data, offset)
144 | offset += data.byteLength
145 | })
146 |
147 | return result
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/common/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if given object is an integer number
3 | * @param {*} n
4 | * @return {boolean}
5 | */
6 | export function isInt(n) {
7 | return Number(n) === n && n % 1 === 0
8 | }
9 |
10 | /**
11 | * Check if given object is a float number
12 | * @param {*} n
13 | * @return {boolean}
14 | */
15 | export function isFloat(n) {
16 | return Number(n) === n && n % 1 !== 0
17 | }
18 |
19 | /**
20 | * Check if given object is a number
21 | * @param {*} n
22 | * @return {boolean}
23 | */
24 | export function isNumber(n) {
25 | return Number(n) === n
26 | }
27 |
28 | /**
29 | * Check if given object is a string
30 | * @param {*} n
31 | * @return {boolean}
32 | */
33 | export function isString(n) {
34 | return typeof n === 'string'
35 | }
36 |
37 | /**
38 | * Check if given object is a boolean
39 | * @param {*} n
40 | * @return {boolean}
41 | */
42 | export function isBoolean(n) {
43 | return typeof n === 'boolean'
44 | }
45 |
46 | /**
47 | * Check if given object is infinity constant
48 | * @param {*} n
49 | * @return {boolean}
50 | */
51 | export function isInfinity(n) {
52 | return n === Infinity
53 | }
54 |
55 | /**
56 | * Check if given object is an array
57 | * @param {*} n
58 | * @return {boolean}
59 | */
60 | export function isArray(n) {
61 | return Object.prototype.toString.call(n) === '[object Array]'
62 | }
63 |
64 | /**
65 | * Check if given object is an object
66 | * @param {*} n
67 | * @return {boolean}
68 | */
69 | export function isObject(n) {
70 | return Object.prototype.toString.call(n) === '[object Object]'
71 | }
72 |
73 | /**
74 | * Check if given object is a function
75 | * @param {*} n
76 | * @return {boolean}
77 | */
78 | export function isFunction(n) {
79 | return typeof n === 'function'
80 | }
81 |
82 | /**
83 | * Check if given object is a Uint8Array
84 | * @param {*} n
85 | * @return {boolean}
86 | */
87 | export function isBlob(n) {
88 | return n instanceof Uint8Array
89 | }
90 |
91 | /**
92 | * Check if given object is a Date
93 | * @param {*} n
94 | * @return {boolean}
95 | */
96 | export function isDate(n) {
97 | return n instanceof Date
98 | }
99 |
100 | /**
101 | * Check if given object is undefined
102 | * @param {*} n
103 | * @return {boolean}
104 | */
105 | export function isUndefined(n) {
106 | return typeof n === 'undefined'
107 | }
108 |
109 | /**
110 | * Check if given object is null
111 | * @param {*} n
112 | * @return {boolean}
113 | */
114 | export function isNull(n) {
115 | return n === null
116 | }
117 |
118 | /**
119 | * Return the next multiple of four
120 | * @param {number} n
121 | */
122 | export function pad(n) {
123 | return (n + 3) & ~0x03
124 | }
125 |
126 | /**
127 | * Checks if environment provides a feature
128 | * @param {string} name Name of needed feature
129 | * @return {boolean}
130 | */
131 | export function hasProperty(name) {
132 | return Object.prototype.hasOwnProperty.call(
133 | (typeof global !== 'undefined' ? global : window), // eslint-disable-line no-undef
134 | name,
135 | )
136 | }
137 |
138 | /**
139 | * Wrap binary data in DataView
140 | * @param {*} obj
141 | * @return {DataView}
142 | */
143 | export function dataView(obj) {
144 | if (obj.buffer) {
145 | return new DataView(obj.buffer)
146 | } else if (obj instanceof ArrayBuffer) {
147 | return new DataView(obj)
148 | }
149 |
150 | return new DataView(new Uint8Array(obj))
151 | }
152 |
--------------------------------------------------------------------------------
/src/events.js:
--------------------------------------------------------------------------------
1 | import {
2 | dataView,
3 | isArray,
4 | isFunction,
5 | isInt,
6 | isString,
7 | } from './common/utils'
8 |
9 | import {
10 | prepareAddress,
11 | prepareRegExPattern,
12 | } from './common/helpers'
13 |
14 | import Bundle from './bundle'
15 | import Message from './message'
16 | import Packet from './packet'
17 |
18 | /**
19 | * Default options
20 | * @private
21 | */
22 | const defaultOptions = {
23 | discardLateMessages: false,
24 | }
25 |
26 | /**
27 | * EventHandler to notify listeners on matching OSC messages and
28 | * status changes of plugins
29 | */
30 | export default class EventHandler {
31 | /**
32 | * Create an EventHandler instance
33 | * @param {object} options Custom options
34 | */
35 | constructor(options) {
36 | /**
37 | * @type {object} options
38 | * @private
39 | */
40 | this.options = { ...defaultOptions, ...options }
41 | /**
42 | * @type {array} addressHandlers
43 | * @private
44 | */
45 | this.addressHandlers = []
46 | /**
47 | * @type {object} eventHandlers
48 | * @private
49 | */
50 | this.eventHandlers = {
51 | open: [],
52 | error: [],
53 | close: [],
54 | }
55 | /**
56 | * @type {number} uuid
57 | * @private
58 | */
59 | this.uuid = 0
60 | }
61 |
62 | /**
63 | * Internally used method to dispatch OSC Packets. Extracts
64 | * given Timetags and dispatches them accordingly
65 | * @param {Packet} packet
66 | * @param {*} [rinfo] Remote address info
67 | * @return {boolean} Success state
68 | * @private
69 | */
70 | dispatch(packet, rinfo) {
71 | if (!(packet instanceof Packet)) {
72 | throw new Error('OSC EventHander dispatch() accepts only arguments of type Packet')
73 | }
74 |
75 | if (!packet.value) {
76 | throw new Error('OSC EventHander dispatch() can\'t read empty Packets')
77 | }
78 |
79 | if (packet.value instanceof Bundle) {
80 | const bundle = packet.value
81 |
82 | return bundle.bundleElements.forEach((bundleItem) => {
83 | if (bundleItem instanceof Bundle) {
84 | if (bundle.timetag.value.timestamp() < bundleItem.timetag.value.timestamp()) {
85 | throw new Error('OSC Bundle timestamp is older than the timestamp of enclosed Bundles')
86 | }
87 | return this.dispatch(new Packet(bundleItem))
88 | } else if (bundleItem instanceof Message) {
89 | const message = bundleItem
90 | return this.notify(
91 | message.address,
92 | message,
93 | bundle.timetag.value.timestamp(),
94 | rinfo,
95 | )
96 | }
97 |
98 | throw new Error('OSC EventHander dispatch() can\'t dispatch unknown Packet value')
99 | })
100 | } else if (packet.value instanceof Message) {
101 | const message = packet.value
102 | return this.notify(message.address, message, 0, rinfo)
103 | }
104 |
105 | throw new Error('OSC EventHander dispatch() can\'t dispatch unknown Packet value')
106 | }
107 |
108 | /**
109 | * Internally used method to invoke listener callbacks. Uses regular
110 | * expression pattern matching for OSC addresses
111 | * @param {string} name OSC address or event name
112 | * @param {*} [data] The data of the event
113 | * @param {*} [rinfo] Remote address info
114 | * @return {boolean} Success state
115 | * @private
116 | */
117 | call(name, data, rinfo) {
118 | let success = false
119 |
120 | // call event handlers
121 | if (isString(name) && name in this.eventHandlers) {
122 | this.eventHandlers[name].forEach((handler) => {
123 | handler.callback(data, rinfo)
124 | success = true
125 | })
126 |
127 | return success
128 | }
129 |
130 | // call address handlers
131 | const handlerKeys = Object.keys(this.addressHandlers)
132 | const handlers = this.addressHandlers
133 |
134 | handlerKeys.forEach((key) => {
135 | let foundMatch = false
136 |
137 | const regex = new RegExp(prepareRegExPattern(prepareAddress(name)), 'g')
138 | const test = regex.test(key)
139 |
140 | // found a matching address in our callback handlers
141 | if (test && key.length === regex.lastIndex) {
142 | foundMatch = true
143 | }
144 |
145 | if (!foundMatch) {
146 | // try matching address from callback handlers (when given)
147 | const reverseRegex = new RegExp(prepareRegExPattern(prepareAddress(key)), 'g')
148 | const reverseTest = reverseRegex.test(name)
149 |
150 | if (reverseTest && name.length === reverseRegex.lastIndex) {
151 | foundMatch = true
152 | }
153 | }
154 |
155 | if (foundMatch) {
156 | handlers[key].forEach((handler) => {
157 | handler.callback(data, rinfo)
158 | success = true
159 | })
160 | }
161 | })
162 |
163 | return success
164 | }
165 |
166 | /**
167 | * Notify the EventHandler of incoming OSC messages or status
168 | * changes (*open*, *close*, *error*). Handles OSC address patterns
169 | * and executes timed messages. Use binary arrays when
170 | * handling directly incoming network data. Packet's or Messages can
171 | * also be used
172 | * @param {...*} args
173 | * The OSC address pattern / event name as string}. For convenience and
174 | * Plugin API communication you can also use Message or Packet instances
175 | * or ArrayBuffer, Buffer instances (low-level access). The latter will
176 | * automatically be unpacked
177 | * When using a string you can also pass on data as a second argument
178 | * (any type). All regarding listeners will be notified with this data.
179 | * As a third argument you can define a javascript timestamp (number or
180 | * Date instance) for timed notification of the listeners.
181 | * @return {boolean} Success state of notification
182 | *
183 | * @example
184 | * const socket = dgram.createSocket('udp4')
185 | * socket.on('message', (message) => {
186 | * this.notify(message)
187 | * })
188 | *
189 | * @example
190 | * this.notify('error', error.message)
191 | *
192 | * @example
193 | * const message = new OSC.Message('/test/path', 55)
194 | * this.notify(message)
195 | *
196 | * @example
197 | * const message = new OSC.Message('/test/path', 55)
198 | * // override timestamp
199 | * this.notify(message.address, message, Date.now() + 5000)
200 | */
201 | notify(...args) {
202 | if (args.length === 0) {
203 | throw new Error('OSC EventHandler can not be called without any argument')
204 | }
205 |
206 | try {
207 | // check for incoming dispatchable OSC data
208 | if (args[0] instanceof Packet) {
209 | return this.dispatch(args[0], args[1])
210 | } else if (args[0] instanceof Bundle || args[0] instanceof Message) {
211 | return this.dispatch(new Packet(args[0]), args[1])
212 | } else if (!isString(args[0])) {
213 | const packet = new Packet()
214 | packet.unpack(dataView(args[0]))
215 | return this.dispatch(packet, args[1])
216 | }
217 |
218 | const name = args[0]
219 |
220 | // data argument
221 | let data = null
222 |
223 | if (args.length > 1) {
224 | data = args[1]
225 | }
226 |
227 | // timestamp argument
228 | let timestamp = null
229 |
230 | if (args.length > 2) {
231 | if (isInt(args[2])) {
232 | timestamp = args[2]
233 | } else if (args[2] instanceof Date) {
234 | timestamp = args[2].getTime()
235 | } else {
236 | throw new Error('OSC EventHandler timestamp has to be a number or Date')
237 | }
238 | }
239 |
240 | // remote address info
241 | let rinfo = null
242 |
243 | if (args.length >= 3) {
244 | rinfo = args[3]
245 | }
246 |
247 | // notify now or later
248 | if (timestamp) {
249 | const now = Date.now()
250 |
251 | // is message outdated?
252 | if (now > timestamp) {
253 | if (!this.options.discardLateMessages) {
254 | return this.call(name, data, rinfo)
255 | }
256 | }
257 |
258 | // notify later
259 | const that = this
260 |
261 | setTimeout(() => {
262 | that.call(name, data, rinfo)
263 | }, timestamp - now)
264 |
265 | return true
266 | }
267 |
268 | return this.call(name, data, rinfo)
269 | } catch (error) {
270 | this.notify('error', error)
271 | return false
272 | }
273 | }
274 |
275 | /**
276 | * Subscribe to a new address or event you want to listen to
277 | * @param {string} name The OSC address or event name
278 | * @param {function} callback Callback function on notification
279 | * @return {number} Subscription identifier (needed to unsubscribe)
280 | */
281 | on(name, callback) {
282 | if (!(isString(name) || isArray(name))) {
283 | throw new Error('OSC EventHandler accepts only strings or arrays for address patterns')
284 | }
285 |
286 | if (!isFunction(callback)) {
287 | throw new Error('OSC EventHandler callback has to be a function')
288 | }
289 |
290 | // get next id
291 | this.uuid += 1
292 |
293 | // prepare handler
294 | const handler = {
295 | id: this.uuid,
296 | callback,
297 | }
298 |
299 | // register event listener
300 | if (isString(name) && name in this.eventHandlers) {
301 | this.eventHandlers[name].push(handler)
302 | return this.uuid
303 | }
304 |
305 | // register address listener
306 | const address = prepareAddress(name)
307 |
308 | if (!(address in this.addressHandlers)) {
309 | this.addressHandlers[address] = []
310 | }
311 |
312 | this.addressHandlers[address].push(handler)
313 |
314 | return this.uuid
315 | }
316 |
317 | /**
318 | * Unsubscribe listener from event notification or address handler
319 | * @param {string} name The OSC address or event name
320 | * @param {number} subscriptionId Subscription id to identify the handler
321 | * @return {boolean} Success state
322 | */
323 | off(name, subscriptionId) {
324 | if (!(isString(name) || isArray(name))) {
325 | throw new Error('OSC EventHandler accepts only strings or arrays for address patterns')
326 | }
327 |
328 | if (!isInt(subscriptionId)) {
329 | throw new Error('OSC EventHandler subscription id has to be a number')
330 | }
331 |
332 | let key
333 | let haystack
334 |
335 | // event or address listener
336 | if (isString(name) && name in this.eventHandlers) {
337 | key = name
338 | haystack = this.eventHandlers
339 | } else {
340 | key = prepareAddress(name)
341 | haystack = this.addressHandlers
342 | }
343 |
344 | // remove the entry
345 | if (key in haystack) {
346 | return haystack[key].some((item, index) => {
347 | if (item.id === subscriptionId) {
348 | haystack[key].splice(index, 1)
349 | return true
350 | }
351 |
352 | return false
353 | })
354 | }
355 |
356 | return false
357 | }
358 | }
359 |
--------------------------------------------------------------------------------
/src/external/dgram.js:
--------------------------------------------------------------------------------
1 | // This file gets used instead of the NodeJS `dgram` module during rollup
2 | // builds targeting browser environments. It simply returns "nothing".
3 | const noop = undefined
4 | export default noop
5 |
--------------------------------------------------------------------------------
/src/external/ws.js:
--------------------------------------------------------------------------------
1 | // This file gets used instead of the `ws` package during rollup builds
2 | // targeting browser environments.
3 | /* eslint-disable no-undef */
4 | /* eslint-disable no-restricted-globals */
5 | function fillWs() {
6 | if (typeof WebSocket !== 'undefined') {
7 | return WebSocket
8 | } else if (typeof MozWebSocket !== 'undefined') {
9 | return MozWebSocket
10 | } else if (typeof global !== 'undefined') {
11 | return global.WebSocket || global.MozWebSocket
12 | } else if (typeof window !== 'undefined') {
13 | return window.WebSocket || window.MozWebSocket
14 | } else if (typeof self !== 'undefined') {
15 | return self.WebSocket || self.MozWebSocket
16 | }
17 | return undefined
18 | }
19 | /* eslint-enable no-undef */
20 | /* eslint-enable no-restricted-globals */
21 |
22 | const ws = fillWs()
23 |
24 | /**
25 | * Do not export server for browser environments.
26 | * @private
27 | */
28 | export const WebSocketServer = undefined
29 |
30 | /**
31 | * Return WebSocket client for browser environments.
32 | * @private
33 | */
34 | export default ws
35 |
--------------------------------------------------------------------------------
/src/message.js:
--------------------------------------------------------------------------------
1 | import {
2 | isArray,
3 | isString,
4 | isUndefined,
5 | } from './common/utils'
6 |
7 | import Helper, { typeTag, prepareAddress } from './common/helpers'
8 |
9 | import AtomicBlob from './atomic/blob'
10 | import AtomicFloat32 from './atomic/float32'
11 | import AtomicFloat64 from './atomic/float64'
12 | import AtomicInt32 from './atomic/int32'
13 | import AtomicInt64 from './atomic/int64'
14 | import AtomicUInt64 from './atomic/uint64'
15 | import AtomicString from './atomic/string'
16 | import {
17 | VALUE_NONE, VALUE_TRUE, VALUE_FALSE, VALUE_INFINITY,
18 | } from './atomic/constant'
19 |
20 | /**
21 | * A TypedMessage consists of an OSC address and an optional array of typed OSC arguments.
22 | *
23 | * @typedef {'i'|'f'|'s'|'b'|'h'|'t'|'d'|'T'|'F'|'N'|'I'} MessageArgType
24 | *
25 | * - `i` - int32
26 | * - `f` - float32
27 | * - `s` - string
28 | * - `b` - blob
29 | * - `h` - int64
30 | * - `t` - uint64
31 | * - `d` - double
32 | * - `T` - True (no argument data)
33 | * - `F` - False (no argument data)
34 | * - `N` - Nil (no argument data)
35 | * - `I` - Infinitum (no argument data)
36 | *
37 | * @typedef {number|string|Blob|VALUE_TRUE|VALUE_FALSE|VALUE_NONE|VALUE_INFINITY} MessageArgValue
38 | *
39 | * @typedef {object} MessageArgObject
40 | * @property {MessageArgType} type
41 | * @property {MessageArgValue} value
42 | *
43 | * @example
44 | * const messageArgObject = {
45 | * type: 'i', value: 123
46 | * }
47 | */
48 | export class TypedMessage {
49 | /**
50 | * Create a TypedMessage instance
51 | * @param {string[]|string} address Address
52 | * @param {MessageArgValue[]} args Arguments
53 | *
54 | * @example
55 | * const message = new TypedMessage(['test', 'path'])
56 | * message.add('d', 123.123456789)
57 | * message.add('s', 'hello')
58 | *
59 | * @example
60 | * const message = new TypedMessage('/test/path', [
61 | * { type: 'i', value: 123 },
62 | * { type: 'd', value: 123.123 },
63 | * { type: 'h', value: 0xFFFFFFn },
64 | * { type: 'T', value: null },
65 | * ])
66 | */
67 | constructor(address, args) {
68 | /**
69 | * @type {number} offset
70 | * @private
71 | */
72 | this.offset = 0
73 | /** @type {string} address */
74 | this.address = ''
75 | /** @type {string} types */
76 | this.types = ''
77 | /** @type {MessageArgValue[]} args */
78 | this.args = []
79 |
80 | if (!isUndefined(address)) {
81 | if (!(isString(address) || isArray(address))) {
82 | throw new Error('OSC Message constructor first argument (address) must be a string or array')
83 | }
84 | this.address = prepareAddress(address)
85 | }
86 |
87 | if (!isUndefined(args)) {
88 | if (!isArray(args)) {
89 | throw new Error('OSC Message constructor second argument (args) must be an array')
90 | }
91 | args.forEach((item) => this.add(item.type, item.value))
92 | }
93 | }
94 |
95 | /**
96 | * Add an OSC Atomic Data Type to the list of elements
97 | * @param {MessageArgType} type
98 | * @param {MessageArgValue} item
99 | */
100 | add(type, item) {
101 | if (isUndefined(type)) {
102 | throw new Error('OSC Message needs a valid OSC Atomic Data Type')
103 | }
104 |
105 | if (type === 'N') {
106 | this.args.push(VALUE_NONE)
107 | } else if (type === 'T') {
108 | this.args.push(VALUE_TRUE)
109 | } else if (type === 'F') {
110 | this.args.push(VALUE_FALSE)
111 | } else if (type === 'I') {
112 | this.args.push(VALUE_INFINITY)
113 | } else {
114 | this.args.push(item)
115 | }
116 |
117 | this.types += type
118 | }
119 |
120 | /**
121 | * Interpret the Message as packed binary data
122 | * @return {Uint8Array} Packed binary data
123 | */
124 | pack() {
125 | if (this.address.length === 0 || this.address[0] !== '/') {
126 | throw new Error('OSC Message has an invalid address')
127 | }
128 |
129 | const encoder = new Helper()
130 |
131 | // OSC Address Pattern and Type string
132 | encoder.add(new AtomicString(this.address))
133 | encoder.add(new AtomicString(`,${this.types}`))
134 |
135 | // followed by zero or more OSC Arguments
136 | if (this.args.length > 0) {
137 | let argument
138 |
139 | if (this.args.length > this.types.length) {
140 | throw new Error('OSC Message argument and type tag mismatch')
141 | }
142 |
143 | this.args.forEach((value, index) => {
144 | const type = this.types[index]
145 | if (type === 'i') {
146 | argument = new AtomicInt32(value)
147 | } else if (type === 'h') {
148 | argument = new AtomicInt64(value)
149 | } else if (type === 't') {
150 | argument = new AtomicUInt64(value)
151 | } else if (type === 'f') {
152 | argument = new AtomicFloat32(value)
153 | } else if (type === 'd') {
154 | argument = new AtomicFloat64(value)
155 | } else if (type === 's') {
156 | argument = new AtomicString(value)
157 | } else if (type === 'b') {
158 | argument = new AtomicBlob(value)
159 | } else if (type === 'T') {
160 | argument = VALUE_TRUE
161 | } else if (type === 'F') {
162 | argument = VALUE_FALSE
163 | } else if (type === 'N') {
164 | argument = VALUE_NONE
165 | } else if (type === 'I') {
166 | argument = VALUE_INFINITY
167 | } else {
168 | throw new Error('OSC Message found unknown argument type')
169 | }
170 |
171 | encoder.add(argument)
172 | })
173 | }
174 |
175 | return encoder.merge()
176 | }
177 |
178 | /**
179 | * Unpack binary data to read a Message
180 | * @param {DataView} dataView The DataView holding the binary representation of a Message
181 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
182 | * @return {number} Offset after unpacking
183 | */
184 | unpack(dataView, initialOffset = 0) {
185 | if (!(dataView instanceof DataView)) {
186 | throw new Error('OSC Message expects an instance of type DataView.')
187 | }
188 |
189 | // read address pattern
190 | const address = new AtomicString()
191 | address.unpack(dataView, initialOffset)
192 |
193 | // read type string
194 | const types = new AtomicString()
195 | types.unpack(dataView, address.offset)
196 |
197 | if (address.value.length === 0 || address.value[0] !== '/') {
198 | throw new Error('OSC Message found malformed or missing address string')
199 | }
200 |
201 | if (types.value.length === 0 && types.value[0] !== ',') {
202 | throw new Error('OSC Message found malformed or missing type string')
203 | }
204 |
205 | let { offset } = types
206 | let next
207 | let type
208 |
209 | const args = []
210 |
211 | // read message arguments (OSC Atomic Data Types)
212 | for (let i = 1; i < types.value.length; i += 1) {
213 | type = types.value[i]
214 | next = null
215 |
216 | if (type === 'i') {
217 | next = new AtomicInt32()
218 | } else if (type === 'h') {
219 | next = new AtomicInt64()
220 | } else if (type === 't') {
221 | next = new AtomicUInt64()
222 | } else if (type === 'f') {
223 | next = new AtomicFloat32()
224 | } else if (type === 'd') {
225 | next = new AtomicFloat64()
226 | } else if (type === 's') {
227 | next = new AtomicString()
228 | } else if (type === 'b') {
229 | next = new AtomicBlob()
230 | } else if (type === 'T') {
231 | args.push(VALUE_TRUE)
232 | } else if (type === 'F') {
233 | args.push(VALUE_FALSE)
234 | } else if (type === 'N') {
235 | args.push(VALUE_NONE)
236 | } else if (type === 'I') {
237 | args.push(VALUE_INFINITY)
238 | } else {
239 | throw new Error('OSC Message found unsupported argument type')
240 | }
241 |
242 | if (next) {
243 | offset = next.unpack(dataView, offset)
244 | args.push(next.value)
245 | }
246 | }
247 |
248 | this.offset = offset
249 | this.address = address.value
250 | this.types = types.value
251 | this.args = args
252 |
253 | return this.offset
254 | }
255 | }
256 |
257 | /**
258 | * An OSC message consists of an OSC Address Pattern followed
259 | * by an OSC Type Tag String followed by zero or more OSC Arguments
260 | */
261 | export default class Message extends TypedMessage {
262 | /**
263 | * Create a Message instance
264 | * @param {string[]|string} address Address
265 | * @param {...MessageArgValue} args OSC Atomic Data Types
266 | *
267 | * @example
268 | * const message = new Message(['test', 'path'], 50, 100.52, 'test')
269 | *
270 | * @example
271 | * const message = new Message('/test/path', 51.2)
272 | */
273 | constructor(address, ...args) {
274 | let oscArgs
275 | if (args.length > 0) {
276 | if (args[0] instanceof Array) {
277 | oscArgs = args.shift()
278 | }
279 | }
280 |
281 | super(address, oscArgs)
282 |
283 | if (args.length > 0) {
284 | this.types = args.map((item) => typeTag(item)).join('')
285 | this.args = args
286 | }
287 | }
288 |
289 | /**
290 | * Add an OSC Atomic Data Type to the list of elements
291 | * @param {MessageArgValue} item
292 | */
293 | add(item) {
294 | super.add(typeTag(item), item)
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/src/osc.js:
--------------------------------------------------------------------------------
1 | import {
2 | isFunction,
3 | isInt,
4 | isObject,
5 | isString,
6 | } from './common/utils'
7 |
8 | import Bundle from './bundle'
9 | import EventHandler from './events'
10 | import Message, { TypedMessage } from './message'
11 | import Packet from './packet'
12 |
13 | import DatagramPlugin from './plugin/dgram'
14 | import BridgePlugin from './plugin/bridge'
15 | import WebsocketClientPlugin from './plugin/wsclient'
16 | import WebsocketServerPlugin from './plugin/wsserver'
17 | import Plugin from './plugin/plugin'
18 |
19 | /**
20 | * Default options
21 | * @private
22 | */
23 | const defaultOptions = {
24 | discardLateMessages: false,
25 | }
26 |
27 | /**
28 | * Status flags
29 | */
30 | const STATUS = {
31 | IS_NOT_INITIALIZED: -1,
32 | IS_CONNECTING: 0,
33 | IS_OPEN: 1,
34 | IS_CLOSING: 2,
35 | IS_CLOSED: 3,
36 | }
37 |
38 | /**
39 | * OSC interface to send OSC Packets and listen to status changes and
40 | * incoming message events. Offers a Plugin API for different network
41 | * protocols, defaults to a simple Websocket client for OSC communication
42 | * between a browser js-app and a js-node server
43 | *
44 | * @example
45 | * const osc = new OSC()
46 | *
47 | * osc.on('/input/test', message => {
48 | * // print incoming OSC message arguments
49 | * console.log(message.args)
50 | * })
51 | *
52 | * osc.on('open', () => {
53 | * const message = new Message('/test/path', 55.12, 'hello')
54 | * osc.send(message)
55 | * })
56 | *
57 | * osc.open({ host: '192.168.178.115', port: 9012 })
58 | */
59 | class OSC {
60 | /**
61 | * Create an OSC instance with given options
62 | * @param {object} [options] Custom options
63 | * @param {boolean} [options.discardLateMessages=false] Ignore incoming
64 | * messages when given timetag lies in the past
65 | * @param {Plugin} [options.plugin=WebsocketClientPlugin] Add a connection plugin
66 | * to this interface, defaults to a plugin with Websocket client.
67 | * Open README.md for further information on how to handle plugins or
68 | * how to write your own with the Plugin API
69 | *
70 | * @example
71 | * const osc = new OSC() // default options with Websocket client
72 | *
73 | * @example
74 | * const osc = new OSC({ discardLateMessages: true })
75 | *
76 | * @example
77 | * const websocketPlugin = new OSC.WebsocketClientPlugin()
78 | * const osc = new OSC({ plugin: websocketPlugin })
79 | */
80 | constructor(options) {
81 | if (options && !isObject(options)) {
82 | throw new Error('OSC options argument has to be an object.')
83 | }
84 |
85 | /**
86 | * @type {object} options
87 | * @private
88 | */
89 | this.options = { ...defaultOptions, ...options }
90 | // create default plugin with default options
91 | if (!this.options.plugin) {
92 | this.options.plugin = new WebsocketClientPlugin()
93 | }
94 | /**
95 | * @type {EventHandler} eventHandler
96 | * @private
97 | */
98 | this.eventHandler = new EventHandler({
99 | discardLateMessages: this.options.discardLateMessages,
100 | })
101 |
102 | // pass EventHandler's notify() to plugin
103 | const { eventHandler } = this
104 | if (this.options.plugin && this.options.plugin.registerNotify) {
105 | this.options.plugin.registerNotify((...args) => eventHandler.notify(...args))
106 | }
107 | }
108 |
109 | /**
110 | * Listen to a status-change event or incoming OSC message with
111 | * address pattern matching
112 | * @param {string} eventName Event name or OSC address pattern
113 | * @param {function} callback Function which is called on notification
114 | * @return {number} Subscription id (needed to unsubscribe)
115 | *
116 | * @example
117 | * // will be called when server receives /in!trument/* for example
118 | * osc.on('/instrument/1', message => {
119 | * console.log(message)
120 | * })
121 | *
122 | * @example
123 | * // will be called for every message since it uses the wildcard symbol
124 | * osc.on('*', message => {
125 | * console.log(message)
126 | * })
127 | *
128 | * @example
129 | * // will be called on network socket error
130 | * osc.on('error', message => {
131 | * console.log(message)
132 | * })
133 | */
134 | on(eventName, callback) {
135 | if (!(isString(eventName) && isFunction(callback))) {
136 | throw new Error('OSC on() needs event- or address string and callback function')
137 | }
138 |
139 | return this.eventHandler.on(eventName, callback)
140 | }
141 |
142 | /**
143 | * Unsubscribe an event listener
144 | * @param {string} eventName Event name or OSC address pattern
145 | * @param {number} subscriptionId The subscription id
146 | * @return {boolean} Success state
147 | *
148 | * @example
149 | * const listenerId = osc.on('error', message => {
150 | * console.log(message)
151 | * })
152 | * osc.off('error', listenerId) // unsubscribe from error event
153 | */
154 | off(eventName, subscriptionId) {
155 | if (!(isString(eventName) && isInt(subscriptionId))) {
156 | throw new Error('OSC off() needs string and number (subscriptionId) to unsubscribe')
157 | }
158 |
159 | return this.eventHandler.off(eventName, subscriptionId)
160 | }
161 |
162 | /**
163 | * Open network socket with plugin. This method is used by
164 | * plugins and is not available without (see Plugin API for more information)
165 | * @param {object} [options] Custom global options for plugin instance
166 | *
167 | * @example
168 | * const osc = new OSC({ plugin: new OSC.DatagramPlugin() })
169 | * osc.open({ host: '127.0.0.1', port: 8080 })
170 | */
171 | open(options) {
172 | if (options && !isObject(options)) {
173 | throw new Error('OSC open() options argument needs to be an object')
174 | }
175 |
176 | if (!(this.options.plugin && isFunction(this.options.plugin.open))) {
177 | throw new Error('OSC Plugin API #open is not implemented!')
178 | }
179 |
180 | return this.options.plugin.open(options)
181 | }
182 |
183 | /**
184 | * Returns the current status of the connection. See *STATUS* for
185 | * different possible states. This method is used by plugins
186 | * and is not available without (see Plugin API for more information)
187 | * @return {number} Status identifier
188 | *
189 | * @example
190 | * import OSC, { STATUS } from 'osc'
191 | * const osc = new OSC()
192 | * if (osc.status() === STATUS.IS_CONNECTING) {
193 | * // do something
194 | * }
195 | */
196 | status() {
197 | if (!(this.options.plugin && isFunction(this.options.plugin.status))) {
198 | throw new Error('OSC Plugin API #status is not implemented!')
199 | }
200 |
201 | return this.options.plugin.status()
202 | }
203 |
204 | /**
205 | * Close connection. This method is used by plugins and is not
206 | * available without (see Plugin API for more information)
207 | */
208 | close() {
209 | if (!(this.options.plugin && isFunction(this.options.plugin.close))) {
210 | throw new Error('OSC Plugin API #close is not implemented!')
211 | }
212 |
213 | return this.options.plugin.close()
214 | }
215 |
216 | /**
217 | * Send an OSC Packet, Bundle or Message. This method is used by plugins
218 | * and is not available without (see Plugin API for more information)
219 | * @param {Packet|Bundle|Message|TypedMessage} packet OSC Packet, Bundle or Message instance
220 | * @param {object} [options] Custom options
221 | *
222 | * @example
223 | * const osc = new OSC({ plugin: new OSC.DatagramPlugin() })
224 | * osc.open({ host: '127.0.0.1', port: 8080 })
225 | *
226 | * const message = new OSC.Message('/test/path', 55.1, 57)
227 | * osc.send(message)
228 | *
229 | * // send message again to custom address
230 | * osc.send(message, { host: '192.168.178.115', port: 9001 })
231 | */
232 | send(packet, options) {
233 | if (!(this.options.plugin && isFunction(this.options.plugin.send))) {
234 | throw new Error('OSC Plugin API #send is not implemented!')
235 | }
236 |
237 | if (!(packet instanceof TypedMessage
238 | || packet instanceof Message
239 | || packet instanceof Bundle
240 | || packet instanceof Packet)
241 | ) {
242 | throw new Error('OSC send() needs Messages, Bundles or Packets')
243 | }
244 |
245 | if (options && !isObject(options)) {
246 | throw new Error('OSC send() options argument has to be an object')
247 | }
248 |
249 | return this.options.plugin.send(packet.pack(), options)
250 | }
251 | }
252 |
253 | // expose status flags
254 | OSC.STATUS = STATUS
255 |
256 | // expose OSC classes
257 | OSC.Packet = Packet
258 | OSC.Bundle = Bundle
259 | OSC.Message = Message
260 | OSC.TypedMessage = TypedMessage
261 |
262 | // expose plugins
263 | OSC.Plugin = Plugin
264 | OSC.DatagramPlugin = DatagramPlugin
265 | OSC.WebsocketClientPlugin = WebsocketClientPlugin
266 | OSC.WebsocketServerPlugin = WebsocketServerPlugin
267 | OSC.BridgePlugin = BridgePlugin
268 |
269 | export default OSC
270 |
--------------------------------------------------------------------------------
/src/packet.js:
--------------------------------------------------------------------------------
1 | import AtomicString from './atomic/string'
2 | import Bundle, { BUNDLE_TAG } from './bundle'
3 | import Message from './message'
4 |
5 | /**
6 | * The unit of transmission of OSC is an OSC Packet. The contents
7 | * of an OSC packet must be either an OSC Message or an OSC Bundle
8 | */
9 | export default class Packet {
10 | /**
11 | * Create a Packet instance holding a Message or Bundle
12 | * @param {Message|Bundle} [value] Initial Packet value
13 | */
14 | constructor(value) {
15 | if (value && !(value instanceof Message || value instanceof Bundle)) {
16 | throw new Error('OSC Packet value has to be Message or Bundle')
17 | }
18 |
19 | /** @type {Message|Bundle} value */
20 | this.value = value
21 | /**
22 | * @type {number} offset
23 | * @private
24 | */
25 | this.offset = 0
26 | }
27 |
28 | /**
29 | * Packs the Packet value. This implementation is more like
30 | * a wrapper due to OSC specifications, you could also skip the
31 | * Packet and directly work with the Message or Bundle instance
32 | * @return {Uint8Array} Packed binary data
33 | *
34 | * @example
35 | * const message = new Message('/test/path', 21.5, 'test')
36 | * const packet = new Packet(message)
37 | * const packetBinary = packet.pack() // then send it via udp etc.
38 | *
39 | * // or skip the Packet for convenience
40 | * const messageBinary = message.pack()
41 | */
42 | pack() {
43 | if (!this.value) {
44 | throw new Error('OSC Packet can not be encoded with empty body')
45 | }
46 |
47 | return this.value.pack()
48 | }
49 |
50 | /**
51 | * Unpack binary data from DataView to read Messages or Bundles
52 | * @param {DataView} dataView The DataView holding a binary representation of a Packet
53 | * @param {number} [initialOffset=0] Offset of DataView before unpacking
54 | * @return {number} Offset after unpacking
55 | */
56 | unpack(dataView, initialOffset = 0) {
57 | if (!(dataView instanceof DataView)) {
58 | throw new Error('OSC Packet expects an instance of type DataView')
59 | }
60 |
61 | if (dataView.byteLength % 4 !== 0) {
62 | throw new Error('OSC Packet byteLength has to be a multiple of four')
63 | }
64 |
65 | const head = new AtomicString()
66 | head.unpack(dataView, initialOffset)
67 |
68 | let item
69 |
70 | // check if Packet is a Bundle or a Message
71 | if (head.value === BUNDLE_TAG) {
72 | item = new Bundle()
73 | } else {
74 | item = new Message()
75 | }
76 |
77 | item.unpack(dataView, initialOffset)
78 |
79 | this.offset = item.offset
80 | this.value = item
81 |
82 | return this.offset
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/plugin/bridge.js:
--------------------------------------------------------------------------------
1 | import dgram from 'dgram'
2 | import { WebSocketServer } from 'ws'
3 |
4 | import Plugin from './plugin'
5 |
6 | /**
7 | * Status flags
8 | * @private
9 | */
10 | const STATUS = {
11 | IS_NOT_INITIALIZED: -1,
12 | IS_CONNECTING: 0,
13 | IS_OPEN: 1,
14 | IS_CLOSING: 2,
15 | IS_CLOSED: 3,
16 | }
17 |
18 | /**
19 | * Default options
20 | * @private
21 | */
22 | const defaultOptions = {
23 | udpServer: {
24 | host: 'localhost',
25 | port: 41234,
26 | exclusive: false,
27 | },
28 | udpClient: {
29 | host: 'localhost',
30 | port: 41235,
31 | },
32 | wsServer: {
33 | host: 'localhost',
34 | port: 8080,
35 | },
36 | receiver: 'ws',
37 | }
38 |
39 | /**
40 | * Helper method to merge nested objects
41 | * @private
42 | */
43 | function mergeOptions(base, custom) {
44 | return {
45 | ...defaultOptions,
46 | ...base,
47 | ...custom,
48 | udpServer: { ...defaultOptions.udpServer, ...base.udpServer, ...custom.udpServer },
49 | udpClient: { ...defaultOptions.udpClient, ...base.udpClient, ...custom.udpClient },
50 | wsServer: { ...defaultOptions.wsServer, ...base.wsServer, ...custom.wsServer },
51 | }
52 | }
53 |
54 | /**
55 | * OSC plugin for setting up communication between a Websocket
56 | * client and a udp client with a bridge inbetween
57 | */
58 | export default class BridgePlugin extends Plugin {
59 | /**
60 | * Create an OSC Bridge instance with given options. Defaults to
61 | * localhost:41234 for udp server, localhost:41235 for udp client and
62 | * localhost:8080 for Websocket server
63 | * @param {object} [options] Custom options
64 | * @param {string} [options.udpServer.host='localhost'] Hostname of udp server to bind to
65 | * @param {number} [options.udpServer.port=41234] Port of udp server to bind to
66 | * @param {boolean} [options.udpServer.exclusive=false] Exclusive flag
67 | * @param {string} [options.udpClient.host='localhost'] Hostname of udp client for messaging
68 | * @param {number} [options.udpClient.port=41235] Port of udp client for messaging
69 | * @param {string} [options.wsServer.host='localhost'] Hostname of Websocket server
70 | * @param {number} [options.wsServer.port=8080] Port of Websocket server
71 | * @param {http.Server|https.Server} [options.wsServer.server] Use existing Node.js HTTP/S server
72 | * @param {string} [options.receiver='ws'] Where messages sent via 'send' method will be
73 | * delivered to, 'ws' for Websocket clients, 'udp' for udp client
74 | *
75 | * @example
76 | * const plugin = new OSC.BridgePlugin({ wsServer: { port: 9912 } })
77 | * const osc = new OSC({ plugin: plugin })
78 | *
79 | * @example Using an existing HTTP server
80 | * const http = require('http')
81 | * const httpServer = http.createServer();
82 | * const plugin = new OSC.BridgePlugin({ wsServer: { server: httpServer } })
83 | * const osc = new OSC({ plugin: plugin })
84 | */
85 | constructor(options = {}) {
86 | super()
87 |
88 | // `dgram` and `WebSocketServer` get replaced with an undefined value in
89 | // builds targeting browser environments
90 | if (!dgram || !WebSocketServer) {
91 | throw new Error('BridgePlugin can not be used in browser context')
92 | }
93 |
94 | /** @type {object} options
95 | * @private
96 | */
97 | this.options = mergeOptions({}, options)
98 |
99 | /**
100 | * @type {object} websocket
101 | * @private
102 | */
103 | this.websocket = null
104 |
105 | /**
106 | * @type {object} socket
107 | * @private
108 | */
109 | this.socket = dgram.createSocket('udp4')
110 | /**
111 | * @type {number} socketStatus
112 | * @private
113 | */
114 | this.socketStatus = STATUS.IS_NOT_INITIALIZED
115 |
116 | // register udp events
117 | this.socket.on('message', (message) => {
118 | this.send(message, { receiver: 'ws' })
119 | this.notify(message.buffer)
120 | })
121 |
122 | this.socket.on('error', (error) => {
123 | this.notify('error', error)
124 | })
125 |
126 | /**
127 | * @type {function} notify
128 | * @private
129 | */
130 | this.notify = () => {}
131 | }
132 |
133 | /**
134 | * Internal method to hook into osc library's
135 | * EventHandler notify method
136 | * @param {function} fn Notify callback
137 | * @private
138 | */
139 | registerNotify(fn) {
140 | this.notify = fn
141 | }
142 |
143 | /**
144 | * Returns the current status of the connection
145 | * @return {number} Status ID
146 | */
147 | status() {
148 | return this.socketStatus
149 | }
150 |
151 | /**
152 | * Bind a udp socket to a hostname and port
153 | * @param {object} [customOptions] Custom options
154 | * @param {string} [customOptions.host='localhost'] Hostname of udp server to bind to
155 | * @param {number} [customOptions.port=41234] Port of udp server to bind to
156 | * @param {boolean} [customOptions.exclusive=false] Exclusive flag
157 | */
158 | open(customOptions = {}) {
159 | const options = mergeOptions(this.options, customOptions)
160 |
161 | this.socketStatus = STATUS.IS_CONNECTING
162 |
163 | // bind udp server
164 | this.socket.bind({
165 | address: options.udpServer.host,
166 | port: options.udpServer.port,
167 | exclusive: options.udpServer.exclusive,
168 | }, () => {
169 | let wsServerOptions = {}
170 | if (options.wsServer.server) {
171 | wsServerOptions.server = options.wsServer.server
172 | } else {
173 | wsServerOptions = options.wsServer
174 | }
175 |
176 | // bind Websocket server
177 | this.websocket = new WebSocketServer(wsServerOptions)
178 | this.websocket.binaryType = 'arraybuffer'
179 |
180 | // register Websocket events
181 | this.websocket.on('listening', () => {
182 | this.socketStatus = STATUS.IS_OPEN
183 | this.notify('open')
184 | })
185 |
186 | this.websocket.on('error', (error) => {
187 | this.notify('error', error)
188 | })
189 |
190 | this.websocket.on('connection', (client) => {
191 | client.on('message', (message, rinfo) => {
192 | this.send(message, { receiver: 'udp' })
193 | this.notify(new Uint8Array(message), rinfo)
194 | })
195 | })
196 | })
197 | }
198 |
199 | /**
200 | * Close udp socket and Websocket server
201 | */
202 | close() {
203 | this.socketStatus = STATUS.IS_CLOSING
204 |
205 | // close udp socket
206 | this.socket.close(() => {
207 | // close Websocket
208 | this.websocket.close(() => {
209 | this.socketStatus = STATUS.IS_CLOSED
210 | this.notify('close')
211 | })
212 | })
213 | }
214 |
215 | /**
216 | * Send an OSC Packet, Bundle or Message. Use options here for
217 | * custom receiver, otherwise the global options will be taken
218 | * @param {Uint8Array} binary Binary representation of OSC Packet
219 | * @param {object} [customOptions] Custom options
220 | * @param {string} [customOptions.udpClient.host='localhost'] Hostname of udp client for messaging
221 | * @param {number} [customOptions.udpClient.port=41235] Port of udp client for messaging
222 | * @param {string} [customOptions.receiver='ws'] Messages will be delivered to Websocket ('ws')
223 | * clients or udp client ('udp')
224 | */
225 | send(binary, customOptions = {}) {
226 | const options = mergeOptions(this.options, customOptions)
227 | const { receiver } = options
228 |
229 | if (receiver === 'udp') {
230 | // send data to udp client
231 | const data = binary instanceof Buffer ? binary : Buffer.from(binary)
232 | this.socket.send(
233 | data,
234 | 0,
235 | data.byteLength,
236 | options.udpClient.port,
237 | options.udpClient.host,
238 | )
239 | } else if (receiver === 'ws') {
240 | // send data to all Websocket clients
241 | this.websocket.clients.forEach((client) => {
242 | client.send(binary, { binary: true })
243 | })
244 | } else {
245 | throw new Error('BridgePlugin can not send message to unknown receiver')
246 | }
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/src/plugin/dgram.js:
--------------------------------------------------------------------------------
1 | import dgram from 'dgram'
2 |
3 | import Plugin from './plugin'
4 |
5 | /**
6 | * Status flags
7 | * @private
8 | */
9 | const STATUS = {
10 | IS_NOT_INITIALIZED: -1,
11 | IS_CONNECTING: 0,
12 | IS_OPEN: 1,
13 | IS_CLOSING: 2,
14 | IS_CLOSED: 3,
15 | }
16 |
17 | /**
18 | * Default options for open method
19 | * @private
20 | */
21 | const defaultOpenOptions = {
22 | host: 'localhost',
23 | port: 41234,
24 | exclusive: false,
25 | }
26 |
27 | /**
28 | * Default options for send method
29 | * @private
30 | */
31 | const defaultSendOptions = {
32 | host: 'localhost',
33 | port: 41235,
34 | }
35 |
36 | /**
37 | * Default options
38 | * @private
39 | */
40 | const defaultOptions = {
41 | type: 'udp4',
42 | open: defaultOpenOptions,
43 | send: defaultSendOptions,
44 | }
45 |
46 | /**
47 | * Helper method to merge nested objects
48 | * @private
49 | */
50 | function mergeOptions(base, custom) {
51 | return {
52 | ...defaultOptions,
53 | ...base,
54 | ...custom,
55 | open: { ...defaultOptions.open, ...base.open, ...custom.open },
56 | send: { ...defaultOptions.send, ...base.send, ...custom.send },
57 | }
58 | }
59 |
60 | /**
61 | * OSC plugin for simple OSC messaging via udp client
62 | * and udp server
63 | */
64 | export default class DatagramPlugin extends Plugin {
65 | /**
66 | * Create an OSC Plugin instance with given options. Defaults to
67 | * localhost:41234 for server and localhost:41235 for client messaging
68 | * @param {object} [options] Custom options
69 | * @param {string} [options.type='udp4'] 'udp4' or 'udp6'
70 | * @param {string} [options.open.host='localhost'] Hostname of udp server to bind to
71 | * @param {number} [options.open.port=41234] Port of udp server to bind to
72 | * @param {boolean} [options.open.exclusive=false] Exclusive flag
73 | * @param {string} [options.send.host='localhost'] Hostname of udp client for messaging
74 | * @param {number} [options.send.port=41235] Port of udp client for messaging
75 | *
76 | * @example
77 | * const plugin = new OSC.DatagramPlugin({ send: { port: 9912 } })
78 | * const osc = new OSC({ plugin: plugin })
79 | */
80 | constructor(options = {}) {
81 | super()
82 |
83 | // `dgram` gets replaced with an undefined value in builds targeting
84 | // browser environments
85 | if (!dgram) {
86 | throw new Error('DatagramPlugin can not be used in browser context')
87 | }
88 |
89 | /**
90 | * @type {object} options
91 | * @private
92 | */
93 | this.options = mergeOptions({}, options)
94 |
95 | /**
96 | * @type {object} socket
97 | * @private
98 | */
99 | this.socket = dgram.createSocket(this.options.type)
100 | /**
101 | * @type {number} socketStatus
102 | * @private
103 | */
104 | this.socketStatus = STATUS.IS_NOT_INITIALIZED
105 |
106 | // register events
107 | this.socket.on('message', (message, rinfo) => {
108 | this.notify(message, rinfo)
109 | })
110 |
111 | this.socket.on('error', (error) => {
112 | this.notify('error', error)
113 | })
114 |
115 | /**
116 | * @type {function} notify
117 | * @private
118 | */
119 | this.notify = () => {}
120 | }
121 |
122 | /**
123 | * Internal method to hook into osc library's
124 | * EventHandler notify method
125 | * @param {function} fn Notify callback
126 | * @private
127 | */
128 | registerNotify(fn) {
129 | this.notify = fn
130 | }
131 |
132 | /**
133 | * Returns the current status of the connection
134 | * @return {number} Status ID
135 | */
136 | status() {
137 | return this.socketStatus
138 | }
139 |
140 | /**
141 | * Bind a udp socket to a hostname and port
142 | * @param {object} [customOptions] Custom options
143 | * @param {string} [customOptions.host='localhost'] Hostname of udp server to bind to
144 | * @param {number} [customOptions.port=41234] Port of udp server to bind to
145 | * @param {boolean} [customOptions.exclusive=false] Exclusive flag
146 | */
147 | open(customOptions = {}) {
148 | const options = { ...this.options.open, ...customOptions }
149 | const { port, exclusive } = options
150 |
151 | this.socketStatus = STATUS.IS_CONNECTING
152 |
153 | this.socket.bind({
154 | address: options.host,
155 | port,
156 | exclusive,
157 | }, () => {
158 | this.socketStatus = STATUS.IS_OPEN
159 | this.notify('open')
160 | })
161 | }
162 |
163 | /**
164 | * Close udp socket
165 | */
166 | close() {
167 | this.socketStatus = STATUS.IS_CLOSING
168 |
169 | this.socket.close(() => {
170 | this.socketStatus = STATUS.IS_CLOSED
171 | this.notify('close')
172 | })
173 | }
174 |
175 | /**
176 | * Send an OSC Packet, Bundle or Message. Use options here for
177 | * custom port and hostname, otherwise the global options will
178 | * be taken
179 | * @param {Uint8Array} binary Binary representation of OSC Packet
180 | * @param {object} [customOptions] Custom options for udp socket
181 | * @param {string} [customOptions.host] Hostname of udp client
182 | * @param {number} [customOptions.port] Port of udp client
183 | */
184 | send(binary, customOptions = {}) {
185 | const options = { ...this.options.send, ...customOptions }
186 | const { port, host } = options
187 |
188 | this.socket.send(Buffer.from(binary), 0, binary.byteLength, port, host)
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/plugin/plugin.js:
--------------------------------------------------------------------------------
1 | // /**
2 | // @constructor
3 | // @abstract
4 | // */
5 | // const Plugin = () => {
6 | // if (this.constructor === Plugin) {
7 | // }
8 | // }
9 |
10 | // /**
11 | // @abstract
12 | // */
13 | // Plugin.prototype.close = () => {
14 | // throw new Error('Abstract method!')
15 | // }
16 |
17 | export default class Plugin {
18 | constructor() {
19 | if (this.constructor === Plugin) {
20 | throw new Error('Plugin is an abstract class. Please create or use an implementation!')
21 | }
22 | }
23 |
24 | /**
25 | * Returns the current status of the connection
26 | * @return {number} Status ID
27 | */
28 | status() {
29 | throw new Error('Abstract method!')
30 | }
31 |
32 | /**
33 | * Open socket connection. Specifics depend on implementation.
34 | * @param {object} [customOptions] Custom options. See implementation specifics.
35 | */
36 | // eslint-disable-next-line no-unused-vars
37 | open(customOptions = {}) {
38 | throw new Error('Abstract method!')
39 | }
40 |
41 | /**
42 | * Close socket connection and anything else used in the implementation.
43 | */
44 | close() {
45 | throw new Error('Abstract method!')
46 | }
47 |
48 | /**
49 | * Send an OSC Packet, Bundle or Message. Use options here for
50 | * custom receiver, otherwise the global options will be taken
51 | * @param {Uint8Array} binary Binary representation of OSC Packet
52 | * @param {object} [customOptions] Custom options. Specifics depend on implementation.
53 | */
54 | // eslint-disable-next-line no-unused-vars
55 | send(binary, customOptions = {}) {
56 | throw new Error('Abstract method!')
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/plugin/wsclient.js:
--------------------------------------------------------------------------------
1 | import WebSocket from 'ws'
2 |
3 | import Plugin from './plugin'
4 |
5 | /**
6 | * Status flags
7 | * @private
8 | */
9 | const STATUS = {
10 | IS_NOT_INITIALIZED: -1,
11 | IS_CONNECTING: 0,
12 | IS_OPEN: 1,
13 | IS_CLOSING: 2,
14 | IS_CLOSED: 3,
15 | }
16 |
17 | /**
18 | * Default options
19 | * @private
20 | */
21 | const defaultOptions = {
22 | host: 'localhost',
23 | port: 8080,
24 | secure: false,
25 | protocol: [],
26 | }
27 |
28 | /**
29 | * OSC plugin for a Websocket client running in node or browser context
30 | */
31 | export default class WebsocketClientPlugin extends Plugin {
32 | /**
33 | * Create an OSC WebsocketClientPlugin instance with given options.
34 | * Defaults to *localhost:8080* for connecting to a Websocket server
35 | * @param {object} [options] Custom options
36 | * @param {string} [options.host='localhost'] Hostname of Websocket server
37 | * @param {number} [options.port=8080] Port of Websocket server
38 | * @param {boolean} [options.secure=false] Use wss:// for secure connections
39 | * @param {string|string[]} [options.protocol=''] Subprotocol of Websocket server
40 | *
41 | * @example
42 | * const plugin = new OSC.WebsocketClientPlugin({ port: 9912 })
43 | * const osc = new OSC({ plugin: plugin })
44 | */
45 | constructor(options) {
46 | super()
47 |
48 | if (!WebSocket) {
49 | throw new Error('WebsocketClientPlugin can\'t find a WebSocket class')
50 | }
51 |
52 | /**
53 | * @type {object} options
54 | * @private
55 | */
56 | this.options = { ...defaultOptions, ...options }
57 |
58 | /**
59 | * @type {object} socket
60 | * @private
61 | */
62 | this.socket = null
63 | /**
64 | * @type {number} socketStatus
65 | * @private
66 | */
67 | this.socketStatus = STATUS.IS_NOT_INITIALIZED
68 |
69 | /**
70 | * @type {function} notify
71 | * @private
72 | */
73 | this.notify = () => {}
74 | }
75 |
76 | /**
77 | * Internal method to hook into osc library's
78 | * EventHandler notify method
79 | * @param {function} fn Notify callback
80 | * @private
81 | */
82 | registerNotify(fn) {
83 | this.notify = fn
84 | }
85 |
86 | /**
87 | * Returns the current status of the connection
88 | * @return {number} Status identifier
89 | */
90 | status() {
91 | return this.socketStatus
92 | }
93 |
94 | /**
95 | * Connect to a Websocket server. Defaults to global options
96 | * @param {object} [customOptions] Custom options
97 | * @param {string} [customOptions.host] Hostname of Websocket server
98 | * @param {number} [customOptions.port] Port of Websocket server
99 | * @param {boolean} [customOptions.secure] Use wss:// for secure connections
100 | * @param {string|string[]} [options.protocol] Subprotocol of Websocket server
101 | */
102 | open(customOptions = {}) {
103 | const options = { ...this.options, ...customOptions }
104 | const {
105 | port, host, secure, protocol,
106 | } = options
107 |
108 | // close socket when already given
109 | if (this.socket) {
110 | this.close()
111 | }
112 |
113 | // create websocket client
114 | const scheme = secure ? 'wss' : 'ws'
115 | const rinfo = {
116 | address: host,
117 | family: scheme,
118 | port,
119 | size: 0,
120 | }
121 |
122 | this.socket = new WebSocket(`${scheme}://${host}:${port}`, protocol)
123 | this.socket.binaryType = 'arraybuffer'
124 | this.socketStatus = STATUS.IS_CONNECTING
125 |
126 | // register events
127 | this.socket.onopen = () => {
128 | this.socketStatus = STATUS.IS_OPEN
129 | this.notify('open')
130 | }
131 |
132 | this.socket.onclose = () => {
133 | this.socketStatus = STATUS.IS_CLOSED
134 | this.notify('close')
135 | }
136 |
137 | this.socket.onerror = (error) => {
138 | this.notify('error', error)
139 | }
140 |
141 | this.socket.onmessage = (message) => {
142 | this.notify(message.data, rinfo)
143 | }
144 | }
145 |
146 | /**
147 | * Close Websocket
148 | */
149 | close() {
150 | this.socketStatus = STATUS.IS_CLOSING
151 | this.socket.close()
152 | }
153 |
154 | /**
155 | * Send an OSC Packet, Bundle or Message to Websocket server
156 | * @param {Uint8Array} binary Binary representation of OSC Packet
157 | */
158 | send(binary) {
159 | this.socket.send(binary)
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/plugin/wsserver.js:
--------------------------------------------------------------------------------
1 | import { WebSocketServer } from 'ws'
2 |
3 | import Plugin from './plugin'
4 |
5 | /**
6 | * Status flags
7 | * @private
8 | */
9 | const STATUS = {
10 | IS_NOT_INITIALIZED: -1,
11 | IS_CONNECTING: 0,
12 | IS_OPEN: 1,
13 | IS_CLOSING: 2,
14 | IS_CLOSED: 3,
15 | }
16 |
17 | /**
18 | * Default options
19 | * @private
20 | */
21 | const defaultOptions = {
22 | host: 'localhost',
23 | port: 8080,
24 | }
25 |
26 | /**
27 | * This will import the types for JSDoc/Type declarations without
28 | * impacting the runtime
29 | * @typedef {import('http').Server|import('https').Server} Server
30 | */
31 |
32 | /**
33 | * OSC plugin for a Websocket client running in node or browser context
34 | */
35 | export default class WebsocketServerPlugin extends Plugin {
36 | /**
37 | * Create an OSC WebsocketServerPlugin instance with given options.
38 | * Defaults to *localhost:8080* for the Websocket server
39 | * @param {object} [options] Custom options
40 | * @param {string} [options.host='localhost'] Hostname of Websocket server
41 | * @param {number} [options.port=8080] Port of Websocket server
42 | * @param {Server} [options.server] Use existing Node.js HTTP/S server
43 | *
44 | * @example
45 | * const plugin = new OSC.WebsocketServerPlugin({ port: 9912 })
46 | * const osc = new OSC({ plugin: plugin })
47 | *
48 | * osc.open() // start server
49 | * @example Using an existing HTTP server
50 | * const http = require('http')
51 | * const httpServer = http.createServer();
52 | * const plugin = new OSC.WebsocketServerPlugin({ server: httpServer })
53 | * const osc = new OSC({ plugin: plugin })
54 | */
55 | constructor(options) {
56 | super()
57 |
58 | // `WebSocketServer` gets replaced with an undefined value in builds
59 | // targeting browser environments
60 | if (!WebSocketServer) {
61 | throw new Error('WebsocketServerPlugin can not be used in browser context')
62 | }
63 |
64 | /**
65 | * @type {object} options
66 | * @private
67 | */
68 | this.options = { ...defaultOptions, ...options }
69 |
70 | /**
71 | * @type {object} socket
72 | * @private
73 | */
74 | this.socket = null
75 | /**
76 | * @type {number} socketStatus
77 | * @private
78 | */
79 | this.socketStatus = STATUS.IS_NOT_INITIALIZED
80 |
81 | /**
82 | * @type {function} notify
83 | * @private
84 | */
85 | this.notify = () => {}
86 | }
87 |
88 | /**
89 | * Internal method to hook into osc library's
90 | * EventHandler notify method
91 | * @param {function} fn Notify callback
92 | * @private
93 | */
94 | registerNotify(fn) {
95 | this.notify = fn
96 | }
97 |
98 | /**
99 | * Returns the current status of the connection
100 | * @return {number} Status identifier
101 | */
102 | status() {
103 | return this.socketStatus
104 | }
105 |
106 | /**
107 | * Start a Websocket server. Defaults to global options
108 | * @param {object} [customOptions] Custom options
109 | * @param {string} [customOptions.host] Hostname of Websocket server
110 | * @param {number} [customOptions.port] Port of Websocket server
111 | */
112 | open(customOptions = {}) {
113 | const options = { ...this.options, ...customOptions }
114 | const { port, host } = options
115 | const rinfo = {
116 | address: host,
117 | family: 'wsserver',
118 | port,
119 | size: 0,
120 | }
121 |
122 | // close socket when already given
123 | if (this.socket) {
124 | this.close()
125 | }
126 |
127 | // create websocket server
128 | if (options.server) {
129 | this.socket = new WebSocketServer({ server: options.server })
130 | } else {
131 | this.socket = new WebSocketServer({ host, port })
132 | }
133 |
134 | this.socket.binaryType = 'arraybuffer'
135 | this.socketStatus = STATUS.IS_CONNECTING
136 |
137 | // register events
138 | this.socket.on('listening', () => {
139 | this.socketStatus = STATUS.IS_OPEN
140 | this.notify('open')
141 | })
142 |
143 | this.socket.on('error', (error) => {
144 | this.notify('error', error)
145 | })
146 |
147 | this.socket.on('connection', (client) => {
148 | client.on('message', (message) => {
149 | this.notify(new Uint8Array(message), rinfo)
150 | })
151 | })
152 | }
153 |
154 | /**
155 | * Close Websocket server
156 | */
157 | close() {
158 | this.socketStatus = STATUS.IS_CLOSING
159 |
160 | this.socket.close(() => {
161 | this.socketStatus = STATUS.IS_CLOSED
162 | this.notify('close')
163 | })
164 | }
165 |
166 | /**
167 | * Send an OSC Packet, Bundle or Message to Websocket clients
168 | * @param {Uint8Array} binary Binary representation of OSC Packet
169 | */
170 | send(binary) {
171 | this.socket.clients.forEach((client) => {
172 | client.send(binary, { binary: true })
173 | })
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/test/atomic.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import Atomic from '../src/atomic'
4 | import AtomicBlob from '../src/atomic/blob'
5 | import AtomicFloat32 from '../src/atomic/float32'
6 | import AtomicFloat64 from '../src/atomic/float64'
7 | import AtomicInt32 from '../src/atomic/int32'
8 | import AtomicInt64 from '../src/atomic/int64'
9 | import AtomicString from '../src/atomic/string'
10 | import AtomicTimetag, {
11 | Timetag,
12 | SECONDS_70_YEARS,
13 | } from '../src/atomic/timetag'
14 | import AtomicUInt64 from '../src/atomic/uint64'
15 |
16 | /** @test {Atomic} */
17 | describe('Atomic', () => {
18 | let atomic
19 | let atomicChildren
20 |
21 | before(() => {
22 | atomic = new Atomic(2)
23 |
24 | atomicChildren = [
25 | new AtomicInt32(0),
26 | new AtomicInt32(123132132),
27 | new AtomicInt64(BigInt('0x7FFFFFFFFFFFFFFF')),
28 | new AtomicUInt64(BigInt('0xFFFFFFFFFFFFFFFF')),
29 | new AtomicFloat32(1299389992.342243),
30 | new AtomicFloat64(1299389992.342243),
31 | new AtomicString('hello'),
32 | new AtomicString(''),
33 | new AtomicBlob(new Uint8Array([5, 4, 3, 2, 1])),
34 | new AtomicTimetag(new Timetag(SECONDS_70_YEARS + 123, 3312123)),
35 | ]
36 | })
37 |
38 | it('sets the given value on construction', () => {
39 | expect(atomic.value).to.equal(2)
40 | })
41 |
42 | it('sets an initial offset of zero', () => {
43 | expect(atomic.offset).to.be.equals(0)
44 | })
45 |
46 | /** @test {Atomic#unpack} */
47 | describe('unpack', () => {
48 | it('exists', () => {
49 | atomicChildren.forEach((atomicItem) => {
50 | expect(atomicItem).to.have.property('unpack')
51 | })
52 | })
53 | })
54 |
55 | describe('pack', () => {
56 | it('returns a multiple of 32', () => {
57 | atomicChildren.forEach((atomicItem) => {
58 | expect((atomicItem.pack().byteLength * 8) % 32).to.equal(0)
59 | })
60 | })
61 |
62 | it('returns an object of type Uint8Array', () => {
63 | atomicChildren.forEach((atomicItem) => {
64 | expect(atomicItem.pack()).to.be.a('uint8Array')
65 | })
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/test/atomic/blob.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import AtomicBlob from '../../src/atomic/blob'
4 |
5 | /** @test {AtomicBlob} */
6 | describe('AtomicBlob', () => {
7 | const bitArray = {
8 | 0: 0, 1: 0, 2: 0, 3: 5, 4: 54, 5: 42, 6: 11, 7: 33, 8: 66, 9: 0, 10: 0, 11: 0,
9 | }
10 |
11 | let atomic
12 |
13 | before(() => {
14 | atomic = new AtomicBlob(new Uint8Array([54, 42, 11, 33, 66]))
15 | })
16 |
17 | /** @test {AtomicBlob#pack} */
18 | describe('pack', () => {
19 | let result
20 |
21 | before(() => {
22 | result = atomic.pack()
23 | })
24 |
25 | it('returns correct bits', () => {
26 | expect(JSON.stringify(result)).to.equal(JSON.stringify(bitArray))
27 | })
28 |
29 | it('returns the first 8 bit for the size of the data', () => {
30 | const dataView = new DataView(result.buffer)
31 | expect(dataView.getInt32(0, false)).to.equal(5)
32 | })
33 | })
34 |
35 | /** @test {AtomicBlob#unpack} */
36 | describe('unpack', () => {
37 | let returnValue
38 |
39 | before(() => {
40 | const data = new Uint8Array([0, 0, 0, 7, 1, 2, 3, 4, 5, 6, 7])
41 | const dataView = new DataView(data.buffer)
42 |
43 | returnValue = atomic.unpack(dataView, 0)
44 | })
45 |
46 | it('returns a number', () => {
47 | expect(returnValue).to.be.a('number')
48 | })
49 |
50 | it('sets the offset to 12', () => {
51 | expect(atomic.offset).to.equal(12)
52 | })
53 |
54 | it('sets the value to our blob', () => {
55 | expect(JSON.stringify(atomic.value)).to.equal(
56 | JSON.stringify({
57 | 0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7,
58 | }),
59 | )
60 | })
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/test/atomic/float32.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import AtomicFloat32 from '../../src/atomic/float32'
4 |
5 | /** @test {AtomicFloat32} */
6 | describe('AtomicFloat32', () => {
7 | const bitArray = {
8 | 0: 70, 1: 25, 2: 124, 3: 237,
9 | }
10 |
11 | let atomic
12 |
13 | before(() => {
14 | atomic = new AtomicFloat32(9823.2312155)
15 | })
16 |
17 | /** @test {AtomicFloat32#pack} */
18 | describe('pack', () => {
19 | let result
20 |
21 | before(() => {
22 | result = atomic.pack()
23 | })
24 |
25 | it('returns correct bits', () => {
26 | expect(JSON.stringify(result)).to.equal(JSON.stringify(bitArray))
27 | })
28 | })
29 |
30 | /** @test {AtomicFloat32#unpack} */
31 | describe('unpack', () => {
32 | let returnValue
33 |
34 | before(() => {
35 | const data = new Uint8Array(8)
36 | const dataView = new DataView(data.buffer)
37 |
38 | dataView.setFloat32(0, 1.254999123, false)
39 |
40 | returnValue = atomic.unpack(dataView, 0)
41 | })
42 |
43 | it('returns a number', () => {
44 | expect(returnValue).to.be.a('number')
45 | })
46 |
47 | it('sets the offset to 4', () => {
48 | expect(atomic.offset).to.equal(4)
49 | })
50 |
51 | it('sets the value to a human readable float number', () => {
52 | expect(atomic.value).to.equal(Math.fround(1.254999123))
53 | })
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/test/atomic/int32.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import AtomicInt32 from '../../src/atomic/int32'
4 |
5 | /** @test {AtomicInt32} */
6 | describe('AtomicInt32', () => {
7 | const bitArray = {
8 | 0: 0, 1: 0, 2: 0, 3: 42,
9 | }
10 |
11 | let atomic
12 |
13 | before(() => {
14 | atomic = new AtomicInt32(42)
15 | })
16 |
17 | /** @test {AtomicInt32#pack} */
18 | describe('pack', () => {
19 | let result
20 |
21 | before(() => {
22 | result = atomic.pack()
23 | })
24 |
25 | it('returns correct bits', () => {
26 | expect(JSON.stringify(result)).to.equal(JSON.stringify(bitArray))
27 | })
28 | })
29 |
30 | /** @test {AtomicInt32#unpack} */
31 | describe('unpack', () => {
32 | let returnValue
33 |
34 | before(() => {
35 | const data = new Uint8Array(4)
36 | const dataView = new DataView(data.buffer)
37 |
38 | dataView.setInt32(0, 214748364, false)
39 |
40 | returnValue = atomic.unpack(dataView, 0)
41 | })
42 |
43 | it('returns a number', () => {
44 | expect(returnValue).to.be.a('number')
45 | })
46 |
47 | it('sets the offset to 4', () => {
48 | expect(atomic.offset).to.equal(4)
49 | })
50 |
51 | it('sets the value to a human readable number', () => {
52 | expect(atomic.value).to.equal(214748364)
53 | })
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/test/atomic/int64.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import AtomicInt64 from '../../src/atomic/int64'
4 |
5 | const MAX_INT64 = BigInt('9223372036854775807')
6 | const MIN_INT64 = BigInt('-9223372036854775808')
7 |
8 | /** @test {AtomicInt64} */
9 | describe('AtomicInt64', () => {
10 | const bitArray = {
11 | 0: 127, 1: 255, 2: 255, 3: 255, 4: 255, 5: 255, 6: 255, 7: 255,
12 | }
13 |
14 | let atomic
15 |
16 | before(() => {
17 | atomic = new AtomicInt64(MAX_INT64)
18 | })
19 |
20 | describe('bounds', () => {
21 | it('throws an error in constructor if out of bounds', () => {
22 | /* eslint-disable no-new */
23 | expect(() => { new AtomicInt64(MAX_INT64 + BigInt('1')) }).to.throw('OSC AtomicInt64 value is out of bounds')
24 | expect(() => { new AtomicInt64(MIN_INT64 + BigInt('-1')) }).to.throw('OSC AtomicInt64 value is out of bounds')
25 | /* eslint-enable no-new */
26 | })
27 | })
28 |
29 | /** @test {AtomicInt64#pack} */
30 | describe('pack', () => {
31 | let result
32 |
33 | before(() => {
34 | result = atomic.pack()
35 | })
36 |
37 | it('returns correct bits', () => {
38 | expect(JSON.stringify(result)).to.equal(JSON.stringify(bitArray))
39 | })
40 | })
41 |
42 | /** @test {AtomicInt64#unpack} */
43 | describe('unpack', () => {
44 | let returnValue
45 |
46 | before(() => {
47 | const data = new Uint8Array(8)
48 | const dataView = new DataView(data.buffer)
49 |
50 | dataView.setBigInt64(0, BigInt.asIntN(64, MAX_INT64), false)
51 |
52 | returnValue = atomic.unpack(dataView, 0)
53 | })
54 |
55 | it('returns a number', () => {
56 | expect(returnValue).to.be.a('number')
57 | })
58 |
59 | it('sets the offset to 4', () => {
60 | expect(atomic.offset).to.equal(8)
61 | })
62 |
63 | it('sets the value to a human readable number', () => {
64 | const res = atomic.value === MAX_INT64
65 | expect(res).to.be.true
66 | })
67 | })
68 | })
69 |
--------------------------------------------------------------------------------
/test/atomic/string.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import AtomicString from '../../src/atomic/string'
4 |
5 | function generateLongString(length = 500000) {
6 | let str = ''
7 |
8 | for (let i = 0; i < length; i += 1) {
9 | str += 'a'
10 | }
11 |
12 | return str
13 | }
14 |
15 | /** @test {AtomicString} */
16 | describe('AtomicString', () => {
17 | const bitArrayHello = [104, 97, 108, 108, 111, 0, 0, 0]
18 |
19 | let atomic
20 |
21 | before(() => {
22 | atomic = new AtomicString('hallo')
23 | })
24 |
25 | /** @test {AtomicString#unpack} */
26 | describe('unpack', () => {
27 | let returnValue
28 |
29 | before(() => {
30 | const data = new Uint8Array(bitArrayHello)
31 | const dataView = new DataView(data.buffer)
32 |
33 | returnValue = atomic.unpack(dataView, 0)
34 | })
35 |
36 | it('returns a number', () => {
37 | expect(returnValue).to.be.a('number')
38 | })
39 |
40 | it('sets the offset to a multiple of 4', () => {
41 | expect(atomic.offset % 4).to.equal(0)
42 | })
43 |
44 | it('sets the value to a human readable string', () => {
45 | expect(atomic.value).to.equal('hallo')
46 | })
47 | })
48 |
49 | /** @test {AtomicString#pack} */
50 | describe('pack', () => {
51 | it('returns correct bits', () => {
52 | expect(JSON.stringify(atomic.pack())).to.equal(
53 | JSON.stringify(new Int8Array(bitArrayHello)),
54 | )
55 | })
56 |
57 | it('converts a long string without throwing RangeError', () => {
58 | const longString = generateLongString()
59 | const largeAtomic = new AtomicString(longString)
60 | const dataView = new DataView(largeAtomic.pack().buffer)
61 |
62 | expect(() => {
63 | largeAtomic.unpack(dataView, 0)
64 | }).to.not.throw(RangeError)
65 | })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/test/atomic/timetag.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import AtomicTimetag, {
4 | Timetag,
5 | SECONDS_70_YEARS,
6 | } from '../../src/atomic/timetag'
7 |
8 | /** @test {Timetag} */
9 | describe('Timetag', () => {
10 | let timetag
11 | let anotherTimetag
12 |
13 | before(() => {
14 | timetag = new Timetag(SECONDS_70_YEARS + 1234, 0)
15 | anotherTimetag = new Timetag(3718482449, 131799040)
16 | })
17 |
18 | it('sets the values correctly on initialization', () => {
19 | expect(timetag.seconds).to.be.equals(SECONDS_70_YEARS + 1234)
20 | expect(timetag.fractions).to.be.equals(0)
21 | })
22 |
23 | /** @test {Timetag#timestamp} */
24 | describe('timestamp', () => {
25 | it('converts correctly to js timestamps', () => {
26 | expect(timetag.timestamp()).to.be.equals(1234 * 1000)
27 | expect(anotherTimetag.timestamp()).to.be.equals(1509493649000)
28 | })
29 |
30 | it('converts correctly to NTP timestamps', () => {
31 | timetag.timestamp(1)
32 |
33 | expect(timetag.seconds).to.be.equals(SECONDS_70_YEARS)
34 | expect(timetag.fractions).to.be.equals(4294967)
35 | })
36 | })
37 | })
38 |
39 | /** @test {AtomicTimetag} */
40 | describe('AtomicTimetag', () => {
41 | const bitArray = {
42 | 0: 0, 1: 1, 2: 248, 3: 99, 4: 0, 5: 4, 6: 84, 7: 63,
43 | }
44 |
45 | let atomic
46 |
47 | before(() => {
48 | atomic = new AtomicTimetag(new Timetag(129123, 283711))
49 | })
50 |
51 | /** @test {AtomicTimetag#pack} */
52 | describe('pack', () => {
53 | let result
54 |
55 | before(() => {
56 | result = atomic.pack()
57 | })
58 |
59 | it('returns correct bits', () => {
60 | expect(JSON.stringify(result)).to.equal(JSON.stringify(bitArray))
61 | })
62 |
63 | it('consists of 64 bits', () => {
64 | expect(result.byteLength * 8).to.equal(64)
65 | })
66 | })
67 |
68 | /** @test {AtomicTimetag#unpack} */
69 | describe('unpack', () => {
70 | let returnValue
71 |
72 | before(() => {
73 | const data = new Uint8Array([1, 1, 1, 0, 0, 0, 1, 0])
74 | const dataView = new DataView(data.buffer)
75 |
76 | returnValue = atomic.unpack(dataView, 0)
77 | })
78 |
79 | it('returns a number', () => {
80 | expect(returnValue).to.be.a('number')
81 | })
82 |
83 | it('sets the offset to 8', () => {
84 | expect(atomic.offset).to.equal(8)
85 | })
86 |
87 | it('sets the correct NTP values', () => {
88 | expect(atomic.value.seconds).to.equal(16843008)
89 | expect(atomic.value.fractions).to.equal(256)
90 | })
91 | })
92 |
93 | describe('constructor', () => {
94 | it('with an integer timestamp', () => {
95 | atomic = new AtomicTimetag(5000)
96 | expect(atomic.value.seconds).to.equal(2208988805)
97 | })
98 |
99 | it('with a Date instance', () => {
100 | const date = new Date(2015, 2, 21, 5, 0, 21)
101 | date.setUTCHours(4)
102 | atomic = new AtomicTimetag(date)
103 | expect(atomic.value.seconds).to.equal(3635899221)
104 | })
105 | })
106 | })
107 |
--------------------------------------------------------------------------------
/test/atomic/uint64.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import AtomicUInt64 from '../../src/atomic/uint64'
4 |
5 | const MAX_UINT64 = BigInt('18446744073709551615')
6 |
7 | /** @test {AtomicUInt64} */
8 | describe('AtomicUInt64', () => {
9 | const bitArray = {
10 | 0: 255, 1: 255, 2: 255, 3: 255, 4: 255, 5: 255, 6: 255, 7: 255,
11 | }
12 |
13 | let atomic
14 |
15 | before(() => {
16 | atomic = new AtomicUInt64(MAX_UINT64)
17 | })
18 |
19 | describe('bounds', () => {
20 | it('throws an error in constructor if out of bounds', () => {
21 | /* eslint-disable no-new */
22 | expect(() => { new AtomicUInt64(MAX_UINT64 + BigInt('1')) }).to.throw('OSC AtomicUInt64 value is out of bounds')
23 | expect(() => { new AtomicUInt64(BigInt('-1')) }).to.throw('OSC AtomicUInt64 value is out of bounds')
24 | /* eslint-enable no-new */
25 | })
26 | })
27 |
28 | /** @test {AtomicUInt64#pack} */
29 | describe('pack', () => {
30 | let result
31 |
32 | before(() => {
33 | result = atomic.pack()
34 | })
35 |
36 | it('returns correct bits', () => {
37 | expect(JSON.stringify(result)).to.equal(JSON.stringify(bitArray))
38 | })
39 | })
40 |
41 | /** @test {AtomicUInt64#unpack} */
42 | describe('unpack', () => {
43 | let returnValue
44 |
45 | before(() => {
46 | const data = new Uint8Array(8)
47 | const dataView = new DataView(data.buffer)
48 |
49 | dataView.setBigInt64(0, BigInt.asUintN(64, MAX_UINT64), false)
50 |
51 | returnValue = atomic.unpack(dataView, 0)
52 | })
53 |
54 | it('returns a number', () => {
55 | expect(returnValue).to.be.a('number')
56 | })
57 |
58 | it('sets the offset to 4', () => {
59 | expect(atomic.offset).to.equal(8)
60 | })
61 |
62 | it('sets the value to a human readable number', () => {
63 | const res = atomic.value === MAX_UINT64
64 | expect(res).to.be.true
65 | })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/test/bundle.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import Bundle from '../src/bundle'
4 | import Message from '../src/message'
5 |
6 | /** @test {Bundle} */
7 | describe('Bundle', () => {
8 | let bundle
9 |
10 | it('contains a set of osc bundle data', () => {
11 | bundle = new Bundle()
12 | expect(bundle.timetag).to.exist
13 | })
14 |
15 | describe('add', () => {
16 | before(() => {
17 | const message = new Message('/foo/bar', 1, 2, 'ho')
18 |
19 | bundle = new Bundle([message])
20 | bundle.add(new Message('/some/path', 42.1))
21 | bundle.add(new Bundle(Date.now() + 500))
22 | })
23 |
24 | it('contains 3 bundle elements', () => {
25 | expect(bundle.bundleElements.length).to.equals(3)
26 | })
27 | })
28 |
29 | describe('pack', () => {
30 | let result
31 |
32 | before(() => {
33 | bundle = new Bundle([new Message('/super/path', 12)])
34 | result = bundle.pack()
35 | })
36 |
37 | it('returns a multiple of 32', () => {
38 | expect((result.byteLength * 8) % 32).to.equal(0)
39 | })
40 |
41 | it('can be unpacked again', () => {
42 | const anotherBundle = new Bundle()
43 | anotherBundle.unpack(new DataView(result.buffer), 0)
44 |
45 | expect(anotherBundle.bundleElements[0].address).to.equal('/super/path')
46 | expect(anotherBundle.bundleElements[0].args[0]).to.equal(12)
47 | })
48 | })
49 |
50 | describe('unpack', () => {
51 | let result
52 |
53 | before(() => {
54 | const data = new Uint8Array([35, 98, 117, 110, 100, 108, 101, 0, 220, 10,
55 | 223, 251, 100, 221, 48, 0, 0, 0, 0, 20, 47, 116, 101, 115, 116, 47, 112,
56 | 97, 116, 104, 0, 0, 44, 102, 0, 0, 66, 76, 204, 205])
57 | const dataView = new DataView(data.buffer)
58 |
59 | bundle = new Bundle()
60 | result = bundle.unpack(dataView, 0)
61 | })
62 |
63 | it('decodes the correct timetag', () => {
64 | expect(bundle.timetag.value.seconds).to.equal(3691700219)
65 | })
66 |
67 | it('returns a number', () => {
68 | expect(result).to.be.a('number')
69 | })
70 | })
71 |
72 | describe('timestamp', () => {
73 | before(() => {
74 | bundle = new Bundle()
75 | bundle.timestamp(1234)
76 | })
77 |
78 | it('sets the timetag', () => {
79 | expect(bundle.timetag.value.seconds).to.equal(2208988801)
80 | })
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/test/common/helpers.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import EncodeHelper, { typeTag, prepareAddress } from '../../src/common/helpers'
4 |
5 | import AtomicFloat32 from '../../src/atomic/float32'
6 | import AtomicString from '../../src/atomic/string'
7 |
8 | /** @test {typeTag} */
9 | describe('typeTag', () => {
10 | it('returns the right OSC Type Tag characters', () => {
11 | expect(typeTag(2)).to.be.equals('i')
12 | expect(typeTag(2.2)).to.be.equals('f')
13 | expect(typeTag('joe')).to.be.equals('s')
14 | expect(typeTag(new Uint8Array([1, 2, 3]))).to.be.equals('b')
15 | })
16 | })
17 |
18 | /** @test {prepareAddress} */
19 | describe('prepareAddress', () => {
20 | it('builds an valid address from an array', () => {
21 | expect(prepareAddress(['hello', 'world'])).to.be.equals('/hello/world')
22 | })
23 |
24 | it('builds an valid address from an invalid string', () => {
25 | expect(prepareAddress('hello/world')).to.be.equals('/hello/world')
26 | })
27 |
28 | it('removes the last slash', () => {
29 | expect(prepareAddress('/hello/world/')).to.be.equals('/hello/world')
30 | })
31 | })
32 |
33 | /** @test {EncodeHelper} */
34 | describe('EncodeHelper', () => {
35 | let encoder
36 |
37 | before(() => {
38 | encoder = new EncodeHelper()
39 | encoder.add(new AtomicFloat32(24.12))
40 | encoder.add(new AtomicString('joe'))
41 | })
42 |
43 | it('adds items up and increases the byteLength accordingly', () => {
44 | expect(encoder.byteLength).to.be.equals(8)
45 | expect(encoder.data.length).to.be.equals(2)
46 | })
47 |
48 | it('merges the items to one Uint8Array', () => {
49 | const merged = encoder.merge()
50 |
51 | expect(merged.length).to.be.equals(8)
52 | expect(merged).to.be.a('uint8array')
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/test/common/utils.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import { pad, isNull, isUndefined } from '../../src/common/utils'
4 |
5 | /** @test {pad} */
6 | describe('pad', () => {
7 | it('returns the next multiple of 4', () => {
8 | expect(pad(2)).to.be.equals(4)
9 | expect(pad(8)).to.be.equals(8)
10 | expect(pad(31)).to.be.equals(32)
11 | expect(pad(0)).to.be.equals(0)
12 | })
13 | })
14 |
15 | /** @test {isNull} */
16 | describe('isNull', () => {
17 | it('correctly identifies null value', () => {
18 | expect(isNull(0)).to.be.false
19 | expect(isNull(undefined)).to.be.false
20 | expect(isNull(null)).to.be.true
21 | })
22 | })
23 |
24 | /** @test {isUndefined} */
25 | describe('isUndefined', () => {
26 | it('correctly identifies undefined value', () => {
27 | expect(isUndefined(0)).to.be.false
28 | expect(isUndefined(undefined)).to.be.true
29 | expect(isUndefined(null)).to.be.false
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/test/events.spec.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai'
2 | import spies from 'chai-spies-next'
3 |
4 | import EventHandler from '../src/events'
5 | import Message from '../src/message'
6 |
7 | chai.use(spies)
8 |
9 | /** @test {EventHandler} */
10 | describe('EventHandler', () => {
11 | let handler
12 |
13 | before(() => {
14 | handler = new EventHandler()
15 | })
16 |
17 | /** @test {EventHandler#on} */
18 | describe('on', () => {
19 | let spy
20 | let id
21 |
22 | before(() => {
23 | spy = chai.spy()
24 | id = handler.on('/test/path', spy)
25 | })
26 |
27 | it('returns different subscription ids for each listener', () => {
28 | const anotherId = handler.on(['test', 'path'], () => {})
29 | expect(id !== anotherId).to.be.true
30 | })
31 |
32 | it('registers a handler which can be called', () => {
33 | handler.notify('/test/path', {})
34 | expect(spy).to.have.been.called()
35 | })
36 | })
37 |
38 | /** @test {EventHandler#off} */
39 | describe('off', () => {
40 | let spy
41 | let id
42 |
43 | before(() => {
44 | spy = chai.spy()
45 | id = handler.on('/test/path', spy)
46 | })
47 |
48 | it('removes a handler', () => {
49 | const success = handler.off('/test/path', id)
50 | handler.notify('/test/path', {})
51 |
52 | expect(spy).to.not.have.been.called()
53 | expect(success).to.be.true
54 | })
55 |
56 | it('returns false when handler was not found', () => {
57 | const success = handler.off('/test/path/which/does/not/exist', id)
58 | expect(success).to.be.false
59 | })
60 | })
61 |
62 | /** @test {EventHandler#notify} */
63 | describe('notify', () => {
64 | const testdata = {
65 | test: 'data',
66 | }
67 |
68 | const spy = []
69 |
70 | before(() => {
71 | for (let i = 0; i < 12; i += 1) {
72 | spy.push(chai.spy())
73 | }
74 |
75 | // regular address handlers
76 | handler.on('/', spy[0])
77 | handler.on('/one/test', spy[1])
78 | handler.on('/and/another', spy[2])
79 | handler.on('/two/test/path', spy[3])
80 | handler.on(['two', 'test', 'path'], spy[4])
81 | handler.on('/two/some/path', spy[5])
82 |
83 | // system event handlers
84 | handler.on('error', spy[6])
85 | handler.on('close', spy[7])
86 | handler.on('open', spy[8])
87 |
88 | // pattern address handlers
89 | handler.on('/two/*/path', spy[9])
90 | handler.on('*', spy[10])
91 | handler.on('/o?e/{test,bar}', spy[11])
92 | })
93 |
94 | afterEach(() => {
95 | spy.forEach((item) => {
96 | item.reset()
97 | })
98 | })
99 |
100 | it('passes over the event data', () => {
101 | handler.notify('/and/another', testdata)
102 | expect(spy[2]).have.been.called.with(testdata)
103 | })
104 |
105 | it('accepts messages', () => {
106 | handler.notify(new Message(['and', 'another']))
107 | expect(spy[2]).have.been.called()
108 | })
109 |
110 | it('accepts binary packets', () => {
111 | const binary = new Uint8Array([
112 | 47, 97, 110, 100, 47, 97, 110,
113 | 111, 116, 104, 101, 114, 0, 0, 0, 0, 44, 0, 0, 0,
114 | ])
115 |
116 | handler.notify(binary)
117 | expect(spy[2]).have.been.called()
118 | })
119 |
120 | describe('event listeners', () => {
121 | it('notifies error callbacks', () => {
122 | handler.notify('error', testdata)
123 | expect(spy[6]).have.been.called.with(testdata)
124 | })
125 |
126 | it('notifies close callbacks', () => {
127 | handler.notify('close', testdata)
128 | expect(spy[7]).have.been.called.with(testdata)
129 | })
130 |
131 | it('notifies open callbacks', () => {
132 | handler.notify('open', testdata)
133 | expect(spy[8]).have.been.called.with(testdata)
134 | })
135 | })
136 |
137 | describe('address listeners with timetags', () => {
138 | it('calls the handler later', () => {
139 | handler.notify('/', testdata, Date.now() + 5000)
140 |
141 | expect(spy[0]).not.have.been.called()
142 | })
143 | })
144 |
145 | describe('address listeners with regular strings', () => {
146 | it('calls the root listener', () => {
147 | handler.notify('/', testdata)
148 |
149 | expect(spy[0]).have.been.called()
150 | expect(spy[1]).not.have.been.called()
151 | expect(spy[4]).not.have.been.called()
152 | })
153 |
154 | it('calls two listeners with the same address', () => {
155 | handler.notify('/two/test/path', testdata)
156 |
157 | expect(spy[3]).have.been.called()
158 | expect(spy[4]).have.been.called()
159 | })
160 |
161 | it('works with {} wildcard', () => {
162 | handler.notify('/two/{test,some}/path', testdata)
163 |
164 | expect(spy[1]).not.have.been.called()
165 | expect(spy[3]).have.been.called()
166 | expect(spy[4]).have.been.called()
167 | expect(spy[5]).have.been.called()
168 | })
169 |
170 | it('works with [] wildcard', () => {
171 | handler.notify('/[pawgfo]ne/[bnit]est', testdata)
172 |
173 | expect(spy[1]).have.been.called()
174 | expect(spy[2]).not.have.been.called()
175 | })
176 |
177 | it('works with [!] wildcard', () => {
178 | handler.notify('/two/[!s][eso][tspm][tea]/path', testdata)
179 |
180 | expect(spy[3]).have.been.called()
181 | expect(spy[5]).not.have.been.called()
182 | })
183 |
184 | it('works with [a-z] wildcard', () => {
185 | handler.notify('/two/[a-z]est/p[a-c]t[e-i]', testdata)
186 |
187 | expect(spy[3]).have.been.called()
188 | expect(spy[5]).not.have.been.called()
189 | })
190 |
191 | it('works with * wildcard', () => {
192 | handler.notify('/two/*', testdata)
193 |
194 | expect(spy[3]).have.been.called()
195 | expect(spy[4]).have.been.called()
196 | expect(spy[5]).have.been.called()
197 | expect(spy[1]).not.have.been.called()
198 | })
199 |
200 | it('works with * wildcard calling all', () => {
201 | handler.notify('/*', testdata)
202 |
203 | expect(spy[0]).have.been.called()
204 | expect(spy[1]).have.been.called()
205 | expect(spy[2]).have.been.called()
206 | expect(spy[3]).have.been.called()
207 | expect(spy[4]).have.been.called()
208 | expect(spy[5]).have.been.called()
209 | })
210 |
211 | it('works with ? wildcard', () => {
212 | handler.notify('/two/????/pa?h', testdata)
213 |
214 | expect(spy[0]).not.have.been.called()
215 | expect(spy[3]).have.been.called()
216 | expect(spy[5]).have.been.called()
217 | })
218 | })
219 |
220 | describe('address listeners with pattern matching', () => {
221 | it('calls wildcard listeners', () => {
222 | handler.notify('/two/bar/path', testdata)
223 |
224 | expect(spy[9]).have.been.called()
225 | expect(spy[10]).have.been.called()
226 | expect(spy[4]).not.have.been.called()
227 | })
228 |
229 | it('calls group matching listener', () => {
230 | handler.notify('/ose/test', testdata)
231 |
232 | expect(spy[10]).have.been.called()
233 | expect(spy[11]).have.been.called()
234 | expect(spy[1]).not.have.been.called()
235 | expect(spy[9]).not.have.been.called()
236 | })
237 | })
238 | })
239 | })
240 |
--------------------------------------------------------------------------------
/test/message.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import Message, { TypedMessage } from '../src/message'
4 |
5 | /** @test {TypedMessage} */
6 | describe('TypedMessage', () => {
7 | let typedMessage
8 |
9 | before(() => {
10 | typedMessage = new TypedMessage()
11 | })
12 |
13 | it('contains a set of osc message data', () => {
14 | expect(typedMessage.address).to.exist
15 | expect(typedMessage.types).to.exist
16 | expect(typedMessage.args).to.exist
17 | })
18 |
19 | it('can be initialized with an address', () => {
20 | const anotherMessage = new TypedMessage('somekind/of/path')
21 | expect(anotherMessage.address).to.be.equals('/somekind/of/path')
22 | })
23 |
24 | it('can be initialized with an address and argument array', () => {
25 | const anotherMessage = new TypedMessage('/a/path', [
26 | { type: 'i', value: 123 },
27 | { type: 'd', value: 123.123 },
28 | { type: 'h', value: BigInt('0xFFFFFF') },
29 | { type: 'T', value: null },
30 | ])
31 | expect(anotherMessage.types.length).to.equal(4)
32 | expect(anotherMessage.args.length).to.equal(4)
33 | })
34 |
35 | /** @test {TypedMessage#add} */
36 | describe('add', () => {
37 | before(() => {
38 | typedMessage = new TypedMessage()
39 |
40 | typedMessage.add('s', 'Hello World')
41 | typedMessage.add('i', 121123)
42 | typedMessage.add('d', 123.123456789)
43 | typedMessage.add('T')
44 | typedMessage.add('i', 10)
45 | })
46 |
47 | it('pushes the values to our args array', () => {
48 | expect(typedMessage.args).to.deep.equal(['Hello World', 121123, 123.123456789, true, 10])
49 | })
50 |
51 | it('adds to the types string accordingly', () => {
52 | expect(typedMessage.types).to.equal('sidTi')
53 | })
54 | })
55 |
56 | /** @test {TypedMessage#pack} */
57 | describe('pack', () => {
58 | let result
59 |
60 | before(() => {
61 | typedMessage = new TypedMessage('/test/types')
62 |
63 | typedMessage.add('i', 1)
64 | typedMessage.add('h', BigInt('0x7FFFFFFFFFFFFFFF'))
65 | typedMessage.add('t', BigInt('0xFFFFFFFFFFFFFFFF'))
66 | typedMessage.add('f', 123.123)
67 | typedMessage.add('d', 123.123456789)
68 | typedMessage.add('s', 'stringValue')
69 | typedMessage.add('b', new Uint8Array([100, 52]))
70 | typedMessage.add('T') // true
71 | typedMessage.add('F') // false
72 | typedMessage.add('N') // Nil
73 | typedMessage.add('I') // Infinitum
74 |
75 | result = typedMessage.pack()
76 | })
77 |
78 | it('returns an object we can unpack again', () => {
79 | const anotherMessage = new TypedMessage()
80 | anotherMessage.unpack(new DataView(result.buffer), 0)
81 |
82 | expect(anotherMessage.address).to.equal('/test/types')
83 | expect(anotherMessage.args.length).to.equal(11)
84 | expect(anotherMessage.args[0]).to.equal(1)
85 | // chai.expect cannot handle BigInt directly
86 | expect(anotherMessage.args[1] === BigInt('0x7FFFFFFFFFFFFFFF')).to.be.true
87 | expect(anotherMessage.args[2] === BigInt('0xFFFFFFFFFFFFFFFF')).to.be.true
88 | expect(anotherMessage.args[3]).to.be.closeTo(123.123, 0.00001)
89 | expect(anotherMessage.args[4]).to.be.closeTo(123.123456789, 0.00001)
90 | expect(anotherMessage.args[5]).to.equal('stringValue')
91 | expect(anotherMessage.args[6][0]).to.equal(100)
92 | expect(anotherMessage.args[7]).to.equal(true)
93 | expect(anotherMessage.args[8]).to.equal(false)
94 | expect(anotherMessage.args[9]).to.equal(null)
95 | expect(anotherMessage.args[10]).to.equal(Infinity)
96 |
97 | expect(anotherMessage.types.length).to.equal(12)
98 | expect(anotherMessage.types).to.equal(',ihtfdsbTFNI')
99 | })
100 |
101 | it('returns a multiple of 32', () => {
102 | expect((result.byteLength * 8) % 32).to.equal(0)
103 | })
104 | })
105 |
106 | /** @test {TypedMessage#unpack} */
107 | describe('unpack', () => {
108 | let result
109 | let anotherMessage
110 |
111 | before(() => {
112 | anotherMessage = new TypedMessage()
113 | const data = new Uint8Array([47, 115, 111, 109, 101, 47, 97,
114 | 100, 100, 114, 0, 0, 44, 100, 115, 105, 0, 0, 0, 0, 64, 94,
115 | 199, 230, 183, 77, 206, 89, 116, 101, 115, 116,
116 | 0, 0, 0, 0, 0, 0, 0, 0])
117 | const dataView = new DataView(data.buffer, 0)
118 | result = anotherMessage.unpack(dataView)
119 | })
120 |
121 | it('decodes the message correctly', () => {
122 | expect(anotherMessage.address).to.equal('/some/addr')
123 | expect(anotherMessage.args[1]).to.equal('test')
124 | expect(anotherMessage.args[2]).to.equal(0)
125 | })
126 |
127 | it('returns a number', () => {
128 | expect(result).to.be.a('number')
129 | })
130 | })
131 | })
132 |
133 | /** @test {Message} */
134 | describe('Message', () => {
135 | let message
136 |
137 | before(() => {
138 | message = new Message()
139 | })
140 |
141 | it('contains a set of osc message data', () => {
142 | expect(message.address).to.exist
143 | expect(message.types).to.exist
144 | expect(message.args).to.exist
145 | })
146 |
147 | it('fills the arguments and address during its construction', () => {
148 | const anotherMessage = new Message('somekind/of/path', 221.21, 317, 'test', false, null)
149 |
150 | expect(anotherMessage.address).to.be.equals('/somekind/of/path')
151 | expect(anotherMessage.args[0]).to.be.equals(221.21)
152 | expect(anotherMessage.args[3]).to.be.equals(false)
153 | expect(anotherMessage.args[4]).to.be.equals(null)
154 | expect(anotherMessage.types).to.be.equals('fisFN')
155 | })
156 |
157 | /** @test {Message#add} */
158 | describe('add', () => {
159 | before(() => {
160 | message = new Message()
161 |
162 | message.add('Hello World')
163 | message.add(121123)
164 | })
165 |
166 | it('pushes the values to our args array', () => {
167 | expect(message.args).to.deep.equal(['Hello World', 121123])
168 | })
169 |
170 | it('changes the types string accordingly', () => {
171 | expect(message.types).to.equal('si')
172 | })
173 | })
174 |
175 | /** @test {Message#pack} */
176 | describe('pack', () => {
177 | let result
178 |
179 | before(() => {
180 | message = new Message()
181 |
182 | message.address = '/sssss/osc/sssssadss'
183 | message.add(12)
184 | message.add(null)
185 | message.add('Hello World')
186 | message.add(Infinity)
187 | message.add(22111.344)
188 | message.add(new Uint8Array([100, 52]))
189 |
190 | result = message.pack()
191 | })
192 |
193 | it('returns an object we can unpack again', () => {
194 | const anotherMessage = new Message()
195 | anotherMessage.unpack(new DataView(result.buffer), 0)
196 |
197 | expect(anotherMessage.address).to.equal('/sssss/osc/sssssadss')
198 | expect(anotherMessage.args[1]).to.equal(null)
199 | expect(anotherMessage.args[3]).to.equal(Infinity)
200 | expect(anotherMessage.args[5][0]).to.equal(100)
201 | })
202 |
203 | it('returns a multiple of 32', () => {
204 | expect((result.byteLength * 8) % 32).to.equal(0)
205 | })
206 | })
207 |
208 | /** @test {Message#unpack} */
209 | describe('unpack', () => {
210 | let result
211 | let anotherMessage
212 |
213 | before(() => {
214 | anotherMessage = new Message()
215 | const data = new Uint8Array([
216 | 47, 116, 101, 115, 116, 47, 112, 97,
217 | 116, 104, 0, 0, 44, 105, 0, 0, 0, 0, 2, 141])
218 | const dataView = new DataView(data.buffer, 0)
219 |
220 | result = anotherMessage.unpack(dataView)
221 | })
222 |
223 | it('decodes the message correctly', () => {
224 | expect(anotherMessage.address).to.equal('/test/path')
225 | expect(anotherMessage.args[0]).to.equal(653)
226 | })
227 |
228 | it('returns a number', () => {
229 | expect(result).to.be.a('number')
230 | })
231 | })
232 | })
233 |
--------------------------------------------------------------------------------
/test/osc.spec.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai'
2 | import spies from 'chai-spies-next'
3 |
4 | import OSC from '../src/osc'
5 |
6 | import Packet from '../src/packet'
7 | import Message from '../src/message'
8 |
9 | chai.use(spies)
10 |
11 | class TestPlugin {
12 | constructor() {
13 | this.socketStatus = -1
14 | this.notify = null
15 | }
16 |
17 | registerNotify(fn) {
18 | this.notify = fn
19 | }
20 |
21 | status() {
22 | return this.socketStatus
23 | }
24 |
25 | open() {
26 | this.socketStatus = 1
27 | this.notify('open')
28 | }
29 |
30 | send() {
31 | // unused
32 | }
33 |
34 | close() {
35 | this.socketStatus = 3
36 | this.notify('close')
37 | }
38 |
39 | // mocking helpers
40 | mockError() {
41 | this.notify('error', { message: 'An error' })
42 | }
43 |
44 | mockMessage() {
45 | this.notify(new Message(['test/path'], 55.1, 224))
46 | }
47 | }
48 |
49 | /** @test {OSC} */
50 | describe('OSC', () => {
51 | let osc
52 | let plugin
53 |
54 | before(() => {
55 | plugin = new TestPlugin()
56 | osc = new OSC({
57 | discardLateMessages: true,
58 | plugin,
59 | })
60 | })
61 |
62 | it('returns the instance options when created', () => {
63 | expect(osc.options.discardLateMessages).to.be.true
64 | })
65 |
66 | /** @test {OSC#on} */
67 | describe('on', () => {
68 | it('calls my subscription when listening to the right address', () => {
69 | const spy = chai.spy()
70 | osc.on('/test/path', spy)
71 |
72 | plugin.mockMessage()
73 |
74 | expect(spy).to.have.been.called()
75 | })
76 |
77 | it('calls an error', () => {
78 | const spy = chai.spy()
79 | osc.on('error', spy)
80 |
81 | plugin.mockError()
82 |
83 | expect(spy).to.have.been.called()
84 | })
85 |
86 | it('calls an error due to an internal exception', () => {
87 | const spy = chai.spy()
88 |
89 | osc.on('error', spy)
90 |
91 | // Receive broken OSC packet
92 | const bytes = new Uint8Array([1, 2, 3])
93 | osc.eventHandler.notify(bytes)
94 |
95 | expect(spy).to.have.been.called()
96 | })
97 | })
98 |
99 | /** @test {OSC#off} */
100 | describe('off', () => {
101 | it('removes a subscription', () => {
102 | const spy = chai.spy()
103 | const id = osc.on('error', spy)
104 |
105 | osc.off('error', id)
106 |
107 | plugin.mockError()
108 |
109 | expect(spy).to.not.have.been.called()
110 | })
111 | })
112 |
113 | /** @test {OSC#status} */
114 | describe('status', () => {
115 | it('returns the initial status', () => {
116 | expect(osc.status()).to.be.equals(OSC.STATUS.IS_NOT_INITIALIZED)
117 | })
118 | })
119 |
120 | /** @test {OSC#open} */
121 | describe('open', () => {
122 | let spy
123 |
124 | beforeEach(() => {
125 | spy = chai.spy()
126 | osc.on('open', spy)
127 | osc.open()
128 | })
129 |
130 | it('returns the correct status', () => {
131 | expect(osc.status()).to.be.equals(OSC.STATUS.IS_OPEN)
132 | })
133 |
134 | it('calls the open event', () => {
135 | expect(spy).to.have.been.called()
136 | })
137 | })
138 |
139 | /** @test {OSC#close} */
140 | describe('close', () => {
141 | let spy
142 |
143 | beforeEach(() => {
144 | spy = chai.spy()
145 | osc.on('close', spy)
146 | osc.close()
147 | })
148 |
149 | it('returns the correct status', () => {
150 | expect(osc.status()).to.be.equals(OSC.STATUS.IS_CLOSED)
151 | })
152 |
153 | it('calls the close event', () => {
154 | expect(spy).to.have.been.called()
155 | })
156 | })
157 |
158 | /** @test {OSC#send} */
159 | describe('send', () => {
160 | it('passes over a binary object with configs to the plugin', () => {
161 | const message = new Message('/test/path', 122, 554)
162 | const packet = new Packet(message)
163 | const config = { host: 'localhost', port: 9001 }
164 | const spy = chai.spy.on(plugin, 'send')
165 | const binary = packet.pack()
166 |
167 | osc.send(packet, config)
168 |
169 | expect(binary).to.be.a('Uint8Array')
170 | expect(spy).to.have.been.called.with(binary, config)
171 | })
172 | })
173 | })
174 |
--------------------------------------------------------------------------------
/test/packet.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import Packet from '../src/packet'
4 | import Message from '../src/message'
5 |
6 | /** @test {Packet} */
7 | describe('Packet', () => {
8 | let packet
9 |
10 | /** @test {Packet#pack} */
11 | describe('pack', () => {
12 | let result
13 |
14 | before(() => {
15 | packet = new Packet(new Message('/test/path', 21))
16 | result = packet.pack()
17 | })
18 |
19 | it('returns an object we can unpack again', () => {
20 | const anotherPacket = new Packet()
21 | anotherPacket.unpack(new DataView(result.buffer), 0)
22 |
23 | expect(anotherPacket.value.address).to.equal('/test/path')
24 | expect(anotherPacket.value.args[0]).to.equal(21)
25 | })
26 |
27 | it('returns a multiple of 32', () => {
28 | expect((result.byteLength * 8) % 32).to.equal(0)
29 | })
30 | })
31 |
32 | /** @test {Packet#unpack} */
33 | describe('unpack', () => {
34 | let result
35 |
36 | before(() => {
37 | const data = new Uint8Array([
38 | 47, 116, 101, 115, 116, 47, 112, 97,
39 | 116, 104, 0, 0, 44, 105, 0, 0, 0, 0, 2, 141])
40 | const dataView = new DataView(data.buffer, 0)
41 |
42 | packet = new Packet()
43 | result = packet.unpack(dataView)
44 | })
45 |
46 | it('decodes the message correctly', () => {
47 | expect(packet.value.address).to.equal('/test/path')
48 | expect(packet.value.args[0]).to.equal(653)
49 | })
50 |
51 | it('returns the offset of the data', () => {
52 | expect(result).to.equal(20)
53 | })
54 |
55 | it('returns a number', () => {
56 | expect(result).to.be.a('number')
57 | })
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/test/plugin/bridge.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import OSC from '../../src/osc'
4 | import Message from '../../src/message'
5 |
6 | import BridgePlugin from '../../src/plugin/bridge'
7 | import DatagramPlugin from '../../src/plugin/dgram'
8 | import WebsocketClientPlugin from '../../src/plugin/wsclient'
9 |
10 | const PORT_WEBSOCKET = 9129
11 | const PORT_UDP_SERVER = 9130
12 | const PORT_UDP_CLIENT = 9131
13 |
14 | /** @test {BridgePlugin} */
15 | describe('BridgePlugin', () => {
16 | let plugin
17 | let osc
18 | let oscWsClient
19 | let oscUdpClient
20 |
21 | before(() => {
22 | plugin = new BridgePlugin({
23 | wsServer: {
24 | port: PORT_WEBSOCKET,
25 | },
26 | udpClient: {
27 | host: '127.0.0.1',
28 | port: PORT_UDP_CLIENT,
29 | },
30 | udpServer: {
31 | host: '127.0.0.1',
32 | port: PORT_UDP_SERVER,
33 | },
34 | })
35 |
36 | osc = new OSC({
37 | plugin,
38 | })
39 |
40 | oscWsClient = new OSC({
41 | plugin: new WebsocketClientPlugin({
42 | port: PORT_WEBSOCKET,
43 | }),
44 | })
45 |
46 | oscUdpClient = new OSC({
47 | plugin: new DatagramPlugin({
48 | open: {
49 | host: '127.0.0.1',
50 | port: PORT_UDP_CLIENT,
51 | },
52 | }),
53 | })
54 | })
55 |
56 | it('merges the given options correctly', () => {
57 | expect(plugin.options.wsServer.port).to.be.equals(PORT_WEBSOCKET)
58 | expect(plugin.options.udpServer.host).to.be.equals('127.0.0.1')
59 | expect(plugin.options.receiver).to.be.equals('ws')
60 | })
61 |
62 | describe('status', () => {
63 | it('returns the initial status', () => {
64 | expect(plugin.status()).to.be.equals(-1)
65 | })
66 | })
67 |
68 | describe('remote address info', () => {
69 | it('returns the remote address info', (done) => {
70 | const expectedMessage = {
71 | offset: 24,
72 | address: '/test/path',
73 | types: ',ii',
74 | args: [122, 554],
75 | }
76 |
77 | const expectedRinfo = {
78 | address: '127.0.0.1',
79 | family: 'IPv4',
80 | port: PORT_UDP_SERVER,
81 | size: 24,
82 | }
83 |
84 | oscUdpClient.on('/test/path', (message, rinfo) => {
85 | expect(message).to.deep.equal(expectedMessage)
86 | expect(rinfo).to.deep.equal(expectedRinfo)
87 |
88 | done()
89 | })
90 |
91 | oscWsClient.on('open', () => {
92 | oscWsClient.send(new Message('/test/path', 122, 554))
93 | })
94 |
95 | oscUdpClient.on('open', () => {
96 | oscWsClient.open()
97 | })
98 |
99 | osc.on('open', () => {
100 | oscUdpClient.open()
101 | })
102 |
103 | osc.open()
104 | })
105 | })
106 | })
107 |
--------------------------------------------------------------------------------
/test/plugin/dgram.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import DatagramPlugin from '../../src/plugin/dgram'
4 | import Message from '../../src/message'
5 | import OSC from '../../src/osc'
6 |
7 | const PORT_UDP = 8129
8 |
9 | /** @test {DatagramPlugin} */
10 | describe('DatagramPlugin', () => {
11 | let plugin
12 | let osc
13 |
14 | before(() => {
15 | plugin = new DatagramPlugin({
16 | send: {
17 | port: PORT_UDP,
18 | },
19 | open: {
20 | host: '127.0.0.1',
21 | port: PORT_UDP,
22 | },
23 | })
24 |
25 | osc = new OSC({
26 | discardLateMessages: true,
27 | plugin,
28 | })
29 | })
30 |
31 | it('merges the given options correctly', () => {
32 | expect(plugin.options.send.port).to.be.equals(PORT_UDP)
33 | expect(plugin.options.open.host).to.be.equals('127.0.0.1')
34 | })
35 |
36 | describe('status', () => {
37 | it('returns the initial status', () => {
38 | expect(plugin.status()).to.be.equals(-1)
39 | })
40 | })
41 |
42 | describe('remote address info', () => {
43 | it('returns the remote address info', (done) => {
44 | const expectedMessage = {
45 | offset: 24,
46 | address: '/test/path',
47 | types: ',ii',
48 | args: [122, 554],
49 | }
50 |
51 | const expectedRinfo = {
52 | address: '127.0.0.1',
53 | family: 'IPv4',
54 | port: PORT_UDP,
55 | size: 24,
56 | }
57 |
58 | osc.open()
59 |
60 | osc.on('/test/path', (message, rinfo) => {
61 | expect(message).to.deep.equal(expectedMessage)
62 | expect(rinfo).to.deep.equal(expectedRinfo)
63 |
64 | done()
65 | })
66 |
67 | osc.send(new Message('/test/path', 122, 554))
68 | })
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/test/plugin/ws.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import OSC from '../../src/osc'
4 | import Message from '../../src/message'
5 |
6 | import WebsocketClientPlugin from '../../src/plugin/wsclient'
7 | import WebsocketServerPlugin from '../../src/plugin/wsserver'
8 |
9 | const PORT_WEBSOCKET = 8129
10 |
11 | /** @test {WebsocketClientPlugin} */
12 | describe('WebsocketClient/ServerPlugin', () => {
13 | let plugin
14 | let osc
15 | let oscServer
16 |
17 | before(() => {
18 | plugin = new WebsocketClientPlugin({
19 | port: PORT_WEBSOCKET,
20 | host: '127.0.0.1',
21 | })
22 |
23 | osc = new OSC({
24 | discardLateMessages: true,
25 | plugin,
26 | })
27 |
28 | oscServer = new OSC({
29 | discardLateMessages: true,
30 | plugin: new WebsocketServerPlugin({
31 | port: PORT_WEBSOCKET,
32 | host: '127.0.0.1',
33 | }),
34 | })
35 | })
36 |
37 | describe('remote address info', () => {
38 | it('returns the remote address info', (done) => {
39 | const expectedMessage = {
40 | offset: 24,
41 | address: '/test/path',
42 | types: ',ii',
43 | args: [122, 554],
44 | }
45 |
46 | const expectedRinfo = {
47 | address: '127.0.0.1',
48 | family: 'wsserver',
49 | port: PORT_WEBSOCKET,
50 | size: 0,
51 | }
52 |
53 | oscServer.on('/test/path', (message, rinfo) => {
54 | expect(message).to.deep.equal(expectedMessage)
55 | expect(rinfo).to.deep.equal(expectedRinfo)
56 |
57 | done()
58 | })
59 |
60 | osc.on('open', () => osc.send(new Message('/test/path', 122, 554)))
61 |
62 | oscServer.open()
63 | osc.open()
64 | })
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/test/plugin/wsclient.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import WebsocketClientPlugin from '../../src/plugin/wsclient'
4 |
5 | /** @test {WebsocketClientPlugin} */
6 | describe('WebsocketClientPlugin', () => {
7 | let plugin
8 |
9 | before(() => {
10 | plugin = new WebsocketClientPlugin({
11 | port: 8129,
12 | host: '127.0.0.1',
13 | })
14 | })
15 |
16 | it('merges the given options correctly', () => {
17 | expect(plugin.options.port).to.be.equals(8129)
18 | expect(plugin.options.host).to.be.equals('127.0.0.1')
19 | expect(plugin.options.secure).to.be.equals(false)
20 | })
21 |
22 | describe('status', () => {
23 | it('returns the initial status', () => {
24 | expect(plugin.status()).to.be.equals(-1)
25 | })
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/test/plugin/wsserver.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 |
3 | import WebsocketServerPlugin from '../../src/plugin/wsserver'
4 |
5 | /** @test {WebsocketServerPlugin} */
6 | describe('WebsocketServerPlugin', () => {
7 | let plugin
8 |
9 | before(() => {
10 | plugin = new WebsocketServerPlugin({
11 | udpServer: {
12 | port: 8129,
13 | },
14 | wsServer: {
15 | host: '127.0.0.1',
16 | },
17 | })
18 | })
19 |
20 | it('merges the given options correctly', () => {
21 | expect(plugin.options.udpServer.port).to.be.equals(8129)
22 | expect(plugin.options.wsServer.host).to.be.equals('127.0.0.1')
23 | })
24 |
25 | describe('status', () => {
26 | it('returns the initial status', () => {
27 | expect(plugin.status()).to.be.equals(-1)
28 | })
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "declaration": true,
5 | "declarationMap": true,
6 | "emitDeclarationOnly": true,
7 | "outDir": "lib"
8 | },
9 | "include": ["src/**/*"]
10 | }
11 |
--------------------------------------------------------------------------------