···34343535 // Identity
3636 char handle[64];
3737+ char machine_id[64];
37383839 pthread_mutex_t mu;
3940} ACUdp;
···46474748// Queue a fairy point to send (non-blocking, main thread)
4849void udp_send_fairy(ACUdp *udp, float x, float y);
5050+5151+// Update identifying metadata used by outgoing packets.
5252+void udp_set_identity(ACUdp *udp, const char *handle, const char *machine_id);
5353+5454+// Send note relay events to the session server over UDP.
5555+void udp_send_midi(ACUdp *udp, const char *event, int note, int velocity, int channel, const char *piece);
5656+void udp_send_midi_heartbeat(ACUdp *udp, const char *piece);
49575058// Poll received fairies (returns count, fills out[] up to max)
5159// Clears the buffer after reading.
+512
plans/notepat-udp-midi-relay.md
···11+# Notepat UDP MIDI Relay Plan
22+33+## Overview
44+55+Let running `ac-native` `notepat` sessions automatically publish note events to the Aesthetic Computer network, so a Max for Live device or `notepat.com`-based device can subscribe to those events and play them back live.
66+77+The important twist is identity:
88+99+- every note stream should be attributable to a `@handle`
1010+- we should also preserve `machineId` so multiple devices owned by the same person can be distinguished
1111+- subscribers should be able to filter by `@handle`, by `machineId`, or by "everyone"
1212+1313+This would make an `ac-native` machine feel like a network instrument source, while Ableton / Max acts like a remote listener, mirror, recorder, or re-player.
1414+1515+## Why This Shape
1616+1717+We already have the right footholds:
1818+1919+- `fedac/native/src/udp-client.c` already sends compact UDP packets from `ac-native`
2020+- `fedac/native/src/js-bindings.c` already injects the current `handle` into the UDP client
2121+- `system.machineId` already exists in native
2222+- `notepat.mjs` already has a clean note-on / note-off seam for outgoing MIDI events
2323+- `ac-m4l/AC Notepat.amxd` and related M4L assets already exist
2424+2525+The part that does not exist yet is a note-event relay path and a subscription model on the receiver side.
2626+2727+## Recommended Architecture
2828+2929+Use a hybrid model:
3030+3131+1. `ac-native` sends note events to a UDP relay endpoint for low-latency ingest.
3232+2. The relay normalizes those events and republishes them to WebSocket subscribers.
3333+3. Max for Live / `notepat.com` devices subscribe over WebSocket by `@handle` or `machineId`.
3434+3535+This is better than raw UDP end-to-end because:
3636+3737+- browser-based receivers do not want to deal with raw UDP directly
3838+- WebSocket subscription/filtering is much easier to debug in M4L and web UIs
3939+- the relay can enrich events with metadata, timestamps, and presence
4040+- we can keep the native sender lightweight
4141+4242+## Architecture
4343+4444+```text
4545+┌──────────────────────┐
4646+│ ac-native / notepat │
4747+│ on Yoga / AC machine │
4848+└──────────┬───────────┘
4949+ │
5050+ │ UDP note events
5151+ │ handle + machineId + midi
5252+ ▼
5353+┌──────────────────────────────┐
5454+│ session-server MIDI relay │
5555+│ - UDP ingest │
5656+│ - presence / last-seen map │
5757+│ - WS fanout │
5858+│ - handle + machine filters │
5959+└──────────┬───────────────────┘
6060+ │
6161+ │ WebSocket subscribe:
6262+ │ notepat:midi:subscribe
6363+ ▼
6464+┌──────────────────────────────┐
6565+│ Max for Live / AC Notepat │
6666+│ or notepat.com device │
6767+│ - choose source handle │
6868+│ - optional machine filter │
6969+│ - playback / monitor UI │
7070+└──────────────────────────────┘
7171+```
7272+7373+## Current Repo Footholds
7474+7575+### Native sender
7676+7777+- `fedac/native/pieces/notepat.mjs`
7878+ - already computes note-on / note-off centrally
7979+ - already knows `system.usbMidi`, `system.udp`, and `system.machineId`
8080+- `fedac/native/src/js-bindings.c`
8181+ - already exposes `system.udp`
8282+ - already exposes `system.machineId`
8383+ - already sets UDP identity from the config `handle`
8484+- `fedac/native/src/udp-client.c`
8585+ - currently supports fairy packets only
8686+ - already has background send / recv thread
8787+8888+### Receiver side
8989+9090+- `ac-m4l/AC Notepat.amxd`
9191+- `ac-m4l/AC-Notepat.maxpat`
9292+- `ac-m4l/ac-ws-server.js`
9393+- `plans/kidlisp-m4l-integration.md`
9494+9595+These give us a plausible place for a handle-selectable network note receiver.
9696+9797+## Event Model
9898+9999+### Minimum event payload
100100+101101+Each note event should carry:
102102+103103+- `type`: `note_on` or `note_off`
104104+- `note`: MIDI note number
105105+- `velocity`: 0-127
106106+- `channel`: MIDI channel
107107+- `handle`: performer handle without `@`, if known
108108+- `machineId`: native machine identity
109109+- `piece`: usually `notepat`
110110+- `ts`: sender timestamp in ms
111111+112112+### Suggested normalized relay JSON
113113+114114+```json
115115+{
116116+ "type": "notepat:midi",
117117+ "event": "note_on",
118118+ "note": 60,
119119+ "velocity": 109,
120120+ "channel": 0,
121121+ "handle": "jeffrey",
122122+ "machineId": "ac-1234abcd",
123123+ "piece": "notepat",
124124+ "ts": 1710000000000
125125+}
126126+```
127127+128128+### Optional later fields
129129+130130+- `source`: `ac-native`
131131+- `wave`: current notepat wave mode
132132+- `usbMidi`: whether it was also forwarded to a local USB MIDI host
133133+- `session`: optional room / channel
134134+- `pressure`: for aftertouch-like experiments
135135+136136+## UDP Protocol Plan
137137+138138+### Phase 1
139139+140140+Add new packet types to `fedac/native/src/udp-client.c`:
141141+142142+- `0x10` = note on
143143+- `0x11` = note off
144144+- `0x12` = heartbeat / source status
145145+146146+Suggested binary shape:
147147+148148+```text
149149+[1 type]
150150+[1 note]
151151+[1 velocity]
152152+[1 channel]
153153+[1 handle_len][N handle_bytes]
154154+[1 machine_len][N machine_bytes]
155155+[4 timestamp_ms_u32]
156156+```
157157+158158+This keeps the packet compact while still carrying identity.
159159+160160+### Alternative
161161+162162+If we want to move faster and accept a slightly fatter payload, we could send short JSON strings over UDP first, then tighten to binary later. That would reduce implementation friction in the relay.
163163+164164+Recommendation:
165165+166166+- binary if we want to keep it native-first and stable long-term
167167+- JSON if we want the fastest prototype
168168+169169+My recommendation is:
170170+171171+- prototype with JSON-over-UDP
172172+- switch to binary once the event schema settles
173173+174174+## Relay / Server Plan
175175+176176+The relay should do four jobs:
177177+178178+1. accept note events from native senders
179179+2. maintain a live source index
180180+3. expose source discovery to listeners
181181+4. fan events out to subscribers
182182+183183+### Source index
184184+185185+Maintain a map keyed by `handle + machineId`:
186186+187187+```js
188188+{
189189+ "jeffrey:ac-1234abcd": {
190190+ handle: "jeffrey",
191191+ machineId: "ac-1234abcd",
192192+ lastSeen: 1710000000000,
193193+ lastEvent: "note_on",
194194+ piece: "notepat"
195195+ }
196196+}
197197+```
198198+199199+### WebSocket API
200200+201201+Add a small family of messages:
202202+203203+```json
204204+{ "type": "notepat:midi:sources" }
205205+```
206206+207207+Response:
208208+209209+```json
210210+{
211211+ "type": "notepat:midi:sources",
212212+ "sources": [
213213+ { "handle": "jeffrey", "machineId": "ac-1234abcd", "piece": "notepat", "lastSeen": 1710000000000 }
214214+ ]
215215+}
216216+```
217217+218218+Subscribe:
219219+220220+```json
221221+{ "type": "notepat:midi:subscribe", "handle": "jeffrey" }
222222+```
223223+224224+Or:
225225+226226+```json
227227+{ "type": "notepat:midi:subscribe", "handle": "jeffrey", "machineId": "ac-1234abcd" }
228228+```
229229+230230+Or:
231231+232232+```json
233233+{ "type": "notepat:midi:subscribe", "all": true }
234234+```
235235+236236+Broadcast event:
237237+238238+```json
239239+{
240240+ "type": "notepat:midi",
241241+ "event": "note_on",
242242+ "handle": "jeffrey",
243243+ "machineId": "ac-1234abcd",
244244+ "note": 60,
245245+ "velocity": 96,
246246+ "channel": 0,
247247+ "ts": 1710000000000
248248+}
249249+```
250250+251251+## Native Sender Changes
252252+253253+### `notepat.mjs`
254254+255255+Extend the current note send helpers so note events can also go to the network:
256256+257257+- note on -> UDP relay send
258258+- note off -> UDP relay send
259259+- all notes off -> optional cleanup event
260260+261261+Suggested behavior:
262262+263263+- default off until user opts in
264264+- per-boot toggle in prompt or `notepat`
265265+- persist in `/mnt/config.json`
266266+267267+Suggested config key:
268268+269269+```json
270270+{ "udpMidiBroadcast": true }
271271+```
272272+273273+### `js-bindings.c`
274274+275275+Expose one small higher-level helper on `system.udp`, for example:
276276+277277+- `system.udp.sendMidi(event, note, velocity, channel, machineId, piece)`
278278+279279+That keeps the piece code clean and avoids reimplementing packet shape in JS.
280280+281281+### `udp-client.c`
282282+283283+Add:
284284+285285+- note packet encoding
286286+- light send queue / coalescing
287287+- heartbeat support
288288+289289+The heartbeat should announce:
290290+291291+- `handle`
292292+- `machineId`
293293+- `piece=notepat`
294294+- `broadcast=true`
295295+296296+every few seconds while the piece is active.
297297+298298+## Max for Live / notepat.com Receiver Plan
299299+300300+### UX
301301+302302+The receiver device should have:
303303+304304+- source mode: `all`, `handle`, or `handle + machine`
305305+- handle selector
306306+- machine selector
307307+- monitor list showing live note events
308308+- armed / muted toggle
309309+- optional MIDI passthrough to Live track
310310+311311+### Playback behavior
312312+313313+For the first version:
314314+315315+- incoming `note_on` -> emit MIDI note on in Max
316316+- incoming `note_off` -> emit MIDI note off in Max
317317+318318+Later:
319319+320320+- optional quantization
321321+- optional octave remap
322322+- optional channel remap
323323+- optional monitor-only mode
324324+325325+### Good first target
326326+327327+Build this into `AC Notepat.amxd` or a sibling device such as:
328328+329329+- `AC Notepat Relay.amxd`
330330+331331+That keeps the "instrument" device separate from the "remote listener" device until the UI settles.
332332+333333+## Filtering By Handle
334334+335335+This is the core product behavior.
336336+337337+### Rules
338338+339339+- if `handle` is selected, accept events from all machines owned by that handle
340340+- if `handle + machineId` is selected, accept only that machine
341341+- if `all` is selected, accept all public broadcasts
342342+343343+### Why both handle and machineId matter
344344+345345+`@handle` alone is not enough if:
346346+347347+- someone has two AC machines running at once
348348+- one machine is a practice keyboard and one is a stage performer
349349+- we want to mirror a specific machine into a specific Ableton set
350350+351351+So `handle` should be the human-facing grouping key, while `machineId` is the precise routing key.
352352+353353+## Presence and Discovery
354354+355355+The receiver should be able to see active sources without typing blindly.
356356+357357+Recommended behavior:
358358+359359+- native sender heartbeats every 5-10 seconds
360360+- relay expires sources after 15-20 seconds of silence
361361+- M4L receiver requests source list on load and periodically refreshes
362362+363363+Displayed label:
364364+365365+```text
366366+@jeffrey ac-1234abcd notepat seen 2s ago
367367+```
368368+369369+## Logging and Observability
370370+371371+We should keep a paper trail because this will be hard to debug live.
372372+373373+### Native
374374+375375+Log to `/mnt/ac-native.log`:
376376+377377+- UDP relay connected
378378+- UDP note-on / note-off send errors
379379+- broadcast enabled / disabled
380380+381381+### Relay
382382+383383+Log:
384384+385385+- source registration
386386+- malformed packets
387387+- subscriber counts
388388+- fanout counts by handle
389389+390390+### M4L / receiver
391391+392392+Expose a small event monitor:
393393+394394+- last event time
395395+- last source handle
396396+- last note
397397+- dropped note count if any
398398+399399+## Privacy / Safety
400400+401401+Because note streams can become ambient network presence, this should be opt-in.
402402+403403+### Recommendation
404404+405405+- sender broadcasting is off by default
406406+- enabling it is explicit in prompt / config
407407+- receiver subscribes explicitly; nothing auto-plays by surprise
408408+409409+Optional later:
410410+411411+- `public` vs `friends` vs `private`
412412+- signed session tokens
413413+- authenticated per-handle subscriptions
414414+415415+## Phased Implementation
416416+417417+### Phase 1: Native -> relay prototype
418418+419419+- [ ] Add `udpMidiBroadcast` config flag
420420+- [ ] Add note send hook in `fedac/native/pieces/notepat.mjs`
421421+- [ ] Add UDP note packet support in `fedac/native/src/udp-client.c`
422422+- [ ] Add relay listener endpoint
423423+- [ ] Log received events server-side
424424+425425+Success condition:
426426+427427+- notes from one `ac-native` machine appear in relay logs with `handle` and `machineId`
428428+429429+### Phase 2: Relay -> WebSocket subscribers
430430+431431+- [ ] Add `notepat:midi:sources`
432432+- [ ] Add `notepat:midi:subscribe`
433433+- [ ] Add source expiry / heartbeat tracking
434434+- [ ] Broadcast normalized note events over WebSocket
435435+436436+Success condition:
437437+438438+- a browser console can subscribe to `@handle` and print incoming notes
439439+440440+### Phase 3: Max for Live receiver
441441+442442+- [ ] Create or fork `AC Notepat` receiver device
443443+- [ ] Subscribe by `handle`
444444+- [ ] Emit Live MIDI notes
445445+- [ ] Show source / last-note monitor
446446+447447+Success condition:
448448+449449+- notes played on `ac-native` trigger a Live instrument from the selected handle
450450+451451+### Phase 4: Refinement
452452+453453+- [ ] machine-level filtering
454454+- [ ] monitor-only mode
455455+- [ ] quantize / transform options
456456+- [ ] optional recording into Live clips
457457+- [ ] note-pressure / CC extension
458458+459459+## Open Questions
460460+461461+### 1. Should the relay live inside the existing session server?
462462+463463+Recommendation:
464464+465465+- yes for the first version
466466+467467+Reason:
468468+469469+- it already handles realtime identity-oriented traffic
470470+- it already has WebSocket clients
471471+- it is the easiest place to hang `handle`-filtered subscriptions
472472+473473+### 2. Should the receiver use raw UDP too?
474474+475475+Recommendation:
476476+477477+- no for the first version
478478+479479+Reason:
480480+481481+- browser / M4L environments are easier over WebSocket
482482+- UDP ingest + WS fanout already gets most of the benefit
483483+484484+### 3. Should we mirror every note or only notes that go to USB MIDI?
485485+486486+Recommendation:
487487+488488+- every note played in `notepat`, independent of USB MIDI state
489489+490490+Reason:
491491+492492+- network relay and local USB MIDI solve different problems
493493+- it is more musically useful if network relay does not depend on a cable
494494+495495+### 4. Should this be public performance presence?
496496+497497+Recommendation:
498498+499499+- start opt-in and semi-private
500500+- add public browsing later if it feels magical
501501+502502+## First Concrete Build Slice
503503+504504+If we want the smallest useful first slice:
505505+506506+1. add `udpMidiBroadcast` toggle in native prompt
507507+2. send JSON note events from `notepat` over UDP
508508+3. add relay logging + source list in session server
509509+4. build a tiny browser subscriber page that filters by `@handle`
510510+5. only then move into Max for Live playback
511511+512512+That gets us from idea to audible proof fast, without overcommitting to a packet format or Max UI too early.