diff --git a/eslint.config.mjs b/eslint.config.mjs index c9ff40ca1..e30b8e8a6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,7 +42,6 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', - 'packages/supradough/dough-export.mjs', '**/samples', ], }, diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 473ea82d1..58a30652c 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -309,17 +309,6 @@ export const { fmvelocity } = registerControl('fmvelocity'); */ export const { bank } = registerControl('bank'); -/** - * mix control for the chorus effect - * - * @name chorus - * @param {string | Pattern} chorus mix amount between 0 and 1 - * @example - * note("d d a# a").s("sawtooth").chorus(.5) - * - */ -export const { chorus } = registerControl('chorus'); - // analyser node send amount 0 - 1 (used by scope) export const { analyze } = registerControl('analyze'); // fftSize of analyser @@ -1108,27 +1097,14 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback'] * */ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb'); - -/** - * Sets the level of the signal that is fed back into the delay. - * Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it - * - * @name delayfeedback - * @param {number | Pattern} feedback between 0 and 1 - * @synonyms delayfb, dfb - * @example - * s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>") - * - */ -export const { delayspeed } = registerControl('delayspeed'); /** * Sets the time of the delay effect. * - * @name delayspeed - * @param {number | Pattern} delayspeed controls the pitch of the delay feedback + * @name delaytime + * @param {number | Pattern} seconds between 0 and Infinity * @synonyms delayt, dt * @example - * note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>") + * s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>") * */ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 9188c17c3..18d1b7797 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -196,52 +196,6 @@ function getSamplesPrefixHandler(url) { return; } -export async function fetchSampleMap(url) { - // check if custom prefix handler - const handler = getSamplesPrefixHandler(url); - if (handler) { - return handler(url); - } - url = resolveSpecialPaths(url); - if (url.startsWith('github:')) { - url = githubPath(url, 'strudel.json'); - } - if (url.startsWith('local:')) { - url = `http://localhost:5432`; - } - if (url.startsWith('shabda:')) { - let [_, path] = url.split('shabda:'); - url = `https://shabda.ndre.gr/${path}.json?strudel=1`; - } - if (url.startsWith('shabda/speech')) { - let [_, path] = url.split('shabda/speech'); - path = path.startsWith('/') ? path.substring(1) : path; - let [params, words] = path.split(':'); - let gender = 'f'; - let language = 'en-GB'; - if (params) { - [language, gender] = params.split('/'); - } - url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; - } - if (typeof fetch !== 'function') { - // not a browser - return; - } - const base = url.split('/').slice(0, -1).join('/'); - if (typeof fetch === 'undefined') { - // skip fetch when in node / testing - return; - } - const json = await fetch(url) - .then((res) => res.json()) - .catch((error) => { - console.error(error); - throw new Error(`error loading "${url}"`); - }); - return [json, json._base || base]; -} - /** * Loads a collection of samples to use with `s` * @example @@ -263,8 +217,49 @@ export async function fetchSampleMap(url) { export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { if (typeof sampleMap === 'string') { - const [json, base] = await fetchSampleMap(sampleMap); - return samples(json, baseUrl || base, options); + // check if custom prefix handler + const handler = getSamplesPrefixHandler(sampleMap); + if (handler) { + return handler(sampleMap); + } + sampleMap = resolveSpecialPaths(sampleMap); + if (sampleMap.startsWith('github:')) { + sampleMap = githubPath(sampleMap, 'strudel.json'); + } + if (sampleMap.startsWith('local:')) { + sampleMap = `http://localhost:5432`; + } + if (sampleMap.startsWith('shabda:')) { + let [_, path] = sampleMap.split('shabda:'); + sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`; + } + if (sampleMap.startsWith('shabda/speech')) { + let [_, path] = sampleMap.split('shabda/speech'); + path = path.startsWith('/') ? path.substring(1) : path; + let [params, words] = path.split(':'); + let gender = 'f'; + let language = 'en-GB'; + if (params) { + [language, gender] = params.split('/'); + } + sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; + } + 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( diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0c084e3e9..792e5a28d 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -196,7 +196,7 @@ export const resetLoadedSounds = () => soundMap.set({}); let audioContext; export const setDefaultAudioContext = () => { - audioContext = new AudioContext({ latencyHint: 'playback' }); + audioContext = new AudioContext(); return audioContext; }; @@ -212,17 +212,11 @@ export function getAudioContextCurrentTime() { return getAudioContext().currentTime; } -let externalWorklets = []; -export function registerWorklet(url) { - externalWorklets.push(url); -} - let workletsLoading; function loadWorklets() { if (!workletsLoading) { const audioCtx = getAudioContext(); - const allWorkletURLs = externalWorklets.concat([workletsUrl]); - workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))); + workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl); } return workletsLoading; diff --git a/packages/supradough/.gitignore b/packages/supradough/.gitignore deleted file mode 100644 index d21cbdf3e..000000000 --- a/packages/supradough/.gitignore +++ /dev/null @@ -1 +0,0 @@ -pattern.wav diff --git a/packages/supradough/README.md b/packages/supradough/README.md deleted file mode 100644 index a8cfa84b3..000000000 --- a/packages/supradough/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# supradough - -platform agnostic synth and sampler intended for live coding. a reimplementation of superdough. \ No newline at end of file diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs deleted file mode 100644 index bd4b530b3..000000000 --- a/packages/supradough/dough-export.mjs +++ /dev/null @@ -1,123 +0,0 @@ -// this is a poc of how a pattern can be rendered as a wav file using node -// run via: node dough-export.mjs -import fs from 'node:fs'; -import WavEncoder from 'wav-encoder'; -import { evalScope } from '@strudel/core'; -import { miniAllStrings } from '@strudel/mini'; -import { Dough } from './dough.mjs'; - -await evalScope( - import('@strudel/core'), - import('@strudel/mini'), - import('@strudel/tonal'), - // import('@strudel/tonal'), -); - -miniAllStrings(); // allows using single quotes for mini notation / skip transpilation - -let sampleRate = 48000, - cps = 0.4; - -/* await doughsamples('github:eddyflux/crate'); -await doughsamples('github:eddyflux/wax'); */ - -let pat = note('c,eb,g,') - .s('sine') - .press() - .add(note(24)) - .fmi(3) - .fmh(5.01) - .dec(0.4) - .delay('.6:<.12 .22>:.8') - .jux(press) - .rarely(add(note('12'))) - .lpf(400) - .lpq(0.2) - .lpd(0.4) - .lpenv(3) - .fmdecay(0.4) - .fmenv(1) - .postgain(0.6) - .stack(s('*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2)) - .stack( - s('[- white@3]*2') - .dec(0.4) - .hpf('<2000!3 <4000 8000>>*4') - .hpq(0.6) - .ply('<1 2>*4') - .postgain(0.5) - .delay(0.5) - .jux(rev) - .lpf(5000), - ) - .stack( - note('*2') - .s('square') - .lpf(sine.range(100, 300).slow(4)) - .lpe(1) - .segment(8) - .lpd(0.3) - .lpq(0.2) - .dec(0.2) - .speed('<1 2>') - .ply('<1 2>') - .postgain(1), - ) - .stack( - chord('') - .voicing() - .s('') - .clip(1) - .rel(0.4) - .vib('4:.2') - .gain(0.7) - .hpf(1200) - .fm(0.5) - .att(1) - .lpa(0.5) - .lpf(200) - .lpenv(4) - .chorus(0.8), - ) - .slow(1 / cps); - -let cycles = 30; -let seconds = cycles + 1; // 1s release tail -const haps = pat.queryArc(0, cycles); - -const dough = new Dough(sampleRate); - -console.log('spawn voices...'); -haps.forEach((hap) => { - hap.value._begin = Number(hap.whole.begin); - hap.value._duration = hap.duration /* / cps */; - dough.scheduleSpawn(hap.value); -}); -console.log(`render ${seconds}s long buffer, each dot is 1 second:`); -const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)]; -let t = performance.now(); -while (dough.t <= buffers[0].length) { - dough.update(); - buffers[0][dough.t] = dough.out[0]; - buffers[1][dough.t] = dough.out[1]; - if (dough.t % sampleRate === 0) { - process.stdout.write('.'); - } -} -const took = (performance.now() - t) / 1000; -const load = (took / seconds) * 100; -const speed = (seconds / took).toFixed(2); -console.log(''); -console.log(`done! -rendered ${seconds}s in ${took.toFixed(2)}s -speed: ${speed}x -load: ${load.toFixed(2)}%`); - -const patternAudio = { - sampleRate, - channelData: buffers, -}; - -WavEncoder.encode(patternAudio).then((buffer) => { - fs.writeFileSync('pattern.wav', new Float32Array(buffer)); -}); diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs deleted file mode 100644 index 18c316b2d..000000000 --- a/packages/supradough/dough-worklet.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { Dough } from './dough.mjs'; - -const clamp = (num, min, max) => Math.min(Math.max(num, min), max); - -class DoughProcessor extends AudioWorkletProcessor { - constructor() { - super(); - this.dough = new Dough(sampleRate, currentTime); - this.port.onmessage = (event) => { - if (event.data.spawn) { - this.dough.scheduleSpawn(event.data.spawn); - } else if (event.data.sample) { - this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate); - } else if (event.data.samples) { - event.data.samples.forEach(([name, channels, sampleRate]) => { - this.dough.loadSample(name, channels, sampleRate); - }); - } else { - console.log('unrecognized event type', event.data); - } - }; - } - process(inputs, outputs, params) { - if (this.disconnected) { - return false; - } - const output = outputs[0]; - for (let i = 0; i < output[0].length; i++) { - this.dough.update(); - for (let c = 0; c < output.length; c++) { - //prevent speaker blowout via clipping if threshold exceeds - output[c][i] = clamp(this.dough.out[c], -1, 1); - } - } - return true; // keep the audio processing going - } -} - -registerProcessor('dough-processor', DoughProcessor); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs deleted file mode 100644 index 711922b63..000000000 --- a/packages/supradough/dough.mjs +++ /dev/null @@ -1,976 +0,0 @@ -// this is dough, the superdough without dependencies -const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; -const PI_DIV_SR = Math.PI / SAMPLE_RATE; -const ISR = 1 / SAMPLE_RATE; - -let gainCurveFunc = (val) => Math.pow(val, 2); - -function applyGainCurve(val) { - return gainCurveFunc(val); -} - -/** - * Equal Power Crossfade function. - * Smoothly transitions between signals A and B, maintaining consistent perceived loudness. - * - * @param {number} a - Signal A (can be a single value or an array value in buffer processing). - * @param {number} b - Signal B (can be a single value or an array value in buffer processing). - * @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix). - * @returns {number} Crossfaded output value. - */ -function crossfade(a, b, m) { - const aGain = Math.sin((1 - m) * 0.5 * Math.PI); - const bGain = Math.sin(m * 0.5 * Math.PI); - return a * aGain + b * bGain; -} - -// function setGainCurve(newGainCurveFunc) { -// gainCurveFunc = newGainCurveFunc; -// } -// https://garten.salat.dev/audio-DSP/oscillators.html -export class SineOsc { - phase = 0; - update(freq) { - const value = Math.sin(this.phase * 2 * Math.PI); - this.phase = (this.phase + freq / SAMPLE_RATE) % 1; - return value; - } -} - -export class ZawOsc { - phase = 0; - update(freq) { - this.phase += ISR * freq; - return (this.phase % 1) * 2 - 1; - } -} - -function polyBlep(t, dt) { - // 0 <= t < 1 - if (t < dt) { - t /= dt; - // 2 * (t - t^2/2 - 0.5) - return t + t - t * t - 1; - } - // -1 < t < 0 - if (t > 1 - dt) { - t = (t - 1) / dt; - // 2 * (t^2/2 + t + 0.5) - return t * t + t + t + 1; - } - // 0 otherwise - return 0; -} - -export class SawOsc { - constructor(props = {}) { - this.phase = props.phase ?? 0; - } - update(freq) { - const dt = freq / SAMPLE_RATE; - let p = polyBlep(this.phase, dt); - let s = 2 * this.phase - 1 - p; - this.phase += dt; - if (this.phase > 1) { - this.phase -= 1; - } - return s; - } -} - -function getUnisonDetune(unison, detune, voiceIndex) { - if (unison < 2) { - return 0; - } - const lerp = (a, b, n) => { - return n * (b - a) + a; - }; - return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); -} -function applySemitoneDetuneToFrequency(frequency, detune) { - return frequency * Math.pow(2, detune / 12); -} -export class SupersawOsc { - constructor(props = {}) { - //TODO: figure out a good way to pass in these params - this.voices = props.voices ?? 5; - this.freqspread = props.freqspread ?? 0.2; - this.panspread = props.panspread ?? 0.4; - this.phase = new Float32Array(this.voices).map(() => Math.random()); - } - update(freq) { - const gain1 = Math.sqrt(1 - this.panspread); - const gain2 = Math.sqrt(this.panspread); - let sl = 0; - let sr = 0; - for (let n = 0; n < this.voices; n++) { - const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n)); - const dt = freqAdjusted / SAMPLE_RATE; - const isOdd = (n & 1) == 1; - let gainL = gain1; - let gainR = gain2; - // invert right and left gain - if (isOdd) { - gainL = gain2; - gainR = gain1; - } - let p = polyBlep(this.phase[n], dt); - let s = 2 * this.phase[n] - 1 - p; - sl = sl + s * gainL; - sr = sr + s * gainL; - - this.phase[n] += dt; - if (this.phase[n] > 1) { - this.phase[n] -= 1; - } - } - - return sl + sr; - //TODO: make stereo - // return [sl, sr]; - } -} - -export class TriOsc { - phase = 0; - update(freq) { - this.phase += ISR * freq; - let phase = this.phase % 1; - let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5); - return value * 2 - 1; - } -} - -export class TwoPoleFilter { - s0 = 0; - s1 = 0; - update(s, cutoff, resonance = 0) { - // Out of bound values can produce NaNs - resonance = Math.max(resonance, 0); - - cutoff = Math.min(cutoff, 20000); - const c = 2 * Math.sin(cutoff * PI_DIV_SR); - - const r = Math.pow(0.5, (resonance + 0.125) / 0.125); - const mrc = 1 - r * c; - - this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf - this.s1 = mrc * this.s1 + c * this.s0; // lpf - return this.s1; // return lpf by default - } -} - -class PulseOsc { - constructor(phase = 0) { - this.phase = phase; - } - saw(offset, dt) { - let phase = (this.phase + offset) % 1; - let p = polyBlep(phase, dt); - return 2 * phase - 1 - p; - } - update(freq, pw = 0.5) { - const dt = freq / SAMPLE_RATE; - let pulse = this.saw(0, dt) - this.saw(pw, dt); - this.phase = (this.phase + dt) % 1; - return pulse + pw * 2 - 1; - } -} - -// non bandlimited (has aliasing) -export class PulzeOsc { - phase = 0; - update(freq, duty = 0.5) { - this.phase += ISR * freq; - let cyclePos = this.phase % 1; - return cyclePos < duty ? 1 : -1; - } -} - -export class Dust { - update = (density) => (Math.random() < density * ISR ? Math.random() : 0); -} - -export class WhiteNoise { - update() { - return Math.random() * 2 - 1; - } -} - -export class BrownNoise { - constructor() { - this.out = 0; - } - update() { - let white = Math.random() * 2 - 1; - this.out = (this.out + 0.02 * white) / 1.02; - return this.out; - } -} - -export class PinkNoise { - constructor() { - this.b0 = 0; - this.b1 = 0; - this.b2 = 0; - this.b3 = 0; - this.b4 = 0; - this.b5 = 0; - this.b6 = 0; - } - - update() { - const white = Math.random() * 2 - 1; - - this.b0 = 0.99886 * this.b0 + white * 0.0555179; - this.b1 = 0.99332 * this.b1 + white * 0.0750759; - this.b2 = 0.969 * this.b2 + white * 0.153852; - this.b3 = 0.8665 * this.b3 + white * 0.3104856; - this.b4 = 0.55 * this.b4 + white * 0.5329522; - this.b5 = -0.7616 * this.b5 - white * 0.016898; - - const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; - this.b6 = white * 0.115926; - - return pink * 0.11; - } -} - -export class Impulse { - phase = 1; - update(freq) { - this.phase += ISR * freq; - let v = this.phase >= 1 ? 1 : 0; - this.phase = this.phase % 1; - return v; - } -} - -export class ClockDiv { - inSgn = true; - outSgn = true; - clockCnt = 0; - update(clock, factor) { - let curSgn = clock > 0; - if (this.inSgn != curSgn) { - this.clockCnt++; - if (this.clockCnt >= factor) { - this.clockCnt = 0; - this.outSgn = !this.outSgn; - } - } - - this.inSgn = curSgn; - return this.outSgn ? 1 : -1; - } -} - -export class Hold { - value = 0; - trigSgn = false; - update(input, trig) { - if (!this.trigSgn && trig > 0) this.value = input; - this.trigSgn = trig > 0; - return this.value; - } -} - -function lerp(x, y0, y1, exponent = 1) { - if (x <= 0) return y0; - if (x >= 1) return y1; - - let curvedX; - - if (exponent === 0) { - curvedX = x; // linear - } else if (exponent > 0) { - curvedX = Math.pow(x, exponent); // ease-in - } else { - curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out - } - - return y0 + (y1 - y0) * curvedX; -} - -export class ADSR { - constructor(props = {}) { - this.state = 'off'; - this.startTime = 0; - this.startVal = 0; - this.decayCurve = props.decayCurve ?? 1; - } - - update(curTime, gate, attack, decay, susVal, release) { - switch (this.state) { - case 'off': { - if (gate > 0) { - this.state = 'attack'; - this.startTime = curTime; - this.startVal = 0; - } - return 0; - } - case 'attack': { - let time = curTime - this.startTime; - if (time > attack) { - this.state = 'decay'; - this.startTime = curTime; - return 1; - } - return lerp(time / attack, this.startVal, 1, 1); - } - case 'decay': { - let time = curTime - this.startTime; - let curVal = lerp(time / decay, 1, susVal, -this.decayCurve); - if (gate <= 0) { - this.state = 'release'; - this.startTime = curTime; - this.startVal = curVal; - return curVal; - } - if (time > decay) { - this.state = 'sustain'; - this.startTime = curTime; - return susVal; - } - return curVal; - } - case 'sustain': { - if (gate <= 0) { - this.state = 'release'; - this.startTime = curTime; - this.startVal = susVal; - } - return susVal; - } - case 'release': { - let time = curTime - this.startTime; - - if (time > release) { - this.state = 'off'; - return 0; - } - let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve); - if (gate > 0) { - this.state = 'attack'; - this.startTime = curTime; - this.startVal = curVal; - } - return curVal; - } - } - throw 'invalid envelope state'; - } -} - -/* - impulse(1).ad(.1).mul(sine(200)) -.add(x=>x.delay(.1).mul(.8)) -.out()*/ -const MAX_DELAY_TIME = 10; -export class PitchDelay { - lpf = new TwoPoleFilter(); - constructor(_props = {}) { - this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); - this.writeIdx = 0; - this.readIdx = 0; - this.numSamples = 0; - } - write(s, delayTime) { - // Calculate how far in the past to read - this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); - this.writeIdx = (this.writeIdx + 1) % this.numSamples; - this.buffer[this.writeIdx] = s; - this.readIdx = this.writeIdx - this.numSamples + 1; - - // If past the start of the buffer, wrap around (Q: is this possible?) - if (this.readIdx < 0) this.readIdx += this.numSamples; - } - update(input, delayTime, speed = 1) { - this.write(input, delayTime); - let index = this.readIdx; - if (speed < 0) { - index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples); - } else { - index = Math.floor(this.readIdx * speed) % this.numSamples; - } - const s = this.lpf.update(this.buffer[index], 0.9, 0); - - return s; - } -} - -export class Delay { - writeIdx = 0; - readIdx = 0; - buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0) - write(s, delayTime) { - this.writeIdx = (this.writeIdx + 1) % this.buffer.length; - this.buffer[this.writeIdx] = s; - // Calculate how far in the past to read - let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); - this.readIdx = this.writeIdx - numSamples; - // If past the start of the buffer, wrap around - if (this.readIdx < 0) this.readIdx += this.buffer.length; - } - update(input, delayTime) { - this.write(input, delayTime); - return this.buffer[this.readIdx]; - } -} -//TODO: Figure out why clicking at the start off the buffer -export class Chorus { - delay = new Delay(); - modulator = new TriOsc(); - update(input, mix, delayTime, modulationFreq, modulationDepth) { - const m = this.modulator.update(modulationFreq) * modulationDepth; - const c = this.delay.update(input, delayTime * (1 + m)); - return crossfade(input, c, mix); - } -} - -export class Fold { - update(input = 0, rate = 0) { - if (rate < 0) rate = 0; - rate = rate + 1; - input = input * rate; - return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25); - } -} - -export class Lag { - lagUnit = 4410; - s = 0; - update(input, rate) { - // Remap so the useful range is around [0, 1] - rate = rate * this.lagUnit; - if (rate < 1) rate = 1; - this.s += (1 / rate) * (input - this.s); - return this.s; - } -} - -export class Slew { - last = 0; - update(input, up, dn) { - const upStep = up * ISR; - const downStep = dn * ISR; - let delta = input - this.last; - if (delta > upStep) { - delta = upStep; - } else if (delta < -downStep) { - delta = -downStep; - } - this.last += delta; - return this.last; - } -} - -// overdrive style distortion (adapted from noisecraft) currently unused -export function applyDistortion(x, amount) { - amount = Math.min(Math.max(amount, 0), 1); - amount -= 0.01; - var k = (2 * amount) / (1 - amount); - var y = ((1 + k) * x) / (1 + k * Math.abs(x)); - return y; -} - -export class Sequence { - clockSgn = true; - step = 0; - first = true; - update(clock, ...ins) { - if (!this.clockSgn && clock > 0) { - this.step = (this.step + 1) % ins.length; - this.clockSgn = clock > 0; - return 0; // set first sample to zero to retrigger gates on step change... - } - this.clockSgn = clock > 0; - return ins[this.step]; - } -} - -// sample rate bit crusher -export class Coarse { - hold = 0; - t = 0; - update(input, coarse) { - if (this.t++ % coarse === 0) { - this.t = 0; - this.hold = input; - } - return this.hold; - } -} - -// amplitude bit crusher -export class Crush { - update(input, crush) { - crush = Math.max(1, crush); - const x = Math.pow(2, crush - 1); - return Math.round(input * x) / x; - } -} - -// this is the distort from superdough -export class Distort { - update(input, distort = 0, postgain = 1) { - postgain = Math.max(0.001, Math.min(1, postgain)); - const shape = Math.expm1(distort); - return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain; - } -} -// distortion could be expressed as a function, because it's stateless - -export class BufferPlayer { - static samples = new Map(); // string -> { channels, sampleRate } - buffer; // Float32Array - sampleRate; - pos = 0; - sampleFreq = note2freq(); - constructor(buffer, sampleRate, normalize) { - this.buffer = buffer; - this.sampleRate = sampleRate; - this.duration = this.buffer.length / this.sampleRate; - this.speed = SAMPLE_RATE / this.sampleRate; - if (normalize) { - // this will make the buffer last 1s if freq = sampleFreq - // it's useful to loop samples (e.g. fit function) - this.speed *= this.duration; - } - } - update(freq) { - if (this.pos >= this.buffer.length) { - return 0; - } - const speed = (freq / this.sampleFreq) * this.speed; - let s = this.buffer[Math.floor(this.pos)]; - this.pos = this.pos + speed; - return s; - } -} - -export function _rangex(sig, min, max) { - let logmin = Math.log(min); - let range = Math.log(max) - logmin; - const unipolar = (sig + 1) / 2; - return Math.exp(unipolar * range + logmin); -} - -// duplicate -export const getADSR = (params, curve = 'linear', defaultValues) => { - const envmin = curve === 'exponential' ? 0.001 : 0.001; - const releaseMin = 0.01; - const envmax = 1; - const [a, d, s, r] = params; - if (a == null && d == null && s == null && r == null) { - return defaultValues ?? [envmin, envmin, envmax, releaseMin]; - } - const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin; - return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; -}; - -let shapes = { - sine: SineOsc, - saw: SawOsc, - zaw: ZawOsc, - sawtooth: SawOsc, - zawtooth: ZawOsc, - supersaw: SupersawOsc, - tri: TriOsc, - triangle: TriOsc, - pulse: PulseOsc, - square: PulseOsc, - pulze: PulzeOsc, - dust: Dust, - crackle: Dust, - impulse: Impulse, - white: WhiteNoise, - brown: BrownNoise, - pink: PinkNoise, -}; - -const defaultDefaultValues = { - chorus: 0, - note: 48, - s: 'triangle', - bank: '', - gain: 1, - postgain: 1, - velocity: 1, - density: '.03', - ftype: '12db', - fanchor: 0, - //resonance: 1, // superdough resonance is scaled differently - resonance: 0, - //hresonance: 1, // superdough resonance is scaled differently - hresonance: 0, - // bandq: 1, // superdough resonance is scaled differently - bandq: 0, - channels: [1, 2], - phaserdepth: 0.75, - shapevol: 1, - distortvol: 1, - delay: 0, - byteBeatExpression: '0', - delayfeedback: 0.5, - delayspeed: 1, - delaytime: 0.25, - orbit: 1, - i: 1, - fft: 8, - z: 'triangle', - pan: 0.5, - fmh: 1, - fmenv: 0, // differs from superdough - speed: 1, - pw: 0.5, -}; - -let getDefaultValue = (key) => defaultDefaultValues[key]; - -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 }; -const note2midi = (note, defaultOctave = 3) => { - let [pc, acc = '', oct = ''] = - String(note) - .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/) - ?.slice(1) || []; - 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; - oct = Number(oct || defaultOctave); - return (oct + 1) * 12 + chroma + offset; -}; -const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440; -const note2freq = (note) => { - note = note || getDefaultValue('note'); - if (typeof note === 'string') { - note = note2midi(note, 3); // e.g. c3 => 48 - } - return midi2freq(note); -}; - -export class DoughVoice { - out = [0, 0]; - constructor(value) { - value.freq ??= note2freq(value.note); - let $ = this; - Object.assign($, value); - $.s = $.s ?? getDefaultValue('s'); - $.gain = applyGainCurve($.gain ?? getDefaultValue('gain')); - $.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity')); - $.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain')); - $.density = $.density ?? getDefaultValue('density'); - $.fanchor = $.fanchor ?? getDefaultValue('fanchor'); - $.drive = $.drive ?? 0.69; - $.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth'); - $.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol')); - $.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol')); - $.i = $.i ?? getDefaultValue('i'); - $.chorus = $.chorus ?? getDefaultValue('chorus'); - $.fft = $.fft ?? getDefaultValue('fft'); - $.pan = $.pan ?? getDefaultValue('pan'); - $.orbit = $.orbit ?? getDefaultValue('orbit'); - $.fmenv = $.fmenv ?? getDefaultValue('fmenv'); - $.resonance = $.resonance ?? getDefaultValue('resonance'); - $.hresonance = $.hresonance ?? getDefaultValue('hresonance'); - $.bandq = $.bandq ?? getDefaultValue('bandq'); - $.speed = $.speed ?? getDefaultValue('speed'); - $.pw = $.pw ?? getDefaultValue('pw'); - - [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]); - - $._holdEnd = $._begin + $._duration; // needed for gate - $._end = $._holdEnd + $.release + 0.01; // needed for despawn - - if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) { - $.s = 'zaw'; // polyblepped saw when fm is applied - } - - if (shapes[$.s]) { - const SourceClass = shapes[$.s]; - $._sound = new SourceClass(); - $._channels = 1; - } else if (BufferPlayer.samples.has($.s)) { - const sample = BufferPlayer.samples.get($.s); - $._buffers = []; - $._channels = sample.channels.length; - for (let i = 0; i < $._channels; i++) { - $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c' - } - } else { - console.warn('sound not loaded', $.s); - } - - if ($.penv) { - $._penv = new ADSR({ decayCurve: 4 }); - [$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]); - } - - if ($.vib) { - $._vib = new SineOsc(); - $.vibmod = $.vibmod ?? getDefaultValue('vibmod'); - } - - if ($.fmi) { - $._fm = new SineOsc(); - $.fmh = $.fmh ?? getDefaultValue('fmh'); - if ($.fmenv) { - $._fmenv = new ADSR({ decayCurve: 2 }); - [$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]); - } - } - - // gain envelope - $._adsr = new ADSR({ decayCurve: 2 }); - // delay - $.delay = applyGainCurve($.delay ?? getDefaultValue('delay')); - $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); - $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed'); - $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); - - // filter setup - if ($.lpenv) { - $._lpenv = new ADSR({ decayCurve: 4 }); - [$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]); - } - if ($.hpenv) { - $._hpenv = new ADSR({ decayCurve: 4 }); - [$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]); - } - if ($.bpenv) { - $._bpenv = new ADSR({ decayCurve: 4 }); - [$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]); - } - - // channelwise effects setup - $._chorus = $.chorus ? [] : null; - $._lpf = $.cutoff ? [] : null; - $._hpf = $.hcutoff ? [] : null; - $._bpf = $.bandf ? [] : null; - $._coarse = $.coarse ? [] : null; - $._crush = $.crush ? [] : null; - $._distort = $.distort ? [] : null; - for (let i = 0; i < this._channels; i++) { - $._lpf?.push(new TwoPoleFilter()); - $._hpf?.push(new TwoPoleFilter()); - $._bpf?.push(new TwoPoleFilter()); - $._chorus?.push(new Chorus()); - $._coarse?.push(new Coarse()); - $._crush?.push(new Crush()); - $._distort?.push(new Distort()); - } - } - update(t) { - if (!this._sound && !this._buffers) { - return 0; - } - let gate = Number(t >= this._begin && t <= this._holdEnd); - - let freq = this.freq * this.speed; - - // frequency modulation - if (this._fm) { - let fmi = this.fmi; - if (this._fmenv) { - const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease); - fmi = this.fmenv * env * fmi; - } - const modfreq = freq * this.fmh; - const modgain = modfreq * fmi; - freq = freq + this._fm.update(modfreq) * modgain; - } - - // vibrato - if (this._vib) { - freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12); - } - - // pitch envelope - if (this._penv) { - const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease); - freq = freq + env * this.penv; - } - - // filters - let lpf = this.cutoff; - if (this._lpf) { - if (this._lpenv) { - const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); - lpf = this.lpenv * env * lpf + lpf; - } - } - let hpf = this.hcutoff; - if (this._hpf) { - if (this._hpenv) { - const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); - hpf = 2 ** this.hpenv * env * hpf + hpf; - } - } - let bpf = this.bandf; - if (this._bpf) { - if (this._bpenv) { - const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); - bpf = 2 ** this.bpenv * env * bpf + bpf; - } - } - // gain envelope - const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); - - // channelwise dsp - for (let i = 0; i < this._channels; i++) { - // sound source - if (this._sound && this.s === 'pulse') { - this.out[i] = this._sound.update(freq, this.pw); - } else if (this._sound) { - this.out[i] = this._sound.update(freq); - } else if (this._buffers) { - this.out[i] = this._buffers[i].update(freq); - } - this.out[i] = this.out[i] * this.gain * this.velocity; - if (this._chorus) { - const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11); - this.out[i] = c + this.out[i]; - } - - if (this._lpf) { - this._lpf[i].update(this.out[i], lpf, this.resonance); - this.out[i] = this._lpf[i].s1; - } - if (this._hpf) { - this._hpf[i].update(this.out[i], hpf, this.hresonance); - this.out[i] = this.out[i] - this._hpf[i].s1; - } - if (this._bpf) { - this._bpf[i].update(this.out[i], bpf, this.bandq); - this.out[i] = this._bpf[i].s0; - } - if (this._coarse) { - this.out[i] = this._coarse[i].update(this.out[i], this.coarse); - } - if (this._crush) { - this.out[i] = this._crush[i].update(this.out[i], this.crush); - } - if (this._distort) { - this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol); - } - this.out[i] = this.out[i] * env; - this.out[i] = this.out[i] * this.postgain; - if (!this._buffers) { - this.out[i] = this.out[i] * 0.2; // turn down waveform - } - } - if (this._channels === 1) { - this.out[1] = this.out[0]; - } - if (this.pan !== 0.5) { - const panpos = (this.pan * Math.PI) / 2; - this.out[0] = this.out[0] * Math.cos(panpos); - this.out[1] = this.out[1] * Math.sin(panpos); - } - } -} - -// this class is the interface to the "outer world" -// it handles spawning and despawning of DoughVoice's -export class Dough { - voices = []; // DoughVoice[] - vid = 0; - q = []; - out = [0, 0]; - delaysend = [0, 0]; - delaytime = getDefaultValue('delaytime'); - delayfeedback = getDefaultValue('delayfeedback'); - delayspeed = getDefaultValue('delayspeed'); - t = 0; - // sampleRate: number, currentTime: number (seconds) - constructor(sampleRate = 48000, currentTime = 0) { - this.sampleRate = sampleRate; - this.t = Math.floor(currentTime * sampleRate); // samples - // console.log('init dough', this.sampleRate, this.t); - this._delayL = new PitchDelay(); - this._delayR = new PitchDelay(); - } - loadSample(name, channels, sampleRate) { - BufferPlayer.samples.set(name, { channels, sampleRate }); - } - scheduleSpawn(value) { - if (value._begin === undefined) { - throw new Error('[dough]: scheduleSpawn expected _begin to be set'); - } - if (value._duration === undefined) { - throw new Error('[dough]: scheduleSpawn expected _duration to be set'); - } - value.sampleRate = this.sampleRate; - // convert seconds to samples - const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs - this.schedule({ time, type: 'spawn', arg: value }); - } - spawn(value) { - value.id = this.vid++; - const voice = new DoughVoice(value); - this.voices.push(voice); - // console.log('spawn', voice.id, 'voices:', this.voices.length); - // schedule removal - const endTime = Math.ceil(voice._end * this.sampleRate); - this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id }); - } - despawn(vid) { - this.voices = this.voices.filter((v) => v.id !== vid); - // console.log('despawn', vid, 'voices:', this.voices.length); - } - // schedules a function call with a single argument - // msg = {time:number,type:string, arg: any} - // the Dough method "type" will be called with "arg" at "time" - schedule(msg) { - if (!this.q.length) { - // if empty, just push - this.q.push(msg); - return; - } - // not empty - // find index where msg.time fits in - let i = 0; - while (i < this.q.length && this.q[i].time < msg.time) { - i++; - } - // this ensures q stays sorted by time, so we only need to check q[0] - this.q.splice(i, 0, msg); - } - // maybe update should be called once per block instead for perf reasons? - update() { - // go over q - while (this.q.length > 0 && this.q[0].time <= this.t) { - // console.log('schedule', this.q[0]); - // trigger due messages. q is sorted, so we only need to check q[0] - this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method - this.q.shift(); - } - // add active voices - this.out[0] = 0; - this.out[1] = 0; - for (let v = 0; v < this.voices.length; v++) { - this.voices[v].update(this.t / this.sampleRate); - this.out[0] += this.voices[v].out[0]; - this.out[1] += this.voices[v].out[1]; - if (this.voices[v].delay) { - this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay; - this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay; - this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice - this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice - this.delayfeedback = this.voices[v].delayfeedback; - } - } - // todo: how to change delaytime / delayfeedback from a voice? - const delayL = this._delayL.update(this.delaysend[0], this.delaytime, this.delayspeed); - const delayR = this._delayR.update(this.delaysend[1], this.delaytime, this.delayspeed); - this.delaysend[0] = delayL * this.delayfeedback; - this.delaysend[1] = delayR * this.delayfeedback; - this.out[0] += delayL; - this.out[1] += delayR; - this.t++; - } -} diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs deleted file mode 100644 index 54a835495..000000000 --- a/packages/supradough/index.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import _workletUrl from './dough-worklet.mjs?url'; // todo: change ?url to ?audioworklet before build (?audioworklet doesn't hot reload) - -export * from './dough.mjs'; -export const workletUrl = _workletUrl; diff --git a/packages/supradough/package.json b/packages/supradough/package.json deleted file mode 100644 index 7e465c0a9..000000000 --- a/packages/supradough/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "supradough", - "version": "1.2.3", - "description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.", - "main": "index.mjs", - "type": "module", - "publishConfig": { - "main": "dist/index.mjs" - }, - "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": "^6.0.11", - "vite-plugin-bundle-audioworklet": "workspace:*", - "wav-encoder": "^1.3.0" - }, - "dependencies": {} -} diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 4933b7a01..362e61c44 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -7,5 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th export * from './webaudio.mjs'; export * from './scope.mjs'; export * from './spectrum.mjs'; -export * from './supradough.mjs'; export * from 'superdough'; diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 49da00f23..cbe673a5a 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -35,8 +35,7 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "superdough": "workspace:*", - "supradough": "workspace:*" + "superdough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs deleted file mode 100644 index f97251a07..000000000 --- a/packages/webaudio/supradough.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import { Pattern } from '@strudel/core'; -import { connectToDestination, getAudioContext, getWorklet } from 'superdough'; - -let doughWorklet; - -function initDoughWorklet() { - const ac = getAudioContext(); - doughWorklet = getWorklet( - ac, - 'dough-processor', - {}, - { - outputChannelCount: [2], - }, - ); - connectToDestination(doughWorklet); // channels? -} - -const soundMap = new Map(); -const loadedSounds = new Map(); - -Pattern.prototype.supradough = function () { - return this.onTrigger((hap, __, cps, begin) => { - hap.value._begin = begin; - hap.value._duration = hap.duration / cps; - !doughWorklet && initDoughWorklet(); - const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s; - const n = hap.value.n ?? 0; - const soundKey = `${s}:${n}`; - if (soundMap.has(s)) { - hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers) - } - if (soundMap.has(s) && !loadedSounds.has(soundKey)) { - const urls = soundMap.get(s); - const url = urls[n % urls.length]; - console.log(`load ${soundKey} from ${url}`); - const loadSample = fetchSample(url); - loadedSounds.set(soundKey, loadSample); - loadSample.then(({ channels, sampleRate }) => - doughWorklet.port.postMessage({ - sample: soundKey, - channels, - sampleRate, - }), - ); - } - - doughWorklet.port.postMessage({ spawn: hap.value }); - }, 1); -}; - -function githubPath(base, subpath = '') { - if (!base.startsWith('github:')) { - throw new Error('expected "github:" at the start of pseudoUrl'); - } - let [_, path] = base.split('github:'); - path = path.endsWith('/') ? path.slice(0, -1) : path; - if (path.split('/').length === 2) { - // assume main as default branch if none set - path += '/main'; - } - return `https://raw.githubusercontent.com/${path}/${subpath}`; -} -export async function fetchSampleMap(url) { - if (url.startsWith('github:')) { - url = githubPath(url, 'strudel.json'); - } - if (url.startsWith('local:')) { - url = `http://localhost:5432`; - } - if (url.startsWith('shabda:')) { - let [_, path] = url.split('shabda:'); - url = `https://shabda.ndre.gr/${path}.json?strudel=1`; - } - if (url.startsWith('shabda/speech')) { - let [_, path] = url.split('shabda/speech'); - path = path.startsWith('/') ? path.substring(1) : path; - let [params, words] = path.split(':'); - let gender = 'f'; - let language = 'en-GB'; - if (params) { - [language, gender] = params.split('/'); - } - url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; - } - if (typeof fetch !== 'function') { - // not a browser - return; - } - const base = url.split('/').slice(0, -1).join('/'); - if (typeof fetch === 'undefined') { - // skip fetch when in node / testing - return; - } - const json = await fetch(url) - .then((res) => res.json()) - .catch((error) => { - console.error(error); - throw new Error(`error loading "${url}"`); - }); - return [json, json._base || base]; -} - -// for some reason, only piano and flute work.. is it because mp3?? - -async function fetchSample(url) { - const buffer = await fetch(url) - .then((res) => res.arrayBuffer()) - .then((buf) => getAudioContext().decodeAudioData(buf)); - let channels = []; - for (let i = 0; i < buffer.numberOfChannels; i++) { - channels.push(buffer.getChannelData(i)); - } - return { channels, sampleRate: buffer.sampleRate }; -} - -export async function doughsamples(sampleMap, baseUrl) { - if (typeof sampleMap === 'string') { - const [json, base] = await fetchSampleMap(sampleMap); - // console.log('json', json, 'base', base); - return doughsamples(json, base); - } - Object.entries(sampleMap).map(async ([key, urls]) => { - if (key !== '_base') { - urls = urls.map((url) => baseUrl + url); - // console.log('set', key, urls); - soundMap.set(key, urls); - } - }); -} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 383e87f87..429d2a26b 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,12 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; -import './supradough.mjs'; -import { workletUrl } from 'supradough'; - -registerWorklet(workletUrl); - +import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough'; const { Pattern, logger, repl } = strudel; setLogger(logger); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f3..f4a24f61c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -517,18 +517,6 @@ importers: specifier: workspace:* version: link:../vite-plugin-bundle-audioworklet - packages/supradough: - devDependencies: - vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) - vite-plugin-bundle-audioworklet: - specifier: workspace:* - version: link:../vite-plugin-bundle-audioworklet - wav-encoder: - specifier: ^1.3.0 - version: 1.3.0 - packages/tidal: dependencies: '@strudel/core': @@ -637,9 +625,6 @@ importers: superdough: specifier: workspace:* version: link:../superdough - supradough: - specifier: workspace:* - version: link:../supradough devDependencies: vite: specifier: ^6.0.11 @@ -7552,9 +7537,6 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} - wav-encoder@1.3.0: - resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==} - wav@1.0.2: resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==} @@ -15976,8 +15958,6 @@ snapshots: walk-up-path@3.0.1: {} - wav-encoder@1.3.0: {} - wav@1.0.2: dependencies: buffer-alloc: 1.2.0 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index ecbc33eac..41207620f 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1847,27 +1847,6 @@ exports[`runs examples > example "chop" example index 0 1`] = ` ] `; -exports[`runs examples > example "chorus" example index 0 1`] = ` -[ - "[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]", - "[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]", - "[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]", - "[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]", - "[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]", - "[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]", - "[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]", - "[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]", - "[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]", - "[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]", - "[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]", - "[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]", - "[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]", - "[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]", - "[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]", - "[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]", -] -`; - exports[`runs examples > example "chunk" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:A4 ]", @@ -2606,52 +2585,6 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = ` ] `; -exports[`runs examples > example "delayfeedback" example index 0 2`] = ` -[ - "[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]", - "[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]", - "[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]", - "[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]", -] -`; - -exports[`runs examples > example "delayspeed" example index 0 1`] = ` -[ - "[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", - "[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", - "[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", - "[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", - "[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", -] -`; - exports[`runs examples > example "delaysync" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]", @@ -2665,6 +2598,19 @@ exports[`runs examples > example "delaysync" example index 0 1`] = ` ] `; +exports[`runs examples > example "delaytime" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]", + "[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]", + "[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]", + "[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]", + "[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]", + "[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]", + "[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]", + "[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]", +] +`; + exports[`runs examples > example "density" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:crackle density:0.01 ]",