Merge branch 'tidalcycles:main' into repl_sync_fixes

This commit is contained in:
Jade (Rose) Rowland 2024-03-23 16:47:22 -04:00 committed by GitHub
commit af740c3abb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 3266 additions and 2872 deletions

View File

@ -15,25 +15,15 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
```bash
pnpm run setup
pnpm run repl
pnpm i
pnpm dev
```
## Using Strudel In Your Project
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel).
- [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`transpiler`](./packages/transpiler): user code transpiler
- [`webaudio`](./packages/webaudio): webaudio output
- [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions
- ... [and there are more](./packages/)
Click on the package names to find out more about each one.
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
## Contributing

View File

@ -1,10 +1,9 @@
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
<script>
strudel.initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>

View File

@ -1,10 +1,10 @@
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
<script>
initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'),
});

View File

@ -1,15 +1,20 @@
// this barrel export is currently only used to find undocumented exports
export * from './packages/codemirror/index.mjs';
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/draw/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/hydra/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
export * from './packages/react/index.mjs';
export * from './packages/repl/index.mjs';
export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs';
export * from './packages/superdough/index.mjs';
export * from './packages/tonal/index.mjs';
export * from './packages/transpiler/index.mjs';
export * from './packages/web/index.mjs';
export * from './packages/webaudio/index.mjs';
export * from './packages/xen/index.mjs';

View File

@ -53,9 +53,10 @@
"@strudel/xen": "workspace:*"
},
"devDependencies": {
"dependency-tree": "^10.0.9",
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0",
"acorn": "^8.11.3",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"events": "^3.3.0",

View File

@ -141,7 +141,6 @@ export class StrudelMirror {
this.painters = [];
this.drawTime = drawTime;
this.onDraw = onDraw;
const self = this;
this.id = id || s4();
this.drawer = new Drawer((haps, time) => {
@ -150,13 +149,6 @@ export class StrudelMirror {
this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime);
// this approach does not work with multiple repls on screen
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake();
autodraw && this.drawFirstFrame();
@ -182,6 +174,14 @@ export class StrudelMirror {
beforeEval: async () => {
cleanupDraw();
this.painters = [];
const self = this;
// this is similar to repl.mjs > injectPatternMethods
// maybe there is a solution without prototype hacking, but hey, it works
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
await this.prebaked;
await replOptions?.beforeEval?.();
},

View File

@ -92,7 +92,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.context.color ?? 'var(--foreground)';
const color = hap.value?.color ?? 'var(--foreground)';
// Get explicit channels for color values
/*
const swatch = document.createElement('div');

View File

@ -106,17 +106,23 @@ function getCanvasWidget(id, options = {}) {
registerWidget('_pianoroll', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pianoroll({ fold: 1, ...options, ctx, id });
return pat.id(id).pianoroll({ fold: 1, ...options, ctx, id });
});
/* registerWidget('_spiral', (id, options = {}, pat) => {
options = { width: 200, height: 200, size: 36, ...options };
registerWidget('_punchcard', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.spiral({ ...options, ctx, id });
}); */
return pat.id(id).punchcard({ fold: 1, ...options, ctx, id });
});
registerWidget('_spiral', (id, options = {}, pat) => {
let _size = options.size || 275;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.id(id).spiral({ ...options, ctx, id });
});
registerWidget('_scope', (id, options = {}, pat) => {
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.scope({ ...options, ctx, id });
return pat.id(id).scope({ ...options, ctx, id });
});

View File

@ -895,17 +895,37 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt',
*/
export const { lock } = registerControl('lock');
/**
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
* Set detune for stacked voices of supported oscillators
*
* @name detune
* @param {number | Pattern} amount between 0 and 1
* @param {number | Pattern} amount
* @synonyms det
* @superdirtOnly
* @example
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
* note("d f a a# a d3").fast(2).s("supersaw").detune("<.1 .2 .5 24.1>")
*
*/
export const { detune, det } = registerControl('detune', 'det');
/**
* Set number of stacked voices for supported oscillators
*
* @name unison
* @param {number | Pattern} numvoices
* @example
* note("d f a a# a d3").fast(2).s("supersaw").unison("<1 2 7>")
*
*/
export const { unison } = registerControl('unison');
/**
* Set the stereo pan spread for supported oscillators
*
* @name spread
* @param {number | Pattern} spread between 0 and 1
* @example
* note("d f a a# a d3").fast(2).s("supersaw").spread("<0 .3 1>")
*
*/
export const { spread } = registerControl('spread');
/**
* Set dryness of reverb. See `room` and `size` for more information about reverb.
*
@ -1512,6 +1532,14 @@ export const { zdelay } = registerControl('zdelay');
export const { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx');
/**
* Sets the color of the hap in visualizations like pianoroll or highlighting.
* @name color
* @synonyms colour
* @param {string} color Hexadecimal or CSS color name
*/
export const { color, colour } = registerControl(['color', 'colour']);
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
export let createParams = (...names) =>

View File

@ -262,7 +262,7 @@ export class Pattern {
}
// Flatterns patterns of patterns, by retriggering/resetting inner patterns at onsets of outer pattern haps
trigJoin(cycleZero = false) {
resetJoin(restart = false) {
const pat_of_pats = this;
return new Pattern((state) => {
return (
@ -273,9 +273,9 @@ export class Pattern {
.map((outer_hap) => {
return (
outer_hap.value
// trig = align the inner pattern cycle start to outer pattern haps
// Trigzero = align the inner pattern cycle zero to outer pattern haps
.late(cycleZero ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos())
// reset = align the inner pattern cycle start to outer pattern haps
// restart = align the inner pattern cycle zero to outer pattern haps
.late(restart ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos())
.query(state)
.map((inner_hap) =>
new Hap(
@ -294,8 +294,8 @@ export class Pattern {
});
}
trigzeroJoin() {
return this.trigJoin(true);
restartJoin() {
return this.resetJoin(true);
}
// Like the other joins above, joins a pattern of patterns of values, into a flatter
@ -708,13 +708,13 @@ export class Pattern {
const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
}
_opTrig(other, func) {
_opReset(other, func) {
const otherPat = reify(other);
return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).trigJoin();
return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).resetJoin();
}
_opTrigzero(other, func) {
_opRestart(other, func) {
const otherPat = reify(other);
return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).trigzeroJoin();
return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).restartJoin();
}
//////////////////////////////////////////////////////////////////////
@ -1024,7 +1024,7 @@ function _composeOp(a, b, func) {
func: [(a, b) => b(a)],
};
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero'];
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart'];
// generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) {
@ -1113,10 +1113,10 @@ function _composeOp(a, b, func) {
* s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>")
*/
Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args);
return this.keepif.reset(...args);
};
Pattern.prototype.resetAll = function (...args) {
return this.keep.trig(...args);
return this.keep.reset(...args);
};
/**
* Restarts the pattern for each onset of the restart pattern.
@ -1126,10 +1126,10 @@ function _composeOp(a, b, func) {
* s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>")
*/
Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args);
return this.keepif.restart(...args);
};
Pattern.prototype.restartAll = function (...args) {
return this.keep.trigzero(...args);
return this.keep.restart(...args);
};
})();
@ -2477,15 +2477,14 @@ export const hsl = register('hsl', (h, s, l, pat) => {
});
/**
* Sets the color of the hap in visualizations like pianoroll or highlighting.
* @name color
* @synonyms colour
* @param {string} color Hexadecimal or CSS color name
* Sets the id of the hap in, for filtering in the future.
* @name id
* @noAutocomplete
* @param {string} id anything unique
*/
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
return pat.withContext((context) => ({ ...context, color }));
});
Pattern.prototype.id = function (id) {
return this.withContext((ctx) => ({ ...ctx, id }));
};
//////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of

View File

@ -110,7 +110,7 @@ export function repl({
const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps);
});
evalScope({
return evalScope({
all,
hush,
cpm,
@ -127,7 +127,7 @@ export function repl({
}
try {
updateState({ code, pending: true });
injectPatternMethods();
await injectPatternMethods();
await beforeEval?.({ code });
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler);

View File

@ -269,7 +269,7 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).trigzeroJoin();
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
@ -279,7 +279,7 @@ export const pickRestart = register('pickRestart', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).trigzeroJoin();
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
@ -288,7 +288,7 @@ export const pickmodRestart = register('pickmodRestart', function (lookup, pat)
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).trigJoin();
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
@ -298,7 +298,7 @@ export const pickReset = register('pickReset', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).trigJoin();
return _pick(lookup, pat, true).resetJoin();
});
/**

View File

@ -181,18 +181,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]);
});
it('can Trig() structure', () => {
it('can Reset() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trig(20, 30)
.add.reset(20, 30)
.early(2),
sequence(26, 27, 36, 37),
);
});
it('can Trigzero() structure', () => {
it('can Restart() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trigzero(20, 30)
.add.restart(20, 30)
.early(2),
sequence(21, 22, 31, 32),
);
@ -233,18 +233,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
it('can Reset() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trig(20, 30)
.keep.reset(20, 30)
.early(2),
sequence(6, 7, 6, 7),
);
});
it('can Trigzero() structure', () => {
it('can Restart() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trigzero(20, 30)
.keep.restart(20, 30)
.early(2),
sequence(1, 2, 1, 2),
);
@ -279,18 +279,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
it('can Reset() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trig(false, true)
.keepif.reset(false, true)
.early(2),
sequence(silence, silence, 6, 7),
);
});
it('can Trigzero() structure', () => {
it('can Restart() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trigzero(false, true)
.keepif.restart(false, true)
.early(2),
sequence(silence, silence, 1, 2),
);

View File

@ -96,9 +96,8 @@ export const cleanupDraw = (clearScreen = true) => {
}
};
Pattern.prototype.onPaint = function (onPaint) {
// this is evil! TODO: add pattern.context
this.context = { onPaint };
Pattern.prototype.onPaint = function () {
console.warn('[draw] onPaint was not overloaded. Some drawings might not work');
return this;
};

View File

@ -129,12 +129,17 @@ export function pianoroll({
colorizeInactive = 1,
fontFamily,
ctx,
id,
} = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (id) {
haps = haps.filter((hap) => hap.context.id === id);
}
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
@ -189,7 +194,7 @@ export function pianoroll({
if (hideInactive && !isActive) {
return;
}
let color = event.value?.color || event.context?.color;
let color = event.value?.color;
active = color || active;
inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive;

View File

@ -19,7 +19,7 @@ function spiralSegment(options) {
cy = 100,
rotate = 0,
thickness = margin / 2,
color = '#0000ff30',
color = 'steelblue',
cap = 'round',
stretch = 1,
fromOpacity = 1,
@ -50,18 +50,18 @@ function spiralSegment(options) {
}
function drawSpiral(options) {
const {
let {
stretch = 1,
size = 80,
thickness = size / 2,
cap = 'butt', // round butt squar,
inset = 3, // start angl,
playheadColor = '#ffffff90',
playheadColor = '#ffffff',
playheadLength = 0.02,
playheadThickness = thickness,
padding = 0,
steady = 1,
inactiveColor = '#ffffff20',
inactiveColor = '#ffffff50',
colorizeInactive = 0,
fade = true,
// logSpiral = true,
@ -69,8 +69,13 @@ function drawSpiral(options) {
time,
haps,
drawTime,
id,
} = options;
if (id) {
haps = haps.filter((hap) => hap.context.id === id);
}
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
@ -97,7 +102,7 @@ function drawSpiral(options) {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const { color } = hap.context;
const color = hap.value?.color;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
spiralSegment({
ctx,

View File

@ -2,32 +2,63 @@
This package contains a embeddable web component for the Strudel REPL.
## Usage
## Usage via Script Tag
Either install with `npm i @strudel/embed` or just use a cdn to import the script:
Use this code in any HTML file:
```html
<script src="https://unpkg.com/@strudel/embed@latest"></script>
<strudel-repl>
<!--
note(`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
-->
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-repl>
```
Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
This will load the strudel website in an iframe, using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<div id="strudel"></div>
<script>
let editor = document.createElement('strudel-repl');
editor.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(editor);
</script>
```
When you're using JSX, you could also use the `code` attribute in your markup:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<strudel-repl code={`
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
`}></strudel-repl>
```

View File

@ -4,7 +4,7 @@ class Strudel extends HTMLElement {
}
connectedCallback() {
setTimeout(() => {
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const code = this.getAttribute('code') || (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe');
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;

View File

@ -2,4 +2,95 @@
The Strudel REPL as a web component.
[Usage example](https://github.com/tidalcycles/strudel/blob/main/examples/buildless/web-component-no-iframe.html)
## Add Script Tag
First place this script tag once in your HTML:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
```
You can also pin the version like this:
```html
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
```
This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase.
See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions.
## Use Web Component
When you've added the script tag, you can use the `strudel-editor` web component:
```html
<strudel-editor>
<!--
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-editor>
```
This will load the Strudel REPL using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(repl);
</script>
```
## Interacting with the REPL
If you get a hold of the `strudel-editor` element, you can interact with the strudel REPL from Javascript:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<strudel-editor id="repl">
<!-- ... -->
</strudel-editor>
<script>
const repl = document.getElementById('repl');
console.log(repl.editor);
</script>
```
or
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute('code', `...`);
document.getElementById('strudel').append(repl);
console.log(repl.editor);
</script>
```
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL.
For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code.

View File

@ -186,3 +186,76 @@ export function getVibratoOscillator(param, value, t) {
return vibratoOscillator;
}
}
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = audioContext.createConstantSource();
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
onComplete();
};
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
};
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = frequencyparam.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
export function applyFM(param, value, begin) {
const {
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let modulator;
let stop = () => {};
if (fmModulationIndex) {
const ac = getAudioContext();
const envGain = ac.createGain();
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform);
modulator = fmmod.node;
stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = begin + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
begin,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(param);
}
}
return { stop };
}

View File

@ -50,8 +50,8 @@ function loadWorklets() {
return workletsLoading;
}
function getWorklet(ac, processor, params) {
const node = new AudioWorkletNode(ac, processor);
export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor, config);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
});

View File

@ -1,31 +1,138 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, getWorklet } from './superdough.mjs';
import {
applyFM,
gainNode,
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
const getFrequencyFromValue = (value) => {
let { note, freq } = value;
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
};
const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = osc.frequency.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() {
[...waveforms, ...noises].forEach((s) => {
[...waveforms].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
let sound = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
// turn down
const g = gainNode(0.3);
const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const envEnd = holdEnd + release + 0.01;
triggerRelease?.(envEnd);
stop(envEnd);
return {
node,
stop: (releaseTime) => {},
};
},
{ type: 'synth', prebake: true },
);
});
registerSound(
'supersaw',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n, unison = 5, spread = 0.6, detune } = value;
detune = detune ?? n ?? 0.18;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
const voices = clamp(unison, 1, 100);
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
let o = getWorklet(
ac,
'supersaw-oscillator',
{
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
},
{
outputChannelCount: [2],
},
);
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
ac,
() => {
o.disconnect();
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
},
begin,
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
};
},
{ prebake: true, type: 'synth' },
);
[...noises].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
@ -36,12 +143,9 @@ export function registerSynthSounds() {
);
let sound;
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
let { density } = value;
sound = getNoiseOscillator(s, t, density);
}
let { density } = value;
sound = getNoiseOscillator(s, t, density);
let { node: o, stop, triggerRelease } = sound;
@ -106,24 +210,7 @@ export function waveformN(partials, type) {
// expects one of waveforms as s
export function getOscillator(s, t, value) {
let {
n: partials,
note,
freq,
noise = 0,
// fm
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let ac = getAudioContext();
let { n: partials, duration, noise = 0 } = value;
let o;
// If no partials are given, use stock waveforms
if (!partials || s === 'sine') {
@ -134,55 +221,15 @@ export function getOscillator(s, t, value) {
else {
o = waveformN(partials, s);
}
// get frequency from note...
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// set frequency
o.frequency.value = Number(freq);
o.frequency.value = getFrequencyFromValue(value);
o.start(t);
// FM
let stopFm;
let envGain = ac.createGain();
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = t + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
t,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(o.frequency);
}
stopFm = stop;
}
// Additional oscillator for vibrato effect
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
// pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration);
const fmModulator = applyFM(o.frequency, value, t);
let noiseMix;
if (noise) {
@ -192,9 +239,9 @@ export function getOscillator(s, t, value) {
return {
node: noiseMix?.node || o,
stop: (time) => {
fmModulator.stop(time);
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
stopFm?.(time);
o.stop(time);
},
triggerRelease: (time) => {

View File

@ -129,3 +129,148 @@ class DistortProcessor extends AudioWorkletProcessor {
}
}
registerProcessor('distort-processor', DistortProcessor);
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
const polyBlep = (phase, dt) => {
// 0 <= phase < 1
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
// -1 < phase < 0
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
return phase * phase + phase + phase + 1;
}
// 0 otherwise
else {
return 0;
}
};
const saw = (phase, dt) => {
const v = 2 * phase - 1;
return v - polyBlep(phase, dt);
};
function lerp(a, b, n) {
return n * (b - a) + a;
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.phase = [];
}
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'panspread',
defaultValue: 0.4,
min: 0,
max: 1,
},
{
name: 'freqspread',
defaultValue: 0.2,
min: 0,
},
{
name: 'detune',
defaultValue: 0,
min: 0,
},
{
name: 'voices',
defaultValue: 5,
min: 1,
},
];
}
process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) {
return true;
}
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0];
const voices = params.voices[0];
const freqspread = params.freqspread[0];
const panspread = params.panspread[0] * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) {
this.phase[n] = this.phase[n] ?? Math.random();
const v = saw(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR;
this.phase[n] += dt;
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
}
}
}
return true;
}
}
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);

View File

@ -186,18 +186,35 @@ export const scale = register('scale', function (scale, pat) {
// legacy..
return pure(step);
}
const asNumber = Number(step);
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
logger(`[tonal] invalid scale step "${step}", expected number`, 'error');
return silence;
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
}
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
}
try {
let note;
if (value.anchor) {
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');

View File

@ -7,20 +7,17 @@ This package provides an easy to use bundle of multiple strudel packages for the
Save this code as a `.html` file and double click it:
```html
<!DOCTYPE html>
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button>
<button id="stop">stop</button>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
<script>
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').jux(rev).play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
As soon as you call `initStrudel()`, all strudel functions are made available.
In this case, we are using the `note` function to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
@ -79,4 +76,4 @@ There will probably be an escapte hatch for that in the future.
## More Examples
Check out the examples folder for more examples, both using plain html and vite!
Check out the examples folder for more examples, both using plain html and vite!

View File

@ -37,10 +37,10 @@
"@strudel/mini": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@rollup/plugin-replace": "^5.0.5"
"@strudel/webaudio": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
"vite": "^5.0.10",
"@rollup/plugin-replace": "^5.0.5"
}
}

15
pnpm-lock.yaml generated
View File

@ -33,6 +33,9 @@ importers:
'@vitest/ui':
specifier: ^1.1.0
version: 1.1.0(vitest@1.1.0)
acorn:
specifier: ^8.11.3
version: 8.11.3
dependency-tree:
specifier: ^10.0.9
version: 10.0.9
@ -449,9 +452,6 @@ importers:
packages/web:
dependencies:
'@rollup/plugin-replace':
specifier: ^5.0.5
version: 5.0.5
'@strudel/core':
specifier: workspace:*
version: link:../core
@ -468,6 +468,9 @@ importers:
specifier: workspace:*
version: link:../webaudio
devDependencies:
'@rollup/plugin-replace':
specifier: ^5.0.5
version: 5.0.5
vite:
specifier: ^5.0.10
version: 5.0.10
@ -572,9 +575,6 @@ importers:
'@strudel/osc':
specifier: workspace:*
version: link:../packages/osc
'@strudel/repl':
specifier: workspace:*
version: link:../packages/repl
'@strudel/serial':
specifier: workspace:*
version: link:../packages/serial
@ -3930,6 +3930,7 @@ packages:
dependencies:
'@rollup/pluginutils': 5.1.0
magic-string: 0.30.5
dev: true
/@rollup/pluginutils@3.1.0(rollup@2.79.1):
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
@ -3955,6 +3956,7 @@ packages:
'@types/estree': 1.0.0
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@rollup/rollup-android-arm-eabi@4.13.0:
resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==}
@ -7478,6 +7480,7 @@ packages:
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}

View File

@ -1939,18 +1939,54 @@ exports[`runs examples > example "delaytime" example index 0 1`] = `
exports[`runs examples > example "detune" example index 0 1`] = `
[
"[ 0/1 → 1/3 | n:0 s:superzow octave:3 detune:0 ]",
"[ 1/3 → 2/3 | n:3 s:superzow octave:3 detune:0 ]",
"[ 2/3 → 1/1 | n:7 s:superzow octave:3 detune:0 ]",
"[ 1/1 → 4/3 | n:0 s:superzow octave:3 detune:0.25 ]",
"[ 4/3 → 5/3 | n:3 s:superzow octave:3 detune:0.25 ]",
"[ 5/3 → 2/1 | n:7 s:superzow octave:3 detune:0.25 ]",
"[ 2/1 → 7/3 | n:0 s:superzow octave:3 detune:0.5 ]",
"[ 7/3 → 8/3 | n:3 s:superzow octave:3 detune:0.5 ]",
"[ 8/3 → 3/1 | n:7 s:superzow octave:3 detune:0.5 ]",
"[ 3/1 → 10/3 | n:0 s:superzow octave:3 detune:1 ]",
"[ 10/3 → 11/3 | n:3 s:superzow octave:3 detune:1 ]",
"[ 11/3 → 4/1 | n:7 s:superzow octave:3 detune:1 ]",
"[ 0/1 → 1/12 | note:d s:supersaw detune:0.1 ]",
"[ 1/12 → 1/6 | note:f s:supersaw detune:0.1 ]",
"[ 1/6 → 1/4 | note:a s:supersaw detune:0.1 ]",
"[ 1/4 → 1/3 | note:a# s:supersaw detune:0.1 ]",
"[ 1/3 → 5/12 | note:a s:supersaw detune:0.1 ]",
"[ 5/12 → 1/2 | note:d3 s:supersaw detune:0.1 ]",
"[ 1/2 → 7/12 | note:d s:supersaw detune:0.1 ]",
"[ 7/12 → 2/3 | note:f s:supersaw detune:0.1 ]",
"[ 2/3 → 3/4 | note:a s:supersaw detune:0.1 ]",
"[ 3/4 → 5/6 | note:a# s:supersaw detune:0.1 ]",
"[ 5/6 → 11/12 | note:a s:supersaw detune:0.1 ]",
"[ 11/12 → 1/1 | note:d3 s:supersaw detune:0.1 ]",
"[ 1/1 → 13/12 | note:d s:supersaw detune:0.2 ]",
"[ 13/12 → 7/6 | note:f s:supersaw detune:0.2 ]",
"[ 7/6 → 5/4 | note:a s:supersaw detune:0.2 ]",
"[ 5/4 → 4/3 | note:a# s:supersaw detune:0.2 ]",
"[ 4/3 → 17/12 | note:a s:supersaw detune:0.2 ]",
"[ 17/12 → 3/2 | note:d3 s:supersaw detune:0.2 ]",
"[ 3/2 → 19/12 | note:d s:supersaw detune:0.2 ]",
"[ 19/12 → 5/3 | note:f s:supersaw detune:0.2 ]",
"[ 5/3 → 7/4 | note:a s:supersaw detune:0.2 ]",
"[ 7/4 → 11/6 | note:a# s:supersaw detune:0.2 ]",
"[ 11/6 → 23/12 | note:a s:supersaw detune:0.2 ]",
"[ 23/12 → 2/1 | note:d3 s:supersaw detune:0.2 ]",
"[ 2/1 → 25/12 | note:d s:supersaw detune:0.5 ]",
"[ 25/12 → 13/6 | note:f s:supersaw detune:0.5 ]",
"[ 13/6 → 9/4 | note:a s:supersaw detune:0.5 ]",
"[ 9/4 → 7/3 | note:a# s:supersaw detune:0.5 ]",
"[ 7/3 → 29/12 | note:a s:supersaw detune:0.5 ]",
"[ 29/12 → 5/2 | note:d3 s:supersaw detune:0.5 ]",
"[ 5/2 → 31/12 | note:d s:supersaw detune:0.5 ]",
"[ 31/12 → 8/3 | note:f s:supersaw detune:0.5 ]",
"[ 8/3 → 11/4 | note:a s:supersaw detune:0.5 ]",
"[ 11/4 → 17/6 | note:a# s:supersaw detune:0.5 ]",
"[ 17/6 → 35/12 | note:a s:supersaw detune:0.5 ]",
"[ 35/12 → 3/1 | note:d3 s:supersaw detune:0.5 ]",
"[ 3/1 → 37/12 | note:d s:supersaw detune:24.1 ]",
"[ 37/12 → 19/6 | note:f s:supersaw detune:24.1 ]",
"[ 19/6 → 13/4 | note:a s:supersaw detune:24.1 ]",
"[ 13/4 → 10/3 | note:a# s:supersaw detune:24.1 ]",
"[ 10/3 → 41/12 | note:a s:supersaw detune:24.1 ]",
"[ 41/12 → 7/2 | note:d3 s:supersaw detune:24.1 ]",
"[ 7/2 → 43/12 | note:d s:supersaw detune:24.1 ]",
"[ 43/12 → 11/3 | note:f s:supersaw detune:24.1 ]",
"[ 11/3 → 15/4 | note:a s:supersaw detune:24.1 ]",
"[ 15/4 → 23/6 | note:a# s:supersaw detune:24.1 ]",
"[ 23/6 → 47/12 | note:a s:supersaw detune:24.1 ]",
"[ 47/12 → 4/1 | note:d3 s:supersaw detune:24.1 ]",
]
`;
@ -2406,8 +2442,6 @@ exports[`runs examples > example "fast" example index 0 1`] = `
exports[`runs examples > example "fastChunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C2 ]",
"[ 1/4 → 1/2 | note:D2 ]",
"[ 1/2 → 3/4 | note:E2 ]",
"[ 3/4 → 1/1 | note:F2 ]",
"[ 1/1 → 5/4 | note:G2 ]",
@ -2416,8 +2450,6 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
"[ 7/4 → 2/1 | note:C3 ]",
"[ 2/1 → 9/4 | note:D3 ]",
"[ 9/4 → 5/2 | note:D2 ]",
"[ 5/2 → 11/4 | note:E2 ]",
"[ 11/4 → 3/1 | note:F2 ]",
"[ 3/1 → 13/4 | note:G2 ]",
"[ 13/4 → 7/2 | note:A2 ]",
"[ 7/2 → 15/4 | note:B2 ]",
@ -4738,34 +4770,34 @@ exports[`runs examples > example "phasersweep" example index 0 1`] = `
exports[`runs examples > example "pianoroll" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:C2 s:piano clip:1 ]",
"[ (1/4 → 1/3) ⇝ 3/8 | note:C2 s:piano clip:1 ]",
"[ 1/4 ⇜ (1/3 → 3/8) | note:A2 s:piano clip:1 ]",
"[ 3/8 → 1/2 | note:A2 s:piano clip:1 ]",
"[ (5/8 → 2/3) ⇝ 3/4 | note:A2 s:piano clip:1 ]",
"[ 5/8 ⇜ (2/3 → 3/4) | note:G2 s:piano clip:1 ]",
"[ 3/4 → 7/8 | note:G2 s:piano clip:1 ]",
"[ 1/1 → 9/8 | note:C2 s:piano clip:1 ]",
"[ (5/4 → 4/3) ⇝ 11/8 | note:C2 s:piano clip:1 ]",
"[ 5/4 ⇜ (4/3 → 11/8) | note:A2 s:piano clip:1 ]",
"[ 11/8 → 3/2 | note:A2 s:piano clip:1 ]",
"[ (13/8 → 5/3) ⇝ 7/4 | note:A2 s:piano clip:1 ]",
"[ 13/8 ⇜ (5/3 → 7/4) | note:G2 s:piano clip:1 ]",
"[ 7/4 → 15/8 | note:G2 s:piano clip:1 ]",
"[ 2/1 → 17/8 | note:C2 s:piano clip:1 ]",
"[ (9/4 → 7/3) ⇝ 19/8 | note:C2 s:piano clip:1 ]",
"[ 9/4 ⇜ (7/3 → 19/8) | note:A2 s:piano clip:1 ]",
"[ 19/8 → 5/2 | note:A2 s:piano clip:1 ]",
"[ (21/8 → 8/3) ⇝ 11/4 | note:A2 s:piano clip:1 ]",
"[ 21/8 ⇜ (8/3 → 11/4) | note:G2 s:piano clip:1 ]",
"[ 11/4 → 23/8 | note:G2 s:piano clip:1 ]",
"[ 3/1 → 25/8 | note:C2 s:piano clip:1 ]",
"[ (13/4 → 10/3) ⇝ 27/8 | note:C2 s:piano clip:1 ]",
"[ 13/4 ⇜ (10/3 → 27/8) | note:A2 s:piano clip:1 ]",
"[ 27/8 → 7/2 | note:A2 s:piano clip:1 ]",
"[ (29/8 → 11/3) ⇝ 15/4 | note:A2 s:piano clip:1 ]",
"[ 29/8 ⇜ (11/3 → 15/4) | note:G2 s:piano clip:1 ]",
"[ 15/4 → 31/8 | note:G2 s:piano clip:1 ]",
"[ 0/1 → 1/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ (1/4 → 1/3) ⇝ 3/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ 1/4 ⇜ (1/3 → 3/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 3/8 → 1/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (5/8 → 2/3) ⇝ 3/4 | note:A2 s:piano clip:1 color:salmon ]",
"[ 5/8 ⇜ (2/3 → 3/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 3/4 → 7/8 | note:G2 s:piano clip:1 color:salmon ]",
"[ 1/1 → 9/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ (5/4 → 4/3) ⇝ 11/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ 5/4 ⇜ (4/3 → 11/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 11/8 → 3/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (13/8 → 5/3) ⇝ 7/4 | note:A2 s:piano clip:1 color:salmon ]",
"[ 13/8 ⇜ (5/3 → 7/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 7/4 → 15/8 | note:G2 s:piano clip:1 color:salmon ]",
"[ 2/1 → 17/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ (9/4 → 7/3) ⇝ 19/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ 9/4 ⇜ (7/3 → 19/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 19/8 → 5/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (21/8 → 8/3) ⇝ 11/4 | note:A2 s:piano clip:1 color:salmon ]",
"[ 21/8 ⇜ (8/3 → 11/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 11/4 → 23/8 | note:G2 s:piano clip:1 color:salmon ]",
"[ 3/1 → 25/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ (13/4 → 10/3) ⇝ 27/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ 13/4 ⇜ (10/3 → 27/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 27/8 → 7/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (29/8 → 11/3) ⇝ 15/4 | note:A2 s:piano clip:1 color:salmon ]",
"[ 29/8 ⇜ (11/3 → 15/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 15/4 → 31/8 | note:G2 s:piano clip:1 color:salmon ]",
]
`;
@ -6806,6 +6838,59 @@ exports[`runs examples > example "splice" example index 0 1`] = `
]
`;
exports[`runs examples > example "spread" example index 0 1`] = `
[
"[ 0/1 → 1/12 | note:d s:supersaw spread:0 ]",
"[ 1/12 → 1/6 | note:f s:supersaw spread:0 ]",
"[ 1/6 → 1/4 | note:a s:supersaw spread:0 ]",
"[ 1/4 → 1/3 | note:a# s:supersaw spread:0 ]",
"[ 1/3 → 5/12 | note:a s:supersaw spread:0 ]",
"[ 5/12 → 1/2 | note:d3 s:supersaw spread:0 ]",
"[ 1/2 → 7/12 | note:d s:supersaw spread:0 ]",
"[ 7/12 → 2/3 | note:f s:supersaw spread:0 ]",
"[ 2/3 → 3/4 | note:a s:supersaw spread:0 ]",
"[ 3/4 → 5/6 | note:a# s:supersaw spread:0 ]",
"[ 5/6 → 11/12 | note:a s:supersaw spread:0 ]",
"[ 11/12 → 1/1 | note:d3 s:supersaw spread:0 ]",
"[ 1/1 → 13/12 | note:d s:supersaw spread:0.3 ]",
"[ 13/12 → 7/6 | note:f s:supersaw spread:0.3 ]",
"[ 7/6 → 5/4 | note:a s:supersaw spread:0.3 ]",
"[ 5/4 → 4/3 | note:a# s:supersaw spread:0.3 ]",
"[ 4/3 → 17/12 | note:a s:supersaw spread:0.3 ]",
"[ 17/12 → 3/2 | note:d3 s:supersaw spread:0.3 ]",
"[ 3/2 → 19/12 | note:d s:supersaw spread:0.3 ]",
"[ 19/12 → 5/3 | note:f s:supersaw spread:0.3 ]",
"[ 5/3 → 7/4 | note:a s:supersaw spread:0.3 ]",
"[ 7/4 → 11/6 | note:a# s:supersaw spread:0.3 ]",
"[ 11/6 → 23/12 | note:a s:supersaw spread:0.3 ]",
"[ 23/12 → 2/1 | note:d3 s:supersaw spread:0.3 ]",
"[ 2/1 → 25/12 | note:d s:supersaw spread:1 ]",
"[ 25/12 → 13/6 | note:f s:supersaw spread:1 ]",
"[ 13/6 → 9/4 | note:a s:supersaw spread:1 ]",
"[ 9/4 → 7/3 | note:a# s:supersaw spread:1 ]",
"[ 7/3 → 29/12 | note:a s:supersaw spread:1 ]",
"[ 29/12 → 5/2 | note:d3 s:supersaw spread:1 ]",
"[ 5/2 → 31/12 | note:d s:supersaw spread:1 ]",
"[ 31/12 → 8/3 | note:f s:supersaw spread:1 ]",
"[ 8/3 → 11/4 | note:a s:supersaw spread:1 ]",
"[ 11/4 → 17/6 | note:a# s:supersaw spread:1 ]",
"[ 17/6 → 35/12 | note:a s:supersaw spread:1 ]",
"[ 35/12 → 3/1 | note:d3 s:supersaw spread:1 ]",
"[ 3/1 → 37/12 | note:d s:supersaw spread:0 ]",
"[ 37/12 → 19/6 | note:f s:supersaw spread:0 ]",
"[ 19/6 → 13/4 | note:a s:supersaw spread:0 ]",
"[ 13/4 → 10/3 | note:a# s:supersaw spread:0 ]",
"[ 10/3 → 41/12 | note:a s:supersaw spread:0 ]",
"[ 41/12 → 7/2 | note:d3 s:supersaw spread:0 ]",
"[ 7/2 → 43/12 | note:d s:supersaw spread:0 ]",
"[ 43/12 → 11/3 | note:f s:supersaw spread:0 ]",
"[ 11/3 → 15/4 | note:a s:supersaw spread:0 ]",
"[ 15/4 → 23/6 | note:a# s:supersaw spread:0 ]",
"[ 23/6 → 47/12 | note:a s:supersaw spread:0 ]",
"[ 47/12 → 4/1 | note:d3 s:supersaw spread:0 ]",
]
`;
exports[`runs examples > example "square" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C3 ]",
@ -7267,6 +7352,59 @@ exports[`runs examples > example "undegradeBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "unison" example index 0 1`] = `
[
"[ 0/1 → 1/12 | note:d s:supersaw unison:1 ]",
"[ 1/12 → 1/6 | note:f s:supersaw unison:1 ]",
"[ 1/6 → 1/4 | note:a s:supersaw unison:1 ]",
"[ 1/4 → 1/3 | note:a# s:supersaw unison:1 ]",
"[ 1/3 → 5/12 | note:a s:supersaw unison:1 ]",
"[ 5/12 → 1/2 | note:d3 s:supersaw unison:1 ]",
"[ 1/2 → 7/12 | note:d s:supersaw unison:1 ]",
"[ 7/12 → 2/3 | note:f s:supersaw unison:1 ]",
"[ 2/3 → 3/4 | note:a s:supersaw unison:1 ]",
"[ 3/4 → 5/6 | note:a# s:supersaw unison:1 ]",
"[ 5/6 → 11/12 | note:a s:supersaw unison:1 ]",
"[ 11/12 → 1/1 | note:d3 s:supersaw unison:1 ]",
"[ 1/1 → 13/12 | note:d s:supersaw unison:2 ]",
"[ 13/12 → 7/6 | note:f s:supersaw unison:2 ]",
"[ 7/6 → 5/4 | note:a s:supersaw unison:2 ]",
"[ 5/4 → 4/3 | note:a# s:supersaw unison:2 ]",
"[ 4/3 → 17/12 | note:a s:supersaw unison:2 ]",
"[ 17/12 → 3/2 | note:d3 s:supersaw unison:2 ]",
"[ 3/2 → 19/12 | note:d s:supersaw unison:2 ]",
"[ 19/12 → 5/3 | note:f s:supersaw unison:2 ]",
"[ 5/3 → 7/4 | note:a s:supersaw unison:2 ]",
"[ 7/4 → 11/6 | note:a# s:supersaw unison:2 ]",
"[ 11/6 → 23/12 | note:a s:supersaw unison:2 ]",
"[ 23/12 → 2/1 | note:d3 s:supersaw unison:2 ]",
"[ 2/1 → 25/12 | note:d s:supersaw unison:7 ]",
"[ 25/12 → 13/6 | note:f s:supersaw unison:7 ]",
"[ 13/6 → 9/4 | note:a s:supersaw unison:7 ]",
"[ 9/4 → 7/3 | note:a# s:supersaw unison:7 ]",
"[ 7/3 → 29/12 | note:a s:supersaw unison:7 ]",
"[ 29/12 → 5/2 | note:d3 s:supersaw unison:7 ]",
"[ 5/2 → 31/12 | note:d s:supersaw unison:7 ]",
"[ 31/12 → 8/3 | note:f s:supersaw unison:7 ]",
"[ 8/3 → 11/4 | note:a s:supersaw unison:7 ]",
"[ 11/4 → 17/6 | note:a# s:supersaw unison:7 ]",
"[ 17/6 → 35/12 | note:a s:supersaw unison:7 ]",
"[ 35/12 → 3/1 | note:d3 s:supersaw unison:7 ]",
"[ 3/1 → 37/12 | note:d s:supersaw unison:1 ]",
"[ 37/12 → 19/6 | note:f s:supersaw unison:1 ]",
"[ 19/6 → 13/4 | note:a s:supersaw unison:1 ]",
"[ 13/4 → 10/3 | note:a# s:supersaw unison:1 ]",
"[ 10/3 → 41/12 | note:a s:supersaw unison:1 ]",
"[ 41/12 → 7/2 | note:d3 s:supersaw unison:1 ]",
"[ 7/2 → 43/12 | note:d s:supersaw unison:1 ]",
"[ 43/12 → 11/3 | note:f s:supersaw unison:1 ]",
"[ 11/3 → 15/4 | note:a s:supersaw unison:1 ]",
"[ 15/4 → 23/6 | note:a# s:supersaw unison:1 ]",
"[ 23/6 → 47/12 | note:a s:supersaw unison:1 ]",
"[ 47/12 → 4/1 | note:d3 s:supersaw unison:1 ]",
]
`;
exports[`runs examples > example "unit" example index 0 1`] = `
[
"[ 0/1 → 1/4 | speed:1 s:bd unit:c ]",

File diff suppressed because it is too large Load Diff

View File

@ -222,7 +222,6 @@ export const testCycles = {
randomBells: 24,
waa: 16,
waar: 16,
hyperpop: 10,
festivalOfFingers3: 16,
};

View File

@ -1,6 +1,86 @@
{
"/doc.json": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/codemirror/html.mjs": [
"html",
"h"
],
"/packages/codemirror/autocomplete.mjs": [
"Autocomplete",
"strudelAutocomplete",
"isAutoCompletionEnabled"
],
"/packages/codemirror/tooltip.mjs": [
"strudelTooltip",
"isTooltipEnabled"
],
"/packages/codemirror/flash.mjs": [
"setFlash",
"flashField",
"flash",
"isFlashEnabled"
],
"/packages/codemirror/highlight.mjs": [
"setMiniLocations",
"showMiniLocations",
"updateMiniLocations",
"highlightMiniLocations",
"highlightExtension",
"isPatternHighlightingEnabled"
],
"/packages/codemirror/keybindings.mjs": [
"keybindings"
],
"/packages/codemirror/themes/strudel-theme.mjs": [],
"/packages/codemirror/themes/bluescreen.mjs": [
"settings"
],
"/packages/codemirror/themes/blackscreen.mjs": [
"settings"
],
"/packages/codemirror/themes/whitescreen.mjs": [
"settings"
],
"/packages/codemirror/themes/teletext.mjs": [
"settings"
],
"/packages/codemirror/themes/algoboy.mjs": [
"settings"
],
"/packages/codemirror/themes/terminal.mjs": [
"settings"
],
"/packages/codemirror/themes.mjs": [
"themes",
"settings",
"themeColors",
"theme",
"injectStyle",
"initTheme",
"activateTheme"
],
"/packages/codemirror/slider.mjs": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/codemirror/widget.mjs": [
"addWidget",
"updateWidgets",
"setWidget",
"BlockWidget",
"widgetPlugin",
"registerWidget"
],
"/packages/codemirror/codemirror.mjs": [
"defaultSettings",
"codemirrorSettings",
"initEditor",
"StrudelMirror"
],
"/packages/codemirror/index.mjs": [],
"/packages/core/fraction.mjs": [
"gcd"
"gcd",
"lcm"
],
"/packages/core/timespan.mjs": [
"TimeSpan"
@ -11,6 +91,10 @@
"/packages/core/state.mjs": [
"State"
],
"/packages/core/logger.mjs": [
"logKey",
"logger"
],
"/packages/core/util.mjs": [
"isNoteWithOctave",
"isNote",
@ -20,13 +104,14 @@
"freqToMidi",
"valueToMidi",
"_mod",
"nanFallback",
"getSoundIndex",
"getPlayableNoteValue",
"getFrequency",
"rotate",
"pipe",
"compose",
"flatten",
"id",
"constant",
"listRange",
"curry",
@ -42,7 +127,8 @@
"unicodeToBase64",
"base64ToUnicode",
"code2hash",
"hash2code"
"hash2code",
"objectMap"
],
"/packages/core/value.mjs": [
"unionWithObj",
@ -51,18 +137,20 @@
"map"
],
"/packages/core/drawLine.mjs": [],
"/packages/core/logger.mjs": [
"logKey",
"logger"
],
"/packages/core/pattern.mjs": [
"setStringParser",
"polyrhythm",
"pr",
"pm",
"nothing",
"isPattern",
"reify",
"stackLeft",
"stackRight",
"stackCentre",
"stackBy",
"fastcat",
"_polymeterListSteps",
"set",
"keep",
"keepif",
@ -99,16 +187,195 @@
"stutWith",
"stutwith",
"iterback",
"slowchunk",
"slowChunk",
"chunkback",
"fastchunk",
"bypass",
"duration",
"hsla",
"hsl",
"colour",
"loopat",
"loopatcps"
],
"/packages/core/controls.mjs": [],
"/packages/core/controls.mjs": [
"createParam",
"sound",
"src",
"att",
"fmi",
"fmrelease",
"fmvelocity",
"analyze",
"fft",
"dec",
"sus",
"rel",
"hold",
"bandf",
"bp",
"bandq",
"loopb",
"loope",
"ch",
"phaserrate",
"phasr",
"ph",
"phs",
"phc",
"phd",
"phasdp",
"cutoff",
"ctf",
"lp",
"lpe",
"hpe",
"bpe",
"lpa",
"hpa",
"bpa",
"lpd",
"hpd",
"bpd",
"lps",
"hps",
"bps",
"lpr",
"hpr",
"bpr",
"fanchor",
"vibrato",
"v",
"vmod",
"hcutoff",
"hp",
"hresonance",
"resonance",
"delayfb",
"dfb",
"delayt",
"dt",
"lock",
"det",
"fadeTime",
"fadeOutTime",
"fadeInTime",
"patt",
"pdec",
"psustain",
"psus",
"prel",
"gate",
"gat",
"activeLabel",
"degree",
"mtranspose",
"ctranspose",
"harmonic",
"stepsPerOctave",
"octaveR",
"nudge",
"overgain",
"overshape",
"panspan",
"pansplay",
"panwidth",
"panorient",
"rate",
"slide",
"semitone",
"voice",
"chord",
"dictionary",
"dict",
"anchor",
"offset",
"octaves",
"mode",
"rlp",
"rdim",
"rfade",
"ir",
"size",
"sz",
"rsize",
"dist",
"compressorKnee",
"compressorRatio",
"compressorAttack",
"compressorRelease",
"waveloss",
"density",
"expression",
"sustainpedal",
"tremolodepth",
"tremdp",
"tremolorate",
"tremr",
"fshift",
"fshiftnote",
"fshiftphase",
"triode",
"krush",
"kcutoff",
"octer",
"octersub",
"octersubsub",
"ring",
"ringf",
"ringdf",
"freeze",
"xsdelay",
"tsdelay",
"real",
"imag",
"enhance",
"partials",
"comb",
"smear",
"scram",
"binshift",
"hbrick",
"lbrick",
"midichan",
"control",
"ccn",
"ccv",
"polyTouch",
"midibend",
"miditouch",
"ctlNum",
"frameRate",
"frames",
"hours",
"midicmd",
"minutes",
"progNum",
"seconds",
"songPtr",
"uid",
"val",
"cps",
"legato",
"dur",
"zrand",
"curve",
"deltaSlide",
"pitchJump",
"pitchJumpTime",
"lfo",
"repeatTime",
"znoise",
"zmod",
"zcrush",
"zdelay",
"tremolo",
"zzfx",
"colour",
"createParams",
"ad",
"ds",
"ar"
],
"/packages/core/euclid.mjs": [
"bjork",
"euclidrot"
@ -128,6 +395,8 @@
"brandBy",
"brand",
"_irand",
"pickSqueeze",
"pickmodSqueeze",
"__chooseWith",
"randcat",
"wchoose",
@ -143,6 +412,9 @@
"evalScope",
"evaluate"
],
"/packages/core/neocyclist.mjs": [
"NeoCyclist"
],
"/packages/core/zyklus.mjs": [],
"/packages/core/cyclist.mjs": [
"Cyclist"
@ -155,31 +427,6 @@
"repl",
"getTrigger"
],
"/packages/core/draw.mjs": [
"getDrawContext",
"cleanupDraw",
"Framer",
"Drawer"
],
"/packages/core/animate.mjs": [
"x",
"y",
"w",
"h",
"angle",
"r",
"fill",
"smear",
"rescale",
"moveXY",
"zoomIn"
],
"/packages/core/pianoroll.mjs": [
"getDrawOptions",
"getPunchcardPainter",
"drawPianoroll"
],
"/packages/core/spiral.mjs": [],
"/packages/core/ui.mjs": [
"backgroundImage",
"cleanupUi"
@ -199,6 +446,37 @@
"/packages/desktopbridge/midibridge.mjs": [],
"/packages/desktopbridge/oscbridge.mjs": [],
"/packages/desktopbridge/index.mjs": [],
"/packages/draw/draw.mjs": [
"getDrawContext",
"cleanupDraw",
"Framer",
"Drawer"
],
"/packages/draw/animate.mjs": [
"x",
"y",
"w",
"h",
"angle",
"r",
"fill",
"smear",
"rescale",
"moveXY",
"zoomIn"
],
"/packages/draw/color.mjs": [
"colorMap",
"convertColorToNumber",
"convertHexToNumber"
],
"/packages/draw/pianoroll.mjs": [
"getDrawOptions",
"getPunchcardPainter",
"drawPianoroll"
],
"/packages/draw/spiral.mjs": [],
"/packages/draw/index.mjs": [],
"/packages/midi/midi.mjs": [
"WebMidi",
"enableWebMidi",
@ -219,6 +497,13 @@
"miniAllStrings"
],
"/packages/mini/index.mjs": [],
"/packages/repl/prebake.mjs": [
"prebake"
],
"/packages/repl/repl-component.mjs": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/repl/index.mjs": [],
"/packages/soundfonts/gm.mjs": [],
"/packages/soundfonts/fontloader.mjs": [
"getFontBufferSource",
@ -234,7 +519,92 @@
"loadSoundfont"
],
"/packages/soundfonts/index.mjs": [],
"/packages/tonal/tonal.mjs": [],
"/packages/superdough/feedbackdelay.mjs": [],
"/packages/superdough/reverbGen.mjs": [],
"/packages/superdough/reverb.mjs": [],
"/packages/superdough/vowel.mjs": [
"vowelFormant"
],
"/packages/superdough/logger.mjs": [
"logger",
"setLogger"
],
"/packages/superdough/util.mjs": [
"tokenizeNote",
"noteToMidi",
"midiToFreq",
"clamp",
"freqToMidi",
"valueToMidi",
"nanFallback",
"_mod",
"getSoundIndex"
],
"/packages/superdough/helpers.mjs": [
"gainNode",
"getParamADSR",
"getCompressor",
"getADSRValues",
"createFilter",
"drywet",
"getPitchEnvelope",
"getVibratoOscillator",
"webAudioTimeout",
"applyFM"
],
"/packages/superdough/sampler.mjs": [
"getCachedBuffer",
"getSampleBufferSource",
"loadBuffer",
"reverseBuffer",
"getLoadedBuffer",
"processSampleMap",
"registerSamplesPrefix",
"onTriggerSample"
],
"/packages/superdough/superdough.mjs": [
"soundMap",
"registerSound",
"getSound",
"resetLoadedSounds",
"setDefaultAudioContext",
"getAudioContext",
"getWorklet",
"initAudio",
"initAudioOnFirstClick",
"initializeAudioOutput",
"connectToDestination",
"panic",
"analysers",
"analysersData",
"getAnalyserById",
"getAnalyzerData",
"resetGlobalEffects",
"superdough",
"superdoughTrigger"
],
"/packages/superdough/noise.mjs": [
"getNoiseOscillator",
"getNoiseMix"
],
"/packages/superdough/synth.mjs": [
"registerSynthSounds",
"waveformN",
"getOscillator"
],
"/packages/superdough/zzfx_fork.mjs": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/superdough/zzfx.mjs": [
"getZZFX",
"registerZZFXSounds"
],
"/packages/superdough/dspworklet.mjs": [
"dspWorklet",
"dough",
"doughTrigger"
],
"/packages/superdough/index.mjs": [],
"/packages/tonal/tonleiter.mjs": [
"pc2chroma",
"rotateChroma",
@ -242,30 +612,40 @@
"tokenizeChord",
"note2pc",
"note2oct",
"note2midi",
"note2chroma",
"midi2chroma",
"pitch2chroma",
"step2semitones",
"x2midi",
"scaleStep",
"nearestNumberIndex",
"stepInNamedScale",
"renderVoicing",
"accidentalOffset",
"Step",
"Note"
],
"/packages/tonal/tonal.mjs": [],
"/packages/tonal/ireal.mjs": [
"simple",
"complex"
],
"/packages/tonal/voicings.mjs": [
"voicingRegistry",
"setDefaultVoicings",
"setVoicingRange",
"registerVoicings",
"voicingAlias"
"voicingAlias",
"resetVoicings"
],
"/packages/tonal/index.mjs": [
"packageName"
],
"/packages/tonal/index.mjs": [],
"/packages/transpiler/transpiler.mjs": [
"transpiler"
"registerWidgetType",
"transpiler",
"getWidgetID"
],
"/packages/transpiler/index.mjs": [
"evaluate"

View File

@ -33,7 +33,6 @@
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/repl": "workspace:*",
"@strudel/serial": "workspace:*",
"@strudel/soundfonts": "workspace:*",
"@strudel/tonal": "workspace:*",

View File

@ -102,9 +102,10 @@ export const SIDEBAR: Sidebar = {
{ text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' },
],
Development: [
{ text: 'Strudel in your Project', link: 'technical-manual/project-start' },
{ text: 'Packages', link: 'technical-manual/packages' },
{ text: 'REPL', link: 'technical-manual/repl' },
{ text: 'Sounds', link: 'technical-manual/sounds' },
{ text: 'Packages', link: 'technical-manual/packages' },
{ text: 'Docs', link: 'technical-manual/docs' },
{ text: 'Testing', link: 'technical-manual/testing' },
// { text: 'Internals', link: 'technical-manual/internals' },

View File

@ -5,7 +5,6 @@ import { getPunchcardPainter } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
import { StrudelMirror } from '@strudel/codemirror';
// import { prebake } from '@strudel/repl';
import { prebake } from '../repl/prebake.mjs';
import { loadModules } from '../repl/util.mjs';
import Claviature from '@components/Claviature';

View File

@ -41,8 +41,8 @@ This makes way for other ways to align the pattern, and several are already defi
- `mix` - structures from both patterns are combined, so that the new events are not fragments but are created at intersections of events from both sides.
- `squeeze` - cycles from the pattern on the right are squeezed into events on the left. So that e.g. `"0 1 2".add.squeeze("10 20")` is equivalent to `"[10 20] [11 21] [12 22]"`.
- `squeezeout` - as with `squeeze`, but cycles from the left are squeezed into events on the right. So, `"0 1 2".add.squeezeout("10 20")` is equivalent to `[10 11 12] [20 21 22]`.
- `trig` is similar to `squeezeout` in that cycles from the right are aligned with events on the left. However those cycles are not 'squeezed', rather they are truncated to fit the event. So `"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'trigger' cycles on the left.
- `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.
- `reset` is similar to `squeezeout` in that cycles from the right are aligned with events on the left. However those cycles are not 'squeezed', rather they are truncated to fit the event. So `"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")` would be equivalent to `10 11 12 13 20 21 30 31`. In effect, events on the right 'trigger' cycles on the left.
- `restart` is similar to `reset`, but the pattern is 'restarted' from its very first cycle, rather than from the current cycle. `reset` and `restart` 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.

View File

@ -60,13 +60,13 @@ These functions are more low level, probably not needed by the live coder.
<JsDoc client:idle name="Pattern#innerJoin" h={0} />
## trigJoin
## resetJoin
<JsDoc client:idle name="Pattern#trigJoin" h={0} />
<JsDoc client:idle name="Pattern#resetJoin" h={0} />
## trigzeroJoin
## restartJoin
<JsDoc client:idle name="Pattern#trigzeroJoin" h={0} />
<JsDoc client:idle name="Pattern#restartJoin" h={0} />
## squeezeJoin

View File

@ -19,6 +19,15 @@ The purpose of the multiple packages is to
[See the latest published packages on npm](https://www.npmjs.com/search?q=%40strudel).
Here is an overview of all the packages:
### Umbrella Packages
These packages give you a batteries-included point of getting started, and most likely the thing you'd want to use in your project:
- [repl](https://github.com/tidalcycles/strudel/tree/main/packages/repl): The Strudel REPL as a web component.
- [web](https://github.com/tidalcycles/strudel/tree/main/packages/web): Strudel library for the browser, without UI.
To find out more about these two, read [Using Strudel in Your Project](/technical-manual/project-start)
### Essential Packages
These package are the most essential. You might want to use all of those if you're using strudel in your project:

View File

@ -0,0 +1,129 @@
---
title: Using Strudel in your Project
layout: ../../layouts/MainLayout.astro
---
# Using Strudel in your Project
This Guide shows you the different ways to get started with using Strudel in your own project.
## Embedding the Strudel REPL
There are 3 quick ways to embed strudel in your website:
1. Embed the strudel website as an iframe directly
2. Embed the strudel website as an iframe using `@strudel/embed`
3. Embed the REPL directly using `@strudel/repl`
### Inside an iframe
Using an iframe is the most easy way to embed a studel tune.
You can embed any pattern of your choice via an iframe and the URL of the pattern of your choice:
```html
<iframe src="https://strudel.cc/?xwWRfuCE8TAR" width="600" height="300"></iframe>
```
The URL can be obtained by pressing `share` in the REPL.
Note that these share links depend on a database, which is not guaranteed to live forever.
To make sure your code is not lost, you can also use the long url:
```html
<iframe
src="https://strudel.cc/#c2V0Y3BzKDEpCm4oIjwwIDEgMiAzIDQ%2BKjgiKS5zY2FsZSgnRzQgbWlub3InKQoucygiZ21fbGVhZF82X3ZvaWNlIikKLmNsaXAoc2luZS5yYW5nZSguMiwuOCkuc2xvdyg4KSkKLmp1eChyZXYpCi5yb29tKDIpCi5zb21ldGltZXMoYWRkKG5vdGUoIjEyIikpKQoubHBmKHBlcmxpbi5yYW5nZSgyMDAsMjAwMDApLnNsb3coNCkp"
width="600"
height="300"
></iframe>
```
That long URL can just be copy pasted from the URL bar when you're on the strudel website. It always reflects the latest evaluation of your code.
### @strudel/embed
To simplify the process of emebdding via an iframe, you can use the package `@strudel/embed`:
```html
<script src="https://unpkg.com/@strudel/embed@latest"></script>
<strudel-repl>
<!--
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-repl>
```
This will load the strudel website in an iframe, using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
For alternative ways to load this package, see the [@strudel/embed README](https://github.com/tidalcycles/strudel/tree/main/packages/embed#strudelembed).
### @strudel/repl
Loading strudel directly in your site, without an iframe, looks similar to the iframe variant:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<strudel-editor>
<!--
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-editor>
```
Here, we're loading `@strudel/repl` instead of `@strudel/embed`, and the component is called `strudel-editor` instead of `strudel-repl`.
Yes the naming is a bit confusing..
The upside of using the repl without an iframe is that you can pin the strudel version you're using:
```html
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
<strudel-editor>
<!--
...
-->
</strudel-editor>
```
This will guarantee your pattern wont break due to changes to the strudel project in the future.
For more info on this package, see the [@strudel/repl README](https://github.com/tidalcycles/strudel/tree/main/packages/repl#strudelrepl).
## With your own UI
The above approach assumes you want to use the builtin [codemirror](https://codemirror.net/) editor.
If you'd rather use your own UI, you can use the `@strudel/web` package:
```html
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button>
<button id="stop">stop</button>
<script>
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').jux(rev).play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
For more info on this package, see the [@strudel/web README](https://github.com/tidalcycles/strudel/tree/main/packages/web#strudelweb).
## Via npm
[All the packages and many more are available on npm under the @strudel namespace](https://www.npmjs.com/search?q=%40strudel).
There are actually many more packages you can use to have fine grained control over what you use and what not.
To use these packages, you have to use a bundler that supports es modules, like [vite](https://vitejs.dev/).
To find out more about the purpose of each package, see [Packages](/technical-manual/packages)

View File

@ -106,8 +106,7 @@ stack(
[B3@2 D4] [A3@2 [G3 A3]] [B3@2 D4] [A3]
[B3@2 D4] [A4@2 G4] D5@2
[D5@2 [C5 B4]] [[C5 B4] G4@2] [C5@2 [B4 A4]] [[B4 A4] E4@2]
[D5@2 [C5 B4]] [[C5 B4] G4 C5] [G5] [~ ~ B3]\`
.color('#9C7C38'),
[D5@2 [C5 B4]] [[C5 B4] G4 C5] [G5] [~ ~ B3]\`,
// bass
\`[[C2 G2] E3@2] [[C2 G2] F#3@2] [[C2 G2] E3@2] [[C2 G2] F#3@2]
[[B1 D3] G3@2] [[Bb1 Db3] G3@2] [[A1 C3] G3@2] [[D2 C3] F#3@2]
@ -115,7 +114,6 @@ stack(
[[B1 D3] G3@2] [[Bb1 Db3] G3@2] [[A1 C3] G3@2] [[D2 C3] F#3@2]
[[F2 C3] E3@2] [[E2 B2] D3@2] [[D2 A2] C3@2] [[C2 G2] B2@2]
[[F2 C3] E3@2] [[E2 B2] D3@2] [[Eb2 Bb2] Db3@2] [[D2 A2] C3 [F3,G2]]\`
.color('#4C4646')
).transpose(12).slow(48)
.superimpose(x=>x.add(0.06)) // add slightly detuned voice
.note()
@ -447,58 +445,6 @@ note(
.room(.5)
.lpa(.125).lpenv(-2).v("8:.125").fanchor(.25)`;
export const hyperpop = `// "Hyperpop"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
const lfo = cosine.slow(15);
const lfo2 = sine.slow(16);
const filter1 = x=>x.cutoff(lfo2.range(300,3000));
const filter2 = x=>x.hcutoff(lfo.range(1000,6000)).cutoff(4000)
const scales = cat('D3 major', 'G3 major').slow(8)
samples({
bd: '344/344757_1676145-lq.mp3',
sn: '387/387186_7255534-lq.mp3',
hh: '561/561241_12517458-lq.mp3',
hh2:'44/44944_236326-lq.mp3',
hh3: '44/44944_236326-lq.mp3',
}, 'https://cdn.freesound.org/previews/')
stack(
"-7 0 -7 7".struct("x(5,8,1)").fast(2).sub(7)
.scale(scales)
.note()
.s("sawtooth,square")
.gain(.3).attack(0.01).decay(0.1).sustain(.5)
.apply(filter1)
.lpa(.1).lpenv(2).ftype('24db')
,
n("~@3 [<2 3>,<4 5>]")
.echo(4,1/16,.7)
.scale(scales)
.s('square').gain(.7)
.attack(0.01).decay(0.1).sustain(0)
.apply(filter1),
"6 4 2".add(14)
.superimpose(sub("5"))
.fast(1).euclidLegato(3,8)
.mask("<1 0@7>")
.fast(2).n()
.echo(32, 1/8, .8)
.scale(scales)
.s("sawtooth")
.mul(gain(sine.range(.1,.4).slow(8)))
.attack(.001).decay(.2).sustain(0)
.apply(filter2)
).stack(
stack(
"bd <~@7 [~ bd]>".fast(2),
"~ sn",
"[~ hh3]*2"
).s().fast(2).gain(.7)
).slow(2)`;
export const festivalOfFingers3 = `// "Festival of fingers 3"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos
@ -615,7 +561,7 @@ setcps(1)
"<8(3,8) <7 7*2> [4 5@3] 8>".sub(1) // sub 1 -> 1-indexed
.layer(
x=>x,
x=>x.add(7).color('steelblue')
x=>x.add(7)
.off(1/8,x=>x.add("2,4").off(1/8,x=>x.add(5).echo(4,.125,.5)))
.slow(2),
).n().scale('A1 minor')