From 6e26f3975165ff4430b2079242937be351c40317 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Sep 2023 22:34:31 +0200 Subject: [PATCH 01/10] add dough function for raw dsp --- packages/superdough/dspworklet.mjs | 80 ++++++++++++++++++++++++++++++ packages/superdough/index.mjs | 1 + website/src/repl/Repl.jsx | 7 ++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 packages/superdough/dspworklet.mjs diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs new file mode 100644 index 00000000..ac51c8fc --- /dev/null +++ b/packages/superdough/dspworklet.mjs @@ -0,0 +1,80 @@ +import { Pattern } from '@strudel.cycles/core'; +import { getAudioContext } from './superdough.mjs'; + +let worklet; +export async function dspWorklet(ac, code) { + const name = `dsp-worklet-${Date.now()}`; + const workletCode = `${code} +let __q = []; // trigger queue +class MyProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.t = 0; + this.stopped = false; + this.port.onmessage = (e) => { + if(e.data==='stop') { + this.stopped = true; + } else if(e.data?.dough) { + const deadline = e.data.time-currentTime; + __q.push(e.data) + } else { + msg?.(e.data) + } + }; + } + process(inputs, outputs, parameters) { + const output = outputs[0]; + if(__q.length) { + __q = __q.filter((el) => { + const deadline = el.time-currentTime; + return deadline>0 ? true : trigger(el.dough) + }) + } + for (let i = 0; i < output[0].length; i++) { + const out = dsp(this.t / sampleRate); + output.forEach((channel) => { + channel[i] = out; + }); + this.t++; + } + return !this.stopped; + } +} +registerProcessor('${name}', MyProcessor); +`; + const base64String = btoa(workletCode); + const dataURL = `data:text/javascript;base64,${base64String}`; + await ac.audioWorklet.addModule(dataURL); + const node = new AudioWorkletNode(ac, name); + const stop = () => node.port.postMessage('stop'); + return { node, stop }; +} +const stop = () => { + if (worklet) { + worklet?.stop(); + worklet?.node?.disconnect(); + } +}; + +if (typeof window !== 'undefined') { + window.addEventListener('message', (e) => { + if (e.data === 'strudel-stop') { + stop(); + } else if (e.data?.dough) { + worklet?.node.port.postMessage(e.data); + } + }); +} + +export const dough = async (code) => { + const ac = getAudioContext(); + stop(); + worklet = await dspWorklet(ac, code); + worklet.node.connect(ac.destination); +}; + +Pattern.prototype.dough = function () { + return this.onTrigger((t, hap) => { + window.postMessage({ time: t, dough: hap.value }); + }, 1); +}; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index e5d4498b..3247c5b4 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -10,3 +10,4 @@ export * from './helpers.mjs'; export * from './synth.mjs'; export * from './zzfx.mjs'; export * from './logger.mjs'; +export * from './dspworklet.mjs'; diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 173bb455..fe32e741 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -149,7 +149,12 @@ export function Repl({ embedded = false }) { onEvalError: (err) => { setPending(false); }, - onToggle: (play) => !play && cleanupDraw(false), + onToggle: (play) => { + if (!play) { + cleanupDraw(false); + window.postMessage('strudel-stop'); + } + }, drawContext, // drawTime: [0, 6], paintOptions, From 52c01abbe9e62abc18820143bfea0569e2c7d31a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Sep 2023 22:39:03 +0200 Subject: [PATCH 02/10] encapsulate .dough --- packages/superdough/dspworklet.mjs | 9 +++------ packages/webaudio/webaudio.mjs | 6 +++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index ac51c8fc..2a77de53 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -1,4 +1,3 @@ -import { Pattern } from '@strudel.cycles/core'; import { getAudioContext } from './superdough.mjs'; let worklet; @@ -73,8 +72,6 @@ export const dough = async (code) => { worklet.node.connect(ac.destination); }; -Pattern.prototype.dough = function () { - return this.onTrigger((t, hap) => { - window.postMessage({ time: t, dough: hap.value }); - }, 1); -}; +export function doughTrigger(t, hap, currentTime, duration, cps) { + window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps }); +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8b32a90c..fb4a3d7d 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel.cycles/core'; -import { superdough, getAudioContext, setLogger } from 'superdough'; +import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough'; const { Pattern, logger } = strudel; setLogger(logger); @@ -35,3 +35,7 @@ export function webaudioScheduler(options = {}) { onTrigger: strudel.getTrigger({ defaultOutput, getTime }), }); } + +Pattern.prototype.dough = function () { + return this.onTrigger(doughTrigger, 1); +}; From 7078e20200bcbcc98e61de795604fc174d9495e3 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Sep 2023 22:56:44 +0200 Subject: [PATCH 03/10] less garbage --- packages/superdough/dspworklet.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index 2a77de53..1c109452 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -24,10 +24,13 @@ class MyProcessor extends AudioWorkletProcessor { process(inputs, outputs, parameters) { const output = outputs[0]; if(__q.length) { - __q = __q.filter((el) => { - const deadline = el.time-currentTime; - return deadline>0 ? true : trigger(el.dough) - }) + for(let i=0;i<__q.length;++i) { + const deadline = __q[i].time-currentTime; + if(deadline<=0) { + trigger(__q[i].dough) + __q.splice(i,1) + } + } } for (let i = 0; i < output[0].length; i++) { const out = dsp(this.t / sampleRate); From eec3752b5a9b2ba0318029fbe4c51e03e9bf4dc7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Sep 2023 23:57:09 +0200 Subject: [PATCH 04/10] cleanup --- packages/superdough/dspworklet.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index 1c109452..deff485a 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -14,7 +14,6 @@ class MyProcessor extends AudioWorkletProcessor { if(e.data==='stop') { this.stopped = true; } else if(e.data?.dough) { - const deadline = e.data.time-currentTime; __q.push(e.data) } else { msg?.(e.data) From 62743edf4540356e7e7aae474ff4adbf4a3bfe53 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 25 Sep 2023 23:57:19 +0200 Subject: [PATCH 05/10] bump superdough to 0.9.9 --- packages/superdough/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 69ba6f8c..387749c8 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "0.9.8", + "version": "0.9.9", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", From cf72e3bba5f22a6109c381af47634ebb34c2141a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 27 Sep 2023 21:25:30 +0200 Subject: [PATCH 06/10] fix: add conditional imports to eval scope + fire postMessage on start --- website/src/repl/Repl.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index fe32e741..4002eba8 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -33,7 +33,7 @@ const supabase = createClient( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM', ); -const modules = [ +let modules = [ import('@strudel.cycles/core'), import('@strudel.cycles/tonal'), import('@strudel.cycles/mini'), @@ -45,13 +45,13 @@ const modules = [ import('@strudel.cycles/csound'), ]; if (isTauri()) { - modules.concat([ + modules = modules.concat([ import('@strudel/desktopbridge/loggerbridge.mjs'), import('@strudel/desktopbridge/midibridge.mjs'), import('@strudel/desktopbridge/oscbridge.mjs'), ]); } else { - modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]); + modules = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]); } const modulesLoading = evalScope( @@ -153,6 +153,8 @@ export function Repl({ embedded = false }) { if (!play) { cleanupDraw(false); window.postMessage('strudel-stop'); + } else { + window.postMessage('strudel-start'); } }, drawContext, From 68ab43b3ab2c79691e55e3cf2ca288573bc00e72 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 27 Sep 2023 21:26:24 +0200 Subject: [PATCH 07/10] support midi clock via "clock" control (not on desktop yet) --- packages/core/controls.mjs | 1 + packages/midi/midi.mjs | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6cac6e54..62cfdd46 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1066,6 +1066,7 @@ const generic_params = [ */ ['waveloss'], // TODO: midi effects? + ['clock'], ['dur'], // ['modwheel'], ['expression'], diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 98509b46..32583d75 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -90,6 +90,13 @@ Pattern.prototype.midi = function (output) { enableWebMidi({ onEnabled: ({ outputs }) => { const device = getDevice(output, outputs); + if (typeof window !== 'undefined') { + window.addEventListener('message', (e) => { + if (e.data === 'strudel-stop') { + device.sendStop(); + } + }); + } const otherOutputs = outputs.filter((o) => o.name !== device.name); logger( `Midi enabled! Using "${device.name}". ${ @@ -103,6 +110,7 @@ Pattern.prototype.midi = function (output) { return this.onTrigger((time, hap, currentTime, cps) => { if (!WebMidi.enabled) { + console.log('not enabled'); return; } const device = getDevice(output, WebMidi.outputs); @@ -113,7 +121,7 @@ Pattern.prototype.midi = function (output) { const timeOffsetString = `+${offset}`; // destructure value - const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value; + const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, clock } = hap.value; const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length @@ -125,7 +133,7 @@ Pattern.prototype.midi = function (output) { time: timeOffsetString, }); } - if (ccv && ccn) { + if (ccv !== undefined && ccn !== undefined) { if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) { throw new Error('expected ccv to be a number between 0 and 1'); } @@ -135,5 +143,12 @@ Pattern.prototype.midi = function (output) { const scaled = Math.round(ccv * 127); device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); } + const begin = hap.whole.begin + 0; + if (begin === 0) { + device.sendStart({ time: timeOffsetString }); + } + if (clock) { + device.sendClock({ time: timeOffsetString }); + } }); }; From dea1c31701a9ad8b0bbb081c85401b088f480f88 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 27 Sep 2023 22:10:21 +0200 Subject: [PATCH 08/10] use midicmd instead of clock --- packages/core/controls.mjs | 2 +- packages/midi/midi.mjs | 21 ++++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 62cfdd46..d1a9ce7f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1066,7 +1066,7 @@ const generic_params = [ */ ['waveloss'], // TODO: midi effects? - ['clock'], + ['midicmd'], ['dur'], // ['modwheel'], ['expression'], diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 32583d75..ee111d7e 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -90,13 +90,6 @@ Pattern.prototype.midi = function (output) { enableWebMidi({ onEnabled: ({ outputs }) => { const device = getDevice(output, outputs); - if (typeof window !== 'undefined') { - window.addEventListener('message', (e) => { - if (e.data === 'strudel-stop') { - device.sendStop(); - } - }); - } const otherOutputs = outputs.filter((o) => o.name !== device.name); logger( `Midi enabled! Using "${device.name}". ${ @@ -121,7 +114,7 @@ Pattern.prototype.midi = function (output) { const timeOffsetString = `+${offset}`; // destructure value - const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, clock } = hap.value; + const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value; const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length @@ -143,12 +136,14 @@ Pattern.prototype.midi = function (output) { const scaled = Math.round(ccv * 127); device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); } - const begin = hap.whole.begin + 0; - if (begin === 0) { - device.sendStart({ time: timeOffsetString }); - } - if (clock) { + if (['clock', 'midiClock'].includes(midicmd)) { device.sendClock({ time: timeOffsetString }); + } else if (['start'].includes(midicmd)) { + device.sendStart({ time: timeOffsetString }); + } else if (['stop'].includes(midicmd)) { + device.sendStop({ time: timeOffsetString }); + } else if (['continue'].includes(midicmd)) { + device.sendContinue({ time: timeOffsetString }); } }); }; From f11462bf41ee9898cac562970e6252035cdd77a4 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 27 Sep 2023 22:28:52 +0200 Subject: [PATCH 09/10] sync start / stop automatically too --- packages/midi/midi.mjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index ee111d7e..e3ce0b3e 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -78,6 +78,20 @@ function getDevice(output, outputs) { return IACOutput ?? outputs[0]; } +// send start/stop messages to outputs when repl starts/stops +if (typeof window !== 'undefined') { + window.addEventListener('message', (e) => { + if (!WebMidi?.enabled) { + return; + } + if (e.data === 'strudel-stop') { + WebMidi.outputs.forEach((output) => output.sendStop()); + } else if (e.data === 'strudel-start') { + WebMidi.outputs.forEach((output) => output.sendStart()); + } + }); +} + Pattern.prototype.midi = function (output) { if (isPattern(output)) { throw new Error( From 4eb0a7b7c0606acd5469b93f5275481dff86132a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 27 Sep 2023 22:42:35 +0200 Subject: [PATCH 10/10] send start with accurate timing --- packages/midi/midi.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index e3ce0b3e..07e6e65c 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -86,9 +86,8 @@ if (typeof window !== 'undefined') { } if (e.data === 'strudel-stop') { WebMidi.outputs.forEach((output) => output.sendStop()); - } else if (e.data === 'strudel-start') { - WebMidi.outputs.forEach((output) => output.sendStart()); } + // cannot start here, since we have no timing info, see sendStart below }); } @@ -150,6 +149,10 @@ Pattern.prototype.midi = function (output) { const scaled = Math.round(ccv * 127); device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString }); } + if (hap.whole.begin + 0 === 0) { + // we need to start here because we have the timing info + device.sendStart({ time: timeOffsetString }); + } if (['clock', 'midiClock'].includes(midicmd)) { device.sendClock({ time: timeOffsetString }); } else if (['start'].includes(midicmd)) {