From 30fe2dd5026a8e26b8d91c1b77d892f37e340962 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Fri, 11 Aug 2023 10:42:59 +0200 Subject: [PATCH] add superdough package --- packages/superdough/README.md | 35 +++ packages/superdough/example/.gitignore | 24 ++ packages/superdough/example/index.html | 12 + packages/superdough/example/main.js | 25 ++ packages/superdough/example/package.json | 17 ++ packages/superdough/feedbackdelay.mjs | 31 +++ packages/superdough/helpers.mjs | 70 +++++ packages/superdough/index.mjs | 10 + packages/superdough/package.json | 41 +++ packages/superdough/reverb.mjs | 23 ++ packages/superdough/sampler.mjs | 288 +++++++++++++++++++ packages/superdough/superdough.mjs | 249 +++++++++++++++++ packages/superdough/synth.mjs | 44 +++ packages/superdough/util.mjs | 29 ++ packages/superdough/vite.config.js | 19 ++ packages/superdough/vowel.mjs | 38 +++ packages/superdough/worklets.mjs | 108 ++++++++ pnpm-lock.yaml | 337 +++++++++++++++++++++-- pnpm-workspace.yaml | 1 + 19 files changed, 1384 insertions(+), 17 deletions(-) create mode 100644 packages/superdough/README.md create mode 100644 packages/superdough/example/.gitignore create mode 100644 packages/superdough/example/index.html create mode 100644 packages/superdough/example/main.js create mode 100644 packages/superdough/example/package.json create mode 100644 packages/superdough/feedbackdelay.mjs create mode 100644 packages/superdough/helpers.mjs create mode 100644 packages/superdough/index.mjs create mode 100644 packages/superdough/package.json create mode 100644 packages/superdough/reverb.mjs create mode 100644 packages/superdough/sampler.mjs create mode 100644 packages/superdough/superdough.mjs create mode 100644 packages/superdough/synth.mjs create mode 100644 packages/superdough/util.mjs create mode 100644 packages/superdough/vite.config.js create mode 100644 packages/superdough/vowel.mjs create mode 100644 packages/superdough/worklets.mjs diff --git a/packages/superdough/README.md b/packages/superdough/README.md new file mode 100644 index 00000000..7a441841 --- /dev/null +++ b/packages/superdough/README.md @@ -0,0 +1,35 @@ +# superdough + +superdough is a simple web audio sampler and synth, intended for live coding. +It is the default output of [strudel](https://strudel.tidalcycles.org/). +This package has no ties to strudel and can be used to quickly bake your own music system on the web. + +## Install + +via npm: + +```js +npm i superdough --save +``` + +## Use + +```js +import { superdough, samples } from 'superdough'; +// load samples from github +const loadSamples = samples('github:tidalcycles/Dirt-Samples/master'); + +// play some sounds when a button is clicked +document.getElementById('play').addEventListener('click', () => { + superdough({ s: "bd", delay: .5 }, 0); + superdough({ s: "sawtooth", cutoff: 600, resonance: 8 }, 0); + superdough({ s: "hh" }, 0.25); + superdough({ s: "sd", room: .5 }, 0.5); + superdough({ s: "hh" }, 0.75); +}) +``` + +## Credits + +- [SuperDirt](https://github.com/musikinformatik/SuperDirt) +- [WebDirt](https://github.com/dktr0/WebDirt) \ No newline at end of file diff --git a/packages/superdough/example/.gitignore b/packages/superdough/example/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/packages/superdough/example/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/superdough/example/index.html b/packages/superdough/example/index.html new file mode 100644 index 00000000..806cb2f1 --- /dev/null +++ b/packages/superdough/example/index.html @@ -0,0 +1,12 @@ + + + + Superdough Example + + + + + + + + diff --git a/packages/superdough/example/main.js b/packages/superdough/example/main.js new file mode 100644 index 00000000..50861938 --- /dev/null +++ b/packages/superdough/example/main.js @@ -0,0 +1,25 @@ +import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough'; + +initAudioOnFirstClick(); + +const load = Promise.all([samples('github:tidalcycles/Dirt-Samples/master'), registerSynthSounds()]); + +let button = document.getElementById('play'); + +const loop = (t = 0) => { + superdough({ s: 'bd', delay: 0.5 }, t); + superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125); + superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125); + superdough({ s: 'hh' }, t + 0.25); + superdough({ s: 'sd', room: 0.5 }, t + 0.5); + superdough({ s: 'hh' }, t + 0.75); +}; + +button.addEventListener('click', async () => { + console.log('play'); + await load; + let t = 0.1; + while (t < 16) { + loop(t++); + } +}); diff --git a/packages/superdough/example/package.json b/packages/superdough/example/package.json new file mode 100644 index 00000000..abe57b82 --- /dev/null +++ b/packages/superdough/example/package.json @@ -0,0 +1,17 @@ +{ + "name": "superdough-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "superdough": "workspace:*" + }, + "devDependencies": { + "vite": "^4.4.5" + } +} diff --git a/packages/superdough/feedbackdelay.mjs b/packages/superdough/feedbackdelay.mjs new file mode 100644 index 00000000..c182d655 --- /dev/null +++ b/packages/superdough/feedbackdelay.mjs @@ -0,0 +1,31 @@ +if (typeof DelayNode !== 'undefined') { + class FeedbackDelayNode extends DelayNode { + constructor(ac, wet, time, feedback) { + super(ac); + wet = Math.abs(wet); + this.delayTime.value = time; + + const feedbackGain = ac.createGain(); + feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995); + this.feedback = feedbackGain.gain; + + const delayGain = ac.createGain(); + delayGain.gain.value = wet; + this.delayGain = delayGain; + + this.connect(feedbackGain); + this.connect(delayGain); + feedbackGain.connect(this); + + this.connect = (target) => delayGain.connect(target); + return this; + } + start(t) { + this.delayGain.gain.setValueAtTime(this.delayGain.gain.value, t + this.delayTime.value); + } + } + + AudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) { + return new FeedbackDelayNode(this, wet, time, feedback); + }; +} diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs new file mode 100644 index 00000000..7cc54c8d --- /dev/null +++ b/packages/superdough/helpers.mjs @@ -0,0 +1,70 @@ +import { getAudioContext } from './superdough.mjs'; + +export function gainNode(value) { + const node = getAudioContext().createGain(); + node.gain.value = value; + return node; +} + +export const getOscillator = ({ s, freq, t }) => { + // make oscillator + const o = getAudioContext().createOscillator(); + o.type = s || 'triangle'; + o.frequency.value = Number(freq); + o.start(t); + //o.stop(t + duration + release); + const stop = (time) => o.stop(time); + return { node: o, stop }; +}; + +// alternative to getADSR returning the gain node and a stop handle to trigger the release anytime in the future +export const getEnvelope = (attack, decay, sustain, release, velocity, begin) => { + const gainNode = getAudioContext().createGain(); + gainNode.gain.setValueAtTime(0, begin); + gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack + gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start + // sustain end + return { + node: gainNode, + stop: (t) => { + //if (typeof gainNode.gain.cancelAndHoldAtTime === 'function') { + // gainNode.gain.cancelAndHoldAtTime(t); // this seems to release instantly.... + // see https://discord.com/channels/779427371270275082/937365093082079272/1086053607360712735 + //} else { + // firefox: this will glitch when the sustain has not been reached yet at the time of release + gainNode.gain.setValueAtTime(sustain * velocity, t); + //} + gainNode.gain.linearRampToValueAtTime(0, t + release); + }, + }; +}; + +export const getADSR = (attack, decay, sustain, release, velocity, begin, end) => { + const gainNode = getAudioContext().createGain(); + gainNode.gain.setValueAtTime(0, begin); + gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack + gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start + gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end + gainNode.gain.linearRampToValueAtTime(0, end + release); // release + // for some reason, using exponential ramping creates little cracklings + /* let t = begin; + gainNode.gain.setValueAtTime(0, t); + gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack)); + const sustainGain = Math.max(sustain * velocity, 0.001); + gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay)); + if (end - begin < attack + decay) { + gainNode.gain.cancelAndHoldAtTime(end); + } else { + gainNode.gain.setValueAtTime(sustainGain, end); + } + gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */ + return gainNode; +}; + +export const getFilter = (type, frequency, Q) => { + const filter = getAudioContext().createBiquadFilter(); + filter.type = type; + filter.frequency.value = frequency; + filter.Q.value = Q; + return filter; +}; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs new file mode 100644 index 00000000..9b2067e6 --- /dev/null +++ b/packages/superdough/index.mjs @@ -0,0 +1,10 @@ +/* +index.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +export * from './superdough.mjs'; +export * from './sampler.mjs'; +export * from './helpers.mjs'; +export * from './synth.mjs'; diff --git a/packages/superdough/package.json b/packages/superdough/package.json new file mode 100644 index 00000000..1065d3f8 --- /dev/null +++ b/packages/superdough/package.json @@ -0,0 +1,41 @@ +{ + "name": "superdough", + "version": "0.9.0", + "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.cjs", + "module": "dist/index.mjs" + }, + "directories": { + "example": "examples" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "devDependencies": { + "vite": "^4.3.3" + }, + "dependencies": { + "nanostores": "^0.8.1" + } +} diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs new file mode 100644 index 00000000..e6d31f6a --- /dev/null +++ b/packages/superdough/reverb.mjs @@ -0,0 +1,23 @@ +if (typeof AudioContext !== 'undefined') { + AudioContext.prototype.impulseResponse = function (duration, channels = 1) { + const length = this.sampleRate * duration; + const impulse = this.createBuffer(channels, length, this.sampleRate); + const IR = impulse.getChannelData(0); + for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration); + return impulse; + }; + + AudioContext.prototype.createReverb = function (duration) { + const convolver = this.createConvolver(); + convolver.setDuration = (d) => { + convolver.buffer = this.impulseResponse(d); + convolver.duration = duration; + return convolver; + }; + convolver.setDuration(duration); + return convolver; + }; +} + +// TODO: make the reverb more exciting +// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs new file mode 100644 index 00000000..17275a99 --- /dev/null +++ b/packages/superdough/sampler.mjs @@ -0,0 +1,288 @@ +import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core'; +import { getAudioContext, registerSound } from './index.mjs'; +import { getEnvelope } from './helpers.mjs'; + +const bufferCache = {}; // string: Promise +const loadCache = {}; // string: Promise + +export const getCachedBuffer = (url) => bufferCache[url]; + +function humanFileSize(bytes, si) { + var thresh = si ? 1000 : 1024; + if (bytes < thresh) return bytes + ' B'; + var units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + var u = -1; + do { + bytes /= thresh; + ++u; + } while (bytes >= thresh); + return bytes.toFixed(1) + ' ' + units[u]; +} + +export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => { + let transpose = 0; + if (freq !== undefined && note !== undefined) { + logger('[sampler] hap has note and freq. ignoring note', 'warning'); + } + let midi = valueToMidi({ freq, note }, 36); + transpose = midi - 36; // C3 is middle C + + const ac = getAudioContext(); + let sampleUrl; + if (Array.isArray(bank)) { + sampleUrl = bank[n % bank.length]; + } else { + const midiDiff = (noteA) => noteToMidi(noteA) - midi; + // object format will expect keys as notes + const closest = Object.keys(bank) + .filter((k) => !k.startsWith('_')) + .reduce( + (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), + null, + ); + transpose = -midiDiff(closest); // semitones to repitch + sampleUrl = bank[closest][n % bank[closest].length]; + } + if (resolveUrl) { + sampleUrl = await resolveUrl(sampleUrl); + } + let buffer = await loadBuffer(sampleUrl, ac, s, n); + if (speed < 0) { + // should this be cached? + buffer = reverseBuffer(buffer); + } + const bufferSource = ac.createBufferSource(); + bufferSource.buffer = buffer; + const playbackRate = 1.0 * Math.pow(2, transpose / 12); + // bufferSource.playbackRate.value = Math.pow(2, transpose / 12); + bufferSource.playbackRate.value = playbackRate; + return bufferSource; +}; + +export const loadBuffer = (url, ac, s, n = 0) => { + const label = s ? `sound "${s}:${n}"` : 'sample'; + if (!loadCache[url]) { + logger(`[sampler] load ${label}..`, 'load-sample', { url }); + const timestamp = Date.now(); + loadCache[url] = fetch(url) + .then((res) => res.arrayBuffer()) + .then(async (res) => { + const took = Date.now() - timestamp; + const size = humanFileSize(res.byteLength); + // const downSpeed = humanFileSize(res.byteLength / took); + logger(`[sampler] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-sample', { url }); + const decoded = await ac.decodeAudioData(res); + bufferCache[url] = decoded; + return decoded; + }); + } + return loadCache[url]; +}; + +export function reverseBuffer(buffer) { + const ac = getAudioContext(); + const reversed = ac.createBuffer(buffer.numberOfChannels, buffer.length, ac.sampleRate); + for (let channel = 0; channel < buffer.numberOfChannels; channel++) { + reversed.copyToChannel(buffer.getChannelData(channel).slice().reverse(), channel, channel); + } + return reversed; +} + +export const getLoadedBuffer = (url) => { + return bufferCache[url]; +}; + +export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { + return Object.entries(sampleMap).forEach(([key, value]) => { + if (typeof value === 'string') { + value = [value]; + } + if (typeof value !== 'object') { + throw new Error('wrong sample map format for ' + key); + } + baseUrl = value._base || baseUrl; + const replaceUrl = (v) => (baseUrl + v).replace('github:', 'https://raw.githubusercontent.com/'); + if (Array.isArray(value)) { + //return [key, value.map(replaceUrl)]; + value = value.map(replaceUrl); + } else { + // must be object + value = Object.fromEntries( + Object.entries(value).map(([note, samples]) => { + return [note, (typeof samples === 'string' ? [samples] : samples).map(replaceUrl)]; + }), + ); + } + fn(key, value); + }); +}; + +// allows adding a custom url prefix handler +// for example, it is used by the desktop app to load samples starting with '~/music' +let resourcePrefixHandlers = {}; +export function registerSamplesPrefix(prefix, resolve) { + resourcePrefixHandlers[prefix] = resolve; +} +// finds a prefix handler for the given url (if any) +function getSamplesPrefixHandler(url) { + const handler = Object.entries(resourcePrefixHandlers).find(([key]) => url.startsWith(key)); + if (handler) { + return handler[1]; + } + return; +} + +/** + * Loads a collection of samples to use with `s` + * @example + * samples('github:tidalcycles/Dirt-Samples/master'); + * s("[bd ~]*2, [~ hh]*2, ~ sd") + * @example + * samples({ + * bd: '808bd/BD0000.WAV', + * sd: '808sd/SD0010.WAV' + * }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/'); + * s("[bd ~]*2, [~ hh]*2, ~ sd") + * + */ + +export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { + if (typeof sampleMap === 'string') { + // check if custom prefix handler + const handler = getSamplesPrefixHandler(sampleMap); + if (handler) { + return handler(sampleMap); + } + if (sampleMap.startsWith('github:')) { + let [_, path] = sampleMap.split('github:'); + path = path.endsWith('/') ? path.slice(0, -1) : path; + sampleMap = `https://raw.githubusercontent.com/${path}/strudel.json`; + } + if (typeof fetch !== 'function') { + // not a browser + return; + } + const base = sampleMap.split('/').slice(0, -1).join('/'); + if (typeof fetch === 'undefined') { + // skip fetch when in node / testing + return; + } + return fetch(sampleMap) + .then((res) => res.json()) + .then((json) => samples(json, baseUrl || json._base || base, options)) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${sampleMap}"`); + }); + } + const { prebake, tag } = options; + processSampleMap( + sampleMap, + (key, value) => + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { + type: 'sample', + samples: value, + baseUrl, + prebake, + tag, + }), + baseUrl, + ); +}; + +const cutGroups = []; + +export async function onTriggerSample(t, value, onended, bank, resolveUrl) { + const { + s, + freq, + unit, + nudge = 0, // TODO: is this in seconds? + cut, + loop, + clip = undefined, // if 1, samples will be cut off when the hap ends + n = 0, + note, + speed = 1, // sample playback speed + begin = 0, + end = 1, + } = value; + // load sample + if (speed === 0) { + // no playback + return; + } + const ac = getAudioContext(); + // destructure adsr here, because the default should be different for synths and samples + const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value; + //const soundfont = getSoundfontKey(s); + const time = t + nudge; + + const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl); + + // asny stuff above took too long? + if (ac.currentTime > t) { + logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); + // console.warn('sample still loading:', s, n); + return; + } + if (!bufferSource) { + logger(`[sampler] could not load "${s}:${n}"`, 'error'); + return; + } + bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; + if (unit === 'c') { + // are there other units? + bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps; + } + // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, + // rather than the current playback rate, so even if the sound is playing at twice its normal speed, + // the midway point through a 10-second audio buffer is still 5." + const offset = begin * bufferSource.buffer.duration; + bufferSource.start(time, offset); + const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; + /*if (loop) { + // TODO: idea for loopBegin / loopEnd + // if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized + // if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number + // this will simplify perfectly looping things, while still keeping the normalized option + // the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case) + bufferSource.loop = true; + bufferSource.loopStart = offset; + bufferSource.loopEnd = offset + duration; + duration = loop * duration; + }*/ + const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); + bufferSource.connect(envelope); + const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... + envelope.connect(out); + bufferSource.onended = function () { + bufferSource.disconnect(); + envelope.disconnect(); + out.disconnect(); + onended(); + }; + const stop = (endTime, playWholeBuffer = clip === undefined) => { + let releaseTime = endTime; + if (playWholeBuffer) { + releaseTime = t + (end - begin) * bufferDuration; + } + bufferSource.stop(releaseTime + release); + releaseEnvelope(releaseTime); + }; + const handle = { node: out, bufferSource, stop }; + + // cut groups + if (cut !== undefined) { + const prev = cutGroups[cut]; + if (prev) { + prev.node.gain.setValueAtTime(1, time); + prev.node.gain.linearRampToValueAtTime(0, time + 0.01); + } + cutGroups[cut] = handle; + } + + return handle; +} diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs new file mode 100644 index 00000000..a413ee9a --- /dev/null +++ b/packages/superdough/superdough.mjs @@ -0,0 +1,249 @@ +/* +superdough.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import './feedbackdelay.mjs'; +import './reverb.mjs'; +import './vowel.mjs'; +import { clamp } from './util.mjs'; +import workletsUrl from './worklets.mjs?url'; +import { getFilter, gainNode } from './helpers.mjs'; +import { map } from 'nanostores'; + +export const soundMap = map(); +export function registerSound(key, onTrigger, data = {}) { + soundMap.setKey(key, { onTrigger, data }); +} +export function getSound(s) { + return soundMap.get()[s]; +} +export const resetLoadedSounds = () => soundMap.set({}); + +let audioContext; +export const getAudioContext = () => { + if (!audioContext) { + audioContext = new AudioContext(); + } + return audioContext; +}; + +let destination; +const getDestination = () => { + const ctx = getAudioContext(); + if (!destination) { + destination = ctx.createGain(); + destination.connect(ctx.destination); + } + return destination; +}; + +export const panic = () => { + getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); + destination = null; +}; + +let workletsLoading; +function loadWorklets() { + if (workletsLoading) { + return workletsLoading; + } + workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl); + return workletsLoading; +} + +function getWorklet(ac, processor, params) { + const node = new AudioWorkletNode(ac, processor); + Object.entries(params).forEach(([key, value]) => { + node.parameters.get(key).value = value; + }); + return node; +} + +// this function should be called on first user interaction (to avoid console warning) +export async function initAudio() { + if (typeof window !== 'undefined') { + try { + await getAudioContext().resume(); + await loadWorklets(); + } catch (err) { + console.warn('could not load AudioWorklet effects coarse, crush and shape', err); + } + } +} + +export async function initAudioOnFirstClick() { + return new Promise((resolve) => { + document.addEventListener('click', async function listener() { + await initAudio(); + resolve(); + document.removeEventListener('click', listener); + }); + }); +} + +let delays = {}; +const maxfeedback = 0.98; +function getDelay(orbit, delaytime, delayfeedback, t) { + if (delayfeedback > maxfeedback) { + //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); + } + delayfeedback = clamp(delayfeedback, 0, 0.98); + if (!delays[orbit]) { + const ac = getAudioContext(); + const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); + dly.start?.(t); // for some reason, this throws when audion extension is installed.. + dly.connect(getDestination()); + delays[orbit] = dly; + } + delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); + delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t); + return delays[orbit]; +} + +let reverbs = {}; +function getReverb(orbit, duration = 2) { + if (!reverbs[orbit]) { + const ac = getAudioContext(); + const reverb = ac.createReverb(duration); + reverb.connect(getDestination()); + reverbs[orbit] = reverb; + } + if (reverbs[orbit].duration !== duration) { + reverbs[orbit] = reverbs[orbit].setDuration(duration); + reverbs[orbit].duration = duration; + } + return reverbs[orbit]; +} + +function effectSend(input, effect, wet) { + const send = gainNode(wet); + input.connect(send); + send.connect(effect); + return send; +} + +export const superdough = async (value, deadline, hapDuration, cps) => { + const ac = getAudioContext(); + if (typeof value !== 'object') { + throw new Error( + `expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`, + 'error', + ); + } + + // calculate absolute time + let t = ac.currentTime + deadline; + // destructure + let { + s = 'triangle', + bank, + source, + gain = 0.8, + // low pass + cutoff, + resonance = 1, + // high pass + hcutoff, + hresonance = 1, + // band pass + bandf, + bandq = 1, + // + coarse, + crush, + shape, + pan, + vowel, + delay = 0, + delayfeedback = 0.5, + delaytime = 0.25, + orbit = 1, + room, + size = 2, + velocity = 1, + } = value; + // const { velocity = 1 } = hap.context; + gain *= velocity; // legacy fix for velocity + let toDisconnect = []; // audio nodes that will be disconnected when the source has ended + const onended = () => { + toDisconnect.forEach((n) => n?.disconnect()); + }; + if (bank && s) { + s = `${bank}_${s}`; + } + // get source AudioNode + let sourceNode; + if (source) { + sourceNode = source(t, value, hapDuration); + } else if (getSound(s)) { + const { onTrigger } = getSound(s); + const soundHandle = await onTrigger(t, value, onended); + if (soundHandle) { + sourceNode = soundHandle.node; + soundHandle.stop(t + hapDuration); + } + } else { + throw new Error(`sound ${s} not found! Is it loaded?`); + } + if (!sourceNode) { + // if onTrigger does not return anything, we will just silently skip + // this can be used for things like speed(0) in the sampler + return; + } + if (ac.currentTime > t) { + // logger('[webaudio] skip hap: still loading', ac.currentTime - t); + return; + } + const chain = []; // audio nodes that will be connected to each other sequentially + chain.push(sourceNode); + + // gain stage + chain.push(gainNode(gain)); + + // filters + cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); + hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); + bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq)); + vowel !== undefined && chain.push(ac.createVowelFilter(vowel)); + + // effects + coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); + crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); + shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape })); + + // panning + if (pan !== undefined) { + const panner = ac.createStereoPanner(); + panner.pan.value = 2 * pan - 1; + chain.push(panner); + } + + // last gain + const post = gainNode(1); + chain.push(post); + post.connect(getDestination()); + + // delay + let delaySend; + if (delay > 0 && delaytime > 0 && delayfeedback > 0) { + const delyNode = getDelay(orbit, delaytime, delayfeedback, t); + delaySend = effectSend(post, delyNode, delay); + } + // reverb + let reverbSend; + if (room > 0 && size > 0) { + const reverbNode = getReverb(orbit, size); + reverbSend = effectSend(post, reverbNode, room); + } + + // connect chain elements together + chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); + + // toDisconnect = all the node that should be disconnected in onended callback + // this is crucial for performance + toDisconnect = chain.concat([delaySend, reverbSend]); +}; + +export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs new file mode 100644 index 00000000..73174f65 --- /dev/null +++ b/packages/superdough/synth.mjs @@ -0,0 +1,44 @@ +import { midiToFreq, noteToMidi } from './util.mjs'; +import { registerSound } from './superdough.mjs'; +import { getOscillator, gainNode, getEnvelope } from './helpers.mjs'; + +export function registerSynthSounds() { + ['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => { + registerSound( + wave, + (t, value, onended) => { + // destructure adsr here, because the default should be different for synths and samples + const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value; + let { n, note, freq } = value; + // with synths, n and note are the same thing + n = note || n || 36; + if (typeof n === 'string') { + n = noteToMidi(n); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof n === 'number') { + freq = midiToFreq(n); // + 48); + } + // maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default) + // make oscillator + const { node: o, stop } = getOscillator({ t, s: wave, freq }); + const g = gainNode(0.3); + // envelope + const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); + o.onended = () => { + o.disconnect(); + g.disconnect(); + onended(); + }; + return { + node: o.connect(g).connect(envelope), + stop: (releaseTime) => { + releaseEnvelope(releaseTime); + stop(releaseTime + release); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + }); +} diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs new file mode 100644 index 00000000..860aa9c1 --- /dev/null +++ b/packages/superdough/util.mjs @@ -0,0 +1,29 @@ +// currently duplicate with core util.mjs to skip dependency +// TODO: add separate util module? + +export const tokenizeNote = (note) => { + if (typeof note !== 'string') { + return []; + } + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || []; + if (!pc) { + return []; + } + return [pc, acc, oct ? Number(oct) : undefined]; +}; +const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; +const accs = { '#': 1, b: -1, s: 1, f: -1 }; + +export const noteToMidi = (note, defaultOctave = 3) => { + const [pc, acc, oct = defaultOctave] = tokenizeNote(note); + if (!pc) { + throw new Error('not a note: "' + note + '"'); + } + const chroma = chromas[pc.toLowerCase()]; + const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; + return (Number(oct) + 1) * 12 + chroma + offset; +}; +export const midiToFreq = (n) => { + return Math.pow(2, (n - 69) / 12) * 440; +}; +export const clamp = (num, min, max) => Math.min(Math.max(num, min), max); diff --git a/packages/superdough/vite.config.js b/packages/superdough/vite.config.js new file mode 100644 index 00000000..ccf68b35 --- /dev/null +++ b/packages/superdough/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import { dependencies } from './package.json'; +import { resolve } from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + build: { + lib: { + entry: resolve(__dirname, 'superdough.mjs'), + formats: ['es', 'cjs'], + fileName: (ext) => ({ es: 'superdough.mjs', cjs: 'superdough.cjs' }[ext]), + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs new file mode 100644 index 00000000..25357642 --- /dev/null +++ b/packages/superdough/vowel.mjs @@ -0,0 +1,38 @@ +// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597 +export var vowelFormant = { + a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] }, + e: { freqs: [440, 1800, 2700, 3000, 3300], gains: [1, 0.1995, 0.1259, 0.1, 0.1], qs: [70, 80, 100, 120, 120] }, + i: { freqs: [270, 1850, 2900, 3350, 3590], gains: [1, 0.0631, 0.0631, 0.0158, 0.0158], qs: [40, 90, 100, 120, 120] }, + o: { freqs: [430, 820, 2700, 3000, 3300], gains: [1, 0.3162, 0.0501, 0.0794, 0.01995], qs: [40, 80, 100, 120, 120] }, + u: { freqs: [370, 630, 2750, 3000, 3400], gains: [1, 0.1, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] }, +}; +if (typeof GainNode !== 'undefined') { + class VowelNode extends GainNode { + constructor(ac, letter) { + super(ac); + if (!vowelFormant[letter]) { + throw new Error('vowel: unknown vowel ' + letter); + } + const { gains, qs, freqs } = vowelFormant[letter]; + const makeupGain = ac.createGain(); + for (let i = 0; i < 5; i++) { + const gain = ac.createGain(); + gain.gain.value = gains[i]; + const filter = ac.createBiquadFilter(); + filter.type = 'bandpass'; + filter.Q.value = qs[i]; + filter.frequency.value = freqs[i]; + this.connect(filter); + filter.connect(gain); + gain.connect(makeupGain); + } + makeupGain.gain.value = 8; // how much makeup gain to add? + this.connect = (target) => makeupGain.connect(target); + return this; + } + } + + AudioContext.prototype.createVowelFilter = function (letter) { + return new VowelNode(this, letter); + }; +} diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs new file mode 100644 index 00000000..7bb43f87 --- /dev/null +++ b/packages/superdough/worklets.mjs @@ -0,0 +1,108 @@ +// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE +// all the credit goes to dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js +// <3 + +class CoarseProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [{ name: 'coarse', defaultValue: 1 }]; + } + + constructor() { + super(); + this.notStarted = true; + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + const coarse = parameters.coarse; + const blockSize = 128; + const hasInput = !(input[0] === undefined); + if (hasInput) { + this.notStarted = false; + output[0][0] = input[0][0]; + for (let n = 1; n < blockSize; n++) { + for (let o = 0; o < output.length; o++) { + output[o][n] = n % coarse == 0 ? input[0][n] : output[o][n - 1]; + } + } + } + return this.notStarted || hasInput; + } +} + +registerProcessor('coarse-processor', CoarseProcessor); + +class CrushProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [{ name: 'crush', defaultValue: 0 }]; + } + + constructor() { + super(); + this.notStarted = true; + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + const crush = parameters.crush; + const blockSize = 128; + const hasInput = !(input[0] === undefined); + if (hasInput) { + this.notStarted = false; + if (crush.length === 1) { + const x = Math.pow(2, crush[0] - 1); + for (let n = 0; n < blockSize; n++) { + const value = Math.round(input[0][n] * x) / x; + for (let o = 0; o < output.length; o++) { + output[o][n] = value; + } + } + } else { + for (let n = 0; n < blockSize; n++) { + let x = Math.pow(2, crush[n] - 1); + const value = Math.round(input[0][n] * x) / x; + for (let o = 0; o < output.length; o++) { + output[o][n] = value; + } + } + } + } + return this.notStarted || hasInput; + } +} +registerProcessor('crush-processor', CrushProcessor); + +class ShapeProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [{ name: 'shape', defaultValue: 0 }]; + } + + constructor() { + super(); + this.notStarted = true; + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + const shape0 = parameters.shape[0]; + const shape1 = shape0 < 1 ? shape0 : 1.0 - 4e-10; + const shape = (2.0 * shape1) / (1.0 - shape1); + const blockSize = 128; + const hasInput = !(input[0] === undefined); + if (hasInput) { + this.notStarted = false; + for (let n = 0; n < blockSize; n++) { + const value = ((1 + shape) * input[0][n]) / (1 + shape * Math.abs(input[0][n])); + for (let o = 0; o < output.length; o++) { + output[o][n] = value; + } + } + } + return this.notStarted || hasInput; + } +} + +registerProcessor('shape-processor', ShapeProcessor); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a1cd78d..86d83437 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -386,6 +386,26 @@ importers: specifier: ^4.3.3 version: 4.3.3(@types/node@18.16.3) + packages/superdough: + dependencies: + nanostores: + specifier: ^0.8.1 + version: 0.8.1 + devDependencies: + vite: + specifier: ^4.3.3 + version: 4.3.3(@types/node@18.16.3) + + packages/superdough/example: + dependencies: + superdough: + specifier: workspace:* + version: link:.. + devDependencies: + vite: + specifier: ^4.4.5 + version: 4.4.5(@types/node@18.16.3) + packages/tonal: dependencies: '@strudel.cycles/core': @@ -498,7 +518,7 @@ importers: version: 4.17.0 '@astrojs/mdx': specifier: ^0.19.0 - version: 0.19.0(astro@2.3.2)(rollup@3.21.0) + version: 0.19.0(astro@2.3.2)(rollup@3.28.0) '@astrojs/react': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@18.2.1)(@types/react@18.2.0)(react-dom@18.2.0)(react@18.2.0) @@ -628,7 +648,7 @@ importers: version: 3.0.3 vite-plugin-pwa: specifier: ^0.14.7 - version: 0.14.7(vite@4.3.3)(workbox-build@6.5.4)(workbox-window@6.5.4) + version: 0.14.7(vite@4.4.5)(workbox-build@6.5.4)(workbox-window@6.5.4) workbox-window: specifier: ^6.5.4 version: 6.5.4 @@ -849,14 +869,14 @@ packages: transitivePeerDependencies: - supports-color - /@astrojs/mdx@0.19.0(astro@2.3.2)(rollup@3.21.0): + /@astrojs/mdx@0.19.0(astro@2.3.2)(rollup@3.28.0): resolution: {integrity: sha512-McFpMV+npinIEKnY5t9hsdzLd76g78GgIRUPxem2OeXPNB8xr2pNS28GeU0+6Pn5STnB+sgcyyeqXLgzauOlMQ==} engines: {node: '>=16.12.0'} dependencies: '@astrojs/markdown-remark': 2.1.4(astro@2.3.2) '@astrojs/prism': 2.1.1 '@mdx-js/mdx': 2.3.0 - '@mdx-js/rollup': 2.3.0(rollup@3.21.0) + '@mdx-js/rollup': 2.3.0(rollup@3.28.0) acorn: 8.8.2 es-module-lexer: 1.2.1 estree-util-visit: 1.2.1 @@ -2438,6 +2458,15 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true optional: true /@esbuild/android-arm@0.17.18: @@ -2446,6 +2475,15 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true optional: true /@esbuild/android-x64@0.17.18: @@ -2454,6 +2492,15 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true optional: true /@esbuild/darwin-arm64@0.17.18: @@ -2462,6 +2509,15 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true optional: true /@esbuild/darwin-x64@0.17.18: @@ -2470,6 +2526,15 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true optional: true /@esbuild/freebsd-arm64@0.17.18: @@ -2478,6 +2543,15 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true optional: true /@esbuild/freebsd-x64@0.17.18: @@ -2486,6 +2560,15 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true optional: true /@esbuild/linux-arm64@0.17.18: @@ -2494,6 +2577,15 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-arm@0.17.18: @@ -2502,6 +2594,15 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-ia32@0.17.18: @@ -2510,6 +2611,15 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-loong64@0.17.18: @@ -2518,6 +2628,15 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-mips64el@0.17.18: @@ -2526,6 +2645,15 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-ppc64@0.17.18: @@ -2534,6 +2662,15 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-riscv64@0.17.18: @@ -2542,6 +2679,15 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-s390x@0.17.18: @@ -2550,6 +2696,15 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true optional: true /@esbuild/linux-x64@0.17.18: @@ -2558,6 +2713,15 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true optional: true /@esbuild/netbsd-x64@0.17.18: @@ -2566,6 +2730,15 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true optional: true /@esbuild/openbsd-x64@0.17.18: @@ -2574,6 +2747,15 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true optional: true /@esbuild/sunos-x64@0.17.18: @@ -2582,6 +2764,15 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true optional: true /@esbuild/win32-arm64@0.17.18: @@ -2590,6 +2781,15 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true optional: true /@esbuild/win32-ia32@0.17.18: @@ -2598,6 +2798,15 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true optional: true /@esbuild/win32-x64@0.17.18: @@ -2606,6 +2815,15 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.39.0): @@ -2927,14 +3145,14 @@ packages: - supports-color dev: false - /@mdx-js/rollup@2.3.0(rollup@3.21.0): + /@mdx-js/rollup@2.3.0(rollup@3.28.0): resolution: {integrity: sha512-wLvRfJS/M4UmdqTd+WoaySEE7q4BIejYf1xAHXYvtT1du/1Tl/z2450Gg2+Hu7fh05KwRRiehiTP9Yc/Dtn0fA==} peerDependencies: rollup: '>=2' dependencies: '@mdx-js/mdx': 2.3.0 - '@rollup/pluginutils': 5.0.2(rollup@3.21.0) - rollup: 3.21.0 + '@rollup/pluginutils': 5.0.2(rollup@3.28.0) + rollup: 3.28.0 source-map: 0.7.4 vfile: 5.3.6 transitivePeerDependencies: @@ -3612,7 +3830,7 @@ packages: rollup: 3.12.0 dev: true - /@rollup/pluginutils@5.0.2(rollup@3.21.0): + /@rollup/pluginutils@5.0.2(rollup@3.28.0): resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -3624,7 +3842,7 @@ packages: '@types/estree': 1.0.0 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 3.21.0 + rollup: 3.28.0 dev: false /@sigstore/protobuf-specs@0.1.0: @@ -4554,7 +4772,7 @@ packages: vite-plugin-pwa: ^0.14.0 dependencies: astro: 2.3.2(@types/node@18.16.3) - vite-plugin-pwa: 0.14.7(vite@4.3.3)(workbox-build@6.5.4)(workbox-window@6.5.4) + vite-plugin-pwa: 0.14.7(vite@4.4.5)(workbox-build@6.5.4)(workbox-window@6.5.4) dev: true /@vitejs/plugin-react@4.0.0(vite@4.3.3): @@ -5036,13 +5254,14 @@ packages: typescript: 4.9.4 unist-util-visit: 4.1.2 vfile: 5.3.6 - vite: 4.3.3(@types/node@18.16.3) - vitefu: 0.2.4(vite@4.3.3) + vite: 4.4.5(@types/node@18.16.3) + vitefu: 0.2.4(vite@4.4.5) yargs-parser: 21.1.1 zod: 3.21.4 transitivePeerDependencies: - '@types/node' - less + - lightningcss - sass - stylus - sugarss @@ -6478,6 +6697,36 @@ packages: '@esbuild/win32-arm64': 0.17.18 '@esbuild/win32-ia32': 0.17.18 '@esbuild/win32-x64': 0.17.18 + dev: true + + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} @@ -10854,6 +11103,14 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postcss@8.4.27: + resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + /prebuild-install@7.1.1: resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} engines: {node: '>=10'} @@ -11658,6 +11915,14 @@ packages: hasBin: true optionalDependencies: fsevents: 2.3.2 + dev: true + + /rollup@3.28.0: + resolution: {integrity: sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} @@ -13063,10 +13328,11 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.3.3(@types/node@18.16.3) + vite: 4.4.5(@types/node@18.16.3) transitivePeerDependencies: - '@types/node' - less + - lightningcss - sass - stylus - sugarss @@ -13074,7 +13340,7 @@ packages: - terser dev: true - /vite-plugin-pwa@0.14.7(vite@4.3.3)(workbox-build@6.5.4)(workbox-window@6.5.4): + /vite-plugin-pwa@0.14.7(vite@4.4.5)(workbox-build@6.5.4)(workbox-window@6.5.4): resolution: {integrity: sha512-dNJaf0fYOWncmjxv9HiSa2xrSjipjff7IkYE5oIUJ2x5HKu3cXgA8LRgzOwTc5MhwyFYRSU0xyN0Phbx3NsQYw==} peerDependencies: vite: ^3.1.0 || ^4.0.0 @@ -13086,7 +13352,7 @@ packages: fast-glob: 3.2.12 pretty-bytes: 6.1.0 rollup: 3.12.0 - vite: 4.3.3(@types/node@18.16.3) + vite: 4.4.5(@types/node@18.16.3) workbox-build: 6.5.4 workbox-window: 6.5.4 transitivePeerDependencies: @@ -13124,8 +13390,44 @@ packages: rollup: 3.21.0 optionalDependencies: fsevents: 2.3.2 + dev: true - /vitefu@0.2.4(vite@4.3.3): + /vite@4.4.5(@types/node@18.16.3): + resolution: {integrity: sha512-4m5kEtAWHYr0O1Fu7rZp64CfO1PsRGZlD3TAB32UmQlpd7qg15VF7ROqGN5CyqN7HFuwr7ICNM2+fDWRqFEKaA==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.16.3 + esbuild: 0.18.20 + postcss: 8.4.27 + rollup: 3.28.0 + optionalDependencies: + fsevents: 2.3.2 + + /vitefu@0.2.4(vite@4.4.5): resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} peerDependencies: vite: ^3.0.0 || ^4.0.0 @@ -13133,7 +13435,7 @@ packages: vite: optional: true dependencies: - vite: 4.3.3(@types/node@18.16.3) + vite: 4.4.5(@types/node@18.16.3) /vitest@0.33.0(@vitest/ui@0.28.0): resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} @@ -13193,6 +13495,7 @@ packages: why-is-node-running: 2.2.2 transitivePeerDependencies: - less + - lightningcss - sass - stylus - sugarss diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 984f2447..615475e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,3 +6,4 @@ packages: - "packages/core/examples/vite-vanilla-repl-cm6" - "packages/react/examples/nano-repl" - "packages/web/examples/repl-example" + - "packages/superdough/example"