diff --git a/packages/superdough/README.md b/packages/superdough/README.md new file mode 100644 index 00000000..4d670ac6 --- /dev/null +++ b/packages/superdough/README.md @@ -0,0 +1,165 @@ +# 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, initAudioOnFirstClick, registerSynthSounds } from 'superdough'; + +const init = Promise.all([ + initAudioOnFirstClick(), + samples('github:tidalcycles/Dirt-Samples/master'), + registerSynthSounds(), +]); + +const loop = (t = 0) => { + // superdough(value, time, duration) + 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); +}; + +document.getElementById('play').addEventListener('click', async () => { + await init; + let t = 0.1; + while (t < 16) { + loop(t++); + } +}); +``` + +[Open this in Codesandbox](https://codesandbox.io/s/superdough-demo-forked-sf8djh?file=/src/index.js) + +## API + +### superdough(value, deadline, duration) + +```js +superdough({ s: 'bd', delay: 0.5 }, 0, 1); +``` + +- `value`: the sound properties: + - `s`: the name of the sound as loaded via `samples` or `registerSound` + - `n`: selects sample with given index + - `bank`: prefix_ that is attached to the sound, e.g. `{ s: 'bd', bank: 'RolandTR909' }` = `{ s: 'RolandTR909_bd' }` + - `gain`: gain from 0 to 1 (higher values also work but might clip) + - `velocity`: additional gain multiplier + - `cutoff`: low pass filter cutoff + - `resonance`: low pass filter resonance + - `hcutoff`: high pass filter cutoff + - `hresonance`: high pass filter resonance + - `bandf`: band pass filter cutoff + - `bandq`: band pass filter resonance + - `crush`: amplitude bit crusher using given number of bits + - `shape`: distortion effect from 0 (none) to 1 (full). might get loud! + - `pan`: stereo panning from 0 (left) to 1 (right) + - `vowel`: vowel filter. possible values: "a", "e", "i", "o", "u" + - `delay`: delay mix + - `delayfeedback`: delay feedback + - `delaytime`: delay time + - `room`: reverb mix + - `size`: reverb room size + - `orbit`: bus name for global effects `delay` and `room`. same orbits will get the same effects + - `freq`: repitches sound to given frequency in Hz + - `note`: repitches sound to given note or midi number + - `cut`: sets cut group. Sounds of same group will cut each other off + - `clip`: multiplies duration with given number + - `speed`: repitches sound by given factor + - `begin`: moves beginning of sample to given factor (between 0 and 1) + - `end`: moves end of sample to given factor (between 0 and 1) + - `attack`: seconds of attack phase + - `decay`: seconds of decay phase + - `sustain`: gain of sustain phase + - `release`: seconds of release phase +- `deadline`: seconds until the sound should play (0 = immediate) +- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds + +### registerSynthSounds() + +Loads the default waveforms `sawtooth`, `square`, `triangle` and `sine`. Use them like this: + +```js +superdough({ s:'sawtooth' }, 0, 1) +``` + +The duration needs to be set for these sounds! + +### samples(sampleMap) + +allows you to load samples from URLs. There are 3 ways to load samples + +1. sample map object +2. url of sample map json file +3. github repo + +#### sample map object + +You can pass a sample map like this: + +```js +samples({ + '_base': 'https://raw.githubusercontent.com/felixroos/samples/main/', + 'bd': 'president/president_bd.mp3', + 'sd': ['president/president_sd.mp3', 'president/president_sd2.mp3'], + 'hh': ['president/president_hh.mp3'], +}) +``` + +The `_base` property defines the root url while the others declare one or more sample paths for each sound. + +For example the full URL for `bd` would then be `https://raw.githubusercontent.com/felixroos/samples/main/president/president_bd.mp3` + +A loaded sound can then be played with `superdough({ s: 'bd' }, 0)`. + +If you declare multiple sounds, you can select them with `n`: `superdough({ s: 'sd', n: 1 }, 0)` + +The duration property is not needed for samples. + +#### loading samples from a json file + +Instead of passing an object as a sample map, you can also pass a URL to a json that contains a sample map: + +```js +samples('https://raw.githubusercontent.com/felixroos/samples/main/strudel.json') +``` + +The json file is expected to have the same format as described above. + +#### loading samples from a github repo + +Because it is common to use github for samples, there is a short way to load a sample map from github: + +```js +samples('github:tidalcycles/Dirt-Samples/master') +``` + +The format is `github://`. + +It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo. + +The format is also expected to be the same as explained above. + +### initAudioOnFirstClick() + +Initializes audio and makes sure it is playable after the first click in the document. A click is needed because of the [Autoplay Policy](https://www.w3.org/TR/autoplay-detection/). +You can call this function when the document loads. +Then just make sure your first call of `superdough` happens after a click of something. + +## Credits + +- [SuperDirt](https://github.com/musikinformatik/SuperDirt) +- [WebDirt](https://github.com/dktr0/WebDirt) 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..7b74fb40 --- /dev/null +++ b/packages/superdough/example/main.js @@ -0,0 +1,25 @@ +import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough'; + +const init = Promise.all([ + initAudioOnFirstClick(), + samples('github:tidalcycles/Dirt-Samples/master'), + registerSynthSounds(), +]); + +const loop = (t = 0) => { + // superdough(value, time, duration) + 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); +}; + +document.getElementById('play').addEventListener('click', async () => { + await init; + 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/webaudio/feedbackdelay.mjs b/packages/superdough/feedbackdelay.mjs similarity index 100% rename from packages/webaudio/feedbackdelay.mjs rename to packages/superdough/feedbackdelay.mjs diff --git a/packages/webaudio/helpers.mjs b/packages/superdough/helpers.mjs similarity index 98% rename from packages/webaudio/helpers.mjs rename to packages/superdough/helpers.mjs index 108dfb02..7cc54c8d 100644 --- a/packages/webaudio/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './webaudio.mjs'; +import { getAudioContext } from './superdough.mjs'; export function gainNode(value) { const node = getAudioContext().createGain(); diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs new file mode 100644 index 00000000..b795539a --- /dev/null +++ b/packages/superdough/index.mjs @@ -0,0 +1,11 @@ +/* +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'; +export * from './logger.mjs'; diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs new file mode 100644 index 00000000..a20af1b3 --- /dev/null +++ b/packages/superdough/logger.mjs @@ -0,0 +1,7 @@ +let log = (msg) => console.log(msg); + +export const logger = (...args) => log(...args); + +export const setLogger = (fn) => { + log = fn; +}; diff --git a/packages/superdough/package.json b/packages/superdough/package.json new file mode 100644 index 00000000..7f307e40 --- /dev/null +++ b/packages/superdough/package.json @@ -0,0 +1,41 @@ +{ + "name": "superdough", + "version": "0.9.3", + "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/webaudio/reverb.mjs b/packages/superdough/reverb.mjs similarity index 100% rename from packages/webaudio/reverb.mjs rename to packages/superdough/reverb.mjs diff --git a/packages/webaudio/sampler.mjs b/packages/superdough/sampler.mjs similarity index 99% rename from packages/webaudio/sampler.mjs rename to packages/superdough/sampler.mjs index 17275a99..02e5eada 100644 --- a/packages/webaudio/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,6 +1,7 @@ -import { logger, noteToMidi, valueToMidi } from '@strudel.cycles/core'; +import { noteToMidi, valueToMidi } from './util.mjs'; import { getAudioContext, registerSound } from './index.mjs'; import { getEnvelope } from './helpers.mjs'; +import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise const loadCache = {}; // string: Promise diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs new file mode 100644 index 00000000..1279000c --- /dev/null +++ b/packages/superdough/superdough.mjs @@ -0,0 +1,252 @@ +/* +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'; +import { logger } from './logger.mjs'; + +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(options = {}) { + const { disableWorklets = false } = options; + if (typeof window !== 'undefined') { + await getAudioContext().resume(); + if (!disableWorklets) { + await loadWorklets().catch((err) => { + console.warn('could not load AudioWorklet effects coarse, crush and shape', err); + }); + } else { + console.log('disableWorklets: AudioWorklet effects coarse, crush and shape are skipped!'); + } + } +} + +export async function initAudioOnFirstClick(options) { + return new Promise((resolve) => { + document.addEventListener('click', async function listener() { + await initAudio(options); + 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) => { + 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; + 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/webaudio/synth.mjs b/packages/superdough/synth.mjs similarity index 93% rename from packages/webaudio/synth.mjs rename to packages/superdough/synth.mjs index 242c8fb4..73174f65 100644 --- a/packages/webaudio/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,5 +1,5 @@ -import { midiToFreq, noteToMidi } from '@strudel.cycles/core'; -import { registerSound } from './webaudio.mjs'; +import { midiToFreq, noteToMidi } from './util.mjs'; +import { registerSound } from './superdough.mjs'; import { getOscillator, gainNode, getEnvelope } from './helpers.mjs'; export function registerSynthSounds() { diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs new file mode 100644 index 00000000..db056376 --- /dev/null +++ b/packages/superdough/util.mjs @@ -0,0 +1,53 @@ +// 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); + +export const freqToMidi = (freq) => { + return (12 * Math.log(freq / 440)) / Math.LN2 + 69; +}; + +export const valueToMidi = (value, fallbackValue) => { + if (typeof value !== 'object') { + throw new Error('valueToMidi: expected object value'); + } + let { freq, note } = value; + if (typeof freq === 'number') { + return freqToMidi(freq); + } + if (typeof note === 'string') { + return noteToMidi(note); + } + if (typeof note === 'number') { + return note; + } + if (!fallbackValue) { + throw new Error('valueToMidi: expected freq or note to be set'); + } + return fallbackValue; +}; diff --git a/packages/superdough/vite.config.js b/packages/superdough/vite.config.js new file mode 100644 index 00000000..d6786f44 --- /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, 'index.mjs'), + formats: ['es', 'cjs'], + fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.cjs' }[ext]), + }, + rollupOptions: { + external: [...Object.keys(dependencies)], + }, + target: 'esnext', + }, +}); diff --git a/packages/webaudio/vowel.mjs b/packages/superdough/vowel.mjs similarity index 100% rename from packages/webaudio/vowel.mjs rename to packages/superdough/vowel.mjs diff --git a/packages/webaudio/worklets.mjs b/packages/superdough/worklets.mjs similarity index 100% rename from packages/webaudio/worklets.mjs rename to packages/superdough/worklets.mjs diff --git a/packages/webaudio/README.md b/packages/webaudio/README.md index 4f2f8865..3eb64593 100644 --- a/packages/webaudio/README.md +++ b/packages/webaudio/README.md @@ -1,6 +1,7 @@ # @strudel.cycles/webaudio This package contains helpers to make music with strudel and the Web Audio API. +It is a thin binding to [superdough](https://www.npmjs.com/package/superdough). ## Install diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 2ab74c72..563a367e 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -5,6 +5,4 @@ This program is free software: you can redistribute it and/or modify it under th */ export * from './webaudio.mjs'; -export * from './sampler.mjs'; -export * from './helpers.mjs'; -export * from './synth.mjs'; +export * from 'superdough'; diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 0dc58877..edd53cbd 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -35,7 +35,7 @@ "homepage": "https://github.com/tidalcycles/strudel#readme", "dependencies": { "@strudel.cycles/core": "workspace:*", - "nanostores": "^0.8.1" + "superdough": "workspace:*" }, "devDependencies": { "vite": "^4.3.3" diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c5922303..8b32a90c 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,247 +5,21 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel.cycles/core'; -import './feedbackdelay.mjs'; -import './reverb.mjs'; +import { superdough, getAudioContext, setLogger } from 'superdough'; const { Pattern, logger } = strudel; -import './vowel.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({}); +setLogger(logger); -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 = strudel.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 webaudioOutput = async (t, hap, ct, cps) => { -export const webaudioOutput = async (hap, deadline, hapDuration, cps) => { - const ac = getAudioContext(); +const hap2value = (hap) => { hap.ensureObjectValue(); - - // 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, - } = hap.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, hap.value, hapDuration); - } else if (getSound(s)) { - const { onTrigger } = getSound(s); - const soundHandle = await onTrigger(t, hap.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]); + return { ...hap.value, velocity: hap.context.velocity }; }; -export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps, cps); +// TODO: bind logger +export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); +export const webaudioOutput = (hap, deadline, hapDuration) => superdough(hap2value(hap), deadline, hapDuration); Pattern.prototype.webaudio = function () { - // TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ? return this.onTrigger(webaudioOutputTrigger); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a1cd78d..643a4766 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': @@ -470,9 +490,9 @@ importers: '@strudel.cycles/core': specifier: workspace:* version: link:../core - nanostores: - specifier: ^0.8.1 - version: 0.8.1 + superdough: + specifier: workspace:* + version: link:../superdough devDependencies: vite: specifier: ^4.3.3 @@ -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"