Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

fix: spreadnob M4L→UI bridge — use direct function calls + fix bios send

- M4L patcher: switch to `script window.acSnNote&&window.acSnNote($1)`
(direct function calls, same pattern as acDawTempo)
- index.mjs: define window.acSn* bridge functions in inline script
(replaces broken bindInlet and postMessage approaches)
- bios.mjs: fix _dawConnectSend to use window.acSEND (live reference)
instead of captured stub — fixes DAW bridge for ALL M4L devices
- disk.mjs: store spreadnob state on persistentDawState (sn* keys)
- spreadnob.mjs: read from sound.daw.snNote/snTarget etc. + debug display

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

+57 -41
+8 -8
ac-m4l/AC-KnobMap.amxd.json
··· 166 166 200.0, 167 167 22.0 168 168 ], 169 - "text": "script window.postMessage({type:'spreadnob:ready'},'*')" 169 + "text": "script window.acSnReady&&window.acSnReady()" 170 170 } 171 171 }, 172 172 { ··· 983 983 230.0, 984 984 22.0 985 985 ], 986 - "text": "script window.postMessage({type:'spreadnob:range',low:$1,high:$2},'*')" 986 + "text": "script window.acSnNote&&window.acSnNote($1)" 987 987 } 988 988 }, 989 989 { ··· 1001 1001 205.0, 1002 1002 22.0 1003 1003 ], 1004 - "text": "script window.postMessage({type:'spreadnob:note',note:$1},'*')" 1004 + "text": "script window.acSnNote&&window.acSnNote($1)" 1005 1005 } 1006 1006 }, 1007 1007 { ··· 1019 1019 215.0, 1020 1020 22.0 1021 1021 ], 1022 - "text": "script window.postMessage({type:'spreadnob:value',value:$1},'*')" 1022 + "text": "script window.acSnValue&&window.acSnValue($1)" 1023 1023 } 1024 1024 }, 1025 1025 { ··· 1037 1037 220.0, 1038 1038 22.0 1039 1039 ], 1040 - "text": "script window.postMessage({type:'spreadnob:active',active:$1},'*')" 1040 + "text": "script window.acSnActive&&window.acSnActive($1)" 1041 1041 } 1042 1042 }, 1043 1043 { ··· 1073 1073 255.0, 1074 1074 22.0 1075 1075 ], 1076 - "text": "script window.postMessage({type:'spreadnob:target',name:'$1'},'*')" 1076 + "text": "script window.acSnTarget&&window.acSnTarget('$1')" 1077 1077 } 1078 1078 }, 1079 1079 { ··· 1205 1205 215.0, 1206 1206 22.0 1207 1207 ], 1208 - "text": "script window.postMessage({type:'spreadnob:min',min:$1},'*')" 1208 + "text": "script window.acSnMin&&window.acSnMin($1)" 1209 1209 } 1210 1210 }, 1211 1211 { ··· 1223 1223 215.0, 1224 1224 22.0 1225 1225 ], 1226 - "text": "script window.postMessage({type:'spreadnob:max',max:$1},'*')" 1226 + "text": "script window.acSnMax&&window.acSnMax($1)" 1227 1227 } 1228 1228 }, 1229 1229 {
+8 -7
system/netlify/functions/index.mjs
··· 936 936 console.log("🎹 Max for Live detected - DAW sync connected"); 937 937 }; 938 938 939 - // 🎛️ Spreadnob bridge — catches postMessage from M4L script commands 940 - window.addEventListener("message", function(event) { 941 - var d = event.data; 942 - if (d && d.type && typeof d.type === "string" && d.type.indexOf("spreadnob:") === 0) { 943 - send({ type: d.type, content: d }); 944 - } 945 - }); 939 + // 🎛️ Spreadnob bridge — called directly by M4L script commands 940 + window.acSnNote = function(n) { send({ type: "spreadnob:note", content: { note: n } }); }; 941 + window.acSnTarget = function(name) { send({ type: "spreadnob:target", content: { name: name } }); }; 942 + window.acSnValue = function(v) { send({ type: "spreadnob:value", content: { value: v } }); }; 943 + window.acSnActive = function(v) { send({ type: "spreadnob:active", content: { active: v } }); }; 944 + window.acSnMin = function(v) { send({ type: "spreadnob:min", content: { min: v } }); }; 945 + window.acSnMax = function(v) { send({ type: "spreadnob:max", content: { max: v } }); }; 946 + window.acSnReady = function() { send({ type: "spreadnob:ready", content: {} }); }; 946 947 947 948 // Signal to M4L that we're ready 948 949 if (window.max) {
+5 -3
system/public/aesthetic.computer/bios.mjs
··· 103 103 let frozenUrlPath = null; 104 104 105 105 // Called from boot() to connect the send function 106 + // NOTE: sendFunc is captured by VALUE at call time, but `send` may be reassigned 107 + // later (e.g. noWorker fallback). Use window.acSEND as the live reference. 106 108 function _dawConnectSend(sendFunc, updateMetronome) { 107 109 if (window.acDawConnect) { 108 - // Wrap sendFunc to also update metronome and capture sample rate 109 110 const wrappedSend = (msg) => { 110 111 if (msg.type === "daw:tempo" && updateMetronome) { 111 112 updateMetronome(msg.content.bpm); 112 113 } 113 - // 🎹 Capture DAW sample rate for AudioContext creation 114 114 if (msg.type === "daw:samplerate" && msg.content?.samplerate) { 115 115 _dawSampleRate = msg.content.samplerate; 116 116 } 117 - sendFunc(msg); 117 + // Use window.acSEND (always current) with sendFunc as fallback 118 + const currentSend = window.acSEND || sendFunc; 119 + currentSend(msg); 118 120 }; 119 121 window.acDawConnect(wrappedSend); 120 122 }
+34 -21
system/public/aesthetic.computer/disks/spreadnob.mjs
··· 68 68 return [cx + r * Math.cos(rad), cy - r * Math.sin(rad)]; 69 69 } 70 70 71 - function boot() {} 71 + let simCount = 0; 72 + let dawExists = false; 73 + let dawKeys = ""; 74 + 75 + function boot({ needsPaint }) { needsPaint(); } 72 76 73 77 function sim({ sound, needsPaint }) { 78 + simCount++; 74 79 let dirty = false; 75 - const sn = sound?.spreadnob; 76 - if (sn) { 77 - if (sn.active !== null && sn.active !== undefined) { 78 - active = !!Number(sn.active); 79 - sn.active = null; 80 + // Read spreadnob data from sound.daw (stored as sn* props on persistentDawState) 81 + const daw = sound?.daw; 82 + dawExists = !!daw; 83 + if (daw) { 84 + if (daw.snActive !== undefined && daw.snActive !== null) { 85 + active = !!Number(daw.snActive); 86 + daw.snActive = null; 80 87 dirty = true; 81 88 } 82 - if (sn.target !== null && sn.target !== undefined) { 83 - target = String(sn.target).trim(); 84 - sn.target = null; 89 + if (daw.snTarget !== undefined && daw.snTarget !== null) { 90 + target = String(daw.snTarget).trim(); 91 + daw.snTarget = null; 85 92 dirty = true; 86 93 } 87 - if (sn.min !== null && sn.min !== undefined) { 88 - const n = Number(sn.min); 94 + if (daw.snMin !== undefined && daw.snMin !== null) { 95 + const n = Number(daw.snMin); 89 96 if (Number.isFinite(n)) paramMin = n; 90 - sn.min = null; 97 + daw.snMin = null; 91 98 dirty = true; 92 99 } 93 - if (sn.max !== null && sn.max !== undefined) { 94 - const n = Number(sn.max); 100 + if (daw.snMax !== undefined && daw.snMax !== null) { 101 + const n = Number(daw.snMax); 95 102 if (Number.isFinite(n)) paramMax = n; 96 - sn.max = null; 103 + daw.snMax = null; 97 104 dirty = true; 98 105 } 99 - if (sn.value !== null && sn.value !== undefined) { 100 - const n = Number(sn.value); 106 + if (daw.snValue !== undefined && daw.snValue !== null) { 107 + const n = Number(daw.snValue); 101 108 value = Number.isFinite(n) ? n : null; 102 - sn.value = null; 109 + daw.snValue = null; 103 110 dirty = true; 104 111 } 105 - if (sn.note !== null && sn.note !== undefined) { 106 - const n = Number(sn.note); 112 + if (daw.snNote !== undefined && daw.snNote !== null) { 113 + const n = Number(daw.snNote); 107 114 if (n !== lastSnNote) { 108 115 currentNote = Number.isFinite(n) ? n : null; 109 116 lastSnNote = n; ··· 116 123 dirty = true; 117 124 } 118 125 } 126 + if (simCount % 60 === 0) { 127 + dawKeys = Object.keys(daw).filter(k => k.startsWith("sn") && daw[k] !== null).join(","); 128 + } 119 129 } 120 130 121 131 if (flash > 0) { ··· 123 133 if (flash < 0.025) flash = 0; 124 134 dirty = true; 125 135 } 126 - if (dirty) needsPaint(); 136 + needsPaint(); // Always repaint for debug 127 137 } 128 138 129 139 function paint({ wipe, ink, screen }) { ··· 221 231 222 232 // Title 223 233 ink(200, 120, 170).write("spreadnob", { x: 6, y: 4 }, undefined, undefined, false, MINI_FONT); 234 + 235 + // Debug info 236 + ink(255, 255, 0).write(`sim:${simCount} daw:${dawExists} keys:${dawKeys || "none"}`, { x: 6, y: h - 8 }, undefined, undefined, false, MINI_FONT); 224 237 } 225 238 226 239 function act({ event, screen, needsPaint }) {
+2 -2
system/public/aesthetic.computer/lib/disk.mjs
··· 9798 9798 9799 9799 async function makeFrame({ data: { type, content } }) { 9800 9800 // DEBUG: Log ALL messages starting with "pedal:" or "daw:" 9801 - if (type?.startsWith?.("pedal:") || type?.startsWith?.("daw:")) { 9802 - console.log("🎹🎹🎹 makeFrame received:", type, content); 9801 + if (type?.startsWith?.("pedal:") || type?.startsWith?.("daw:") || type?.startsWith?.("spreadnob:")) { 9802 + console.log("🎹🎹🎹 makeFrame received:", type, JSON.stringify(content)); 9803 9803 } 9804 9804 9805 9805 // Handle permission responses from bios.mjs