15 KiB
node-osc Guide
This guide provides best practices, patterns, and detailed information for using node-osc effectively.
Table of Contents
Events
The Server class extends EventEmitter and emits several events for different scenarios.
Server Events
listening
Emitted when the server starts listening for messages.
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 argumentsrinfo(Object): Remote address informationaddress(string): The sender's IP addressport(number): The sender's port number
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 objecttimetag(number): The bundle's timetagelements(Array): Array of messages or nested bundles
rinfo(Object): Remote address information
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.
// 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 objectrinfo(Object): Remote address information (for decode errors)
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.
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:
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:
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:
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:
server.on('error', (err, rinfo) => {
console.error(`Decode error from ${rinfo.address}: ${err.message}`);
});
Error Handling Patterns
With Callbacks
client.send('/test', 123, (err) => {
if (err) {
console.error('Send failed:', err);
return;
}
console.log('Message sent successfully');
});
With Async/Await
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:
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
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:
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:
// ✅ 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:
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:
// ✅ 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:
// 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:
// ✅ 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:
// 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:
// ✅ 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:
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:
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:
// 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:
-
Firewall blocking UDP traffic
- Check your firewall settings
- Ensure the UDP port is open
- Try with localhost first (
127.0.0.1)
-
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
- Server: Use
-
Port mismatch
- Ensure client and server use the same port number
- Check if another process is using the port
-
Network connectivity
- Test with localhost first (
127.0.0.1) - Verify network connectivity between machines
- Check if devices are on the same network
- Test with localhost first (
"Cannot send message on closed socket"
This error occurs when trying to send after closing the client:
// ❌ 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:
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:
- Use TCP-based OSC if reliability is critical (requires custom implementation)
- Implement acknowledgment messages
- Add retry logic for critical messages
- Use bundles to ensure related messages arrive together
High CPU Usage
If you're seeing high CPU usage:
- Check for infinite loops in event handlers
- Rate limit message sending if sending many messages
- Use bundles instead of many individual messages
- Close unused connections to free resources
Memory Leaks
To prevent memory leaks:
- Always close clients and servers when done
- Remove event listeners when no longer needed
- Avoid creating new clients/servers in loops
- Reuse client/server instances when possible
// ✅ 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:
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:
// 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:
- Reuse client instances instead of creating new ones
- Use bundles to send multiple messages together
- Batch messages and send periodically rather than immediately
- Use binary blobs for large data instead of many arguments
- Profile your code to identify bottlenecks
// ✅ 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 - Complete API reference
- OSC Specification - Official OSC 1.0 specification
- Examples - Working code examples
- Main README - Quick start guide