color-synth

a synth that generates colors instead of sounds
Log | Files | Refs | README

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 {};