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: After cloning the project, you can run the REPL locally:
```bash ```bash
pnpm run setup pnpm i
pnpm run repl pnpm dev
``` ```
## Using Strudel In Your Project ## 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 Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
- [`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.
## Contributing ## Contributing

View file

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

View file

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

View file

@ -1,15 +1,20 @@
// this barrel export is currently only used to find undocumented exports // 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/core/index.mjs';
export * from './packages/csound/index.mjs'; export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/desktopbridge/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/midi/index.mjs';
export * from './packages/mini/index.mjs'; export * from './packages/mini/index.mjs';
export * from './packages/osc/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/serial/index.mjs';
export * from './packages/soundfonts/index.mjs'; export * from './packages/soundfonts/index.mjs';
export * from './packages/superdough/index.mjs';
export * from './packages/tonal/index.mjs'; export * from './packages/tonal/index.mjs';
export * from './packages/transpiler/index.mjs'; export * from './packages/transpiler/index.mjs';
export * from './packages/web/index.mjs';
export * from './packages/webaudio/index.mjs'; export * from './packages/webaudio/index.mjs';
export * from './packages/xen/index.mjs'; export * from './packages/xen/index.mjs';

View file

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

View file

@ -141,7 +141,6 @@ export class StrudelMirror {
this.painters = []; this.painters = [];
this.drawTime = drawTime; this.drawTime = drawTime;
this.onDraw = onDraw; this.onDraw = onDraw;
const self = this;
this.id = id || s4(); this.id = id || s4();
this.drawer = new Drawer((haps, time) => { this.drawer = new Drawer((haps, time) => {
@ -150,13 +149,6 @@ export class StrudelMirror {
this.onDraw?.(haps, time, currentFrame, this.painters); this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime); }, 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(); this.prebaked = prebake();
autodraw && this.drawFirstFrame(); autodraw && this.drawFirstFrame();
@ -182,6 +174,14 @@ export class StrudelMirror {
beforeEval: async () => { beforeEval: async () => {
cleanupDraw(); cleanupDraw();
this.painters = []; 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 this.prebaked;
await replOptions?.beforeEval?.(); await replOptions?.beforeEval?.();
}, },

View file

@ -92,7 +92,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) { if (haps.has(id)) {
const hap = haps.get(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 // Get explicit channels for color values
/* /*
const swatch = document.createElement('div'); const swatch = document.createElement('div');

View file

@ -106,17 +106,23 @@ function getCanvasWidget(id, options = {}) {
registerWidget('_pianoroll', (id, options = {}, pat) => { registerWidget('_pianoroll', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d'); 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) => { registerWidget('_punchcard', (id, options = {}, pat) => {
options = { width: 200, height: 200, size: 36, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d'); 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) => { registerWidget('_scope', (id, options = {}, pat) => {
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options }; options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d'); 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'); 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 * @name detune
* @param {number | Pattern} amount between 0 and 1 * @param {number | Pattern} amount
* @synonyms det * @synonyms det
* @superdirtOnly
* @example * @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'); 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. * 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 { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx'); 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 // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
export let createParams = (...names) => 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 // 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; const pat_of_pats = this;
return new Pattern((state) => { return new Pattern((state) => {
return ( return (
@ -273,9 +273,9 @@ export class Pattern {
.map((outer_hap) => { .map((outer_hap) => {
return ( return (
outer_hap.value outer_hap.value
// trig = align the inner pattern cycle start to outer pattern haps // reset = align the inner pattern cycle start to outer pattern haps
// Trigzero = align the inner pattern cycle zero to outer pattern haps // restart = align the inner pattern cycle zero to outer pattern haps
.late(cycleZero ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos()) .late(restart ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos())
.query(state) .query(state)
.map((inner_hap) => .map((inner_hap) =>
new Hap( new Hap(
@ -294,8 +294,8 @@ export class Pattern {
}); });
} }
trigzeroJoin() { restartJoin() {
return this.trigJoin(true); return this.resetJoin(true);
} }
// Like the other joins above, joins a pattern of patterns of values, into a flatter // 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); const otherPat = reify(other);
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin(); return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
} }
_opTrig(other, func) { _opReset(other, func) {
const otherPat = reify(other); 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); 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)], 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 // generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) { 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)>") * s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>")
*/ */
Pattern.prototype.reset = function (...args) { Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args); return this.keepif.reset(...args);
}; };
Pattern.prototype.resetAll = function (...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. * 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)>") * s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>")
*/ */
Pattern.prototype.restart = function (...args) { Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args); return this.keepif.restart(...args);
}; };
Pattern.prototype.restartAll = function (...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. * Sets the id of the hap in, for filtering in the future.
* @name color * @name id
* @synonyms colour * @noAutocomplete
* @param {string} color Hexadecimal or CSS color name * @param {string} id anything unique
*/ */
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288 Pattern.prototype.id = function (id) {
export const { color, colour } = register(['color', 'colour'], function (color, pat) { return this.withContext((ctx) => ({ ...ctx, id }));
return pat.withContext((context) => ({ ...context, color })); };
});
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of // 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) { const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps); return pat._fast(cpm / 60 / scheduler.cps);
}); });
evalScope({ return evalScope({
all, all,
hush, hush,
cpm, cpm,
@ -127,7 +127,7 @@ export function repl({
} }
try { try {
updateState({ code, pending: true }); updateState({ code, pending: true });
injectPatternMethods(); await injectPatternMethods();
await beforeEval?.({ code }); await beforeEval?.({ code });
shouldHush && hush(); shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler); let { pattern, meta } = await _evaluate(code, transpiler);

View file

@ -269,7 +269,7 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) {
* @returns {Pattern} * @returns {Pattern}
*/ */
export const pickRestart = register('pickRestart', function (lookup, pat) { 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, /** * 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} * @returns {Pattern}
*/ */
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) { 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. /** * 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} * @returns {Pattern}
*/ */
export const pickReset = register('pickReset', function (lookup, pat) { 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, /** * 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} * @returns {Pattern}
*/ */
export const pickmodReset = register('pickmodReset', function (lookup, pat) { 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), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]); ]);
}); });
it('can Trig() structure', () => { it('can Reset() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trig(20, 30) .add.reset(20, 30)
.early(2), .early(2),
sequence(26, 27, 36, 37), sequence(26, 27, 36, 37),
); );
}); });
it('can Trigzero() structure', () => { it('can Restart() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.trigzero(20, 30) .add.restart(20, 30)
.early(2), .early(2),
sequence(21, 22, 31, 32), sequence(21, 22, 31, 32),
); );
@ -233,18 +233,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]); ]);
}); });
it('can Trig() structure', () => { it('can Reset() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trig(20, 30) .keep.reset(20, 30)
.early(2), .early(2),
sequence(6, 7, 6, 7), sequence(6, 7, 6, 7),
); );
}); });
it('can Trigzero() structure', () => { it('can Restart() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.trigzero(20, 30) .keep.restart(20, 30)
.early(2), .early(2),
sequence(1, 2, 1, 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), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]); ]);
}); });
it('can Trig() structure', () => { it('can Reset() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trig(false, true) .keepif.reset(false, true)
.early(2), .early(2),
sequence(silence, silence, 6, 7), sequence(silence, silence, 6, 7),
); );
}); });
it('can Trigzero() structure', () => { it('can Restart() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.trigzero(false, true) .keepif.restart(false, true)
.early(2), .early(2),
sequence(silence, silence, 1, 2), sequence(silence, silence, 1, 2),
); );

View file

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

View file

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

View file

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

View file

@ -2,32 +2,63 @@
This package contains a embeddable web component for the Strudel REPL. 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 ```html
<script src="https://unpkg.com/@strudel/embed@latest"></script> <script src="https://unpkg.com/@strudel/embed@latest"></script>
<strudel-repl> <strudel-repl>
<!-- <!--
note(`[[e5 [b4 c5] d5 [c5 b4]] setcps(1)
[a4 [a4 c5] e5 [d5 c5]] n("<0 1 2 3 4>*8").scale('G4 minor')
[b4 [~ c5] d5 e5] .s("gm_lead_6_voice")
[c5 a4 a4 ~] .clip(sine.range(.2,.8).slow(8))
[[~ d5] [~ f5] a5 [g5 f5]] .jux(rev)
[e5 [~ c5] e5 [d5 c5]] .room(2)
[b4 [b4 c5] d5 e5] .sometimes(add(note("12")))
[c5 a4 a4 ~]], .lpf(perlin.range(200,20000).slow(4))
[[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)
-->
</strudel-repl> </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() { connectedCallback() {
setTimeout(() => { setTimeout(() => {
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim(); const code = this.getAttribute('code') || (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`; const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`; // const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;

View file

@ -2,4 +2,95 @@
The Strudel REPL as a web component. 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; 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; return workletsLoading;
} }
function getWorklet(ac, processor, params) { export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor); const node = new AudioWorkletNode(ac, processor, config);
Object.entries(params).forEach(([key, value]) => { Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value; node.parameters.get(key).value = value;
}); });

View file

@ -1,31 +1,138 @@
import { midiToFreq, noteToMidi } from './util.mjs'; import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs'; import { registerSound, getAudioContext, getWorklet } from './superdough.mjs';
import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import {
applyFM,
gainNode,
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => { const getFrequencyFromValue = (value) => {
const ctx = getAudioContext(); let { note, freq } = value;
const osc = ctx.createOscillator(); note = note || 36;
osc.type = type; if (typeof note === 'string') {
osc.frequency.value = freq; note = noteToMidi(note); // e.g. c3 => 48
osc.start(); }
const g = new GainNode(ctx, { gain: range }); // get frequency
osc.connect(g); // -range, range if (!freq && typeof note === 'number') {
return { node: g, stop: (t) => osc.stop(t) }; freq = midiToFreq(note); // + 48);
}
return Number(freq);
}; };
const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => { const waveforms = ['triangle', 'square', 'sawtooth', '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 noises = ['pink', 'white', 'brown', 'crackle']; const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() { 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( registerSound(
s, s,
(t, value, onended) => { (t, value, onended) => {
@ -36,12 +143,9 @@ export function registerSynthSounds() {
); );
let sound; let sound;
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
let { density } = value; let { density } = value;
sound = getNoiseOscillator(s, t, density); sound = getNoiseOscillator(s, t, density);
}
let { node: o, stop, triggerRelease } = sound; let { node: o, stop, triggerRelease } = sound;
@ -106,24 +210,7 @@ export function waveformN(partials, type) {
// expects one of waveforms as s // expects one of waveforms as s
export function getOscillator(s, t, value) { export function getOscillator(s, t, value) {
let { let { n: partials, duration, noise = 0 } = value;
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 o; let o;
// If no partials are given, use stock waveforms // If no partials are given, use stock waveforms
if (!partials || s === 'sine') { if (!partials || s === 'sine') {
@ -134,55 +221,15 @@ export function getOscillator(s, t, value) {
else { else {
o = waveformN(partials, s); 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 // set frequency
o.frequency.value = Number(freq); o.frequency.value = getFrequencyFromValue(value);
o.start(t); 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); let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
// pitch envelope // pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration); getPitchEnvelope(o.detune, value, t, t + duration);
const fmModulator = applyFM(o.frequency, value, t);
let noiseMix; let noiseMix;
if (noise) { if (noise) {
@ -192,9 +239,9 @@ export function getOscillator(s, t, value) {
return { return {
node: noiseMix?.node || o, node: noiseMix?.node || o,
stop: (time) => { stop: (time) => {
fmModulator.stop(time);
vibratoOscillator?.stop(time); vibratoOscillator?.stop(time);
noiseMix?.stop(time); noiseMix?.stop(time);
stopFm?.(time);
o.stop(time); o.stop(time);
}, },
triggerRelease: (time) => { triggerRelease: (time) => {

View file

@ -129,3 +129,148 @@ class DistortProcessor extends AudioWorkletProcessor {
} }
} }
registerProcessor('distort-processor', DistortProcessor); 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.. // legacy..
return pure(step); return pure(step);
} }
const asNumber = Number(step); let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) { if (isNaN(asNumber)) {
logger(`[tonal] invalid scale step "${step}", expected number`, 'error'); 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; 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 { try {
let note; let note;
if (value.anchor) { if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor); note = stepInNamedScale(asNumber, scale, value.anchor);
} else { } else {
note = scaleStep(asNumber, scale); note = scaleStep(asNumber, scale);
} }
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note); value = pure(isObject ? { ...value, note } : note);
} catch (err) { } catch (err) {
logger(`[tonal] ${err.message}`, 'error'); 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: Save this code as a `.html` file and double click it:
```html ```html
<!DOCTYPE html> <!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button> <button id="play">play</button>
<button id="stop">stop</button> <button id="stop">stop</button>
<script type="module"> <script>
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel(); 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()); document.getElementById('stop').addEventListener('click', () => hush());
</script> </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. 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. 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. To actually play the pattern, you have to append `.play()` to the end.

View file

@ -37,10 +37,10 @@
"@strudel/mini": "workspace:*", "@strudel/mini": "workspace:*",
"@strudel/tonal": "workspace:*", "@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*", "@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*", "@strudel/webaudio": "workspace:*"
"@rollup/plugin-replace": "^5.0.5"
}, },
"devDependencies": { "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': '@vitest/ui':
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0(vitest@1.1.0) version: 1.1.0(vitest@1.1.0)
acorn:
specifier: ^8.11.3
version: 8.11.3
dependency-tree: dependency-tree:
specifier: ^10.0.9 specifier: ^10.0.9
version: 10.0.9 version: 10.0.9
@ -449,9 +452,6 @@ importers:
packages/web: packages/web:
dependencies: dependencies:
'@rollup/plugin-replace':
specifier: ^5.0.5
version: 5.0.5
'@strudel/core': '@strudel/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
@ -468,6 +468,9 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../webaudio version: link:../webaudio
devDependencies: devDependencies:
'@rollup/plugin-replace':
specifier: ^5.0.5
version: 5.0.5
vite: vite:
specifier: ^5.0.10 specifier: ^5.0.10
version: 5.0.10 version: 5.0.10
@ -572,9 +575,6 @@ importers:
'@strudel/osc': '@strudel/osc':
specifier: workspace:* specifier: workspace:*
version: link:../packages/osc version: link:../packages/osc
'@strudel/repl':
specifier: workspace:*
version: link:../packages/repl
'@strudel/serial': '@strudel/serial':
specifier: workspace:* specifier: workspace:*
version: link:../packages/serial version: link:../packages/serial
@ -3930,6 +3930,7 @@ packages:
dependencies: dependencies:
'@rollup/pluginutils': 5.1.0 '@rollup/pluginutils': 5.1.0
magic-string: 0.30.5 magic-string: 0.30.5
dev: true
/@rollup/pluginutils@3.1.0(rollup@2.79.1): /@rollup/pluginutils@3.1.0(rollup@2.79.1):
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
@ -3955,6 +3956,7 @@ packages:
'@types/estree': 1.0.0 '@types/estree': 1.0.0
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.3.1 picomatch: 2.3.1
dev: true
/@rollup/rollup-android-arm-eabi@4.13.0: /@rollup/rollup-android-arm-eabi@4.13.0:
resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==}
@ -7478,6 +7480,7 @@ packages:
/estree-walker@2.0.2: /estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/estree-walker@3.0.3: /estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 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`] = ` exports[`runs examples > example "detune" example index 0 1`] = `
[ [
"[ 0/1 → 1/3 | n:0 s:superzow octave:3 detune:0 ]", "[ 0/1 → 1/12 | note:d s:supersaw detune:0.1 ]",
"[ 1/3 → 2/3 | n:3 s:superzow octave:3 detune:0 ]", "[ 1/12 → 1/6 | note:f s:supersaw detune:0.1 ]",
"[ 2/3 → 1/1 | n:7 s:superzow octave:3 detune:0 ]", "[ 1/6 → 1/4 | note:a s:supersaw detune:0.1 ]",
"[ 1/1 → 4/3 | n:0 s:superzow octave:3 detune:0.25 ]", "[ 1/4 → 1/3 | note:a# s:supersaw detune:0.1 ]",
"[ 4/3 → 5/3 | n:3 s:superzow octave:3 detune:0.25 ]", "[ 1/3 → 5/12 | note:a s:supersaw detune:0.1 ]",
"[ 5/3 → 2/1 | n:7 s:superzow octave:3 detune:0.25 ]", "[ 5/12 → 1/2 | note:d3 s:supersaw detune:0.1 ]",
"[ 2/1 → 7/3 | n:0 s:superzow octave:3 detune:0.5 ]", "[ 1/2 → 7/12 | note:d s:supersaw detune:0.1 ]",
"[ 7/3 → 8/3 | n:3 s:superzow octave:3 detune:0.5 ]", "[ 7/12 → 2/3 | note:f s:supersaw detune:0.1 ]",
"[ 8/3 → 3/1 | n:7 s:superzow octave:3 detune:0.5 ]", "[ 2/3 → 3/4 | note:a s:supersaw detune:0.1 ]",
"[ 3/1 → 10/3 | n:0 s:superzow octave:3 detune:1 ]", "[ 3/4 → 5/6 | note:a# s:supersaw detune:0.1 ]",
"[ 10/3 → 11/3 | n:3 s:superzow octave:3 detune:1 ]", "[ 5/6 → 11/12 | note:a s:supersaw detune:0.1 ]",
"[ 11/3 → 4/1 | n:7 s:superzow octave:3 detune: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`] = ` 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 ]", "[ 1/2 → 3/4 | note:E2 ]",
"[ 3/4 → 1/1 | note:F2 ]", "[ 3/4 → 1/1 | note:F2 ]",
"[ 1/1 → 5/4 | note:G2 ]", "[ 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 ]", "[ 7/4 → 2/1 | note:C3 ]",
"[ 2/1 → 9/4 | note:D3 ]", "[ 2/1 → 9/4 | note:D3 ]",
"[ 9/4 → 5/2 | note:D2 ]", "[ 9/4 → 5/2 | note:D2 ]",
"[ 5/2 → 11/4 | note:E2 ]",
"[ 11/4 → 3/1 | note:F2 ]",
"[ 3/1 → 13/4 | note:G2 ]", "[ 3/1 → 13/4 | note:G2 ]",
"[ 13/4 → 7/2 | note:A2 ]", "[ 13/4 → 7/2 | note:A2 ]",
"[ 7/2 → 15/4 | note:B2 ]", "[ 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`] = ` exports[`runs examples > example "pianoroll" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | note:C2 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 ]", "[ (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 ]", "[ 1/4 ⇜ (1/3 → 3/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 3/8 → 1/2 | note:A2 s:piano clip:1 ]", "[ 3/8 → 1/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (5/8 → 2/3) ⇝ 3/4 | note:A2 s:piano clip:1 ]", "[ (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 ]", "[ 5/8 ⇜ (2/3 → 3/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 3/4 → 7/8 | note:G2 s:piano clip:1 ]", "[ 3/4 → 7/8 | note:G2 s:piano clip:1 color:salmon ]",
"[ 1/1 → 9/8 | note:C2 s:piano clip:1 ]", "[ 1/1 → 9/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ (5/4 → 4/3) ⇝ 11/8 | note:C2 s:piano clip:1 ]", "[ (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 ]", "[ 5/4 ⇜ (4/3 → 11/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 11/8 → 3/2 | note:A2 s:piano clip:1 ]", "[ 11/8 → 3/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (13/8 → 5/3) ⇝ 7/4 | note:A2 s:piano clip:1 ]", "[ (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 ]", "[ 13/8 ⇜ (5/3 → 7/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 7/4 → 15/8 | note:G2 s:piano clip:1 ]", "[ 7/4 → 15/8 | note:G2 s:piano clip:1 color:salmon ]",
"[ 2/1 → 17/8 | note:C2 s:piano clip:1 ]", "[ 2/1 → 17/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ (9/4 → 7/3) ⇝ 19/8 | note:C2 s:piano clip:1 ]", "[ (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 ]", "[ 9/4 ⇜ (7/3 → 19/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 19/8 → 5/2 | note:A2 s:piano clip:1 ]", "[ 19/8 → 5/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (21/8 → 8/3) ⇝ 11/4 | note:A2 s:piano clip:1 ]", "[ (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 ]", "[ 21/8 ⇜ (8/3 → 11/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 11/4 → 23/8 | note:G2 s:piano clip:1 ]", "[ 11/4 → 23/8 | note:G2 s:piano clip:1 color:salmon ]",
"[ 3/1 → 25/8 | note:C2 s:piano clip:1 ]", "[ 3/1 → 25/8 | note:C2 s:piano clip:1 color:salmon ]",
"[ (13/4 → 10/3) ⇝ 27/8 | note:C2 s:piano clip:1 ]", "[ (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 ]", "[ 13/4 ⇜ (10/3 → 27/8) | note:A2 s:piano clip:1 color:salmon ]",
"[ 27/8 → 7/2 | note:A2 s:piano clip:1 ]", "[ 27/8 → 7/2 | note:A2 s:piano clip:1 color:salmon ]",
"[ (29/8 → 11/3) ⇝ 15/4 | note:A2 s:piano clip:1 ]", "[ (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 ]", "[ 29/8 ⇜ (11/3 → 15/4) | note:G2 s:piano clip:1 color:salmon ]",
"[ 15/4 → 31/8 | note:G2 s:piano clip:1 ]", "[ 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`] = ` exports[`runs examples > example "square" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:C3 ]", "[ 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`] = ` exports[`runs examples > example "unit" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | speed:1 s:bd unit:c ]", "[ 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, randomBells: 24,
waa: 16, waa: 16,
waar: 16, waar: 16,
hyperpop: 10,
festivalOfFingers3: 16, 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": [ "/packages/core/fraction.mjs": [
"gcd" "gcd",
"lcm"
], ],
"/packages/core/timespan.mjs": [ "/packages/core/timespan.mjs": [
"TimeSpan" "TimeSpan"
@ -11,6 +91,10 @@
"/packages/core/state.mjs": [ "/packages/core/state.mjs": [
"State" "State"
], ],
"/packages/core/logger.mjs": [
"logKey",
"logger"
],
"/packages/core/util.mjs": [ "/packages/core/util.mjs": [
"isNoteWithOctave", "isNoteWithOctave",
"isNote", "isNote",
@ -20,13 +104,14 @@
"freqToMidi", "freqToMidi",
"valueToMidi", "valueToMidi",
"_mod", "_mod",
"nanFallback",
"getSoundIndex",
"getPlayableNoteValue", "getPlayableNoteValue",
"getFrequency", "getFrequency",
"rotate", "rotate",
"pipe", "pipe",
"compose", "compose",
"flatten", "flatten",
"id",
"constant", "constant",
"listRange", "listRange",
"curry", "curry",
@ -42,7 +127,8 @@
"unicodeToBase64", "unicodeToBase64",
"base64ToUnicode", "base64ToUnicode",
"code2hash", "code2hash",
"hash2code" "hash2code",
"objectMap"
], ],
"/packages/core/value.mjs": [ "/packages/core/value.mjs": [
"unionWithObj", "unionWithObj",
@ -51,18 +137,20 @@
"map" "map"
], ],
"/packages/core/drawLine.mjs": [], "/packages/core/drawLine.mjs": [],
"/packages/core/logger.mjs": [
"logKey",
"logger"
],
"/packages/core/pattern.mjs": [ "/packages/core/pattern.mjs": [
"setStringParser", "setStringParser",
"polyrhythm", "polyrhythm",
"pr", "pr",
"pm", "pm",
"nothing",
"isPattern", "isPattern",
"reify", "reify",
"stackLeft",
"stackRight",
"stackCentre",
"stackBy",
"fastcat", "fastcat",
"_polymeterListSteps",
"set", "set",
"keep", "keep",
"keepif", "keepif",
@ -99,16 +187,195 @@
"stutWith", "stutWith",
"stutwith", "stutwith",
"iterback", "iterback",
"slowchunk",
"slowChunk",
"chunkback", "chunkback",
"fastchunk",
"bypass", "bypass",
"duration",
"hsla", "hsla",
"hsl", "hsl",
"colour",
"loopat", "loopat",
"loopatcps" "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": [ "/packages/core/euclid.mjs": [
"bjork", "bjork",
"euclidrot" "euclidrot"
@ -128,6 +395,8 @@
"brandBy", "brandBy",
"brand", "brand",
"_irand", "_irand",
"pickSqueeze",
"pickmodSqueeze",
"__chooseWith", "__chooseWith",
"randcat", "randcat",
"wchoose", "wchoose",
@ -143,6 +412,9 @@
"evalScope", "evalScope",
"evaluate" "evaluate"
], ],
"/packages/core/neocyclist.mjs": [
"NeoCyclist"
],
"/packages/core/zyklus.mjs": [], "/packages/core/zyklus.mjs": [],
"/packages/core/cyclist.mjs": [ "/packages/core/cyclist.mjs": [
"Cyclist" "Cyclist"
@ -155,31 +427,6 @@
"repl", "repl",
"getTrigger" "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": [ "/packages/core/ui.mjs": [
"backgroundImage", "backgroundImage",
"cleanupUi" "cleanupUi"
@ -199,6 +446,37 @@
"/packages/desktopbridge/midibridge.mjs": [], "/packages/desktopbridge/midibridge.mjs": [],
"/packages/desktopbridge/oscbridge.mjs": [], "/packages/desktopbridge/oscbridge.mjs": [],
"/packages/desktopbridge/index.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": [ "/packages/midi/midi.mjs": [
"WebMidi", "WebMidi",
"enableWebMidi", "enableWebMidi",
@ -219,6 +497,13 @@
"miniAllStrings" "miniAllStrings"
], ],
"/packages/mini/index.mjs": [], "/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/gm.mjs": [],
"/packages/soundfonts/fontloader.mjs": [ "/packages/soundfonts/fontloader.mjs": [
"getFontBufferSource", "getFontBufferSource",
@ -234,7 +519,92 @@
"loadSoundfont" "loadSoundfont"
], ],
"/packages/soundfonts/index.mjs": [], "/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": [ "/packages/tonal/tonleiter.mjs": [
"pc2chroma", "pc2chroma",
"rotateChroma", "rotateChroma",
@ -242,30 +612,40 @@
"tokenizeChord", "tokenizeChord",
"note2pc", "note2pc",
"note2oct", "note2oct",
"note2midi",
"note2chroma", "note2chroma",
"midi2chroma", "midi2chroma",
"pitch2chroma", "pitch2chroma",
"step2semitones", "step2semitones",
"x2midi", "x2midi",
"scaleStep", "scaleStep",
"nearestNumberIndex",
"stepInNamedScale",
"renderVoicing", "renderVoicing",
"accidentalOffset", "accidentalOffset",
"Step", "Step",
"Note" "Note"
], ],
"/packages/tonal/tonal.mjs": [],
"/packages/tonal/ireal.mjs": [ "/packages/tonal/ireal.mjs": [
"simple", "simple",
"complex" "complex"
], ],
"/packages/tonal/voicings.mjs": [ "/packages/tonal/voicings.mjs": [
"voicingRegistry", "voicingRegistry",
"setDefaultVoicings",
"setVoicingRange", "setVoicingRange",
"registerVoicings", "registerVoicings",
"voicingAlias" "voicingAlias",
"resetVoicings"
],
"/packages/tonal/index.mjs": [
"packageName"
], ],
"/packages/tonal/index.mjs": [],
"/packages/transpiler/transpiler.mjs": [ "/packages/transpiler/transpiler.mjs": [
"transpiler" "registerWidgetType",
"transpiler",
"getWidgetID"
], ],
"/packages/transpiler/index.mjs": [ "/packages/transpiler/index.mjs": [
"evaluate" "evaluate"

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import { getPunchcardPainter } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio'; import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
import { StrudelMirror } from '@strudel/codemirror'; import { StrudelMirror } from '@strudel/codemirror';
// import { prebake } from '@strudel/repl';
import { prebake } from '../repl/prebake.mjs'; import { prebake } from '../repl/prebake.mjs';
import { loadModules } from '../repl/util.mjs'; import { loadModules } from '../repl/util.mjs';
import Claviature from '@components/Claviature'; 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. - `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]"`. - `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]`. - `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. - `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.
- `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. - `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. 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} /> <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 ## 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). [See the latest published packages on npm](https://www.npmjs.com/search?q=%40strudel).
Here is an overview of all the packages: 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 ### Essential Packages
These package are the most essential. You might want to use all of those if you're using strudel in your project: 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] [A3@2 [G3 A3]] [B3@2 D4] [A3]
[B3@2 D4] [A4@2 G4] D5@2 [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@2] [C5@2 [B4 A4]] [[B4 A4] E4@2]
[D5@2 [C5 B4]] [[C5 B4] G4 C5] [G5] [~ ~ B3]\` [D5@2 [C5 B4]] [[C5 B4] G4 C5] [G5] [~ ~ B3]\`,
.color('#9C7C38'),
// bass // bass
\`[[C2 G2] E3@2] [[C2 G2] F#3@2] [[C2 G2] E3@2] [[C2 G2] F#3@2] \`[[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] [[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] [[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] [[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]]\` [[F2 C3] E3@2] [[E2 B2] D3@2] [[Eb2 Bb2] Db3@2] [[D2 A2] C3 [F3,G2]]\`
.color('#4C4646')
).transpose(12).slow(48) ).transpose(12).slow(48)
.superimpose(x=>x.add(0.06)) // add slightly detuned voice .superimpose(x=>x.add(0.06)) // add slightly detuned voice
.note() .note()
@ -447,58 +445,6 @@ note(
.room(.5) .room(.5)
.lpa(.125).lpenv(-2).v("8:.125").fanchor(.25)`; .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" export const festivalOfFingers3 = `// "Festival of fingers 3"
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ // @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
// @by Felix Roos // @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 "<8(3,8) <7 7*2> [4 5@3] 8>".sub(1) // sub 1 -> 1-indexed
.layer( .layer(
x=>x, 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))) .off(1/8,x=>x.add("2,4").off(1/8,x=>x.add(5).echo(4,.125,.5)))
.slow(2), .slow(2),
).n().scale('A1 minor') ).n().scale('A1 minor')