mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-25 12:38:35 +00:00
Merge remote-tracking branch 'origin/main' into midi-in
This commit is contained in:
commit
e4eff8c837
@ -1066,6 +1066,7 @@ const generic_params = [
|
|||||||
*/
|
*/
|
||||||
['waveloss'],
|
['waveloss'],
|
||||||
// TODO: midi effects?
|
// TODO: midi effects?
|
||||||
|
['midicmd'],
|
||||||
['dur'],
|
['dur'],
|
||||||
// ['modwheel'],
|
// ['modwheel'],
|
||||||
['expression'],
|
['expression'],
|
||||||
|
|||||||
@ -76,6 +76,19 @@ function getDevice(indexOrName, devices) {
|
|||||||
return IACOutput ?? devices[0];
|
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) {
|
Pattern.prototype.midi = function (output) {
|
||||||
if (isPattern(output)) {
|
if (isPattern(output)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -101,6 +114,7 @@ Pattern.prototype.midi = function (output) {
|
|||||||
|
|
||||||
return this.onTrigger((time, hap, currentTime, cps) => {
|
return this.onTrigger((time, hap, currentTime, cps) => {
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
|
console.log('not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const device = getDevice(output, WebMidi.outputs);
|
const device = getDevice(output, WebMidi.outputs);
|
||||||
@ -111,7 +125,7 @@ Pattern.prototype.midi = function (output) {
|
|||||||
const timeOffsetString = `+${offset}`;
|
const timeOffsetString = `+${offset}`;
|
||||||
|
|
||||||
// destructure value
|
// 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
|
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
|
// 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,
|
time: timeOffsetString,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ccv && ccn) {
|
if (ccv !== undefined && ccn !== undefined) {
|
||||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||||
throw new Error('expected ccv to be a number between 0 and 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);
|
const scaled = Math.round(ccv * 127);
|
||||||
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
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 });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
79
packages/superdough/dspworklet.mjs
Normal file
79
packages/superdough/dspworklet.mjs
Normal file
@ -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 });
|
||||||
|
}
|
||||||
@ -10,3 +10,4 @@ export * from './helpers.mjs';
|
|||||||
export * from './synth.mjs';
|
export * from './synth.mjs';
|
||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
|
export * from './dspworklet.mjs';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"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.",
|
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -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 * as strudel from '@strudel.cycles/core';
|
||||||
import { superdough, getAudioContext, setLogger } from 'superdough';
|
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
||||||
const { Pattern, logger } = strudel;
|
const { Pattern, logger } = strudel;
|
||||||
|
|
||||||
setLogger(logger);
|
setLogger(logger);
|
||||||
@ -35,3 +35,7 @@ export function webaudioScheduler(options = {}) {
|
|||||||
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
|
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.dough = function () {
|
||||||
|
return this.onTrigger(doughTrigger, 1);
|
||||||
|
};
|
||||||
|
|||||||
@ -149,7 +149,14 @@ export function Repl({ embedded = false }) {
|
|||||||
onEvalError: (err) => {
|
onEvalError: (err) => {
|
||||||
setPending(false);
|
setPending(false);
|
||||||
},
|
},
|
||||||
onToggle: (play) => !play && cleanupDraw(false),
|
onToggle: (play) => {
|
||||||
|
if (!play) {
|
||||||
|
cleanupDraw(false);
|
||||||
|
window.postMessage('strudel-stop');
|
||||||
|
} else {
|
||||||
|
window.postMessage('strudel-start');
|
||||||
|
}
|
||||||
|
},
|
||||||
drawContext,
|
drawContext,
|
||||||
// drawTime: [0, 6],
|
// drawTime: [0, 6],
|
||||||
paintOptions,
|
paintOptions,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user