├── .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 | Build status 7 | 8 | 9 | npm version 10 | 11 | 12 | npm licence 13 | 14 | 15 | ESDoc status 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 | --------------------------------------------------------------------------------