node-osc
Version:
pyOSC inspired library for sending and receiving OSC messages
606 lines (454 loc) • 15.1 kB
Markdown
# node-osc Guide
This guide provides best practices, patterns, and detailed information for using node-osc effectively.
## Table of Contents
- [Events](#events)
- [Error Handling](#error-handling)
- [Type System](#type-system)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
## Events
The `Server` class extends `EventEmitter` and emits several events for different scenarios.
### Server Events
#### `listening`
Emitted when the server starts listening for messages.
```javascript
server.on('listening', () => {
console.log('Server is ready to receive messages');
});
```
#### `message`
Emitted when an OSC message is received.
**Parameters:**
- `msg` (Array): The message as an array where the first element is the address and subsequent elements are arguments
- `rinfo` (Object): Remote address information
- `address` (string): The sender's IP address
- `port` (number): The sender's port number
```javascript
server.on('message', (msg, rinfo) => {
const [address, ...args] = msg;
console.log(`Received ${address} from ${rinfo.address}:${rinfo.port}`);
console.log('Arguments:', args);
});
```
#### `bundle`
Emitted when an OSC bundle is received.
**Parameters:**
- `bundle` (Object): The bundle object
- `timetag` (number): The bundle's timetag
- `elements` (Array): Array of messages or nested bundles
- `rinfo` (Object): Remote address information
```javascript
server.on('bundle', (bundle, rinfo) => {
console.log(`Received bundle with timetag ${bundle.timetag}`);
bundle.elements.forEach((element) => {
console.log('Element:', element);
});
});
```
#### Address-Specific Events
The server also emits events for each message address received. This allows you to listen for specific OSC addresses without filtering in your code.
```javascript
// Listen specifically for messages to /note
server.on('/note', (msg, rinfo) => {
const [address, pitch, velocity] = msg;
console.log(`Note: ${pitch}, Velocity: ${velocity}`);
});
// Listen for /oscillator/frequency
server.on('/oscillator/frequency', (msg) => {
const [address, freq] = msg;
console.log(`Frequency set to ${freq} Hz`);
});
```
#### `error`
Emitted when there's an error decoding an incoming message or a socket error.
**Parameters:**
- `error` (Error): The error object
- `rinfo` (Object): Remote address information (for decode errors)
```javascript
server.on('error', (error, rinfo) => {
if (rinfo) {
console.error(`Error from ${rinfo.address}:${rinfo.port}: ${error.message}`);
} else {
console.error('Socket error:', error.message);
}
});
```
### Client Events
#### `error`
Emitted when a socket error occurs.
```javascript
client.on('error', (error) => {
console.error('Client error:', error.message);
});
```
## Error Handling
Proper error handling is essential for robust OSC applications.
### Client Errors
#### Sending on Closed Socket
If you try to send a message after closing the client, a `ReferenceError` will be thrown:
```javascript
const client = new Client('127.0.0.1', 3333);
await client.close();
try {
await client.send('/test', 123);
} catch (err) {
console.error(err.message); // "Cannot send message on closed socket."
console.error(err.code); // "ERR_SOCKET_DGRAM_NOT_RUNNING"
}
```
**Prevention:** Always ensure the client is open before sending:
```javascript
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 123);
} finally {
await client.close(); // Close after sending
}
```
#### Invalid Message Format
Passing an invalid message format will throw a `TypeError`:
```javascript
try {
await client.send(12345); // Not a valid message format
} catch (err) {
console.error(err.message); // "That Message Just Doesn't Seem Right"
}
```
### Server Errors
#### Decoding Errors
When the server receives malformed OSC data, it emits an `'error'` event rather than throwing:
```javascript
server.on('error', (err, rinfo) => {
console.error(`Decode error from ${rinfo.address}: ${err.message}`);
});
```
### Error Handling Patterns
#### With Callbacks
```javascript
client.send('/test', 123, (err) => {
if (err) {
console.error('Send failed:', err);
return;
}
console.log('Message sent successfully');
});
```
#### With Async/Await
```javascript
try {
await client.send('/test', 123);
console.log('Message sent successfully');
} catch (err) {
console.error('Send failed:', err);
}
```
#### Always Close Resources
Use try/finally to ensure resources are cleaned up even if errors occur:
```javascript
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 123);
await client.send('/test', 456);
} catch (err) {
console.error('Error sending:', err);
} finally {
await client.close(); // Always executes
}
```
## Type System
OSC supports several data types. node-osc automatically detects types for common JavaScript values:
| JavaScript Type | OSC Type | Description |
|----------------|----------|-------------|
| Integer number | `integer` | Whole numbers (e.g., 42, -10, 0) |
| Float number | `float` | Decimal numbers (e.g., 3.14, -0.5) |
| String | `string` | Text values (e.g., "hello") |
| Boolean | `boolean` | true or false |
| Buffer | `blob` | Binary data |
| MIDI object/Buffer | `midi` | MIDI messages (4 bytes: port, status, data1, data2) |
### Automatic Type Detection
```javascript
const msg = new Message('/test');
msg.append(42); // → integer
msg.append(3.14); // → float
msg.append('hello'); // → string
msg.append(true); // → boolean
```
### Explicit Type Control
For advanced use cases, you can explicitly specify types:
```javascript
const msg = new Message('/test');
// Force a whole number to be sent as float
msg.append({ type: 'float', value: 42 });
// Use shorthand type notation
msg.append({ type: 'f', value: 42 }); // 'f' = float
msg.append({ type: 'i', value: 3.14 }); // 'i' = integer (truncates)
msg.append({ type: 's', value: 'text' }); // 's' = string
msg.append({ type: 'b', value: Buffer.from('data') }); // 'b' = blob
msg.append({ type: 'm', value: { port: 0, status: 144, data1: 60, data2: 127 } }); // 'm' = MIDI
```
### Supported Type Tags
- `'i'` or `'integer'` - 32-bit integer
- `'f'` or `'float'` - 32-bit float
- `'s'` or `'string'` - OSC string
- `'b'` or `'blob'` - Binary blob
- `'m'` or `'midi'` - MIDI message (4 bytes)
- `'boolean'` - Boolean value (true/false)
- `'T'` - True
- `'F'` - False
## Best Practices
### 1. Use Async/Await for Cleaner Code
Prefer async/await over callbacks for more readable code:
```javascript
// ✅ Good - Clean and readable
async function sendMessages() {
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 1);
await client.send('/test', 2);
await client.send('/test', 3);
} finally {
await client.close();
}
}
// ❌ Less ideal - Callback pyramid
function sendMessages() {
const client = new Client('127.0.0.1', 3333);
client.send('/test', 1, (err) => {
if (err) return console.error(err);
client.send('/test', 2, (err) => {
if (err) return console.error(err);
client.send('/test', 3, (err) => {
if (err) return console.error(err);
client.close();
});
});
});
}
```
### 2. Always Close Resources
Always close clients and servers when done to prevent resource leaks:
```javascript
const client = new Client('127.0.0.1', 3333);
try {
await client.send('/test', 123);
} finally {
await client.close(); // Always close
}
```
### 3. Use Address-Specific Event Listeners
For better code organization, use address-specific event listeners:
```javascript
// ✅ Good - Clear and organized
server.on('/note', (msg) => {
handleNote(msg);
});
server.on('/control', (msg) => {
handleControl(msg);
});
// ❌ Less ideal - Manual routing
server.on('message', (msg) => {
const [address] = msg;
if (address === '/note') handleNote(msg);
else if (address === '/control') handleControl(msg);
});
```
### 4. Handle Errors Gracefully
Always implement error handling for both clients and servers:
```javascript
// Client
try {
await client.send('/test', 123);
} catch (err) {
console.error('Failed to send:', err.message);
}
// Server
server.on('error', (err, rinfo) => {
console.error(`Server error from ${rinfo?.address}:`, err.message);
});
```
### 5. Use Bundles for Related Messages
When sending multiple related messages, use bundles for atomic operations:
```javascript
// ✅ Good - Atomic operation
const bundle = new Bundle(
['/synth/freq', 440],
['/synth/amp', 0.5],
['/synth/gate', 1]
);
await client.send(bundle);
// ❌ Less ideal - Separate messages (not atomic)
await client.send('/synth/freq', 440);
await client.send('/synth/amp', 0.5);
await client.send('/synth/gate', 1);
```
### 6. Listen on All Interfaces for Network Access
If you need to receive messages from other machines:
```javascript
// Listen on all interfaces (accessible from network)
const server = new Server(3333, '0.0.0.0');
// Only localhost (default, more secure)
const server = new Server(3333, '127.0.0.1');
```
### 7. Use Descriptive OSC Addresses
Follow OSC naming conventions with hierarchical addresses:
```javascript
// ✅ Good - Hierarchical and descriptive
await client.send('/synth/oscillator/1/frequency', 440);
await client.send('/mixer/channel/3/volume', 0.8);
// ❌ Less ideal - Flat and unclear
await client.send('/freq1', 440);
await client.send('/vol3', 0.8);
```
### 8. Validate Input Data
Validate data before sending to avoid runtime errors:
```javascript
function sendNote(pitch, velocity) {
if (typeof pitch !== 'number' || pitch < 0 || pitch > 127) {
throw new Error('Invalid pitch: must be 0-127');
}
if (typeof velocity !== 'number' || velocity < 0 || velocity > 127) {
throw new Error('Invalid velocity: must be 0-127');
}
return client.send('/note', pitch, velocity);
}
```
### 9. Wait for Server Ready
Always wait for the server to be listening before sending messages:
```javascript
import { once } from 'node:events';
const server = new Server(3333, '0.0.0.0');
// Wait for server to be ready
await once(server, 'listening');
// Now safe to send messages
console.log('Server ready!');
```
### 10. Use Parallel Sends When Appropriate
When sending multiple independent messages, use `Promise.all` for better performance:
```javascript
// Send multiple messages in parallel
await Promise.all([
client.send('/track/1/volume', 0.8),
client.send('/track/2/volume', 0.6),
client.send('/track/3/volume', 0.9)
]);
```
## Troubleshooting
### Messages Not Being Received
**Possible causes and solutions:**
1. **Firewall blocking UDP traffic**
- Check your firewall settings
- Ensure the UDP port is open
- Try with localhost first (`127.0.0.1`)
2. **Wrong host binding**
- Server: Use `'0.0.0.0'` to listen on all interfaces
- Server: Use `'127.0.0.1'` for localhost only
- Client: Match the server's IP address
3. **Port mismatch**
- Ensure client and server use the same port number
- Check if another process is using the port
4. **Network connectivity**
- Test with localhost first (`127.0.0.1`)
- Verify network connectivity between machines
- Check if devices are on the same network
### "Cannot send message on closed socket"
This error occurs when trying to send after closing the client:
```javascript
// ❌ Wrong - Sending after close
await client.close();
await client.send('/test', 123); // Error!
// ✅ Correct - Send before close
await client.send('/test', 123);
await client.close();
```
### Server Not Listening
Ensure you wait for the server to start before sending messages:
```javascript
const server = new Server(3333, '0.0.0.0');
// Wait for server to be ready
await new Promise(resolve => server.on('listening', resolve));
// Now safe to send messages
console.log('Server ready!');
```
### Messages Lost or Dropped
UDP is unreliable by design and messages can be lost:
**Solutions:**
1. Use TCP-based OSC if reliability is critical (requires custom implementation)
2. Implement acknowledgment messages
3. Add retry logic for critical messages
4. Use bundles to ensure related messages arrive together
### High CPU Usage
If you're seeing high CPU usage:
1. **Check for infinite loops** in event handlers
2. **Rate limit** message sending if sending many messages
3. **Use bundles** instead of many individual messages
4. **Close unused connections** to free resources
### Memory Leaks
To prevent memory leaks:
1. **Always close** clients and servers when done
2. **Remove event listeners** when no longer needed
3. **Avoid** creating new clients/servers in loops
4. **Reuse** client/server instances when possible
```javascript
// ✅ Good - Proper cleanup
const server = new Server(3333);
const handler = (msg) => console.log(msg);
server.on('message', handler);
// Later, clean up
server.removeListener('message', handler);
await server.close();
```
## Advanced Topics
### Custom Transports
The `encode` and `decode` functions allow you to use OSC over custom transports:
```javascript
import { encode, decode, Message } from 'node-osc';
import WebSocket from 'ws';
// WebSocket server
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const oscMsg = decode(data);
console.log('Received:', oscMsg);
});
});
// WebSocket client
const ws = new WebSocket('ws://localhost:8080');
const message = new Message('/test', 123);
ws.send(encode(message));
```
### Timetags in Bundles
OSC bundles support timetags for scheduling:
```javascript
// Immediate execution (timetag = 0)
const bundle = new Bundle(['/test', 1], ['/test', 2]);
// Scheduled execution (timetag in OSC time)
const futureTime = Date.now() / 1000 + 5; // 5 seconds from now
const scheduledBundle = new Bundle(futureTime, ['/test', 1]);
```
**Note:** The server receives the timetag but does not automatically schedule execution. You must implement scheduling logic if needed.
### Performance Optimization
For high-throughput applications:
1. **Reuse client instances** instead of creating new ones
2. **Use bundles** to send multiple messages together
3. **Batch messages** and send periodically rather than immediately
4. **Use binary blobs** for large data instead of many arguments
5. **Profile your code** to identify bottlenecks
```javascript
// ✅ Good - Reuse client
const client = new Client('127.0.0.1', 3333);
for (let i = 0; i < 1000; i++) {
await client.send('/test', i);
}
await client.close();
// ❌ Bad - Creating many clients
for (let i = 0; i < 1000; i++) {
const client = new Client('127.0.0.1', 3333);
await client.send('/test', i);
await client.close();
}
```
## Further Reading
- [API Documentation](./API.md) - Complete API reference
- [OSC Specification](http://opensoundcontrol.org/spec-1_0) - Official OSC 1.0 specification
- [Examples](../examples/) - Working code examples
- [Main README](../README.md) - Quick start guide