Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

notepat-remote: host amxd on lith + local keyboard input

- Self-host AC 🎹 notepat-remote.amxd at /assets/m4l/notepat-remote.amxd
so the notepat piece can link to it directly (no DO Spaces dependency).
- notepat m4l button: jump("ableton") → direct .amxd download via "out:" jump
- notepat-remote piece: add local computer-keyboard input with the same
key→pitch layout as notepat (z-x-c-v-d-s-e-f-w-g-r-a-q-b + h-t-i-y-j-k-u-l-o-m-p-n).
Keyboard notes emit through the same max.outlet bridge as relay notes,
so the device is playable as a standalone instrument in addition to
relaying ac-native notepat from the ThinkPad.
- UI: separate relay / local counters, held-key strip, source tag on last note.
- .gitignore: allow system/public/assets/m4l/*.amxd past the *.amxd block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+120 -44
+5
.gitignore
··· 71 71 !system/public/assets/papers/readings/text/ 72 72 system/public/assets/papers/readings/text/* 73 73 !system/public/assets/papers/readings/text/Gallope-Harren-Hicks-The-Scores-Project-2025.txt 74 + # M4L .amxd binaries hosted on lith directly (small, versioned, no CDN) 75 + !system/public/assets/m4l/ 76 + system/public/assets/m4l/* 77 + !system/public/assets/m4l/*.amxd 74 78 75 79 # AestheticAnts runtime (logs, runs, test output - not part of the score) 76 80 ants/*.log ··· 336 340 system/.env 337 341 ac-vst/vst3sdk/ 338 342 *.amxd 343 + !system/public/assets/m4l/*.amxd 339 344 340 345 # Emacs performance logs (keep directory, ignore log files) 341 346 .emacs-logs/*.log
+114 -43
system/public/aesthetic.computer/disks/notepat-remote.mjs
··· 1 1 // notepat-remote, 26.4.20 2 - // AC 🎹 Notepat Remote — receives notepat:midi events from session-server 3 - // and bridges them to Max for Live via window.max.outlet. 4 - // 5 - // Meant to load inside jweb~ in AC-NotepatRemote.amxd. When opened standalone 6 - // in a browser it still renders the connection status UI (MIDI out is a no-op). 2 + // AC 🎹 Notepat Remote — dual-mode Max for Live device: 3 + // 1) Subscribes to session-server's notepat:midi fanout, so ac-native 4 + // notepat.mjs on a ThinkPad plays this track. 5 + // 2) Accepts local computer-keyboard input (same key→note layout as the 6 + // standard notepat piece) so the device plays as a standalone 7 + // instrument too. 7 8 // 8 - // Wire path: 9 - // ThinkPad ac-native notepat.mjs 10 - // → UDP :10010 → session-server.aesthetic.computer 11 - // → WS fanout → this piece 12 - // → window.max.outlet(["note", pitch, vel]) → Max [route note] → [noteout] 9 + // Both paths emit MIDI to Max via window.max.outlet, which the surrounding 10 + // patcher routes through [route note channel] → [noteout]. 13 11 14 12 const { floor, min, max } = Math; 15 13 16 14 const WS_URL = "wss://session-server.aesthetic.computer/"; 17 15 const RECONNECT_FRAMES = 120; // ~2s @ 60fps 18 16 17 + // ─── Keyboard → MIDI pitch (mirrors notepat.mjs NOTE_TO_KEYBOARD_KEY) ─── 18 + // Row 1 (below default octave): z=A#3, x=B3 19 + // Row 2 (C4..B4): c v d s e f w g r a q b 20 + // Row 3 (C5..B5): h t i y j k u l o m p n 21 + // Row 4 (C6+): ; ' ] 22 + const KEY_TO_PITCH = { 23 + z: 58, x: 59, 24 + c: 60, v: 61, d: 62, s: 63, e: 64, f: 65, w: 66, 25 + g: 67, r: 68, a: 69, q: 70, b: 71, 26 + h: 72, t: 73, i: 74, y: 75, j: 76, k: 77, u: 78, 27 + l: 79, o: 80, m: 81, p: 82, n: 83, 28 + ";": 84, "'": 85, "]": 86, 29 + }; 30 + 19 31 let ws = null; 20 32 let wsState = "idle"; // idle | connecting | open | closed | error 21 33 let wsError = ""; 22 34 let reconnectAt = 0; 23 35 24 36 let sources = []; // [{ handle, machineId, piece, lastSeen }] 25 - let pktCount = 0; 37 + let pktCount = 0; // relay packets received 26 38 let noteOnCount = 0; 27 39 let noteOffCount = 0; 28 - let lastNote = null; // { pitch, vel, chan, event, handle, ts, latencyMs } 40 + 41 + let localOnCount = 0; // keyboard-originated notes 42 + let localOffCount = 0; 43 + 44 + let lastNote = null; // { pitch, vel, chan, source, handle, ts, latencyMs } 29 45 let lastNoteFrame = -9999; 30 46 47 + const heldKeys = new Set(); // currently-held local keyboard keys 48 + 31 49 let frame = 0; 32 - let lastSubscribedCh = -1; 50 + let lastEmittedChannel = -1; 33 51 34 52 // Max for Live jweb~ bridge (exposes window.max.outlet) 35 53 const maxBridge = ··· 42 60 function emitMaxNote(pitch, velocity, channel) { 43 61 if (!maxBridge) return; 44 62 try { 45 - if (channel !== lastSubscribedCh) { 63 + if (channel !== lastEmittedChannel) { 46 64 maxBridge.outlet(["channel", channel]); 47 - lastSubscribedCh = channel; 65 + lastEmittedChannel = channel; 48 66 } 49 67 maxBridge.outlet(["note", pitch, velocity]); 50 68 } catch (_err) {} ··· 77 95 } 78 96 if (!msg || !msg.type) return; 79 97 if (msg.type === "notepat:midi") { 80 - handleMidiEvent(msg.content || {}); 98 + handleRelayEvent(msg.content || {}); 81 99 } else if (msg.type === "notepat:midi:sources") { 82 100 const list = (msg.content && msg.content.sources) || []; 83 101 sources = list.map((s) => ({ ··· 103 121 } 104 122 } 105 123 106 - function handleMidiEvent(ev) { 124 + function handleRelayEvent(ev) { 107 125 const pitch = Number(ev.note); 108 126 const vel = Number(ev.velocity); 109 127 const chan = Number(ev.channel) || 0; ··· 124 142 pitch, 125 143 vel, 126 144 chan, 127 - event: ev.event || (isOff ? "note_off" : "note_on"), 145 + source: "relay", 128 146 handle: ev.handle || "", 129 147 ts: now, 130 148 latencyMs: latency, ··· 132 150 lastNoteFrame = frame; 133 151 } 134 152 153 + function pressLocalKey(key) { 154 + if (heldKeys.has(key)) return; 155 + const pitch = KEY_TO_PITCH[key]; 156 + if (pitch === undefined) return; 157 + heldKeys.add(key); 158 + localOnCount += 1; 159 + emitMaxNote(pitch, 100, 0); 160 + lastNote = { 161 + pitch, 162 + vel: 100, 163 + chan: 0, 164 + source: "local", 165 + handle: "", 166 + ts: Date.now(), 167 + latencyMs: 0, 168 + }; 169 + lastNoteFrame = frame; 170 + } 171 + 172 + function releaseLocalKey(key) { 173 + if (!heldKeys.has(key)) return; 174 + const pitch = KEY_TO_PITCH[key]; 175 + heldKeys.delete(key); 176 + if (pitch === undefined) return; 177 + localOffCount += 1; 178 + emitMaxNote(pitch, 0, 0); 179 + } 180 + 135 181 const PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 136 182 function pitchName(p) { 137 183 const n = PITCH_NAMES[((p % 12) + 12) % 12]; ··· 150 196 if (wsState === "closed" && frame >= reconnectAt) connectWs(); 151 197 } 152 198 199 + function act({ event: e }) { 200 + if (!e?.is) return; 201 + for (const key of Object.keys(KEY_TO_PITCH)) { 202 + if (e.is(`keyboard:down:${key}`)) { 203 + pressLocalKey(key); 204 + return; 205 + } 206 + if (e.is(`keyboard:up:${key}`)) { 207 + releaseLocalKey(key); 208 + return; 209 + } 210 + } 211 + } 212 + 153 213 function paint({ wipe, ink, box, line, screen }) { 154 214 const bg = [8, 10, 18]; 155 215 const fg = [220, 225, 255]; ··· 164 224 const H = screen.height; 165 225 let y = 4; 166 226 167 - // Header + bridge badge 227 + // Header 168 228 ink(...accent).write("NOTEPAT-REMOTE", { x: 4, y, size: 1 }); 169 229 ink(...(maxBridge ? good : warn)).write( 170 230 maxBridge ? "[M4L]" : "[solo]", ··· 172 232 ); 173 233 y += 10; 174 234 175 - // WS status line 235 + // WS + sources on one line 176 236 const stateColor = 177 237 wsState === "open" 178 238 ? good ··· 183 243 : fgDim; 184 244 ink(...fgDim).write("ws", { x: 4, y }); 185 245 ink(...stateColor).write(wsState.toUpperCase(), { x: 18, y }); 186 - if (wsError) ink(...bad).write(wsError.slice(0, 24), { x: 60, y }); 187 - y += 8; 188 - 189 - // Sources 190 - ink(...fgDim).write("src", { x: 4, y }); 191 246 if (sources.length === 0) { 192 - ink(...fgDim).write("(none — start relay on)", { x: 18, y }); 247 + ink(...fgDim).write("src: (none)", { x: 80, y }); 193 248 } else { 194 249 const label = sources 195 - .slice(0, 4) 250 + .slice(0, 3) 196 251 .map((s) => (s.handle ? "@" + s.handle : s.machineId.slice(0, 6))) 197 252 .join(" "); 198 - ink(...fg).write(label.slice(0, 36), { x: 18, y }); 253 + ink(...fg).write("src:" + label.slice(0, 24), { x: 80, y }); 199 254 } 200 255 y += 8; 256 + if (wsError) { 257 + ink(...bad).write(wsError.slice(0, 34), { x: 4, y }); 258 + y += 8; 259 + } 201 260 202 261 // Counters 203 262 ink(...fgDim).write( 204 - `pkt ${pktCount} on ${noteOnCount} off ${noteOffCount}`, 263 + `relay ${pktCount} (on ${noteOnCount} off ${noteOffCount})`, 264 + { x: 4, y }, 265 + ); 266 + y += 8; 267 + ink(...fgDim).write( 268 + `local ${localOnCount + localOffCount} (on ${localOnCount} off ${localOffCount})`, 205 269 { x: 4, y }, 206 270 ); 207 271 y += 10; ··· 211 275 const age = frame - lastNoteFrame; 212 276 const flashing = age < 30; 213 277 const color = flashing ? accent : fg; 214 - const arrow = lastNote.vel === 0 || lastNote.event === "note_off" ? "v" : "^"; 215 - const label = `${arrow} ${pitchName(lastNote.pitch)} (${lastNote.pitch}) vel ${lastNote.vel} ch ${lastNote.chan}`; 278 + const arrow = lastNote.vel === 0 ? "v" : "^"; 279 + const src = lastNote.source === "local" ? "kbd" : "@" + (lastNote.handle || "?"); 280 + const label = `${arrow} ${pitchName(lastNote.pitch)} (${lastNote.pitch}) v${lastNote.vel} ${src}`; 216 281 ink(...color).write(label, { x: 4, y }); 217 282 y += 8; 218 - const who = lastNote.handle ? "@" + lastNote.handle : "?"; 219 - ink(...fgDim).write(`${who} ${lastNote.latencyMs}ms`, { x: 4, y }); 220 - y += 10; 221 283 } else { 222 - ink(...fgDim).write("(waiting for notes…)", { x: 4, y }); 223 - y += 10; 284 + ink(...fgDim).write("(no notes yet — type or start relay)", { x: 4, y }); 285 + y += 8; 286 + } 287 + 288 + // Held-key strip — shows which keyboard keys are currently down 289 + y += 4; 290 + if (heldKeys.size > 0) { 291 + const held = Array.from(heldKeys) 292 + .map((k) => pitchName(KEY_TO_PITCH[k])) 293 + .join(" "); 294 + ink(...accent).write("held: " + held.slice(0, 32), { x: 4, y }); 295 + y += 8; 224 296 } 225 297 226 - // Indicator strip at bottom — one bar per source, bars flash on note 227 - if (H - y > 14) { 228 - const barY = H - 10; 229 - ink(...fgDim).line(2, barY - 1, W - 2, barY - 1); 230 - ink(...fg).write("hint: enable on ThinkPad: 'midi relay on'", { x: 4, y: H - 8 }); 298 + // Footer hint 299 + if (H - y > 12) { 300 + ink(...fgDim).write("c,d,e,f,g,a,b = C4..B4 scale", { x: 4, y: H - 8 }); 231 301 } 232 302 } 233 303 ··· 236 306 ws?.close(); 237 307 } catch {} 238 308 ws = null; 309 + heldKeys.clear(); 239 310 } 240 311 241 312 function meta() { 242 313 return { 243 314 title: "Notepat Remote", 244 - desc: "Max for Live bridge: session-server notepat:midi → MIDI track", 315 + desc: "M4L: ac-native notepat relay + local keyboard notes → MIDI track", 245 316 }; 246 317 } 247 318 248 - export { boot, sim, paint, leave, meta }; 319 + export { boot, sim, paint, act, leave, meta };
+1 -1
system/public/aesthetic.computer/disks/notepat.mjs
··· 7148 7148 down: () => api.beep(400), 7149 7149 push: () => { 7150 7150 api.beep(); 7151 - jump("ableton"); 7151 + jump("out:https://aesthetic.computer/assets/m4l/notepat-remote.amxd"); 7152 7152 }, 7153 7153 }); 7154 7154
system/public/m4l/notepat-remote.amxd

This is a binary file and will not be displayed.