diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6cac6e54..d1a9ce7f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1066,6 +1066,7 @@ const generic_params = [ */ ['waveloss'], // TODO: midi effects? + ['midicmd'], ['dur'], // ['modwheel'], ['expression'], diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index 688b7a9b..1d89b6dc 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -76,6 +76,19 @@ function getDevice(indexOrName, devices) { return IACOutput ?? devices[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()); + } + // cannot start here, since we have no timing info, see sendStart below + }); +} + Pattern.prototype.midi = function (output) { if (isPattern(output)) { throw new Error( @@ -101,6 +114,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); @@ -111,7 +125,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, 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 @@ -123,7 +137,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'); } @@ -133,6 +147,19 @@ 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)) { + device.sendStart({ time: timeOffsetString }); + } else if (['stop'].includes(midicmd)) { + device.sendStop({ time: timeOffsetString }); + } else if (['continue'].includes(midicmd)) { + device.sendContinue({ time: timeOffsetString }); + } }); }; diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs new file mode 100644 index 00000000..deff485a --- /dev/null +++ b/packages/superdough/dspworklet.mjs @@ -0,0 +1,79 @@ +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) { + __q.push(e.data) + } else { + msg?.(e.data) + } + }; + } + process(inputs, outputs, parameters) { + const output = outputs[0]; + if(__q.length) { + 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); + 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); +}; + +export function doughTrigger(t, hap, currentTime, duration, cps) { + window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps }); +} 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/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", 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); +}; diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index eb61a216..4002eba8 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -149,7 +149,14 @@ export function Repl({ embedded = false }) { onEvalError: (err) => { setPending(false); }, - onToggle: (play) => !play && cleanupDraw(false), + onToggle: (play) => { + if (!play) { + cleanupDraw(false); + window.postMessage('strudel-stop'); + } else { + window.postMessage('strudel-start'); + } + }, drawContext, // drawTime: [0, 6], paintOptions,