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
41 changed files with 405 additions and 809 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"jsxBracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
-22
View File
@@ -82,10 +82,6 @@ Please report any problems you've had with the setup instructions!
## Code Style
To make sure the code changes only where it should, we are using prettier to unify the code style.
- You can format all files at once by running `pnpm prettier` from the project root
- Run `pnpm format-check` from the project root to check if all files are well formatted
If you use VSCode, you can
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
@@ -93,24 +89,6 @@ If you use VSCode, you can
3. Choose "Configure Default Formatter..."
4. Select prettier
## ESLint
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
- You can check for lint errors by running `pnpm lint`
There are also eslint extensions / plugins for most editors.
## Running Tests
- Run all tests with `pnpm test`
- Run all tests with UI using `pnpm test-ui`
## Running all CI Checks
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
You can run the same check with `pnpm check`
## Package Workflow
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
+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';
+10 -10
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, sequence, registerControl } from './pattern.mjs';
import { Pattern, sequence } from './pattern.mjs';
const controls = {};
const generic_params = [
@@ -828,26 +828,26 @@ const generic_params = [
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
const makeControl = function (name) {
const func = (...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
const setter = function (...pats) {
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
const _setter = (func, name) =>
function (...pats) {
if (!pats.length) {
return this.fmap((value) => ({ [name]: value }));
}
return this.set(func(...pats));
};
Pattern.prototype[name] = setter;
registerControl(name, func);
return func;
};
generic_params.forEach(([type, name, description]) => {
controls[name] = makeControl(name);
controls[name] = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _setter(controls[name], name);
});
// create custom param
controls.createParam = (name) => {
return makeControl(name);
const func = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _setter(func, name);
return (...pats) => _name(name, ...pats);
};
controls.createParams = (...names) =>
+83 -263
View File
@@ -21,141 +21,6 @@ let stringParser;
// intended to use with mini to automatically interpret all strings as mini notation
export const setStringParser = (parser) => (stringParser = parser);
const alignments = ['in', 'out', 'mix', 'squeeze', 'squeezeout', 'trig', 'trigzero'];
const methodRegistry = [];
const getterRegistry = [];
const controlRegistry = [];
const controlSubscribers = [];
const composifiedRegistry = [];
//////////////////////////////////////////////////////////////////////
// Magic for supporting higher order composition of method chains
// Dresses the given (unary) function with methods for composition chaining, so e.g.
// `fast(2).iter(4)` composes to pattern functions into a new one.
function composify(func) {
if (!func.__composified) {
for (const [name, method] of methodRegistry) {
func[name] = method;
}
for (const [name, getter] of getterRegistry) {
Object.defineProperty(func, name, getter);
}
func.__composified = true;
composifiedRegistry.push(func);
} else {
console.log('Warning: attempt at composifying a function more than once');
}
return func;
}
export function registerMethod(name, addAlignments = false, addControls = false) {
if (addAlignments || addControls) {
// This method needs to make its 'this' object available to chained alignments and/or
// control parameters, so it has to be implemented as a getter
const getter = {
get: function () {
const func = this;
const wrapped = function (...args) {
const composed = (pat) => func(pat)[name](...args);
return composify(composed);
};
if (addAlignments) {
for (const alignment of alignments) {
wrapped[alignment] = function (...args) {
const composed = (pat) => func(pat)[name][alignment](...args);
return composify(composed);
};
for (const [controlname, controlfunc] of controlRegistry) {
wrapped[alignment][controlname] = function (...args) {
const composed = (pat) => func(pat)[name][alignment](controlfunc(...args));
return composify(composed);
};
}
}
}
if (addControls) {
for (const [controlname, controlfunc] of controlRegistry) {
wrapped[controlname] = function (...args) {
const composed = (pat) => func(pat)[name](controlfunc(...args));
return composify(composed);
};
}
}
return wrapped;
},
};
getterRegistry.push([name, getter]);
// Add to functions already 'composified'
for (const composified of composifiedRegistry) {
Object.defineProperty(composified, name, getter);
}
} else {
// No chained alignments/controls needed, so we can just add as a plain method,
// probably more efficient this way?
const method = function (...args) {
const func = this;
const composed = (pat) => func(pat)[name](...args);
return composify(composed);
};
methodRegistry.push([name, method]);
// Add to functions already 'composified'
for (const composified of composifiedRegistry) {
composified[name] = method;
}
}
}
export function registerControl(controlname, controlfunc) {
registerMethod(controlname);
controlRegistry.push([controlname, controlfunc]);
for (const subscriber of controlSubscribers) {
subscriber(controlname, controlfunc);
}
}
export function withControls(func) {
for (const [controlname, controlfunc] of controlRegistry) {
func(controlname, controlfunc);
}
controlSubscribers.push(func);
}
export function addToPrototype(name, func) {
Pattern.prototype[name] = func;
registerMethod(name);
}
export function curryPattern(func, arity = func.length) {
const fn = function curried(...args) {
if (args.length >= arity) {
return func.apply(this, args);
}
const partial = function (...args2) {
return curried.apply(this, args.concat(args2));
};
if (args.length == arity - 1) {
return composify(partial);
}
return partial;
};
if (arity == 1) {
composify(fn);
}
return fn;
}
//////////////////////////////////////////////////////////////////////
// The core Pattern class
/** @class Class representing a pattern. */
export class Pattern {
/**
@@ -778,9 +643,7 @@ export class Pattern {
* @noAutocomplete
*/
get firstCycleValues() {
return this.sortHapsByPart()
.firstCycle()
.map((hap) => hap.value);
return this.firstCycle().map((hap) => hap.value);
}
/**
@@ -830,7 +693,7 @@ export class Pattern {
const otherPat = reify(other);
return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
}
_opSqueezeout(other, func) {
_opSqueezeOut(other, func) {
const thisPat = this;
const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
@@ -957,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));
@@ -997,11 +876,11 @@ function groupHapsBy(eq, haps) {
const congruent = (a, b) => a.spanEquals(b);
// Pattern<Hap<T>> -> Pattern<Hap<T[]>>
// returned pattern contains arrays of congruent haps
addToPrototype('collect', function () {
Pattern.prototype.collect = function () {
return this.withHaps((haps) =>
groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
);
});
};
/**
* Selects indices in in stacked notes.
@@ -1009,12 +888,12 @@ addToPrototype('collect', function () {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
* */
addToPrototype('arpWith', function (func) {
Pattern.prototype.arpWith = function (func) {
return this.collect()
.fmap((v) => reify(func(v)))
.innerJoin()
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
});
};
/**
* Selects indices in in stacked notes.
@@ -1022,9 +901,9 @@ addToPrototype('arpWith', function (func) {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* */
addToPrototype('arp', function (pat) {
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
});
};
//////////////////////////////////////////////////////////////////////
// compose matrix functions
@@ -1122,15 +1001,15 @@ function _composeOp(a, b, func) {
func: [(a, b) => b(a)],
};
const hows = alignments.map((x) => x.charAt(0).toUpperCase() + x.slice(1));
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero'];
// generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) {
// make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat
addToPrototype('_' + what, function (value) {
Pattern.prototype['_' + what] = function (value) {
return this.fmap((x) => op(x, value));
});
};
// make patternified monster version
Object.defineProperty(Pattern.prototype, what, {
@@ -1144,7 +1023,7 @@ function _composeOp(a, b, func) {
// add methods to that function for each behaviour
for (const how of hows) {
const howfunc = function (...other) {
wrapper[how.toLowerCase()] = function (...other) {
var howpat = pat;
other = sequence(other);
if (preprocess) {
@@ -1162,41 +1041,19 @@ function _composeOp(a, b, func) {
}
return result;
};
for (const [controlname, controlfunc] of controlRegistry) {
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
}
wrapper[how.toLowerCase()] = howfunc;
}
wrapper.squeezein = wrapper.squeeze;
for (const [controlname, controlfunc] of controlRegistry) {
wrapper[controlname] = (...args) => wrapper.in(controlfunc(...args));
}
return wrapper;
},
});
registerMethod(what, true, true);
}
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const howLower of alignments) {
// Using a 'get'ted function so that all the controls are added
Object.defineProperty(Pattern.prototype, howLower, {
get: function () {
const pat = this;
const howfunc = function (...args) {
return pat.set[howLower](args);
};
for (const [controlname, controlfunc] of controlRegistry) {
howfunc[controlname] = (...args) => howfunc(controlfunc(...args));
}
return howfunc;
},
});
registerMethod(howLower, false, true);
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of hows) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
}
// binary composers
@@ -1208,36 +1065,36 @@ function _composeOp(a, b, func) {
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(4)
*/
addToPrototype('struct', function (...args) {
Pattern.prototype.struct = function (...args) {
return this.keepif.out(...args);
});
addToPrototype('structAll', function (...args) {
};
Pattern.prototype.structAll = function (...args) {
return this.keep.out(...args);
});
};
/**
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
*/
addToPrototype('mask', function (...args) {
Pattern.prototype.mask = function (...args) {
return this.keepif.in(...args);
});
addToPrototype('maskAll', function (...args) {
};
Pattern.prototype.maskAll = function (...args) {
return this.keep.in(...args);
});
};
/**
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
*/
addToPrototype('reset', function (...args) {
Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args);
});
addToPrototype('resetAll', function (...args) {
};
Pattern.prototype.resetAll = function (...args) {
return this.keep.trig(...args);
});
};
/**
* Restarts the pattern for each onset of the restart pattern.
* While reset will only reset the current cycle, restart will start from cycle 0.
@@ -1245,12 +1102,12 @@ function _composeOp(a, b, func) {
* @example
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
*/
addToPrototype('restart', function (...args) {
Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args);
});
addToPrototype('restartAll', function (...args) {
};
Pattern.prototype.restartAll = function (...args) {
return this.keep.trigzero(...args);
});
};
})();
// aliases
@@ -1495,68 +1352,36 @@ export function pm(...args) {
polymeter(...args);
}
export const mask = curryPattern((a, b) => reify(b).mask(a));
export const struct = curryPattern((a, b) => reify(b).struct(a));
export const superimpose = curryPattern((a, b) => reify(b).superimpose(...a));
const methodToFunction = function (name, addAlignments = false) {
const func = curryPattern((a, b) => reify(b)[name](a));
withControls((controlname, controlfunc) => {
func[controlname] = function (...pats) {
return func(controlfunc(...pats));
};
});
if (addAlignments) {
for (const alignment of alignments) {
func[alignment] = curryPattern((a, b) => reify(b)[name][alignment](a));
withControls((controlname, controlfunc) => {
func[alignment][controlname] = function (...pats) {
return func[alignment](controlfunc(...pats));
};
});
}
}
return func;
};
export const mask = curry((a, b) => reify(b).mask(a));
export const struct = curry((a, b) => reify(b).struct(a));
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
// operators
export const set = methodToFunction('set', true);
export const keep = methodToFunction('keep', true);
export const keepif = methodToFunction('keepif', true);
export const add = methodToFunction('add', true);
export const sub = methodToFunction('sub', true);
export const mul = methodToFunction('mul', true);
export const div = methodToFunction('div', true);
export const mod = methodToFunction('mod', true);
export const pow = methodToFunction('pow', true);
export const band = methodToFunction('band', true);
export const bor = methodToFunction('bor', true);
export const bxor = methodToFunction('bxor', true);
export const blshift = methodToFunction('blshift', true);
export const brshift = methodToFunction('brshift', true);
export const lt = methodToFunction('lt', true);
export const gt = methodToFunction('gt', true);
export const lte = methodToFunction('lte', true);
export const gte = methodToFunction('gte', true);
export const eq = methodToFunction('eq', true);
export const eqt = methodToFunction('eqt', true);
export const ne = methodToFunction('ne', true);
export const net = methodToFunction('net', true);
export const and = methodToFunction('and', true);
export const or = methodToFunction('or', true);
export const func = methodToFunction('func', true);
// alignments
// export const in = methodToFunction('in'); // reserved word :(
export const out = methodToFunction('out');
export const mix = methodToFunction('mix');
export const squeeze = methodToFunction('squeeze');
export const squeezeout = methodToFunction('squeezeout');
export const trig = methodToFunction('trig');
export const trigzero = methodToFunction('trigzero');
export const set = curry((a, b) => reify(b).set(a));
export const keep = curry((a, b) => reify(b).keep(a));
export const keepif = curry((a, b) => reify(b).keepif(a));
export const add = curry((a, b) => reify(b).add(a));
export const sub = curry((a, b) => reify(b).sub(a));
export const mul = curry((a, b) => reify(b).mul(a));
export const div = curry((a, b) => reify(b).div(a));
export const mod = curry((a, b) => reify(b).mod(a));
export const pow = curry((a, b) => reify(b).pow(a));
export const band = curry((a, b) => reify(b).band(a));
export const bor = curry((a, b) => reify(b).bor(a));
export const bxor = curry((a, b) => reify(b).bxor(a));
export const blshift = curry((a, b) => reify(b).blshift(a));
export const brshift = curry((a, b) => reify(b).brshift(a));
export const lt = curry((a, b) => reify(b).lt(a));
export const gt = curry((a, b) => reify(b).gt(a));
export const lte = curry((a, b) => reify(b).lte(a));
export const gte = curry((a, b) => reify(b).gte(a));
export const eq = curry((a, b) => reify(b).eq(a));
export const eqt = curry((a, b) => reify(b).eqt(a));
export const ne = curry((a, b) => reify(b).ne(a));
export const net = curry((a, b) => reify(b).net(a));
export const and = curry((a, b) => reify(b).and(a));
export const or = curry((a, b) => reify(b).or(a));
export const func = curry((a, b) => reify(b).func(a));
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
@@ -1575,10 +1400,9 @@ export function register(name, func) {
return result;
}
const arity = func.length;
var pfunc; // the patternified function
registerMethod(name);
const pfunc = function (...args) {
pfunc = function (...args) {
args = args.map(reify);
const pat = args[args.length - 1];
if (arity === 1) {
@@ -1594,12 +1418,8 @@ export function register(name, func) {
.map((_, i) => args[i] ?? undefined);
return func(...args, pat);
};
mapFn = curryPattern(mapFn, arity - 1);
const app = (acc, p) => acc.appLeft(p);
const start = left.fmap(mapFn);
return right.reduce(app, start).innerJoin();
mapFn = curry(mapFn, null, arity - 1);
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
};
Pattern.prototype[name] = function (...args) {
@@ -1624,7 +1444,7 @@ export function register(name, func) {
// toplevel functions get curried as well as patternified
// because pfunc uses spread args, we need to state the arity explicitly!
return curryPattern(pfunc, arity);
return curry(pfunc, null, arity);
}
//////////////////////////////////////////////////////////////////////
+3 -25
View File
@@ -45,8 +45,6 @@ import {
rev,
time,
run,
hitch,
set,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -206,7 +204,7 @@ describe('Pattern', () => {
),
);
});
it('can squeezeout() structure', () => {
it('can SqueezeOut() structure', () => {
sameFirst(
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
@@ -254,7 +252,7 @@ describe('Pattern', () => {
),
);
});
it('can squeezeout() structure', () => {
it('can SqueezeOut() structure', () => {
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
});
});
@@ -296,7 +294,7 @@ describe('Pattern', () => {
),
);
});
it('can squeezeout() structure', () => {
it('can SqueezeOut() structure', () => {
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
});
});
@@ -931,14 +929,6 @@ describe('Pattern', () => {
});
});
describe('alignments', () => {
it('Can combine controls', () => {
sameFirst(s('bd').set.in.n(3), s('bd').n(3));
sameFirst(s('bd').set.squeeze.n(3, 4), sequence(s('bd').n(3), s('bd').n(4)));
});
it('Can combine functions with alignmed controls', () => {
sameFirst(s('bd').apply(fast(2).set(n(3))), s('bd').fast(2).set.in.n(3));
sameFirst(s('bd').apply(fast(2).set.in.n(3)), s('bd').fast(2).set.in.n(3));
});
it('Can squeeze arguments', () => {
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
});
@@ -969,16 +959,4 @@ describe('Pattern', () => {
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
});
});
describe('composable functions', () => {
it('Can compose functions', () => {
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
});
it('Can compose by method chaining operators with controls', () => {
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
});
it('Can compose by method chaining operators and alignments with controls', () => {
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
});
});
});
+1 -1
View File
@@ -139,7 +139,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
export const constant = curry((a, b) => a);
export const constant = (a, b) => a;
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
+1 -2
View File
@@ -30,5 +30,4 @@ note(`[[e5 [b4 c5] d5 [c5 b4]]
</strudel-repl>
```
- Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
- [Play with this example on stackblitz](https://stackblitz.com/edit/js-75cvww?file=index.html)
Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
+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",
-326
View File
@@ -1,326 +0,0 @@
<svg height="100%" viewBox="0.00 0.00 538.13 507.20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet">
<defs><pattern id="warning" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(45 50 50)">
<line class="line0" stroke-width="6px" x1="3" x2="3" y2="12"></line>
<line class="line1" stroke-width="6px" x1="9" x2="9" y2="12"></line>
</pattern></defs><g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 503.2)">
<!-- @strudel.cycles/core@0.6.8 -->
<g id="node1" class="node module-_strudel_cycles_core maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/core@0.6.8">
<title>@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M174.8,-37.3C174.8,-37.3 56.9,-37.3 56.9,-37.3 53.66,-37.3 50.43,-34.07 50.43,-30.83 50.43,-30.83 50.43,-24.37 50.43,-24.37 50.43,-21.13 53.66,-17.9 56.9,-17.9 56.9,-17.9 174.8,-17.9 174.8,-17.9 178.03,-17.9 181.26,-21.13 181.26,-24.37 181.26,-24.37 181.26,-30.83 181.26,-30.83 181.26,-34.07 178.03,-37.3 174.8,-37.3"></path>
<text text-anchor="middle" x="115.85" y="-24.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/core@0.6.8</text>
</g>
<!-- fraction.js@4.2.0 -->
<g id="node21" class="node module-fraction_js maintainer-infusion license-mit" data-module="fraction.js@4.2.0">
<title>fraction.js@4.2.0</title>
<path fill="none" stroke="black" d="M343.81,-19.3C343.81,-19.3 273.9,-19.3 273.9,-19.3 270.67,-19.3 267.44,-16.07 267.44,-12.83 267.44,-12.83 267.44,-6.37 267.44,-6.37 267.44,-3.13 270.67,0.1 273.9,0.1 273.9,0.1 343.81,0.1 343.81,0.1 347.05,0.1 350.28,-3.13 350.28,-6.37 350.28,-6.37 350.28,-12.83 350.28,-12.83 350.28,-16.07 347.05,-19.3 343.81,-19.3"></path>
<text text-anchor="middle" x="308.86" y="-6.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">fraction.js@4.2.0</text>
</g>
<!-- @strudel.cycles/core@0.6.8&#45;&gt;fraction.js@4.2.0 -->
<!-- @strudel.cycles/csound@0.6.2 -->
<g id="node2" class="node module-_strudel_cycles_csound maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/csound@0.6.2">
<title>@strudel.cycles/csound@0.6.2</title>
<path fill="none" stroke="black" d="M180.53,-296.3C180.53,-296.3 51.16,-296.3 51.16,-296.3 47.93,-296.3 44.7,-293.07 44.7,-289.83 44.7,-289.83 44.7,-283.37 44.7,-283.37 44.7,-280.13 47.93,-276.9 51.16,-276.9 51.16,-276.9 180.53,-276.9 180.53,-276.9 183.77,-276.9 187,-280.13 187,-283.37 187,-283.37 187,-289.83 187,-289.83 187,-293.07 183.77,-296.3 180.53,-296.3"></path>
<text text-anchor="middle" x="115.85" y="-283.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/csound@0.6.2</text>
</g>
<!-- @strudel.cycles/csound@0.6.2&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/webaudio@0.6.1 -->
<g id="node12" class="node module-_strudel_cycles_webaudio maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/webaudio@0.6.1">
<title>@strudel.cycles/webaudio@0.6.1</title>
<path fill="none" stroke="black" d="M186.63,-259.3C186.63,-259.3 45.06,-259.3 45.06,-259.3 41.83,-259.3 38.59,-256.07 38.59,-252.83 38.59,-252.83 38.59,-246.37 38.59,-246.37 38.59,-243.13 41.83,-239.9 45.06,-239.9 45.06,-239.9 186.63,-239.9 186.63,-239.9 189.87,-239.9 193.1,-243.13 193.1,-246.37 193.1,-246.37 193.1,-252.83 193.1,-252.83 193.1,-256.07 189.87,-259.3 186.63,-259.3"></path>
<text text-anchor="middle" x="115.85" y="-246.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/webaudio@0.6.1</text>
</g>
<!-- @strudel.cycles/csound@0.6.2&#45;&gt;@strudel.cycles/webaudio@0.6.1 -->
<!-- @csound/browser@6.18.5 -->
<g id="node14" class="node module-_csound_browser collapsed maintainer-skyne maintainer-kunstmusik maintainer-eddyc maintainer-hlolli license-lgpl_2_1" data-module="@csound/browser@6.18.5">
<title>@csound/browser@6.18.5</title>
<path fill="none" stroke="black" d="M364.3,-257.3C364.3,-257.3 253.41,-257.3 253.41,-257.3 250.18,-257.3 246.95,-254.07 246.95,-250.83 246.95,-250.83 246.95,-244.37 246.95,-244.37 246.95,-241.13 250.18,-237.9 253.41,-237.9 253.41,-237.9 364.3,-237.9 364.3,-237.9 367.54,-237.9 370.77,-241.13 370.77,-244.37 370.77,-244.37 370.77,-250.83 370.77,-250.83 370.77,-254.07 367.54,-257.3 364.3,-257.3"></path>
<text text-anchor="middle" x="308.86" y="-244.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@csound/browser@6.18.5</text>
</g>
<!-- @strudel.cycles/csound@0.6.2&#45;&gt;@csound/browser@6.18.5 -->
<!-- @strudel.cycles/embed@0.2.0 -->
<g id="node3" class="node module-_strudel_cycles_embed maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/embed@0.2.0">
<title>@strudel.cycles/embed@0.2.0</title>
<path fill="none" stroke="black" d="M179.69,-462.3C179.69,-462.3 52.01,-462.3 52.01,-462.3 48.77,-462.3 45.54,-459.07 45.54,-455.83 45.54,-455.83 45.54,-449.37 45.54,-449.37 45.54,-446.13 48.77,-442.9 52.01,-442.9 52.01,-442.9 179.69,-442.9 179.69,-442.9 182.92,-442.9 186.16,-446.13 186.16,-449.37 186.16,-449.37 186.16,-455.83 186.16,-455.83 186.16,-459.07 182.92,-462.3 179.69,-462.3"></path>
<text text-anchor="middle" x="115.85" y="-449.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/embed@0.2.0</text>
</g>
<!-- @strudel.cycles/midi@0.6.0 -->
<g id="node4" class="node module-_strudel_cycles_midi maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/midi@0.6.0">
<title>@strudel.cycles/midi@0.6.0</title>
<path fill="none" stroke="black" d="M175.04,-370.3C175.04,-370.3 56.66,-370.3 56.66,-370.3 53.42,-370.3 50.19,-367.07 50.19,-363.83 50.19,-363.83 50.19,-357.37 50.19,-357.37 50.19,-354.13 53.42,-350.9 56.66,-350.9 56.66,-350.9 175.04,-350.9 175.04,-350.9 178.27,-350.9 181.5,-354.13 181.5,-357.37 181.5,-357.37 181.5,-363.83 181.5,-363.83 181.5,-367.07 178.27,-370.3 175.04,-370.3"></path>
<text text-anchor="middle" x="115.85" y="-357.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/midi@0.6.0</text>
</g>
<!-- @strudel.cycles/midi@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/webaudio@0.6.0 -->
<g id="node15" class="node module-_strudel_cycles_webaudio maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/webaudio@0.6.0">
<title>@strudel.cycles/webaudio@0.6.0</title>
<path fill="none" stroke="black" d="M379.64,-370.3C379.64,-370.3 238.07,-370.3 238.07,-370.3 234.84,-370.3 231.6,-367.07 231.6,-363.83 231.6,-363.83 231.6,-357.37 231.6,-357.37 231.6,-354.13 234.84,-350.9 238.07,-350.9 238.07,-350.9 379.64,-350.9 379.64,-350.9 382.88,-350.9 386.11,-354.13 386.11,-357.37 386.11,-357.37 386.11,-363.83 386.11,-363.83 386.11,-367.07 382.88,-370.3 379.64,-370.3"></path>
<text text-anchor="middle" x="308.86" y="-357.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/webaudio@0.6.0</text>
</g>
<!-- @strudel.cycles/midi@0.6.0&#45;&gt;@strudel.cycles/webaudio@0.6.0 -->
<!-- webmidi@3.1.4 -->
<g id="node25" class="node module-webmidi collapsed maintainer-djipco license-apache_2_0" data-module="webmidi@3.1.4">
<title>webmidi@3.1.4</title>
<path fill="none" stroke="black" d="M340.52,-407.3C340.52,-407.3 277.2,-407.3 277.2,-407.3 273.97,-407.3 270.73,-404.07 270.73,-400.83 270.73,-400.83 270.73,-394.37 270.73,-394.37 270.73,-391.13 273.97,-387.9 277.2,-387.9 277.2,-387.9 340.52,-387.9 340.52,-387.9 343.75,-387.9 346.98,-391.13 346.98,-394.37 346.98,-394.37 346.98,-400.83 346.98,-400.83 346.98,-404.07 343.75,-407.3 340.52,-407.3"></path>
<text text-anchor="middle" x="308.86" y="-394.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">webmidi@3.1.4</text>
</g>
<!-- @strudel.cycles/midi@0.6.0&#45;&gt;webmidi@3.1.4 -->
<!-- @strudel.cycles/mini@0.6.0 -->
<g id="node5" class="node module-_strudel_cycles_mini maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/mini@0.6.0">
<title>@strudel.cycles/mini@0.6.0</title>
<path fill="none" stroke="black" d="M175.04,-111.3C175.04,-111.3 56.66,-111.3 56.66,-111.3 53.42,-111.3 50.19,-108.07 50.19,-104.83 50.19,-104.83 50.19,-98.37 50.19,-98.37 50.19,-95.13 53.42,-91.9 56.66,-91.9 56.66,-91.9 175.04,-91.9 175.04,-91.9 178.27,-91.9 181.5,-95.13 181.5,-98.37 181.5,-98.37 181.5,-104.83 181.5,-104.83 181.5,-108.07 178.27,-111.3 175.04,-111.3"></path>
<text text-anchor="middle" x="115.85" y="-98.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/mini@0.6.0</text>
</g>
<!-- @strudel.cycles/mini@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/osc@0.6.0 -->
<g id="node6" class="node module-_strudel_cycles_osc maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/osc@0.6.0">
<title>@strudel.cycles/osc@0.6.0</title>
<path fill="none" stroke="black" d="M172.53,-222.3C172.53,-222.3 59.16,-222.3 59.16,-222.3 55.93,-222.3 52.7,-219.07 52.7,-215.83 52.7,-215.83 52.7,-209.37 52.7,-209.37 52.7,-206.13 55.93,-202.9 59.16,-202.9 59.16,-202.9 172.53,-202.9 172.53,-202.9 175.76,-202.9 179,-206.13 179,-209.37 179,-209.37 179,-215.83 179,-215.83 179,-219.07 175.76,-222.3 172.53,-222.3"></path>
<text text-anchor="middle" x="115.85" y="-209.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/osc@0.6.0</text>
</g>
<!-- @strudel.cycles/osc@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- osc&#45;js@2.4.0 -->
<g id="node22" class="node module-osc_js collapsed maintainer-andreasdz license-mit" data-module="osc-js@2.4.0">
<title>osc-js@2.4.0</title>
<path fill="none" stroke="black" d="M334.18,-220.3C334.18,-220.3 283.53,-220.3 283.53,-220.3 280.3,-220.3 277.06,-217.07 277.06,-213.83 277.06,-213.83 277.06,-207.37 277.06,-207.37 277.06,-204.13 280.3,-200.9 283.53,-200.9 283.53,-200.9 334.18,-200.9 334.18,-200.9 337.42,-200.9 340.65,-204.13 340.65,-207.37 340.65,-207.37 340.65,-213.83 340.65,-213.83 340.65,-217.07 337.42,-220.3 334.18,-220.3"></path>
<text text-anchor="middle" x="308.86" y="-207.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">osc-js@2.4.0</text>
</g>
<!-- @strudel.cycles/osc@0.6.0&#45;&gt;osc&#45;js@2.4.0 -->
<!-- @strudel.cycles/react@0.6.0 -->
<g id="node7" class="node module-_strudel_cycles_react collapsed maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/react@0.6.0">
<title>@strudel.cycles/react@0.6.0</title>
<path fill="none" stroke="black" d="M175.73,-499.3C175.73,-499.3 55.96,-499.3 55.96,-499.3 52.73,-499.3 49.49,-496.07 49.49,-492.83 49.49,-492.83 49.49,-486.37 49.49,-486.37 49.49,-483.13 52.73,-479.9 55.96,-479.9 55.96,-479.9 175.73,-479.9 175.73,-479.9 178.97,-479.9 182.2,-483.13 182.2,-486.37 182.2,-486.37 182.2,-492.83 182.2,-492.83 182.2,-496.07 178.97,-499.3 175.73,-499.3"></path>
<text text-anchor="middle" x="115.85" y="-486.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/react@0.6.0</text>
</g>
<!-- @strudel.cycles/serial@0.6.0 -->
<g id="node8" class="node module-_strudel_cycles_serial maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/serial@0.6.0">
<title>@strudel.cycles/serial@0.6.0</title>
<path fill="none" stroke="black" d="M177.19,-148.3C177.19,-148.3 54.5,-148.3 54.5,-148.3 51.27,-148.3 48.04,-145.07 48.04,-141.83 48.04,-141.83 48.04,-135.37 48.04,-135.37 48.04,-132.13 51.27,-128.9 54.5,-128.9 54.5,-128.9 177.19,-128.9 177.19,-128.9 180.42,-128.9 183.66,-132.13 183.66,-135.37 183.66,-135.37 183.66,-141.83 183.66,-141.83 183.66,-145.07 180.42,-148.3 177.19,-148.3"></path>
<text text-anchor="middle" x="115.85" y="-135.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/serial@0.6.0</text>
</g>
<!-- @strudel.cycles/serial@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/soundfonts@0.6.0 -->
<g id="node9" class="node module-_strudel_cycles_soundfonts maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/soundfonts@0.6.0">
<title>@strudel.cycles/soundfonts@0.6.0</title>
<path fill="none" stroke="black" d="M189.15,-333.3C189.15,-333.3 42.55,-333.3 42.55,-333.3 39.31,-333.3 36.08,-330.07 36.08,-326.83 36.08,-326.83 36.08,-320.37 36.08,-320.37 36.08,-317.13 39.31,-313.9 42.55,-313.9 42.55,-313.9 189.15,-313.9 189.15,-313.9 192.38,-313.9 195.62,-317.13 195.62,-320.37 195.62,-320.37 195.62,-326.83 195.62,-326.83 195.62,-330.07 192.38,-333.3 189.15,-333.3"></path>
<text text-anchor="middle" x="115.85" y="-320.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/soundfonts@0.6.0</text>
</g>
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;@strudel.cycles/webaudio@0.6.0 -->
<!-- sfumato@0.1.2 -->
<g id="node23" class="node module-sfumato maintainer-felixroos license-isc" data-module="sfumato@0.1.2">
<title>sfumato@0.1.2</title>
<path fill="none" stroke="black" d="M338.96,-333.3C338.96,-333.3 278.76,-333.3 278.76,-333.3 275.52,-333.3 272.29,-330.07 272.29,-326.83 272.29,-326.83 272.29,-320.37 272.29,-320.37 272.29,-317.13 275.52,-313.9 278.76,-313.9 278.76,-313.9 338.96,-313.9 338.96,-313.9 342.19,-313.9 345.43,-317.13 345.43,-320.37 345.43,-320.37 345.43,-326.83 345.43,-326.83 345.43,-330.07 342.19,-333.3 338.96,-333.3"></path>
<text text-anchor="middle" x="308.86" y="-320.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">sfumato@0.1.2</text>
</g>
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;sfumato@0.1.2 -->
<!-- soundfont2@0.4.0 -->
<g id="node24" class="node module-soundfont2 maintainer-mrten license-mit" data-module="soundfont2@0.4.0">
<title>soundfont2@0.4.0</title>
<path fill="none" stroke="black" d="M513.22,-314.3C513.22,-314.3 438.89,-314.3 438.89,-314.3 435.66,-314.3 432.42,-311.07 432.42,-307.83 432.42,-307.83 432.42,-301.37 432.42,-301.37 432.42,-298.13 435.66,-294.9 438.89,-294.9 438.89,-294.9 513.22,-294.9 513.22,-294.9 516.45,-294.9 519.69,-298.13 519.69,-301.37 519.69,-301.37 519.69,-307.83 519.69,-307.83 519.69,-311.07 516.45,-314.3 513.22,-314.3"></path>
<text text-anchor="middle" x="476.06" y="-301.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">soundfont2@0.4.0</text>
</g>
<!-- @strudel.cycles/soundfonts@0.6.0&#45;&gt;soundfont2@0.4.0 -->
<!-- @strudel.cycles/tonal@0.6.0 -->
<g id="node10" class="node module-_strudel_cycles_tonal maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/tonal@0.6.0">
<title>@strudel.cycles/tonal@0.6.0</title>
<path fill="none" stroke="black" d="M176.36,-425.3C176.36,-425.3 55.33,-425.3 55.33,-425.3 52.1,-425.3 48.86,-422.07 48.86,-418.83 48.86,-418.83 48.86,-412.37 48.86,-412.37 48.86,-409.13 52.1,-405.9 55.33,-405.9 55.33,-405.9 176.36,-405.9 176.36,-405.9 179.6,-405.9 182.83,-409.13 182.83,-412.37 182.83,-412.37 182.83,-418.83 182.83,-418.83 182.83,-422.07 179.6,-425.3 176.36,-425.3"></path>
<text text-anchor="middle" x="115.85" y="-412.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/tonal@0.6.0</text>
</g>
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @tonaljs/tonal@4.10.0 -->
<g id="node16" class="node module-_tonaljs_tonal collapsed maintainer-danigb license-mit" data-module="@tonaljs/tonal@4.10.0">
<title>@tonaljs/tonal@4.10.0</title>
<path fill="none" stroke="black" d="M523.73,-463.3C523.73,-463.3 428.38,-463.3 428.38,-463.3 425.15,-463.3 421.91,-460.07 421.91,-456.83 421.91,-456.83 421.91,-450.37 421.91,-450.37 421.91,-447.13 425.15,-443.9 428.38,-443.9 428.38,-443.9 523.73,-443.9 523.73,-443.9 526.96,-443.9 530.2,-447.13 530.2,-450.37 530.2,-450.37 530.2,-456.83 530.2,-456.83 530.2,-460.07 526.96,-463.3 523.73,-463.3"></path>
<text text-anchor="middle" x="476.06" y="-450.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@tonaljs/tonal@4.10.0</text>
</g>
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;@tonaljs/tonal@4.10.0 -->
<!-- chord&#45;voicings@0.0.1 -->
<g id="node18" class="node module-chord_voicings maintainer-felixroos license-isc" data-module="chord-voicings@0.0.1">
<title>chord-voicings@0.0.1</title>
<path fill="none" stroke="black" d="M354.5,-444.3C354.5,-444.3 263.21,-444.3 263.21,-444.3 259.98,-444.3 256.74,-441.07 256.74,-437.83 256.74,-437.83 256.74,-431.37 256.74,-431.37 256.74,-428.13 259.98,-424.9 263.21,-424.9 263.21,-424.9 354.5,-424.9 354.5,-424.9 357.74,-424.9 360.97,-428.13 360.97,-431.37 360.97,-431.37 360.97,-437.83 360.97,-437.83 360.97,-441.07 357.74,-444.3 354.5,-444.3"></path>
<text text-anchor="middle" x="308.86" y="-431.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">chord-voicings@0.0.1</text>
</g>
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;chord&#45;voicings@0.0.1 -->
<!-- @strudel.cycles/tonal@0.6.0&#45;&gt;webmidi@3.1.4 -->
<!-- @strudel.cycles/transpiler@0.6.0 -->
<g id="node11" class="node module-_strudel_cycles_transpiler maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/transpiler@0.6.0">
<title>@strudel.cycles/transpiler@0.6.0</title>
<path fill="none" stroke="black" d="M185.91,-74.3C185.91,-74.3 45.79,-74.3 45.79,-74.3 42.55,-74.3 39.32,-71.07 39.32,-67.83 39.32,-67.83 39.32,-61.37 39.32,-61.37 39.32,-58.13 42.55,-54.9 45.79,-54.9 45.79,-54.9 185.91,-54.9 185.91,-54.9 189.14,-54.9 192.38,-58.13 192.38,-61.37 192.38,-61.37 192.38,-67.83 192.38,-67.83 192.38,-71.07 189.14,-74.3 185.91,-74.3"></path>
<text text-anchor="middle" x="115.85" y="-61.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/transpiler@0.6.0</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- acorn@8.8.2 -->
<g id="node17" class="node module-acorn collapsed maintainer-marijn maintainer-adrianheine maintainer-rreverser license-mit" data-module="acorn@8.8.2">
<title>acorn@8.8.2</title>
<path fill="none" stroke="black" d="M333.45,-130.3C333.45,-130.3 284.27,-130.3 284.27,-130.3 281.03,-130.3 277.8,-127.07 277.8,-123.83 277.8,-123.83 277.8,-117.37 277.8,-117.37 277.8,-114.13 281.03,-110.9 284.27,-110.9 284.27,-110.9 333.45,-110.9 333.45,-110.9 336.68,-110.9 339.92,-114.13 339.92,-117.37 339.92,-117.37 339.92,-123.83 339.92,-123.83 339.92,-127.07 336.68,-130.3 333.45,-130.3"></path>
<text text-anchor="middle" x="308.86" y="-117.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">acorn@8.8.2</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;acorn@8.8.2 -->
<!-- escodegen@2.0.0 -->
<g id="node19" class="node module-escodegen collapsed maintainer-constellation maintainer-michaelficarra license-bsd_2_clause" data-module="escodegen@2.0.0">
<title>escodegen@2.0.0</title>
<path fill="none" stroke="black" d="M344.33,-93.3C344.33,-93.3 273.39,-93.3 273.39,-93.3 270.15,-93.3 266.92,-90.07 266.92,-86.83 266.92,-86.83 266.92,-80.37 266.92,-80.37 266.92,-77.13 270.15,-73.9 273.39,-73.9 273.39,-73.9 344.33,-73.9 344.33,-73.9 347.56,-73.9 350.8,-77.13 350.8,-80.37 350.8,-80.37 350.8,-86.83 350.8,-86.83 350.8,-90.07 347.56,-93.3 344.33,-93.3"></path>
<text text-anchor="middle" x="308.86" y="-80.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">escodegen@2.0.0</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;escodegen@2.0.0 -->
<!-- estree&#45;walker@3.0.3 -->
<g id="node20" class="node module-estree_walker collapsed maintainer-rich_harris license-mit" data-module="estree-walker@3.0.3">
<title>estree-walker@3.0.3</title>
<path fill="none" stroke="black" d="M351.26,-56.3C351.26,-56.3 266.46,-56.3 266.46,-56.3 263.22,-56.3 259.99,-53.07 259.99,-49.83 259.99,-49.83 259.99,-43.37 259.99,-43.37 259.99,-40.13 263.22,-36.9 266.46,-36.9 266.46,-36.9 351.26,-36.9 351.26,-36.9 354.49,-36.9 357.73,-40.13 357.73,-43.37 357.73,-43.37 357.73,-49.83 357.73,-49.83 357.73,-53.07 354.49,-56.3 351.26,-56.3"></path>
<text text-anchor="middle" x="308.86" y="-43.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">estree-walker@3.0.3</text>
</g>
<!-- @strudel.cycles/transpiler@0.6.0&#45;&gt;estree&#45;walker@3.0.3 -->
<!-- @strudel.cycles/webaudio@0.6.1&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/xen@0.6.0 -->
<g id="node13" class="node module-_strudel_cycles_xen maintainer-yaxupaxo maintainer-felixroos license-agpl_3_0_or_later" data-module="@strudel.cycles/xen@0.6.0">
<title>@strudel.cycles/xen@0.6.0</title>
<path fill="none" stroke="black" d="M173.25,-185.3C173.25,-185.3 58.44,-185.3 58.44,-185.3 55.21,-185.3 51.98,-182.07 51.98,-178.83 51.98,-178.83 51.98,-172.37 51.98,-172.37 51.98,-169.13 55.21,-165.9 58.44,-165.9 58.44,-165.9 173.25,-165.9 173.25,-165.9 176.48,-165.9 179.72,-169.13 179.72,-172.37 179.72,-172.37 179.72,-178.83 179.72,-178.83 179.72,-182.07 176.48,-185.3 173.25,-185.3"></path>
<text text-anchor="middle" x="115.85" y="-172.3" font-family="Roboto Condensed, sans-serif" font-size="11.00">@strudel.cycles/xen@0.6.0</text>
</g>
<!-- @strudel.cycles/xen@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- @strudel.cycles/webaudio@0.6.0&#45;&gt;@strudel.cycles/core@0.6.8 -->
<!-- chord&#45;voicings@0.0.1&#45;&gt;@tonaljs/tonal@4.10.0 -->
<g id="edge27" class="edge">
<title>chord-voicings@0.0.1-&gt;@tonaljs/tonal@4.10.0</title>
<path fill="none" stroke="black" d="M361.22,-440.51C377.14,-442.34 394.88,-444.38 411.53,-446.29"></path>
<polygon fill="black" stroke="black" points="411.39,-449.8 421.73,-447.47 412.19,-442.85 411.39,-449.8"></polygon>
</g>
<!-- sfumato@0.1.2&#45;&gt;soundfont2@0.4.0 -->
<g id="edge28" class="edge">
<title>sfumato@0.1.2-&gt;soundfont2@0.4.0</title>
<path fill="none" stroke="black" d="M345.7,-319.48C368.06,-316.91 397.22,-313.55 422.19,-310.68"></path>
<polygon fill="black" stroke="black" points="422.72,-314.14 432.25,-309.52 421.92,-307.19 422.72,-314.14"></polygon>
</g>
<g id="edge1" class="edge">
<title>@strudel.cycles/core@0.6.8-&gt;fraction.js@4.2.0</title>
<path fill="none" stroke="black" d="M181.25,-21.53C206.16,-19.18 234.23,-16.54 257.6,-14.34"></path>
<polygon fill="black" stroke="black" points="258.02,-17.81 267.64,-13.39 257.36,-10.84 258.02,-17.81"></polygon>
</g><g id="edge3" class="edge">
<title>@strudel.cycles/csound@0.6.2-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M44.23,-280.41C33.55,-276.09 24.08,-269.5 17.96,-259.6 -5.99,-220.85 -5.99,-93.35 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge4" class="edge">
<title>@strudel.cycles/csound@0.6.2-&gt;@strudel.cycles/webaudio@0.6.1</title>
<path fill="none" stroke="black" d="M115.85,-276.84C115.85,-274.39 115.85,-271.94 115.85,-269.49"></path>
<polygon fill="black" stroke="black" points="119.35,-269.43 115.85,-259.43 112.35,-269.43 119.35,-269.43"></polygon>
</g><g id="edge6" class="edge">
<title>@strudel.cycles/midi@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M50.17,-356.56C37.1,-352.37 25.19,-345.3 17.96,-333.6 1.66,-307.23 1.66,-80.97 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge7" class="edge">
<title>@strudel.cycles/midi@0.6.0-&gt;@strudel.cycles/webaudio@0.6.0</title>
<path fill="none" stroke="black" d="M181.51,-360.6C194.37,-360.6 208.05,-360.6 221.49,-360.6"></path>
<polygon fill="black" stroke="black" points="221.71,-364.1 231.7,-360.6 221.7,-357.1 221.71,-364.1"></polygon>
</g><g id="edge5" class="edge">
<title>@strudel.cycles/midi@0.6.0-&gt;webmidi@3.1.4</title>
<path fill="none" stroke="black" d="M166.53,-370.22C195.48,-375.83 231.78,-382.86 260.34,-388.4"></path>
<polygon fill="black" stroke="black" points="260.01,-391.9 270.49,-390.36 261.34,-385.02 260.01,-391.9"></polygon>
</g><g id="edge8" class="edge">
<title>@strudel.cycles/mini@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M50.17,-97.56C37.1,-93.37 25.19,-86.3 17.96,-74.6 7.78,-58.12 21.54,-47.3 41.67,-40.25"></path>
<polygon fill="black" stroke="black" points="42.78,-43.57 51.3,-37.27 40.71,-36.88 42.78,-43.57"></polygon>
</g><g id="edge10" class="edge">
<title>@strudel.cycles/osc@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M52.7,-209.33C38.65,-205.26 25.65,-198.05 17.96,-185.6 2.66,-160.84 2.66,-79.36 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge9" class="edge">
<title>@strudel.cycles/osc@0.6.0-&gt;osc-js@2.4.0</title>
<path fill="none" stroke="black" d="M179.13,-211.95C207.93,-211.65 241.33,-211.3 266.9,-211.03"></path>
<polygon fill="black" stroke="black" points="267.02,-214.53 276.99,-210.92 266.95,-207.53 267.02,-214.53"></polygon>
</g><g id="edge11" class="edge">
<title>@strudel.cycles/serial@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M48.17,-133.89C35.89,-129.63 24.82,-122.7 17.96,-111.6 4.64,-90.05 4.64,-76.15 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge14" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M36.71,-313.85C29.15,-309.7 22.6,-304.1 17.96,-296.6 3.83,-273.73 3.83,-77.47 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge15" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;@strudel.cycles/webaudio@0.6.0</title>
<path fill="none" stroke="black" d="M166.53,-333.22C191.53,-338.07 222.01,-343.97 248.26,-349.05"></path>
<polygon fill="black" stroke="black" points="247.6,-352.49 258.09,-350.96 248.94,-345.62 247.6,-352.49"></polygon>
</g><g id="edge12" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;sfumato@0.1.2</title>
<path fill="none" stroke="black" d="M195.64,-323.6C218.22,-323.6 242.08,-323.6 261.97,-323.6"></path>
<polygon fill="black" stroke="black" points="261.98,-327.1 271.98,-323.6 261.98,-320.1 261.98,-327.1"></polygon>
</g><g id="edge13" class="edge">
<title>@strudel.cycles/soundfonts@0.6.0-&gt;soundfont2@0.4.0</title>
<path fill="none" stroke="black" d="M164.14,-313.99C184.8,-310.25 209.37,-306.44 231.73,-304.6 297.18,-299.22 372.86,-300.38 422.21,-302.11"></path>
<polygon fill="black" stroke="black" points="422.21,-305.61 432.33,-302.49 422.47,-298.62 422.21,-305.61"></polygon>
</g><g id="edge19" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M68.74,-405.99C49.45,-399.45 29.07,-388.58 17.96,-370.6 -0.5,-340.73 -0.5,-84.47 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge16" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;@tonaljs/tonal@4.10.0</title>
<path fill="none" stroke="black" d="M168.82,-425.25C177.92,-427.57 187.2,-430.33 195.73,-433.6 212.82,-440.15 214.01,-449.01 231.73,-453.6 291.41,-469.07 362.13,-466.9 411.9,-462.17"></path>
<polygon fill="black" stroke="black" points="412.28,-465.65 421.88,-461.17 411.58,-458.69 412.28,-465.65"></polygon>
</g><g id="edge17" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;chord-voicings@0.0.1</title>
<path fill="none" stroke="black" d="M182.85,-422.17C203.51,-424.22 226.22,-426.48 246.55,-428.5"></path>
<polygon fill="black" stroke="black" points="246.33,-432 256.62,-429.5 247.02,-425.03 246.33,-432"></polygon>
</g><g id="edge18" class="edge">
<title>@strudel.cycles/tonal@0.6.0-&gt;webmidi@3.1.4</title>
<path fill="none" stroke="black" d="M182.85,-409.38C208.4,-406.97 237.07,-404.27 260.51,-402.06"></path>
<polygon fill="black" stroke="black" points="260.91,-405.54 270.54,-401.12 260.26,-398.57 260.91,-405.54"></polygon>
</g><g id="edge23" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M115.85,-54.84C115.85,-52.39 115.85,-49.94 115.85,-47.49"></path>
<polygon fill="black" stroke="black" points="119.35,-47.43 115.85,-37.43 112.35,-47.43 119.35,-47.43"></polygon>
</g><g id="edge20" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;acorn@8.8.2</title>
<path fill="none" stroke="black" d="M168.82,-74.25C177.92,-76.57 187.2,-79.33 195.73,-82.6 212.82,-89.15 214.66,-95.99 231.73,-102.6 243.09,-107 255.84,-110.53 267.61,-113.25"></path>
<polygon fill="black" stroke="black" points="267.12,-116.73 277.64,-115.44 268.61,-109.89 267.12,-116.73"></polygon>
</g><g id="edge21" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;escodegen@2.0.0</title>
<path fill="none" stroke="black" d="M192.61,-72.14C214.17,-74.28 237.12,-76.56 256.83,-78.52"></path>
<polygon fill="black" stroke="black" points="256.51,-82.01 266.81,-79.52 257.21,-75.04 256.51,-82.01"></polygon>
</g><g id="edge22" class="edge">
<title>@strudel.cycles/transpiler@0.6.0-&gt;estree-walker@3.0.3</title>
<path fill="none" stroke="black" d="M192.61,-57.46C211.68,-55.66 231.85,-53.76 249.89,-52.06"></path>
<polygon fill="black" stroke="black" points="250.33,-55.54 259.96,-51.11 249.68,-48.57 250.33,-55.54"></polygon>
</g><g id="edge24" class="edge">
<title>@strudel.cycles/webaudio@0.6.1-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M38.54,-240.82C30.22,-236.58 22.97,-230.7 17.96,-222.6 -1.66,-190.84 -1.66,-86.36 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge25" class="edge">
<title>@strudel.cycles/xen@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M51.94,-172.1C38.18,-168 25.52,-160.82 17.96,-148.6 -4,-113.06 -4,-90.14 17.96,-54.6 23.42,-45.77 31.54,-39.58 40.82,-35.27"></path>
<polygon fill="black" stroke="black" points="42.35,-38.43 50.42,-31.56 39.83,-31.9 42.35,-38.43"></polygon>
</g><g id="edge26" class="edge">
<title>@strudel.cycles/webaudio@0.6.0-&gt;@strudel.cycles/core@0.6.8</title>
<path fill="none" stroke="black" d="M241.66,-350.81C237.99,-348.55 234.63,-345.84 231.73,-342.6 143.1,-243.48 284.75,-144.37 195.73,-45.6 193.96,-43.64 192.03,-41.87 189.96,-40.27"></path>
<polygon fill="black" stroke="black" points="191.74,-37.25 181.4,-34.93 188.03,-43.19 191.74,-37.25"></polygon>
</g><g id="edge2" class="edge">
<title>@strudel.cycles/csound@0.6.2-&gt;@csound/browser@6.18.5</title>
<path fill="none" stroke="black" d="M164.06,-276.96C190.24,-271.62 222.97,-264.93 250.6,-259.29"></path>
<polygon fill="black" stroke="black" points="251.43,-262.69 260.52,-257.26 250.02,-255.84 251.43,-262.69"></polygon>
</g></g>
</svg>

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 KiB

+1 -1
View File
@@ -73,9 +73,9 @@ export const SIDEBAR: Sidebar = {
],
Development: [
{ text: 'REPL', link: 'technical-manual/repl' },
{ text: 'Packages', link: 'technical-manual/packages' },
{ text: 'Docs', link: 'technical-manual/docs' },
{ text: 'Testing', link: 'technical-manual/testing' },
// { text: 'Packages', link: 'technical-manual/packages' },
// { text: 'Internals', link: 'technical-manual/internals' },
],
},
-2
View File
@@ -71,5 +71,3 @@ You can freely mix JS patterns, mini patterns and values! For example, this patt
While mini notation is almost always shorter, it only has a handful of modifiers: \* / ! @.
When using JS patterns, there is a lot more you can do.
What [Pattern Constructors](/learn/factories) does Strudel offer?
@@ -160,5 +160,3 @@ x(sine.range(0, 200)).y(cosine.range(0, 200));
/>
Note that these params will not do anything until you give them meaning in your custom output!
From modifying parameters we transition to the concept of [Signals](/learn/signals).
-2
View File
@@ -31,5 +31,3 @@ import { JsDoc } from '../../docs/JsDoc';
## stut
<JsDoc client:idle name="stut" h={0} />
There are also [Tonal Modifiers](/learn/tonal).
+1 -1
View File
@@ -15,7 +15,7 @@ Let's look at this simple example again. What do we notice?
<MiniRepl client:idle tune={`freq("220 275 330 440").s("sine")`} />
- We have a word `freq` which is followed by some brackets `()` with some words/letters/numbers inside, surrounded by quotes `"a3 c#4 e4 a4"`.
- Then we have a dot `.` followed by another similar piece of code `s("sine")`.
- Then we have a dot `.` followed by another similar piece of code `s("sawtooth")`.
- We can also see these texts are _highlighted_ using colours: word `freq` is purple, the brackets `()` are grey, and the content inside the `""` are green.
What happens if we try to 'break' this pattern in different ways?
@@ -59,5 +59,3 @@ import { JsDoc } from '../../docs/JsDoc';
## invert
<JsDoc client:idle name="invert" h={0} />
After Conditional Modifiers, let's see what [Accumulation Modifiers](/learn/accumulation) have to offer.
-2
View File
@@ -150,5 +150,3 @@ In the future, the integration could be improved by passing all patterned contro
This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels)
for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd).
Also, it might make sense to have a standard library of csound instruments for strudel's effects.
Now, let's dive into the [Functional JavaScript API](/functions/intro)
-2
View File
@@ -146,5 +146,3 @@ global effects use the same chain for all events of the same orbit:
## roomsize
<JsDoc client:idle name="roomsize" h={0} />
Next, we'll look at strudel's support for [Csound](/learn/csound).
-2
View File
@@ -64,5 +64,3 @@ As a chained function:
## run
<JsDoc client:idle name="run" h={0} punchcard />
After Pattern Constructors, let's see what [Time Modifiers](/learn/time-modifiers) are available.
-2
View File
@@ -69,5 +69,3 @@ The following functions can be used with [SuperDirt](https://github.com/musikinf
Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
<br />
But can we use Strudel [offline](/learn/pwa)?
+2 -4
View File
@@ -58,7 +58,7 @@ Try adding or removing notes and notice how the tempo changes!
<MiniRepl client:idle tune={`note("c d e f g a b")`} punchcard />
Note that the overall duration of time does not change, and instead each note length decreases.
Note that the overall duration of time does not change, and instead each note length descreases.
This is a key idea, as it illustrates the 'Cycle' in TidalCycles!
Each space-separated note in this sequence is an _event_.
@@ -103,7 +103,7 @@ Contrary to division, a sequence can be sped up by multiplying it by a number us
<MiniRepl client:idle tune={`note("[e5 b4 d5 c5]*2")`} punchcard />
The multiplication by two here means that the sequence will play twice per cycle.
The multiplication by two here means that the sequence will play twice a cycle.
As with divisions, multiplications can be decimal (`*2.75`):
@@ -235,5 +235,3 @@ Starting with this one `n`, can you make a _pattern string_ that uses every sing
<MiniRepl client:idle tune={`n("60")`} />
<br />
Next: How do [Samples](/learn/samples) play into this?
-76
View File
@@ -7,79 +7,3 @@ layout: ../../layouts/MainLayout.astro
You can use Strudel even without a network! When you first visit the [Strudel REPL](strudel.tidalcycles.org/),
your browser will download the whole web app including documentation.
When the download is finished (&lt;1MB), you can visit the website even when offline,
getting the downloaded website instead of the online one.
When the site gets updated, your browser will download that update on the next online visit.
When an update is available, the site will refresh after the download is finished.
This works because Strudel is implemented as progessive web app (using [Vite PWA](https://vite-pwa-org.netlify.app/)).
## Samples
While the browser will download the app itself, samples are only downloaded when you're actively using them.
So to make sure a specific set of samples is available when offline, just use them.
Also, only samples from these domains will be cached for offline use:
- `https://raw.githubusercontent.com/*` for samples uploaded to github
- `https://freesound.org/*` / `https://cdn.freesound.org/*` for freesound
- `https://shabda.ndre.gr/.*` for shabda
## Inspecting / Clearing Cache
You can view all cached files in your browser.
### Firefox
- Open the Developer Tools (`Tools > Web Developer > Web Developer Tools`)
- go to `Storage` tab and expand `Cache Storage > https://strudel.tidalcycles.org`.
- or go to the `Application` tab and view the latest updates in `Service Workers`
### Chromium based Browsers
- Open Developer Tools (`Right Click > Inspect`)
- go to the `Application` tab
- view downloaded files under `Cache > Cache Storage`
- view the latest updates in `Service Workers`
## Strudel Standalone App
You can also install Strudel as a standalone app on most devices.
A standalone app has its own desktop / homescreen icon and launches in a separate window,
without the browser ui.
<figure>
<img src="./pwa/strudel-macos.png" alt="Strudel on MacOS" />
<figcaption>Strudel on MacOS</figcaption>
</figure>
### Desktop
With a chromium based browser:
1. go to the [Strudel REPL](strudel.tidalcycles.org/).
2. on the right of the adress bar, click `install Strudel REPL`
3. the REPL should now run as a standalone chromium app
Without a chromium based browser, you can use [nativefier](https://github.com/nativefier/nativefier) to generate a desktop app:
1. make sure you have NodeJS installed
2. run `npx nativefier strudel.tidalcycles.org`
<figure>
<img src="./pwa/strudel-linux.png" alt="Strudel on Linux" />
<figcaption>Strudel on Linux</figcaption>
</figure>
### iOS
1. open to the [Strudel REPL](strudel.tidalcycles.org/) in safari
2. press the share icon and tab `Add to homescreen`
3. You should now have a strudel app icon that opens the repl in full screen
### Android
1. open to the [Strudel REPL](strudel.tidalcycles.org/)
2. Tab the install button at the bottom
Ok, what are [Patterns](/technical-manual/patterns) all about?
-2
View File
@@ -322,5 +322,3 @@ Sampler effects are functions that can be used to change the behaviour of sample
### speed
<JsDoc client:idle name="speed" h={0} />
After samples, let's see what [Synths](/learn/synths) afford us.
-2
View File
@@ -106,5 +106,3 @@ These methods add random behavior to your Patterns.
## always
<JsDoc client:idle name="Pattern.always" h={0} />
Next up: [Conditional Modifiers](/learn/conditional-modifiers)
@@ -144,5 +144,3 @@ You can get the same tempo as tidal with:
```
note("c a f e").fast(.5625);
```
Next up: the [REPL](/technical-manual/repl)
+1 -3
View File
@@ -9,7 +9,7 @@ import { JsDoc } from '../../docs/JsDoc';
# Synths
For now, [samples](/learn/samples) are the main way to play with Strudel.
In the future, more powerful synthesis capabilities will be added.
In future, more powerful synthesis capabilities will be added.
If in the meantime you want to dive deeper into audio synthesis with Tidal, you will need to [install SuperCollider and SuperDirt](https://tidalcycles.org/docs/).
## Playing synths with `s`
@@ -27,5 +27,3 @@ The power of patterns allows us to sequence any _param_ independently:
Now we not only pattern the notes, but the sound as well!
`sawtooth` `square` and `triangle` are the basic waveforms available in `s`.
Next up: [Audio Effects](/learn/effects)...
@@ -105,5 +105,3 @@ Some of these have equivalent operators in the Mini Notation:
## ribbon
<JsDoc client:idle name="ribbon" h={0} />
Apart from modifying time, there are ways to [Control Parameters](functions/value-modifiers).
-2
View File
@@ -70,5 +70,3 @@ Together with layer, struct and voicings, this can be used to create a basic bac
x => x.rootNotes(2).note().s('sawtooth').cutoff(800)
)`}
/>
So far, we've stayed within the browser. [MIDI and OSC](/learn/input-output) are ways to break out of it.
@@ -45,5 +45,3 @@ This makes way for other ways to align the pattern, and several are already defi
- `trigzero` is similar to `trig`, but the pattern is 'triggered' from its very first cycle, rather than from the current cycle. `trig` and `trigzero` therefore only give different results where the leftmost pattern differs from one cycle to the next.
We will save going deeper into the background, design and practicalities of these alignment functions for future publications. However in the next section, we take them as a case study for looking at the different design affordances offered by Haskell to Tidal, and JavaScript to Strudel.
Ok, so how do Strudel and Tidal [compare](/learn/strudel-vs-tidal)?
@@ -74,5 +74,3 @@ Documentation is written with [jsdoc](https://jsdoc.app/) comments. Example:
- To regenerate the `doc.json` file manually, run `npm run jsdoc-json`
- The file is used by the `JsDoc` component to find the documentation by name
- Also, it is used for the `examples.test.mjs` snapshot test
How does Strudel do its [Testing](/technical-manual/testing)?
+53 -39
View File
@@ -5,61 +5,75 @@ layout: ../../layouts/MainLayout.astro
import { MiniRepl } from '../../docs/MiniRepl';
# Strudel Packages
## Strudel Packages
The [strudel repo](github.com/tidalcycles/strudel) is organized as a monorepo, containing multiple npm packages.
The purpose of the multiple packages is to
The [strudel repo](github.com/tidalcycles/strudel) is organized into packages, using [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces).
Publishing packages is done with [lerna](https://lerna.js.org/).
- organize the codebase into more modular, encapsulated pieces
- be able to opt out of certain functionalities
- keep the dependencies of the core packages small
There are different packages for different purposes. They..
## Overview
- split up the code into smaller chunks
- can be selectively used to implement some sort of time based system
[See the latest published packages on npm](https://www.npmjs.com/search?q=%40strudel.cycles).
Here is an overview of all the packages:
### Essential Packages
### Important bits
These package are the most essential. You might want to use all of those if you're using strudel in your project:
- The [root package.json](https://github.com/tidalcycles/strudel/blob/main/package.json) specifies `packages/*` as `workspaces`
- Each folder in `packages` comes with its own `package.json`, defining a package name of the format `@strudel.cycles/<name>`
- Running `npm i` from the root folder will symlink all packages to the `node_modules` folder, e.g. `node_modules/@strudel.cycles/core` symlinks `packages/core`
- These symlinks allow importing the packages with their package name, instead of a relative path, e.g. `import { seq } from '@strudel.cycles/core'`, instead of `import { seq } from '../core/`.
This works because the [bare module import](https://vitejs.dev/guide/features.html#npm-dependency-resolving-and-pre-bundling) `@strudel.cycles/core` is resolved to `node_modules/@strudel.cycles/core`.
In a project that installs the published packages from npm, these imports will still work, whereas relative ones might not.
- When a strudel package is importing from another strudel package, the package that is imported from should be listed in the `dependencies` field of the `package.json`.
For example, [@strudel.cycles/mini lists `@strudel.cycles/core` as a dependency](https://github.com/tidalcycles/strudel/blob/main/packages/mini/package.json).
- In development, files in any package can be changed and saved to instantly update the dev server via [hot module replacement](https://vitejs.dev/guide/features.html#hot-module-replacement)
- To publish packages, `npx lerna publish` will check which packages were changed since the last publish and publish only those.
The version numbers in the dependencies of each packages will be updated automatically to the latest version.
- [core](https://github.com/tidalcycles/strudel/tree/main/packages/core#strudelcyclescore): tidal pattern engine with core primitives
- [mini](https://github.com/tidalcycles/strudel/tree/main/packages/mini#strudelcyclesmini): mini notation parser + core bindings
- [transpiler](https://github.com/tidalcycles/strudel/tree/main/packages/transpiler#strudelcyclestranspiler): user code transpiler. syntax sugar + highlighting
### Building & Publishing
### Language Extensions
Currently, all packages are only published as ESM with vite flavour.
To build standardized ESM and CJS files, a `vite.config.js` like that is needed:
These packages extend the pattern language by specific functions
```js
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
- [tonal](https://github.com/tidalcycles/strudel/tree/main/packages/tonal): tonal functions for scales and chords
- [xen](https://github.com/tidalcycles/strudel/tree/main/packages/xen): microtonal / xenharmonic functions
// 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',
},
});
```
### Outputs
This will build `index.mjs` (ESM) and `index.js` (CJS) to the dist folder.
These packages provide bindings for different ways to output strudel patterns:
### What's the main file?
- [webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio#strudelcycleswebaudio): the default webaudio output
- [osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc#strudelcyclesosc): bindings to communicate via OSC
- [midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi#strudelcyclesmidi): webmidi bindings
- [csound](https://github.com/tidalcycles/strudel/tree/main/packages/csound#strudelcyclescsound): csound bindings
- [soundfonts](https://github.com/tidalcycles/strudel/tree/main/packages/serial#strudelcyclessoundfonts): Soundfont support
- [serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial#strudelcyclesserial): webserial bindings
Currently, each package uses the unbundled `index.mjs` as its main file, which must change for the published version.
There are 2 ways to handle this:
### Others
1. `main` = `dist/index.js`, `module` = `dist/index.mjs`. The built files are used. This means that changing a source file won't take effect in the dev server without a rebuild.
2. Use different `package.json` files for dev vs publish. So the unbuilt `index.mjs` could be used in dev, while the built files can be used when publishing.
- [embed](https://github.com/tidalcycles/strudel/tree/main/packages/embed#strudelcyclesembed): embeddable REPL web component
- [react](https://github.com/tidalcycles/strudel/tree/main/packages/react#strudelcyclesreact): react hooks and components for strudel
Option 1 could be done with [workspace watching](https://lerna.js.org/docs/features/workspace-watching), although it might make the dev server less snappy..
Option 2 can be done by [publishing just the dist folder and copying over the `package.json` file](https://stackoverflow.com/questions/37862712/how-to-publish-contents-only-of-a-specific-folder).
Sadly, [this does not fit into how lerna works](https://github.com/lerna/lerna/issues/91).
### No Longer Maintained
https://github.com/changesets/changesets
- [eval](https://github.com/tidalcycles/strudel/tree/main/packages/eval): old code transpiler
- [tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone#strudelcyclestone): bindings for Tone.js instruments and effects
- [webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt): webdirt bindings, replaced by webaudio package
### Tools
- [pnpm workspaces](https://pnpm.io/)
- Publishing packages is done with [lerna](https://lerna.js.org/).
## Usage Examples
https://turbo.build/repo/docs/handbook/publishing-packages/versioning-and-publishing
https://pnpm.io/workspaces
@@ -37,5 +37,3 @@ Each event has a value, a begin time and an end time, where time is represented
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new one is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query the pattern for events, which are then scheduled as sound synthesis or other event triggers.
Can we [align](/technical-manual/alignment) patterns?
+1 -3
View File
@@ -9,7 +9,7 @@ import { MiniRepl } from '../../docs/MiniRepl';
{/* The [REPL](https://strudel.tidalcycles.org/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL (REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.), which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
@@ -186,5 +186,3 @@ function onTrigger(hap, deadline, duration) {
```
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
+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(