Compare commits

...

11 Commits

Author SHA1 Message Date
Tom Burns 14c88b5017 add a function that generates a minimal preset for the currently displayed WAM 2023-02-14 22:33:15 -05:00
Felix Roos ab1d8b065a simplify promise 2023-02-14 22:04:12 +01:00
Felix Roos dd953704bf tweak loadWAM + fix param 2023-02-14 21:28:11 +01:00
Felix Roos d8d349af48 lockfile 2023-02-14 21:27:15 +01:00
Felix Roos f3e0e5e980 implement addTrigger 2023-02-14 21:27:07 +01:00
Felix Roos 23f186ba97 fix deps 2023-02-14 21:26:45 +01:00
Tom Burns 4ca890867d fix code style 2023-02-14 12:09:30 -05:00
Tom Burns 21a64c02e9 fix lint 2023-02-14 11:18:51 -05:00
Tom Burns 0ce1530692 update lockfile 2023-02-14 10:25:38 -05:00
Tom Burns 012d5146cb render WAM GUIs in footer 2023-02-14 08:53:47 -05:00
Tom Burns 12115e7068 WIP: support loading and playing WebAudio Module 2 plugins 2023-02-13 23:12:40 -05:00
9 changed files with 263 additions and 0 deletions
+1
View File
@@ -15,3 +15,4 @@ export * from './packages/transpiler/index.mjs';
export * from './packages/webaudio/index.mjs';
export * from './packages/webdirt/index.mjs';
export * from './packages/xen/index.mjs';
export * from './packages/wam/index.mjs';
+16
View File
@@ -820,6 +820,22 @@ export class Pattern {
);
}
addTrigger(onTrigger) {
return this.withHap((hap) =>
hap.setContext({
...hap.context,
onTrigger: (...args) => {
if (hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
onTrigger(...args);
},
// this flag causes the default output to be ignored
dominantTrigger: true,
}),
);
}
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args));
+100
View File
@@ -0,0 +1,100 @@
import { register, Pattern, toMidi, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { initializeWamHost } from '@webaudiomodules/sdk';
// this is a map of all loaded WebAudioModules
let wams = {};
// this is a map of all WebAudioModule instances (possibly more than one per WAM)
let wamInstances = {};
export const getWamInstances = () => wamInstances;
export const getWamInstance = (key) => {
if (!key) {
throw new Error(`no .wam set`);
}
let instance = wamInstances[key];
if (!instance) {
throw new Error(`wam instance "${key}" not found`);
}
return instance;
};
// holds a promise of the wam host init
let hostInit;
// maps wam keys to init promises
let instanceInit = {};
// maps wam keys to a list of params that are automated
let automatedWamParams = {};
export const getAutomatedWamParams = () => automatedWamParams;
// host groups of WAMs can interact with one another, but not directly across groups
const hostGroupId = 'strudel';
export const loadWAM = async function (name, url) {
// memoized wam host init
hostInit = hostInit || initializeWamHost(getAudioContext(), hostGroupId);
await hostInit;
// memoized wam file import
wams[url] = wams[url] || import(/* @vite-ignore */ url);
const { default: WAM } = await wams[url];
const instance = new WAM(hostGroupId, getAudioContext());
// memoized instance ini
instanceInit[name] =
instanceInit[name] ||
instance.initialize().then(() => {
instance.audioNode.connect(getAudioContext().destination);
return instance;
});
// save loaded instance
wamInstances[name] = await instanceInit[name];
return wamInstances[name];
};
export const loadwam = loadWAM;
export const loadWam = loadWAM;
export const wam = register('wam', function (name, pat) {
return pat.set({ wam: name }).addTrigger((time, hap) => {
const i = getWamInstance(name);
let note = toMidi(hap.value.note);
let velocity = hap.context?.velocity ?? 0.75;
let endTime = time + hap.duration.valueOf();
i.audioNode.scheduleEvents({
type: 'wam-midi',
data: { bytes: [0x90, note, velocity] },
time: time,
});
i.audioNode.scheduleEvents({
type: 'wam-midi',
data: { bytes: [0x80, note, 0] },
time: endTime,
});
});
});
export const param = register('param', function (param, value, pat) {
return pat.addTrigger((time, hap) => {
const { wam } = hap.value;
const i = getWamInstance(wam);
// save param automation
if (!automatedWamParams[wam]) {
automatedWamParams[wam] = {};
}
automatedWamParams[wam][param] = true;
i.audioNode.scheduleEvents({
time: time,
type: 'wam-automation',
data: {
id: param,
normalized: false,
value: value,
},
});
});
});
+41
View File
@@ -0,0 +1,41 @@
{
"name": "@strudel.cycles/wam",
"version": "0.1.0",
"description": "WAM2 API for strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.js",
"module": "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": "Tom Burns <tom@burns.ca>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@webaudiomodules/sdk": "^0.0.10"
},
"devDependencies": {
"vite": "^3.2.2",
"vitest": "^0.25.7"
}
}
+19
View File
@@ -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.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+21
View File
@@ -312,6 +312,21 @@ importers:
vite: 3.2.5
vitest: 0.25.8
packages/wam:
specifiers:
'@strudel.cycles/core': workspace:*
'@strudel.cycles/webaudio': workspace:*
'@webaudiomodules/sdk': ^0.0.10
vite: ^3.2.2
vitest: ^0.25.7
dependencies:
'@strudel.cycles/core': link:../core
'@strudel.cycles/webaudio': link:../webaudio
'@webaudiomodules/sdk': 0.0.10
devDependencies:
vite: 3.2.5
vitest: 0.25.8
packages/webaudio:
specifiers:
'@strudel.cycles/core': workspace:*
@@ -368,6 +383,7 @@ importers:
'@strudel.cycles/soundfonts': workspace:*
'@strudel.cycles/tonal': workspace:*
'@strudel.cycles/transpiler': workspace:*
'@strudel.cycles/wam': workspace:*
'@strudel.cycles/webaudio': workspace:*
'@strudel.cycles/xen': workspace:*
'@supabase/supabase-js': ^1.35.3
@@ -412,6 +428,7 @@ importers:
'@strudel.cycles/soundfonts': link:../packages/soundfonts
'@strudel.cycles/tonal': link:../packages/tonal
'@strudel.cycles/transpiler': link:../packages/transpiler
'@strudel.cycles/wam': link:../packages/wam
'@strudel.cycles/webaudio': link:../packages/webaudio
'@strudel.cycles/xen': link:../packages/xen
'@supabase/supabase-js': 1.35.7
@@ -4249,6 +4266,10 @@ packages:
resolution: {integrity: sha512-ukOMWnCg1tCvT7WnDfsUKQOFDQGsyR5tNgRpwmqi+5/vzU3ghdDXzvIM4IOPdSb3OeSsBNvmSL8nxIVOqi2WXA==}
dev: false
/@webaudiomodules/sdk/0.0.10:
resolution: {integrity: sha512-owzwJL3ETntGH325vxDZ5vJlrEIYlhqVjw9xu1z+M1zebX7vcWAzh/Bf6GxISFeKoVSH4te90q1BYPdzmh+PeA==}
dev: false
/JSONStream/1.3.5:
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
hasBin: true
+1
View File
@@ -31,6 +31,7 @@
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/wam": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*",
"@supabase/supabase-js": "^1.35.3",
+63
View File
@@ -7,6 +7,7 @@ import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { loadedSamples } from './Repl';
import { Reference } from './Reference';
import { themes, themeColors } from './themes.mjs';
import { getAutomatedWamParams, getWamInstances } from '@strudel.cycles/wam';
export function Footer({ context }) {
// const [activeFooter, setActiveFooter] = useState('console');
@@ -68,6 +69,7 @@ export function Footer({ context }) {
if (isZen) {
return null;
}
return (
<footer className="bg-lineHighlight z-[20]">
<div className="flex justify-between px-2">
@@ -77,6 +79,7 @@ export function Footer({ context }) {
<FooterTab name="console" />
<FooterTab name="reference" />
<FooterTab name="theme" />
<FooterTab name="wams" />
</div>
{activeFooter !== '' && (
<button onClick={() => setActiveFooter('')} className="text-foreground" aria-label="Close Panel">
@@ -196,6 +199,19 @@ export function Footer({ context }) {
))}
</div>
)}
{activeFooter === 'wams' && (
<div className="break-normal w-full px-4 dark:text-white text-stone-900">
<span>{Object.keys(getWamInstances()).length} loaded:</span>
<select onChange={(e) => showWAM(e)}>
<option key="--">--</option>
{Object.keys(getWamInstances()).map((w) => (
<option key={w}>{w}</option>
))}
</select>
<div id="wam-gui"></div>
<button onClick={() => wamPreset()}>preset</button>
</div>
)}
</div>
)}
</footer>
@@ -226,3 +242,50 @@ function linkify(inputText) {
return replacedText;
}
let wamGui = null;
let displayedWam = null;
const showWAM = async (e) => {
if (displayedWam !== null && wamGui !== null) {
displayedWam.destroyGui(wamGui);
}
const wam = getWamInstances()[e.target.value];
displayedWam = e.target.value;
const gui = await wam.createGui();
const guiDiv = document.getElementById('wam-gui');
guiDiv.innerHTML = '';
guiDiv.appendChild(gui);
const params = await wam.audioNode.getParameterInfo();
for (let id of Object.keys(params)) {
const param = params[id];
const input = document.createElement('div');
input.innerHTML = `<label>${id}</label>: type ${param.type}, min ${param.minValue}, max ${param.maxValue}`;
guiDiv.appendChild(input);
}
};
const wamPreset = async () => {
const wam = getWamInstances()[displayedWam];
const params = await wam.audioNode.getParameterInfo();
const values = await wam.audioNode.getParameterValues();
let preset = {}
const automatedParams = getAutomatedWamParams()[displayedWam];
for (let id of Object.keys(params)) {
if (automatedParams[id]) {
continue
}
if (values[id].value.toFixed(6) == params[id].defaultValue.toFixed(6)) {
continue
}
preset[id] = values[id].value;
}
console.log("preset", preset);
}
+1
View File
@@ -46,6 +46,7 @@ const modules = [
import('@strudel.cycles/serial'),
import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/csound'),
import('@strudel.cycles/wam'),
];
evalScope(