node-osc 11.1.1 → 11.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.gitattributes +11 -0
  2. package/.github/workflows/bump-version.yml +5 -3
  3. package/.github/workflows/create-release.yml +4 -4
  4. package/.github/workflows/nodejs.yml +6 -3
  5. package/LICENSE +201 -165
  6. package/README.md +135 -42
  7. package/dist/lib/Bundle.js +66 -0
  8. package/dist/lib/Client.js +137 -22
  9. package/dist/lib/Message.js +90 -2
  10. package/dist/lib/Server.js +117 -6
  11. package/dist/lib/index.js +3 -0
  12. package/dist/lib/internal/decode.js +4 -4
  13. package/dist/lib/{internal/osc.js → osc.js} +73 -7
  14. package/dist/test/lib/osc.js +396 -0
  15. package/dist/test/test-client.js +174 -0
  16. package/dist/test/test-e2e.js +9 -3
  17. package/dist/test/test-encode-decode.js +1208 -0
  18. package/dist/test/test-error-handling.js +116 -0
  19. package/dist/test/test-message.js +147 -0
  20. package/dist/test/test-osc-internal.js +399 -41
  21. package/dist/test/test-promises.js +272 -0
  22. package/dist/test/test-types.js +42 -0
  23. package/dist/test/util.js +15 -8
  24. package/docs/API.md +477 -0
  25. package/docs/GUIDE.md +605 -0
  26. package/examples/README.md +119 -0
  27. package/examples/async-await.mjs +57 -0
  28. package/examples/bundle-example.mjs +92 -0
  29. package/examples/client.js +22 -5
  30. package/examples/error-handling.mjs +152 -0
  31. package/examples/esm.mjs +21 -0
  32. package/examples/server.js +16 -0
  33. package/jsdoc.json +16 -0
  34. package/lib/Bundle.mjs +66 -0
  35. package/lib/Client.mjs +137 -22
  36. package/lib/Message.mjs +90 -2
  37. package/lib/Server.mjs +117 -6
  38. package/lib/index.mjs +1 -0
  39. package/lib/internal/decode.mjs +4 -4
  40. package/lib/{internal/osc.mjs → osc.mjs} +74 -6
  41. package/package.json +12 -10
  42. package/rollup.config.mjs +49 -41
  43. package/scripts/generate-docs.mjs +229 -0
  44. package/test/fixtures/types/test-cjs-types.ts +19 -0
  45. package/test/fixtures/types/test-esm-types.ts +35 -0
  46. package/test/fixtures/types/tsconfig-cjs.test.json +17 -0
  47. package/test/fixtures/types/tsconfig-esm.test.json +17 -0
  48. package/test/test-bundle.mjs +0 -1
  49. package/test/test-client.mjs +174 -0
  50. package/test/test-e2e.mjs +9 -3
  51. package/test/test-encode-decode.mjs +1206 -0
  52. package/test/test-error-handling.mjs +115 -0
  53. package/test/test-message.mjs +147 -0
  54. package/test/test-osc-internal.mjs +400 -42
  55. package/test/test-promises.mjs +271 -0
  56. package/test/test-types.mjs +39 -0
  57. package/test/util.mjs +15 -8
  58. package/tsconfig.json +45 -0
  59. package/types/Bundle.d.mts +70 -0
  60. package/types/Bundle.d.mts.map +1 -0
  61. package/types/Client.d.mts +101 -0
  62. package/types/Client.d.mts.map +1 -0
  63. package/types/Message.d.mts +84 -0
  64. package/types/Message.d.mts.map +1 -0
  65. package/types/Server.d.mts +98 -0
  66. package/types/Server.d.mts.map +1 -0
  67. package/types/index.d.mts +6 -0
  68. package/types/index.d.mts.map +1 -0
  69. package/types/internal/decode.d.mts +4 -0
  70. package/types/internal/decode.d.mts.map +1 -0
  71. package/types/osc.d.mts +66 -0
  72. package/types/osc.d.mts.map +1 -0
@@ -4,14 +4,84 @@ var node_dgram = require('node:dgram');
4
4
  var node_events = require('node:events');
5
5
  var decode = require('#decode');
6
6
 
7
+ /**
8
+ * OSC Server for receiving messages and bundles over UDP.
9
+ *
10
+ * Emits the following events:
11
+ * - 'listening': Emitted when the server starts listening
12
+ * - 'message': Emitted when an OSC message is received (receives msg array and rinfo object)
13
+ * - 'bundle': Emitted when an OSC bundle is received (receives bundle object and rinfo object)
14
+ * - 'error': Emitted when a socket error or decoding error occurs (receives error and rinfo)
15
+ * - Address-specific events: Emitted for each message address (e.g., '/test')
16
+ *
17
+ * @class
18
+ * @extends EventEmitter
19
+ *
20
+ * @fires Server#listening
21
+ * @fires Server#message
22
+ * @fires Server#bundle
23
+ * @fires Server#error
24
+ *
25
+ * @example
26
+ * // Create and listen for messages
27
+ * const server = new Server(3333, '0.0.0.0', () => {
28
+ * console.log('Server is listening');
29
+ * });
30
+ *
31
+ * server.on('message', (msg, rinfo) => {
32
+ * console.log('Message:', msg);
33
+ * console.log('From:', rinfo.address, rinfo.port);
34
+ * });
35
+ *
36
+ * @example
37
+ * // Using async/await with events.once
38
+ * import { once } from 'node:events';
39
+ *
40
+ * const server = new Server(3333, '0.0.0.0');
41
+ * await once(server, 'listening');
42
+ *
43
+ * server.on('message', (msg) => {
44
+ * console.log('Message:', msg);
45
+ * });
46
+ *
47
+ * @example
48
+ * // Listen for specific OSC addresses
49
+ * server.on('/note', (msg) => {
50
+ * const [address, pitch, velocity] = msg;
51
+ * console.log(`Note: ${pitch}, Velocity: ${velocity}`);
52
+ * });
53
+ */
7
54
  class Server extends node_events.EventEmitter {
55
+ /**
56
+ * Create an OSC Server.
57
+ *
58
+ * @param {number} port - The port to listen on.
59
+ * @param {string} [host='127.0.0.1'] - The host address to bind to. Use '0.0.0.0' to listen on all interfaces.
60
+ * @param {Function} [cb] - Optional callback function called when server starts listening.
61
+ *
62
+ * @example
63
+ * // Basic server
64
+ * const server = new Server(3333);
65
+ *
66
+ * @example
67
+ * // Server on all interfaces with callback
68
+ * const server = new Server(3333, '0.0.0.0', () => {
69
+ * console.log('Server started');
70
+ * });
71
+ *
72
+ * @example
73
+ * // Host parameter can be omitted, callback as second parameter
74
+ * const server = new Server(3333, () => {
75
+ * console.log('Server started on 127.0.0.1');
76
+ * });
77
+ */
8
78
  constructor(port, host='127.0.0.1', cb) {
9
79
  super();
10
80
  if (typeof host === 'function') {
11
81
  cb = host;
12
82
  host = '127.0.0.1';
13
83
  }
14
- if (!cb) cb = () => {};
84
+
15
85
  let decoded;
16
86
  this.port = port;
17
87
  this.host = host;
@@ -20,10 +90,20 @@ class Server extends node_events.EventEmitter {
20
90
  reuseAddr: true
21
91
  });
22
92
  this._sock.bind(port, host);
23
- this._sock.on('listening', () => {
24
- this.emit('listening');
25
- cb();
26
- });
93
+
94
+ // Support both callback and promise-based listening
95
+ if (cb) {
96
+ this._sock.on('listening', () => {
97
+ this.emit('listening');
98
+ cb();
99
+ });
100
+ } else {
101
+ // For promise support, still emit the event but don't require a callback
102
+ this._sock.on('listening', () => {
103
+ this.emit('listening');
104
+ });
105
+ }
106
+
27
107
  this._sock.on('message', (msg, rinfo) => {
28
108
  try {
29
109
  decoded = decode(msg);
@@ -41,9 +121,40 @@ class Server extends node_events.EventEmitter {
41
121
  this.emit(decoded[0], decoded, rinfo);
42
122
  }
43
123
  });
124
+
125
+ this._sock.on('error', (err) => {
126
+ this.emit('error', err);
127
+ });
44
128
  }
129
+ /**
130
+ * Close the server socket.
131
+ *
132
+ * This method can be used with either a callback or as a Promise.
133
+ *
134
+ * @param {Function} [cb] - Optional callback function called when socket is closed.
135
+ * @returns {Promise<void>|undefined} Returns a Promise if no callback is provided.
136
+ *
137
+ * @example
138
+ * // With callback
139
+ * server.close((err) => {
140
+ * if (err) console.error(err);
141
+ * });
142
+ *
143
+ * @example
144
+ * // With async/await
145
+ * await server.close();
146
+ */
45
147
  close(cb) {
46
- this._sock.close(cb);
148
+ if (cb) {
149
+ this._sock.close(cb);
150
+ } else {
151
+ return new Promise((resolve, reject) => {
152
+ this._sock.close((err) => {
153
+ if (err) reject(err);
154
+ else resolve();
155
+ });
156
+ });
157
+ }
47
158
  }
48
159
  }
49
160
 
package/dist/lib/index.js CHANGED
@@ -4,6 +4,7 @@ var Message = require('./Message.js');
4
4
  var Bundle = require('./Bundle.js');
5
5
  var Server = require('./Server.js');
6
6
  var Client = require('./Client.js');
7
+ var osc = require('./osc.js');
7
8
 
8
9
 
9
10
 
@@ -11,3 +12,5 @@ exports.Message = Message;
11
12
  exports.Bundle = Bundle;
12
13
  exports.Server = Server;
13
14
  exports.Client = Client;
15
+ exports.decode = osc.decode;
16
+ exports.encode = osc.encode;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var _osc = require('#osc');
3
+ var osc = require('../osc.js');
4
4
 
5
5
  function sanitizeMessage(decoded) {
6
6
  const message = [];
@@ -19,8 +19,8 @@ function sanitizeBundle(decoded) {
19
19
  return decoded;
20
20
  }
21
21
 
22
- function decode(data, customFromBuffer = _osc.fromBuffer) {
23
- const decoded = customFromBuffer(data);
22
+ function decodeAndSanitize(data, customDecode = osc.decode) {
23
+ const decoded = customDecode(data);
24
24
  if (decoded.oscType === 'bundle') {
25
25
  return sanitizeBundle(decoded);
26
26
  }
@@ -32,4 +32,4 @@ function decode(data, customFromBuffer = _osc.fromBuffer) {
32
32
  }
33
33
  }
34
34
 
35
- module.exports = decode;
35
+ module.exports = decodeAndSanitize;
@@ -8,8 +8,9 @@ var node_buffer = require('node:buffer');
8
8
 
9
9
  function padString(str) {
10
10
  const nullTerminated = str + '\0';
11
- const padding = 4 - (nullTerminated.length % 4);
12
- return nullTerminated + '\0'.repeat(padding === 4 ? 0 : padding);
11
+ const byteLength = node_buffer.Buffer.byteLength(nullTerminated);
12
+ const padding = (4 - (byteLength % 4)) % 4;
13
+ return nullTerminated + '\0'.repeat(padding);
13
14
  }
14
15
 
15
16
  function readString(buffer, offset) {
@@ -150,6 +151,9 @@ function encodeArgument(arg) {
150
151
  // For doubles, use float for now (OSC 1.0 doesn't have double)
151
152
  return { tag: 'f', data: writeFloat32(arg.value) };
152
153
  case 'T':
154
+ return { tag: 'T', data: node_buffer.Buffer.alloc(0) };
155
+ case 'F':
156
+ return { tag: 'F', data: node_buffer.Buffer.alloc(0) };
153
157
  case 'boolean':
154
158
  return arg.value ? { tag: 'T', data: node_buffer.Buffer.alloc(0) } : { tag: 'F', data: node_buffer.Buffer.alloc(0) };
155
159
  case 'm':
@@ -203,7 +207,37 @@ function decodeArgument(tag, buffer, offset) {
203
207
  }
204
208
  }
205
209
 
206
- function toBuffer(message) {
210
+ /**
211
+ * Encode an OSC message or bundle to a Buffer.
212
+ *
213
+ * This low-level function converts OSC messages and bundles into binary format
214
+ * for transmission or storage. Useful for sending OSC over custom transports
215
+ * (WebSocket, TCP, HTTP), storing to files, or implementing custom OSC routers.
216
+ *
217
+ * @param {Object} message - OSC message or bundle object with oscType property
218
+ * @returns {Buffer} The encoded OSC data ready for transmission
219
+ *
220
+ * @example
221
+ * // Encode a message
222
+ * import { Message, encode } from 'node-osc';
223
+ *
224
+ * const message = new Message('/oscillator/frequency', 440);
225
+ * const buffer = encode(message);
226
+ * console.log('Encoded bytes:', buffer.length);
227
+ *
228
+ * @example
229
+ * // Encode a bundle
230
+ * import { Bundle, encode } from 'node-osc';
231
+ *
232
+ * const bundle = new Bundle(['/one', 1], ['/two', 2]);
233
+ * const buffer = encode(bundle);
234
+ *
235
+ * @example
236
+ * // Send over WebSocket
237
+ * const buffer = encode(message);
238
+ * websocket.send(buffer);
239
+ */
240
+ function encode(message) {
207
241
  if (message.oscType === 'bundle') {
208
242
  return encodeBundleToBuffer(message);
209
243
  } else {
@@ -254,7 +288,39 @@ function encodeBundleToBuffer(bundle) {
254
288
  return node_buffer.Buffer.concat([bundleStringBuffer, timetagBuffer, ...elementBuffers]);
255
289
  }
256
290
 
257
- function fromBuffer(buffer) {
291
+ /**
292
+ * Decode a Buffer containing OSC data into a message or bundle object.
293
+ *
294
+ * This low-level function parses binary OSC data back into JavaScript objects.
295
+ * Useful for receiving OSC over custom transports, reading from files,
296
+ * or implementing custom OSC routers.
297
+ *
298
+ * @param {Buffer} buffer - The Buffer containing OSC data
299
+ * @returns {Object} The decoded OSC message or bundle. Messages have
300
+ * {oscType: 'message', address: string, args: Array}, bundles have
301
+ * {oscType: 'bundle', timetag: number, elements: Array}
302
+ * @throws {Error} If the buffer contains malformed OSC data
303
+ *
304
+ * @example
305
+ * // Decode received data
306
+ * import { decode } from 'node-osc';
307
+ *
308
+ * const decoded = decode(buffer);
309
+ * if (decoded.oscType === 'message') {
310
+ * console.log('Address:', decoded.address);
311
+ * console.log('Arguments:', decoded.args);
312
+ * }
313
+ *
314
+ * @example
315
+ * // Round-trip encode/decode
316
+ * import { Message, encode, decode } from 'node-osc';
317
+ *
318
+ * const original = new Message('/test', 42, 'hello');
319
+ * const buffer = encode(original);
320
+ * const decoded = decode(buffer);
321
+ * console.log(decoded.address); // '/test'
322
+ */
323
+ function decode(buffer) {
258
324
  // Check if it's a bundle or message
259
325
  if (buffer.length >= 8 && buffer.subarray(0, 8).toString() === '#bundle\0') {
260
326
  return decodeBundleFromBuffer(buffer);
@@ -314,7 +380,7 @@ function decodeBundleFromBuffer(buffer) {
314
380
 
315
381
  // Read element data
316
382
  const elementBuffer = buffer.subarray(offset, offset + size);
317
- const element = fromBuffer(elementBuffer);
383
+ const element = decode(elementBuffer);
318
384
  elements.push(element);
319
385
  offset += size;
320
386
  }
@@ -326,5 +392,5 @@ function decodeBundleFromBuffer(buffer) {
326
392
  };
327
393
  }
328
394
 
329
- exports.fromBuffer = fromBuffer;
330
- exports.toBuffer = toBuffer;
395
+ exports.decode = decode;
396
+ exports.encode = encode;
@@ -0,0 +1,396 @@
1
+ 'use strict';
2
+
3
+ var node_buffer = require('node:buffer');
4
+
5
+ // OSC 1.0 Protocol Implementation
6
+ // Based on http://opensoundcontrol.org/spec-1_0
7
+
8
+
9
+ function padString(str) {
10
+ const nullTerminated = str + '\0';
11
+ const byteLength = node_buffer.Buffer.byteLength(nullTerminated);
12
+ const padding = (4 - (byteLength % 4)) % 4;
13
+ return nullTerminated + '\0'.repeat(padding);
14
+ }
15
+
16
+ function readString(buffer, offset) {
17
+ let end = offset;
18
+ while (end < buffer.length && buffer[end] !== 0) {
19
+ end++;
20
+ }
21
+ const str = buffer.subarray(offset, end).toString('utf8');
22
+ // Find next 4-byte boundary
23
+ const paddedLength = Math.ceil((end - offset + 1) / 4) * 4;
24
+ return { value: str, offset: offset + paddedLength };
25
+ }
26
+
27
+ function writeInt32(value) {
28
+ const buffer = node_buffer.Buffer.alloc(4);
29
+ buffer.writeInt32BE(value, 0);
30
+ return buffer;
31
+ }
32
+
33
+ function readInt32(buffer, offset) {
34
+ const value = buffer.readInt32BE(offset);
35
+ return { value, offset: offset + 4 };
36
+ }
37
+
38
+ function writeFloat32(value) {
39
+ const buffer = node_buffer.Buffer.alloc(4);
40
+ buffer.writeFloatBE(value, 0);
41
+ return buffer;
42
+ }
43
+
44
+ function readFloat32(buffer, offset) {
45
+ const value = buffer.readFloatBE(offset);
46
+ return { value, offset: offset + 4 };
47
+ }
48
+
49
+ function writeBlob(value) {
50
+ const length = value.length;
51
+ const lengthBuffer = writeInt32(length);
52
+ const padding = 4 - (length % 4);
53
+ const paddingBuffer = node_buffer.Buffer.alloc(padding === 4 ? 0 : padding);
54
+ return node_buffer.Buffer.concat([lengthBuffer, value, paddingBuffer]);
55
+ }
56
+
57
+ function readBlob(buffer, offset) {
58
+ const lengthResult = readInt32(buffer, offset);
59
+ const length = lengthResult.value;
60
+ const data = buffer.subarray(lengthResult.offset, lengthResult.offset + length);
61
+ const padding = 4 - (length % 4);
62
+ const nextOffset = lengthResult.offset + length + (padding === 4 ? 0 : padding);
63
+ return { value: data, offset: nextOffset };
64
+ }
65
+
66
+ function writeTimeTag(value) {
67
+ // For now, treat timetag as a double (8 bytes)
68
+ // OSC timetag is 64-bit: 32-bit seconds since 1900, 32-bit fractional
69
+ const buffer = node_buffer.Buffer.alloc(8);
70
+ if (typeof value === 'number') {
71
+ // Convert to OSC timetag format
72
+ const seconds = Math.floor(value);
73
+ const fraction = Math.floor((value - seconds) * 0x100000000);
74
+ buffer.writeUInt32BE(seconds + 2208988800, 0); // Add epoch offset (1900 vs 1970)
75
+ buffer.writeUInt32BE(fraction, 4);
76
+ } else {
77
+ // If not a number, write zeros (immediate execution)
78
+ buffer.writeUInt32BE(0, 0);
79
+ buffer.writeUInt32BE(1, 4);
80
+ }
81
+ return buffer;
82
+ }
83
+
84
+ function readTimeTag(buffer, offset) {
85
+ const seconds = buffer.readUInt32BE(offset);
86
+ const fraction = buffer.readUInt32BE(offset + 4);
87
+
88
+ let value;
89
+ if (seconds === 0 && fraction === 1) {
90
+ // Immediate execution
91
+ value = 0;
92
+ } else {
93
+ // Convert from OSC epoch (1900) to Unix epoch (1970)
94
+ const unixSeconds = seconds - 2208988800;
95
+ const fractionalSeconds = fraction / 0x100000000;
96
+ value = unixSeconds + fractionalSeconds;
97
+ }
98
+
99
+ return { value, offset: offset + 8 };
100
+ }
101
+
102
+ function writeMidi(value) {
103
+ // MIDI message is 4 bytes: port id, status byte, data1, data2
104
+ const buffer = node_buffer.Buffer.alloc(4);
105
+
106
+ if (node_buffer.Buffer.isBuffer(value)) {
107
+ if (value.length !== 4) {
108
+ throw new Error('MIDI message must be exactly 4 bytes');
109
+ }
110
+ value.copy(buffer);
111
+ } else if (typeof value === 'object' && value !== null) {
112
+ // Allow object format: { port: 0, status: 144, data1: 60, data2: 127 }
113
+ buffer.writeUInt8(value.port || 0, 0);
114
+ buffer.writeUInt8(value.status || 0, 1);
115
+ buffer.writeUInt8(value.data1 || 0, 2);
116
+ buffer.writeUInt8(value.data2 || 0, 3);
117
+ } else {
118
+ throw new Error('MIDI value must be a 4-byte Buffer or object with port, status, data1, data2 properties');
119
+ }
120
+
121
+ return buffer;
122
+ }
123
+
124
+ function readMidi(buffer, offset) {
125
+ if (offset + 4 > buffer.length) {
126
+ throw new Error('Not enough bytes for MIDI message');
127
+ }
128
+
129
+ const value = buffer.subarray(offset, offset + 4);
130
+ return { value, offset: offset + 4 };
131
+ }
132
+
133
+ function encodeArgument(arg) {
134
+ if (typeof arg === 'object' && arg.type && arg.value !== undefined) {
135
+ // Explicit type specification
136
+ switch (arg.type) {
137
+ case 'i':
138
+ case 'integer':
139
+ return { tag: 'i', data: writeInt32(arg.value) };
140
+ case 'f':
141
+ case 'float':
142
+ return { tag: 'f', data: writeFloat32(arg.value) };
143
+ case 's':
144
+ case 'string':
145
+ return { tag: 's', data: node_buffer.Buffer.from(padString(arg.value)) };
146
+ case 'b':
147
+ case 'blob':
148
+ return { tag: 'b', data: writeBlob(arg.value) };
149
+ case 'd':
150
+ case 'double':
151
+ // For doubles, use float for now (OSC 1.0 doesn't have double)
152
+ return { tag: 'f', data: writeFloat32(arg.value) };
153
+ case 'T':
154
+ return { tag: 'T', data: node_buffer.Buffer.alloc(0) };
155
+ case 'F':
156
+ return { tag: 'F', data: node_buffer.Buffer.alloc(0) };
157
+ case 'boolean':
158
+ return arg.value ? { tag: 'T', data: node_buffer.Buffer.alloc(0) } : { tag: 'F', data: node_buffer.Buffer.alloc(0) };
159
+ case 'm':
160
+ case 'midi':
161
+ return { tag: 'm', data: writeMidi(arg.value) };
162
+ default:
163
+ throw new Error(`Unknown argument type: ${arg.type}`);
164
+ }
165
+ }
166
+
167
+ // Infer type from JavaScript type
168
+ switch (typeof arg) {
169
+ case 'number':
170
+ if (Number.isInteger(arg)) {
171
+ return { tag: 'i', data: writeInt32(arg) };
172
+ } else {
173
+ return { tag: 'f', data: writeFloat32(arg) };
174
+ }
175
+ case 'string':
176
+ return { tag: 's', data: node_buffer.Buffer.from(padString(arg)) };
177
+ case 'boolean':
178
+ return arg ? { tag: 'T', data: node_buffer.Buffer.alloc(0) } : { tag: 'F', data: node_buffer.Buffer.alloc(0) };
179
+ default:
180
+ if (node_buffer.Buffer.isBuffer(arg)) {
181
+ return { tag: 'b', data: writeBlob(arg) };
182
+ }
183
+ throw new Error(`Don't know how to encode argument: ${arg}`);
184
+ }
185
+ }
186
+
187
+ function decodeArgument(tag, buffer, offset) {
188
+ switch (tag) {
189
+ case 'i':
190
+ return readInt32(buffer, offset);
191
+ case 'f':
192
+ return readFloat32(buffer, offset);
193
+ case 's':
194
+ return readString(buffer, offset);
195
+ case 'b':
196
+ return readBlob(buffer, offset);
197
+ case 'T':
198
+ return { value: true, offset };
199
+ case 'F':
200
+ return { value: false, offset };
201
+ case 'N':
202
+ return { value: null, offset };
203
+ case 'm':
204
+ return readMidi(buffer, offset);
205
+ default:
206
+ throw new Error(`I don't understand the argument code ${tag}`);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Encode an OSC message or bundle to a Buffer.
212
+ *
213
+ * This low-level function converts OSC messages and bundles into binary format
214
+ * for transmission or storage. Useful for sending OSC over custom transports
215
+ * (WebSocket, TCP, HTTP), storing to files, or implementing custom OSC routers.
216
+ *
217
+ * @param {Object} message - OSC message or bundle object with oscType property
218
+ * @returns {Buffer} The encoded OSC data ready for transmission
219
+ *
220
+ * @example
221
+ * // Encode a message
222
+ * import { Message, encode } from 'node-osc';
223
+ *
224
+ * const message = new Message('/oscillator/frequency', 440);
225
+ * const buffer = encode(message);
226
+ * console.log('Encoded bytes:', buffer.length);
227
+ *
228
+ * @example
229
+ * // Encode a bundle
230
+ * import { Bundle, encode } from 'node-osc';
231
+ *
232
+ * const bundle = new Bundle(['/one', 1], ['/two', 2]);
233
+ * const buffer = encode(bundle);
234
+ *
235
+ * @example
236
+ * // Send over WebSocket
237
+ * const buffer = encode(message);
238
+ * websocket.send(buffer);
239
+ */
240
+ function encode(message) {
241
+ if (message.oscType === 'bundle') {
242
+ return encodeBundleToBuffer(message);
243
+ } else {
244
+ return encodeMessageToBuffer(message);
245
+ }
246
+ }
247
+
248
+ function encodeMessageToBuffer(message) {
249
+ // OSC Message format:
250
+ // Address pattern (padded string)
251
+ // Type tag string (padded string starting with ,)
252
+ // Arguments (encoded according to type tags)
253
+
254
+ const address = padString(message.address);
255
+ const addressBuffer = node_buffer.Buffer.from(address);
256
+
257
+ const encodedArgs = message.args.map(encodeArgument);
258
+ const typeTags = ',' + encodedArgs.map(arg => arg.tag).join('');
259
+ const typeTagsBuffer = node_buffer.Buffer.from(padString(typeTags));
260
+
261
+ const argumentBuffers = encodedArgs.map(arg => arg.data);
262
+
263
+ return node_buffer.Buffer.concat([addressBuffer, typeTagsBuffer, ...argumentBuffers]);
264
+ }
265
+
266
+ function encodeBundleToBuffer(bundle) {
267
+ // OSC Bundle format:
268
+ // "#bundle" (padded string)
269
+ // Timetag (8 bytes)
270
+ // Elements (each prefixed with size)
271
+
272
+ const bundleString = padString('#bundle');
273
+ const bundleStringBuffer = node_buffer.Buffer.from(bundleString);
274
+
275
+ const timetagBuffer = writeTimeTag(bundle.timetag);
276
+
277
+ const elementBuffers = bundle.elements.map(element => {
278
+ let elementBuffer;
279
+ if (element.oscType === 'bundle') {
280
+ elementBuffer = encodeBundleToBuffer(element);
281
+ } else {
282
+ elementBuffer = encodeMessageToBuffer(element);
283
+ }
284
+ const sizeBuffer = writeInt32(elementBuffer.length);
285
+ return node_buffer.Buffer.concat([sizeBuffer, elementBuffer]);
286
+ });
287
+
288
+ return node_buffer.Buffer.concat([bundleStringBuffer, timetagBuffer, ...elementBuffers]);
289
+ }
290
+
291
+ /**
292
+ * Decode a Buffer containing OSC data into a message or bundle object.
293
+ *
294
+ * This low-level function parses binary OSC data back into JavaScript objects.
295
+ * Useful for receiving OSC over custom transports, reading from files,
296
+ * or implementing custom OSC routers.
297
+ *
298
+ * @param {Buffer} buffer - The Buffer containing OSC data
299
+ * @returns {Object} The decoded OSC message or bundle. Messages have
300
+ * {oscType: 'message', address: string, args: Array}, bundles have
301
+ * {oscType: 'bundle', timetag: number, elements: Array}
302
+ * @throws {Error} If the buffer contains malformed OSC data
303
+ *
304
+ * @example
305
+ * // Decode received data
306
+ * import { decode } from 'node-osc';
307
+ *
308
+ * const decoded = decode(buffer);
309
+ * if (decoded.oscType === 'message') {
310
+ * console.log('Address:', decoded.address);
311
+ * console.log('Arguments:', decoded.args);
312
+ * }
313
+ *
314
+ * @example
315
+ * // Round-trip encode/decode
316
+ * import { Message, encode, decode } from 'node-osc';
317
+ *
318
+ * const original = new Message('/test', 42, 'hello');
319
+ * const buffer = encode(original);
320
+ * const decoded = decode(buffer);
321
+ * console.log(decoded.address); // '/test'
322
+ */
323
+ function decode(buffer) {
324
+ // Check if it's a bundle or message
325
+ if (buffer.length >= 8 && buffer.subarray(0, 8).toString() === '#bundle\0') {
326
+ return decodeBundleFromBuffer(buffer);
327
+ } else {
328
+ return decodeMessageFromBuffer(buffer);
329
+ }
330
+ }
331
+
332
+ function decodeMessageFromBuffer(buffer) {
333
+ let offset = 0;
334
+
335
+ // Read address pattern
336
+ const addressResult = readString(buffer, offset);
337
+ const address = addressResult.value;
338
+ offset = addressResult.offset;
339
+
340
+ // Read type tag string
341
+ const typeTagsResult = readString(buffer, offset);
342
+ const typeTags = typeTagsResult.value;
343
+ offset = typeTagsResult.offset;
344
+
345
+ if (!typeTags.startsWith(',')) {
346
+ throw new Error('Malformed Packet');
347
+ }
348
+
349
+ const tags = typeTags.slice(1); // Remove leading comma
350
+ const args = [];
351
+
352
+ for (const tag of tags) {
353
+ const argResult = decodeArgument(tag, buffer, offset);
354
+ args.push({ value: argResult.value });
355
+ offset = argResult.offset;
356
+ }
357
+
358
+ return {
359
+ oscType: 'message',
360
+ address,
361
+ args
362
+ };
363
+ }
364
+
365
+ function decodeBundleFromBuffer(buffer) {
366
+ let offset = 8; // Skip "#bundle\0"
367
+
368
+ // Read timetag
369
+ const timetagResult = readTimeTag(buffer, offset);
370
+ const timetag = timetagResult.value;
371
+ offset = timetagResult.offset;
372
+
373
+ const elements = [];
374
+
375
+ while (offset < buffer.length) {
376
+ // Read element size
377
+ const sizeResult = readInt32(buffer, offset);
378
+ const size = sizeResult.value;
379
+ offset = sizeResult.offset;
380
+
381
+ // Read element data
382
+ const elementBuffer = buffer.subarray(offset, offset + size);
383
+ const element = decode(elementBuffer);
384
+ elements.push(element);
385
+ offset += size;
386
+ }
387
+
388
+ return {
389
+ oscType: 'bundle',
390
+ timetag,
391
+ elements
392
+ };
393
+ }
394
+
395
+ exports.decode = decode;
396
+ exports.encode = encode;