mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14c88b5017 | |||
| ab1d8b065a | |||
| dd953704bf | |||
| d8d349af48 | |||
| f3e0e5e980 | |||
| 23f186ba97 | |||
| 4ca890867d | |||
| 21a64c02e9 | |||
| 0ce1530692 | |||
| 012d5146cb | |||
| 12115e7068 |
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
Generated
+21
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ const modules = [
|
||||
import('@strudel.cycles/serial'),
|
||||
import('@strudel.cycles/soundfonts'),
|
||||
import('@strudel.cycles/csound'),
|
||||
import('@strudel.cycles/wam'),
|
||||
];
|
||||
|
||||
evalScope(
|
||||
|
||||
Reference in New Issue
Block a user