engine.worker.ts (8591B)
1 import type { 2 EngineMessage, 3 Input, 4 SynthConfig, 5 Unit, 6 UnitId, 7 UnitState, 8 UnitStateMap 9 } from '$lib/types'; 10 import { oklch } from '$lib/color'; 11 import { 12 ROWS, 13 COLS, 14 getUnit, 15 RANGE, 16 rescale, 17 wrangle, 18 unitRange, 19 oscShapes, 20 mathOps, 21 CELLS 22 } from '$lib/types'; 23 import _ from 'lodash'; 24 25 let config: SynthConfig | undefined = undefined; 26 let unitState: UnitStateMap = new Map(); 27 let canvas: OffscreenCanvas = new OffscreenCanvas(1, 1); 28 29 onmessage = (message: { data: EngineMessage }) => { 30 const { data } = message; 31 switch (data.kind) { 32 case 'config': { 33 config = data.content; 34 break; 35 } 36 case 'window': { 37 canvas = new OffscreenCanvas(data.content.width, data.content.height); 38 } 39 } 40 }; 41 42 function getUnitState(id: UnitId): UnitState { 43 if (!config) { 44 throw new Error("no config, can't get unit state."); 45 } 46 const unit = getUnit(null, config.units, id); 47 const theState = unitState.get(id); 48 switch (unit.kind) { 49 case 'osc': { 50 if (theState === undefined) { 51 return 0; 52 } 53 if (typeof theState !== 'number' || Number.isNaN(theState)) { 54 throw new Error(`invalid state for osc unit ${id}: ${theState}`); 55 } 56 break; 57 } 58 case 'smooth': { 59 return theState === undefined ? [] : theState; 60 break; 61 } 62 case 'lag': { 63 return theState === undefined ? [] : theState; 64 } 65 case 'img': { 66 if (theState === undefined) { 67 // NOW: make img sample here. should also store position 68 console.log('fetching...'); 69 const fetcher = async () => { 70 try { 71 const img = await fetch( 72 new Request( 73 'https://t4.ftcdn.net/jpg/01/36/70/67/360_F_136706734_KWhNBhLvY5XTlZVocpxFQK1FfKNOYbMj.jpg', 74 { 75 mode: 'cors' 76 } 77 ) 78 ); 79 const bmp = await createImageBitmap(await img.blob(), { 80 resizeHeight: ROWS, 81 resizeWidth: COLS, 82 resizeQuality: 'pixelated' 83 }); 84 const canvas = new OffscreenCanvas(bmp.width, bmp.height); 85 const ctx = canvas.getContext('2d'); 86 ctx?.drawImage(bmp, 0, 0, COLS, ROWS); 87 const imgData = ctx?.getImageData(0, 0, COLS, ROWS); 88 const newState = { 89 position: 0, 90 data: imgData!.data 91 }; 92 console.log(newState); 93 setUnitState(id, newState); 94 } catch (e) { 95 console.error(e); 96 } 97 }; 98 fetcher(); 99 return 'loading'; 100 } 101 return theState; 102 } 103 default: { 104 throw new Error('state for this invalid or NYI'); 105 } 106 } 107 return theState; 108 } 109 110 function setUnitState(id: UnitId, state: UnitState) { 111 // TODO: type safety 112 unitState.set(id, state); 113 } 114 115 function vUnit(x: { id: UnitId }): number { 116 const { id } = x; 117 const unit: Unit = getUnit(null, config!.units, id); 118 switch (unit.kind) { 119 case 'osc': { 120 const position = getUnitState(id); 121 const shapeIdx = Math.round( 122 wrangle(v(unit.controls.waveshape), RANGE.signal, unitRange.osc.waveshape) 123 ); 124 // to calculate amplitude at position: 125 // take the position as % progress through the wave. 126 // find the osc shape's "value" at that point, range being pmone. 127 // multiply that by the amount control, scaled to pmone, 128 // finally, scale this value to a signal. 129 const scalar = wrangle(v(unit.controls.amount), RANGE.signal, RANGE.pmone); 130 const waveAmp = oscShapes[shapeIdx](position); 131 return wrangle(waveAmp * scalar, RANGE.pmone, RANGE.signal); 132 } 133 case 'number': { 134 return v(unit.controls.value); 135 } 136 case 'noise': { 137 return Math.random() * v(unit.controls.amount); 138 } 139 case 'smooth': { 140 const frames: number[] = getUnitState(id); 141 return frames.reduce((acc, item) => item + acc, 0) / frames.length; 142 } 143 case 'math': { 144 const opIdx = Math.round(wrangle(v(unit.controls.op), RANGE.signal, unitRange.math.op)); 145 if (opIdx === 2) { 146 //mult 147 return ( 148 5_000_000 * mathOps[opIdx](v(unit.controls.a) / 5_000_000, v(unit.controls.b) / 5_000_000) 149 ); 150 } else { 151 return mathOps[opIdx](v(unit.controls.a), v(unit.controls.b)); 152 } 153 } 154 case 'lag': { 155 return getUnitState(id)[0]; 156 } 157 case 'img': { 158 // NOW: make offscreen canvas of the same resolution as the screen 159 // take color measurements of each pixel 160 // store position and increase by one mod cells each frame 161 const state = getUnitState(id); 162 logMap.img = state; 163 if (state instanceof Object) { 164 logMap.test = state.data[0]; 165 return rescale( 166 state.data[Math.abs(Math.round(state.position)) * 4], 167 { min: 0, max: 255 }, 168 RANGE.signal 169 ); 170 } 171 return 0; 172 } 173 } 174 } 175 176 const perf: Record<string, { n: number; cum: number; avg: number; _t: number }> = {}; 177 178 const p = { 179 start: (tag: string) => { 180 if (!perf[tag]) 181 perf[tag] = { 182 n: 0, 183 cum: 0, 184 avg: 0, 185 _t: 0 186 }; 187 perf[tag]._t = performance.now(); 188 }, 189 end: (tag: string) => { 190 const r = perf[tag]; 191 r.n++; 192 r.cum += performance.now() - r._t; 193 r.avg = r.cum / r.n; 194 } 195 }; 196 197 setInterval(() => console.log(perf), 5000); 198 199 function v(input: Input): number { 200 if (typeof input === 'number') { 201 return input; 202 } 203 const result = (input.value || 0) + input.sources.reduce((a, b) => a + vUnit(b), 0); 204 return result; 205 } 206 function update() { 207 // update all unit states 208 if (!config) { 209 throw new Error("no config, can't update unit state."); 210 } 211 const ids = Object.keys(config.units); 212 for (let id of ids) { 213 const unit = getUnit(null, config.units, id); 214 switch (unit.kind) { 215 case 'osc': { 216 const position = getUnitState(id); 217 const coarse = rescale(v(unit.controls.coarse), RANGE.signal, unitRange.osc.coarse); 218 const fine = rescale(v(unit.controls.fine), RANGE.signal, unitRange.osc.fine); 219 const superfine = rescale( 220 v(unit.controls.superfine), 221 RANGE.signal, 222 unitRange.osc.superfine 223 ); 224 // i'm assuming that when these all get set to 0, the position should also reset 225 // that way it isn't stuck at some constant value that effects other units down the line 226 if (coarse === 0 && fine === 0 && superfine === 0) { 227 if (position !== 0) console.log(`Resetting position (${position}) of ${id} to 0.`); 228 setUnitState(id, 0); 229 } else { 230 setUnitState( 231 id, 232 (position + coarse / COLS + fine / CELLS + superfine / (CELLS * 10_000)) % 1 233 ); 234 } 235 break; 236 } 237 case 'smooth': { 238 // wrangle frames since <0 means nothing 239 const n = wrangle(v(unit.controls.frames), RANGE.signal, unitRange.smooth.frames); 240 // keep n frames 241 let frames = [v(unit.controls.signal), ...getUnitState(id)]; 242 if (frames.length > n) { 243 frames = frames.slice(0, n); 244 } 245 setUnitState(id, frames); 246 break; 247 } 248 case 'lag': { 249 const amt = wrangle(v(unit.controls.amount), RANGE.signal, unitRange.lag.amount); 250 if (amt === 0) { 251 setUnitState(id, [v(unit.controls.signal)]); 252 } else { 253 let arr = getUnitState(id) as number[]; 254 arr.push(v(unit.controls.signal)); 255 if (arr.length > amt) { 256 if (arr.length - 2 === amt) { 257 arr.pop(); 258 } else { 259 arr = arr.slice(arr.length - amt); 260 } 261 arr.unshift(); 262 } 263 setUnitState(id, arr); 264 } 265 break; 266 } 267 case 'img': { 268 const state = getUnitState(id); 269 if (state instanceof Object) { 270 const speed = wrangle(v(unit.controls.speed), RANGE.signal, unitRange.img.speed); 271 state.position = (state.position + speed / 100000) % CELLS; 272 } else { 273 setUnitState(id, state); 274 } 275 } 276 } 277 } 278 } 279 function throttleLog(k: string, msg: any) { 280 logMap[k] = msg; 281 } 282 let logMap: Record<string, any> = {}; 283 function logSometimes() { 284 Object.keys(logMap).map((k) => console.log(k, logMap[k])); 285 setTimeout(logSometimes, 250); 286 } 287 logSometimes(); 288 289 function drawSquares() { 290 if (config) { 291 p.start('drawSquares'); 292 const ctx = canvas.getContext('2d', { 293 antialias: false, 294 alpha: false 295 }); 296 if (!ctx) { 297 throw new Error('couldnt get ctx'); 298 } 299 300 // 4 = R G B A 301 const data = new Uint8ClampedArray(ROWS * COLS * 4); 302 let di = 0; 303 304 for (let row = 0; row < ROWS; row++) { 305 for (let col = 0; col < COLS; col++) { 306 const l = config.sinks.l == null ? 0 : v(config.sinks.l); 307 const c = config.sinks.c == null ? 0 : v(config.sinks.c); 308 const h = config.sinks.h == null ? 0 : v(config.sinks.h); 309 const rgb = oklch( 310 wrangle(l, RANGE.signal, RANGE.lch.l), 311 wrangle(c, RANGE.signal, RANGE.lch.c), 312 wrangle(h, RANGE.signal, RANGE.lch.h) 313 ); 314 data[di++] = rgb.r; 315 data[di++] = rgb.g; 316 data[di++] = rgb.b; 317 data[di++] = 255; 318 update(); 319 } 320 } 321 postMessage({ kind: 'buf', content: data.buffer }, [data.buffer]); 322 p.end('drawSquares'); 323 } 324 requestAnimationFrame(drawSquares); 325 } 326 327 drawSquares(); 328 329 export {};