UDP/OSC Real-Time Observer Protocol¶
Overview¶
This document describes the UDP/OSC protocol used for real-time communication between the Ableton Live Remote Script and the AST Server. The protocol enables low-latency (<1ms) event streaming without blocking Ableton's main thread.
Architecture¶
(A) .als file watcher
┌────────────────────────────────┐
│ Python AST Server (Port 8765) │
│ - Maintains AST │
│ - Computes diffs │
│ - WebSocket broadcast to UI │
└──────────┬─────────────────────┘
▲
(D) UDP/OSC ⇡ │ ⇣ WebSocket (to Svelte)
Port 9002 │
▼
┌────────────────────────────────┐
│ UDP Listener Bridge │
│ - Receives OSC events │
│ - Deduplicates messages │
│ - Forwards to AST server │
└──────────┬─────────────────────┘
▲
UDP (fire & forget, < 1ms latency)
│
┌──────────┴─────────────────────┐
│ Ableton Remote Script │
│ - Live API observers │
│ - Emits OSC/UDP events │
│ - Debounces rapid changes │
└────────────────────────────────┘
Port Allocation¶
| Port | Service | Protocol | Purpose |
|---|---|---|---|
| 9001 | Remote Script TCP Server | TCP | Command interface (existing) |
| 9002 | UDP Listener | UDP | Real-time events (new) |
| 8765 | WebSocket Server | WebSocket | AST streaming to UI (existing) |
Why UDP/OSC?¶
Advantages¶
- ✅ Ultra-low latency - < 1ms, fire-and-forget
- ✅ Non-blocking - Remote Script never waits for acknowledgment
- ✅ Ableton-friendly - Same pattern as Max for Live, TouchOSC, OSCulator
- ✅ Lightweight - Just
socket.sendto()in Python - ✅ Easy to debug - Standard OSC tools work (oscdump, Wireshark)
- ✅ No dependencies - Uses Python's built-in
socketmodule
Disadvantages¶
- ⚠️ Unreliable - UDP packets can be lost or arrive out of order
- ⚠️ No acknowledgment - Sender doesn't know if receiver got the message
- ⚠️ Size limits - UDP packets limited to ~64KB (not an issue for our small messages)
Mitigation Strategy¶
- Sequence numbers - Detect missing messages and gaps
- Deduplication - Ignore duplicate packets (in case of network retransmit)
- XML diff fallback - If gaps detected, trigger full XML reload and diff
- Debouncing - Coalesce rapid changes to reduce packet count
OSC Message Format¶
Standard OSC Message Structure¶
┌──────────────────────────────────────────┐
│ OSC Address Pattern (null-terminated) │ e.g., "/live/track/renamed\0\0"
├──────────────────────────────────────────┤
│ OSC Type Tag String (null-terminated) │ e.g., ",is\0"
├──────────────────────────────────────────┤
│ Argument 1 (4-byte aligned) │ e.g., int32: 0
├──────────────────────────────────────────┤
│ Argument 2 (4-byte aligned) │ e.g., string: "Bass\0\0\0"
└──────────────────────────────────────────┘
OSC Type Tags¶
i- 32-bit integer (int32)f- 32-bit float (float32)s- String (null-terminated, padded to 4-byte boundary)T- Boolean TrueF- Boolean False
Sequence Number Wrapper¶
All events are wrapped with sequence metadata:
Example:
Message Catalog¶
Track Events¶
Track Renamed¶
Example:/live/track/renamed 0 "Bass"
When: User renames a track in Live
OSC Types: ,is (int, string)
Track Added¶
Example:/live/track/added 3 "Audio 4" "audio"
When: User creates a new track
OSC Types: ,iss (int, string, string)
Types: "audio", "midi", "return", "master"
Track Deleted¶
Example:/live/track/deleted 2
When: User deletes a track
OSC Types: ,i (int)
Track Mute¶
Example:/live/track/mute 0 T
When: User toggles track mute
OSC Types: ,iT or ,iF (int, bool)
Track Arm¶
Example:/live/track/arm 1 T
When: User arms/disarms a track for recording
OSC Types: ,iT or ,iF (int, bool)
Track Volume¶
Example:/live/track/volume 0 0.85
When: User changes track volume
OSC Types: ,if (int, float)
Range: 0.0 (silent) to 1.0 (0dB)
Debouncing: 50ms minimum interval
Device Events¶
Device Added¶
Example:/live/device/added 0 2 "Reverb"
When: User adds a device to a track
OSC Types: ,iis (int, int, string)
Device Deleted¶
Example:/live/device/deleted 0 1
When: User removes a device from a track
OSC Types: ,ii (int, int)
Device Parameter Changed¶
Example:/live/device/param 0 1 3 0.75
When: User tweaks a device parameter
OSC Types: ,iiif (int, int, int, float)
Debouncing: 50ms minimum interval per parameter
Clip Slot Events¶
Clip Slot Created¶
/live/clip_slot/created <track_idx:int> <scene_idx:int> <has_clip:bool> <has_stop:bool> <playing_status:int>
/live/clip_slot/created 0 1 T T 0
When: A clip slot is created, modified, or a clip is added/removed
OSC Types: ,iiTTi (int, int, bool, bool, int)
Playing Status: 0=Stopped, 1=Playing, 2=Triggered
Scene Events¶
Scene Renamed¶
Example:/live/scene/renamed 0 "Intro"
When: User renames a scene
OSC Types: ,is (int, string)
Scene Added¶
Example:/live/scene/added 2 "New Scene"
When: User inserts a new scene
OSC Types: ,is (int, string)
Scene Removed¶
Example:/live/scene/removed 2
When: User deletes a scene
OSC Types: ,i (int)
Scene Reordered¶
Example:/live/scene/reordered 1 "Verse"
When: User moves a scene
OSC Types: ,is (int, string)
Note: Currently ignored by server in favor of add/remove handling.
Scene Triggered¶
Example:/live/scene/triggered 2
When: User launches an entire scene
OSC Types: ,i (int)
Transport Events¶
Transport Play/Stop¶
Example:/live/transport/play T
When: User starts/stops playback
OSC Types: ,T or ,F (bool)
Transport Tempo¶
Example:/live/transport/tempo 128.0
When: User changes tempo
OSC Types: ,f (float)
Debouncing: 100ms minimum interval
Transport Position¶
Example:/live/transport/position 64.5
When: Playhead moves (HEAVILY DEBOUNCED)
OSC Types: ,f (float)
Debouncing: 500ms minimum interval (or disable entirely)
Batch Events¶
Used to group multiple related changes into a single logical update.
Batch Start¶
Example:/live/batch/start 1001
When: Beginning of a multi-event operation (e.g., loading a project)
OSC Types: ,i (int)
Batch End¶
Example:/live/batch/end 1001
When: End of a multi-event operation
OSC Types: ,i (int)
Use case: When a project loads, group all the "track added" events into a batch so the UI can update once instead of flickering.
Sequence Numbers¶
Format¶
Every message includes a sequence number in the wrapper:
Properties¶
- Monotonically increasing - Each message increments the sequence number
- Starts at 0 - First message after Remote Script startup
- Wraps at 2^31 - After ~2 billion messages (unlikely in practice)
- Reset on restart - Sequence resets when Ableton restarts
Deduplication Algorithm¶
The UDP Listener maintains a circular buffer of recently seen sequence numbers:
class SequenceTracker:
def __init__(self, buffer_size=100):
self.last_seq = -1
self.seen = set() # Recent sequence numbers
self.buffer_size = buffer_size
def is_duplicate(self, seq_num):
if seq_num in self.seen:
return True # Duplicate
# Add to seen set
self.seen.add(seq_num)
# Keep buffer size bounded
if len(self.seen) > self.buffer_size:
# Remove oldest entries (approximation)
self.seen = set(list(self.seen)[-self.buffer_size:])
return False
def detect_gap(self, seq_num):
if self.last_seq == -1:
self.last_seq = seq_num
return 0 # No gap on first message
expected = self.last_seq + 1
gap = seq_num - expected
self.last_seq = seq_num
if gap > 0:
return gap # Messages were lost
elif gap < -1:
return 0 # Out of order, but within tolerance
else:
return 0 # Normal sequential message
Gap Handling¶
When a gap is detected: 1. Log warning - Record the gap size and sequence numbers 2. Continue processing - Don't drop the message just because of a gap 3. Trigger fallback - If gap > 10 messages, trigger full XML reload 4. Update UI - Show yellow warning indicator in UI
Debouncing Strategy¶
Why Debounce?¶
Some Live events fire extremely rapidly (e.g., volume faders, playhead position). Without debouncing, we'd flood the network with thousands of messages per second.
Debounce Configuration¶
| Event Type | Min Interval | Rationale |
|---|---|---|
| Track volume | 50ms | Smooth enough for UI, reduces flood |
| Device parameters | 50ms | Smooth enough for UI, reduces flood |
| Transport position | 500ms | Rarely needed in real-time UI |
| Track rename | 0ms | Infrequent, send immediately |
| Device add/remove | 0ms | Infrequent, send immediately |
| Clip trigger/stop | 0ms | Time-critical, send immediately |
Debounce Implementation¶
class Debouncer:
def __init__(self):
self.last_send_time = {} # event_key -> timestamp
def should_send(self, event_key, min_interval_ms):
now = time.time()
last = self.last_send_time.get(event_key, 0)
if (now - last) * 1000 >= min_interval_ms:
self.last_send_time[event_key] = now
return True
else:
return False # Too soon, skip this event
Event key format: "{event_type}:{track_idx}:{param_id}"
Example: Volume change on track 0 → "track.volume:0"
Testing & Debugging¶
Monitor UDP Traffic with netcat¶
Monitor with OSC Tools¶
Send Test Messages¶
# Install python-osc
pip install python-osc
# Send test message
python tools/osc_send.py /live/track/renamed 0 "Test Track"
Use Wireshark¶
- Start Wireshark capture on loopback interface (
lo0) - Filter:
udp.port == 9002 - Right-click packet → Decode As → OSC
- View OSC message contents
Check Ableton Log¶
Fallback to XML Diff¶
When to Fallback¶
UDP is unreliable, so we need a fallback mechanism to ensure consistency:
- Gap detection - If sequence number gap > 10
- Hash mismatch - If AST hash doesn't match after applying UDP events
- Periodic validation - Every 60 seconds, compare UDP-derived AST with XML
- Manual trigger - User can force full refresh via UI button
Fallback Procedure¶
- Detect inconsistency - Gap, hash mismatch, or timeout
- Log event - Record the reason for fallback
- Request XML export - Send
EXPORT_XMLcommand via TCP (port 9001) - Parse XML - Load full project XML
- Compute diff - Compare UDP-derived AST with XML-derived AST
- Broadcast diff - Send corrected diff to WebSocket clients
- Update UI indicator - Show "Synced via XML" message
Monitoring Fallback Rate¶
Track fallback statistics: - Fallback count - How many times did we fall back? - Fallback reason - Gap, hash mismatch, or periodic? - Average gap size - How many messages were lost?
Target: < 1 fallback per hour under normal use
Performance Targets¶
| Metric | Target | Notes |
|---|---|---|
| End-to-end latency | < 100ms | From Live event to UI update |
| UDP send time | < 1ms | Fire-and-forget, non-blocking |
| Packet loss rate | < 0.1% | Local UDP is very reliable |
| CPU overhead (Remote Script) | < 1% | Minimal observer overhead |
| CPU overhead (UDP Listener) | < 2% | Parsing and forwarding |
| Events per second | 100-1000 | Typical for active editing |
| Max burst rate | 5000/sec | During project load (batched) |
Future Enhancements (Phase 9)¶
ZeroMQ Migration¶
For production/distributed setups, consider migrating to ZeroMQ:
Advantages: - ✅ Reliable delivery with automatic retries - ✅ Built-in reconnection logic - ✅ Message queuing during disconnect - ✅ Multiple subscribers (PUB/SUB pattern) - ✅ No need for sequence numbers or deduplication
Disadvantages:
- ❌ Requires pyzmq dependency in Remote Script
- ❌ Slightly higher latency (~5ms vs ~1ms)
- ❌ More complex to debug
Decision: Start with UDP/OSC for simplicity. Migrate to ZMQ if reliability becomes an issue.
References¶
Last Updated: 2025-11-28 Status: Implemented (Phase 6)