Compare commits

..

38 Commits

Author SHA1 Message Date
Felix Roos b9933c610a really fix lint 2024-06-06 12:11:57 +02:00
Felix Roos 9d81a4d071 fix lint 2024-06-06 12:09:55 +02:00
Felix Roos 86cc9b87c3 looper prototype 2024-06-06 10:05:29 +02:00
Felix Roos 0df6f3e56e simplify idb import 2024-06-06 00:44:41 +02:00
Felix Roos c403a92542 Merge pull request #1126 from tidalcycles/labeled-statements-doc
Labeled statements doc
2024-06-04 00:26:48 +02:00
Felix Roos 269d6dde31 remove stack from workshop 2024-06-04 00:22:49 +02:00
Felix Roos 666c695c63 don't use labeled statements in example for now 2024-06-04 00:10:46 +02:00
Felix Roos 771eb31d3f use labels instead of stacks 2024-06-04 00:05:51 +02:00
Felix Roos c05bbf3422 don't clear hydra in minirepl for now 2024-06-03 23:53:08 +02:00
Felix Roos 364f511a7c Merge pull request #1125 from tidalcycles/viz-doc
doc: visual functions + refactor onPaint
2024-06-03 22:40:21 +02:00
Felix Roos 300022d8ac no custom line height 2024-06-03 20:34:51 +02:00
Felix Roos 357fbf8c82 snapshot 2024-06-02 23:14:32 +02:00
Felix Roos 1acb675f5a doc: visual functions 2024-06-02 23:12:31 +02:00
Jade (Rose) Rowland ab1086318c Merge pull request #1124 from daslyfe/bug_sampledb
Fix indexDB failing with large amount of files
2024-06-02 12:36:29 -04:00
Jade (Rose) Rowland 8c73ad088a support aac 2024-06-02 12:32:54 -04:00
Felix Roos 25981092b1 snapshot 2024-06-02 14:11:52 +02:00
Felix Roos e6b55baed0 fix: step based naming 2024-06-02 14:08:56 +02:00
Felix Roos 6adf1ff977 snapshot 2024-06-02 14:03:30 +02:00
Felix Roos b6e277f1ea use inline scope in examples 2024-06-02 14:03:05 +02:00
Felix Roos d0510f862a clear hydra in mini repl 2024-06-02 14:02:39 +02:00
Felix Roos 4a87df6426 small example tweaks 2024-06-02 14:00:47 +02:00
Felix Roos bf90867685 fix: mini repl punchcard + claviature flags 2024-06-02 10:49:07 +02:00
Felix Roos 3dc5fd0e7e fix: editPattern bug 2024-06-02 10:48:20 +02:00
Felix Roos a7650f1af3 add autodraw flag + scope 2024-06-02 03:08:16 +02:00
Felix Roos aadb2fee84 support onload inline viz with autodraw 2024-06-02 03:05:26 +02:00
Felix Roos bf391ad6ce simplify: painters are now handled by Drawer 2024-06-02 03:03:36 +02:00
Felix Roos faba4a4e4e implement onPaint with pattern state 2024-06-02 03:00:57 +02:00
Felix Roos 94e411aa7a add beforeStart callback 2024-06-02 02:34:33 +02:00
Jade (Rose) Rowland ffa21ddf0d remove console statement 2024-06-01 19:19:47 -04:00
Jade (Rose) Rowland 6b2d080afc remove unessecary change 2024-06-01 19:17:00 -04:00
Jade (Rose) Rowland 321a888921 add catch 2024-06-01 12:42:07 -04:00
Jade (Rose) Rowland bf7fe2bf75 fixed indexdb 2024-06-01 12:39:45 -04:00
Jade (Rose) Rowland 1fd9ba5dbf convert to blob 2024-06-01 12:11:28 -04:00
Jade (Rose) Rowland 1fca6f25b8 faster upload 2024-06-01 11:35:24 -04:00
Felix Roos 0e6a17fc77 start visual feedback article 2024-06-01 15:42:09 +02:00
Felix Roos 375c68775c make sure draw logic works with multiple repls 2024-06-01 15:41:55 +02:00
Felix Roos 827993a8c9 chore: simplify publish docs 2024-05-31 23:29:38 +02:00
Jade (Rose) Rowland e29876e7f6 debugging 2024-05-31 13:57:02 -04:00
37 changed files with 937 additions and 558 deletions
+3 -9
View File
@@ -130,20 +130,14 @@ npx lerna version --no-private
# publish all packages inside /packages using pnpm! don't use lerna to publish!! # publish all packages inside /packages using pnpm! don't use lerna to publish!!
pnpm --filter "./packages/**" publish --dry-run pnpm --filter "./packages/**" publish --dry-run
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag # the last command was only a dry-run. if everything looks ok, run this:
pnpm --filter "./packages/**" publish --access public
``` ```
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`. To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages. Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages.
### New Packages
To add a new package, you have to publish it manually the first time, using:
```sh
cd packages/<package-name> && pnpm publish --access public
```
## Have Fun ## Have Fun
Remember to have fun, and that this project is driven by the passion of volunteers! Remember to have fun, and that this project is driven by the passion of volunteers!
+13 -26
View File
@@ -140,27 +140,25 @@ export class StrudelMirror {
this.root = root; this.root = root;
this.miniLocations = []; this.miniLocations = [];
this.widgets = []; this.widgets = [];
this.painters = [];
this.drawTime = drawTime; this.drawTime = drawTime;
this.drawContext = drawContext; this.drawContext = drawContext;
this.onDraw = onDraw || this.draw; this.onDraw = onDraw || this.draw;
this.id = id || s4(); this.id = id || s4();
this.drawer = new Drawer((haps, time) => { this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time)); const currentFrame = haps.filter((hap) => hap.isActive(time));
this.highlight(currentFrame, time); this.highlight(currentFrame, time);
this.onDraw(haps, time, this.painters); this.onDraw(haps, time, painters);
}, drawTime); }, drawTime);
this.prebaked = prebake(); this.prebaked = prebake();
autodraw && this.drawFirstFrame(); autodraw && this.drawFirstFrame();
this.repl = repl({ this.repl = repl({
...replOptions, ...replOptions,
id,
onToggle: (started) => { onToggle: (started) => {
replOptions?.onToggle?.(started); replOptions?.onToggle?.(started);
if (started) { if (started) {
this.adjustDrawTime();
this.drawer.start(this.repl.scheduler); this.drawer.start(this.repl.scheduler);
// stop other repls when this one is started // stop other repls when this one is started
document.dispatchEvent( document.dispatchEvent(
@@ -171,20 +169,11 @@ export class StrudelMirror {
} else { } else {
this.drawer.stop(); this.drawer.stop();
updateMiniLocations(this.editor, []); updateMiniLocations(this.editor, []);
cleanupDraw(false); cleanupDraw(true, id);
} }
}, },
beforeEval: async () => { beforeEval: async () => {
cleanupDraw(); cleanupDraw(true, id);
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?.();
}, },
@@ -198,8 +187,11 @@ export class StrudelMirror {
updateWidgets(this.editor, widgets); updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations); updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options); replOptions?.afterEval?.(options);
this.adjustDrawTime(); // if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
this.drawer.invalidate(); const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
this.drawer.setDrawTime(drawTime);
// invalidate drawer after we've set the appropriate drawTime
this.drawer.invalidate(this.repl.scheduler);
}, },
}); });
this.editor = initEditor({ this.editor = initEditor({
@@ -234,13 +226,8 @@ export class StrudelMirror {
}; };
document.addEventListener('start-repl', this.onStartRepl); document.addEventListener('start-repl', this.onStartRepl);
} }
// adjusts draw time depending on if there are painters draw(haps, time, painters) {
adjustDrawTime() { painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
// when no painters are set, [0,0] is enough (just highlighting)
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
}
draw(haps, time) {
this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
} }
async drawFirstFrame() { async drawFirstFrame() {
if (!this.onDraw) { if (!this.onDraw) {
@@ -252,7 +239,7 @@ export class StrudelMirror {
await this.repl.evaluate(this.code, false); await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler, -0.001); this.drawer.invalidate(this.repl.scheduler, -0.001);
// draw at -0.001 to avoid haps at 0 to be visualized as active // draw at -0.001 to avoid haps at 0 to be visualized as active
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters); this.onDraw?.(this.drawer.visibleHaps, -0.001, this.drawer.painters);
} catch (err) { } catch (err) {
console.warn('first frame could not be painted'); console.warn('first frame could not be painted');
} }
+1 -1
View File
@@ -12,7 +12,7 @@ export const settings = {
gutterBackground: 'transparent', gutterBackground: 'transparent',
gutterForeground: '#0f380f', gutterForeground: '#0f380f',
light: true, light: true,
customStyle: '.cm-line { line-height: 1 }', // customStyle: '.cm-line { line-height: 1 }',
}; };
export default createTheme({ export default createTheme({
theme: 'light', theme: 'light',
+1 -1
View File
@@ -17,7 +17,7 @@ export const settings = {
lineBackground: '#00000040', lineBackground: '#00000040',
gutterBackground: 'transparent', gutterBackground: 'transparent',
gutterForeground: '#8a919966', gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }', // customStyle: '.cm-line { line-height: 1 }',
}; };
let punctuation = colorD; let punctuation = colorD;
+14 -8
View File
@@ -73,6 +73,8 @@ export function registerControl(names, ...aliases) {
*/ */
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
export const { rec } = registerControl(['rec', 'n']);
/** /**
* Define a custom webaudio node to use as a sound source. * Define a custom webaudio node to use as a sound source.
* *
@@ -191,7 +193,7 @@ export const { attack, att } = registerControl('attack', 'att');
* note("c e g b g e") * note("c e g b g e")
* .fm(4) * .fm(4)
* .fmh("<1 2 1.5 1.61>") * .fmh("<1 2 1.5 1.61>")
* .scope() * ._scope()
* *
*/ */
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh'); export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
@@ -205,7 +207,7 @@ export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
* @example * @example
* note("c e g b g e") * note("c e g b g e")
* .fm("<0 1 2 8 32>") * .fm("<0 1 2 8 32>")
* .scope() * ._scope()
* *
*/ */
export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm'); export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
@@ -221,7 +223,7 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
* .fmdecay(.2) * .fmdecay(.2)
* .fmsustain(0) * .fmsustain(0)
* .fmenv("<exp lin>") * .fmenv("<exp lin>")
* .scope() * ._scope()
* *
*/ */
export const { fmenv } = registerControl('fmenv'); export const { fmenv } = registerControl('fmenv');
@@ -234,7 +236,7 @@ export const { fmenv } = registerControl('fmenv');
* note("c e g b g e") * note("c e g b g e")
* .fm(4) * .fm(4)
* .fmattack("<0 .05 .1 .2>") * .fmattack("<0 .05 .1 .2>")
* .scope() * ._scope()
* *
*/ */
export const { fmattack } = registerControl('fmattack'); export const { fmattack } = registerControl('fmattack');
@@ -248,7 +250,7 @@ export const { fmattack } = registerControl('fmattack');
* .fm(4) * .fm(4)
* .fmdecay("<.01 .05 .1 .2>") * .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4) * .fmsustain(.4)
* .scope() * ._scope()
* *
*/ */
export const { fmdecay } = registerControl('fmdecay'); export const { fmdecay } = registerControl('fmdecay');
@@ -262,7 +264,7 @@ export const { fmdecay } = registerControl('fmdecay');
* .fm(4) * .fm(4)
* .fmdecay(.1) * .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>") * .fmsustain("<1 .75 .5 0>")
* .scope() * ._scope()
* *
*/ */
export const { fmsustain } = registerControl('fmsustain'); export const { fmsustain } = registerControl('fmsustain');
@@ -392,7 +394,7 @@ export const { loop } = registerControl('loop');
* @synonyms loopb * @synonyms loopb
* @example * @example
* s("space").loop(1) * s("space").loop(1)
* .loopBegin("<0 .125 .25>").scope() * .loopBegin("<0 .125 .25>")._scope()
*/ */
export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb'); export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
/** /**
@@ -405,7 +407,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
* @synonyms loope * @synonyms loope
* @example * @example
* s("space").loop(1) * s("space").loop(1)
* .loopEnd("<1 .75 .5 .25>").scope() * .loopEnd("<1 .75 .5 .25>")._scope()
*/ */
export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
/** /**
@@ -800,10 +802,12 @@ export const { fanchor } = registerControl('fanchor');
* @example * @example
* note("a e") * note("a e")
* .vib("<.5 1 2 4 8 16>") * .vib("<.5 1 2 4 8 16>")
* ._scope()
* @example * @example
* // change the modulation depth with ":" * // change the modulation depth with ":"
* note("a e") * note("a e")
* .vib("<.5 1 2 4 8 16>:12") * .vib("<.5 1 2 4 8 16>:12")
* ._scope()
*/ */
export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato', 'v'); export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato', 'v');
/** /**
@@ -824,10 +828,12 @@ export const { noise } = registerControl('noise');
* @example * @example
* note("a e").vib(4) * note("a e").vib(4)
* .vibmod("<.25 .5 1 2 12>") * .vibmod("<.25 .5 1 2 12>")
* ._scope()
* @example * @example
* // change the vibrato frequency with ":" * // change the vibrato frequency with ":"
* note("a e") * note("a e")
* .vibmod("<.25 .5 1 2 12>:8") * .vibmod("<.25 .5 1 2 12>:8")
* ._scope()
*/ */
export const { vibmod, vmod } = registerControl(['vibmod', 'vib'], 'vmod'); export const { vibmod, vmod } = registerControl(['vibmod', 'vib'], 'vmod');
export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp'); export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp');
+16 -4
View File
@@ -8,8 +8,19 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
export class Cyclist { export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { constructor({
interval,
onTrigger,
onToggle,
onError,
getTime,
latency = 0.1,
setInterval,
clearInterval,
beforeStart,
}) {
this.started = false; this.started = false;
this.beforeStart = beforeStart;
this.cps = 0.5; this.cps = 0.5;
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened this.lastTick = 0; // absolute time when last tick (clock callback) happened
@@ -82,7 +93,8 @@ export class Cyclist {
this.started = v; this.started = v;
this.onToggle?.(v); this.onToggle?.(v);
} }
start() { async start() {
await this.beforeStart?.();
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0; this.num_cycles_at_cps_change = 0;
if (!this.pattern) { if (!this.pattern) {
@@ -103,10 +115,10 @@ export class Cyclist {
this.lastEnd = 0; this.lastEnd = 0;
this.setStarted(false); this.setStarted(false);
} }
setPattern(pat, autostart = false) { async setPattern(pat, autostart = false) {
this.pattern = pat; this.pattern = pat;
if (autostart && !this.started) { if (autostart && !this.started) {
this.start(); await this.start();
} }
} }
setCps(cps = 0.5) { setCps(cps = 0.5) {
+2 -2
View File
@@ -37,11 +37,11 @@ function safeEval(str, options = {}) {
return Function(body)(); return Function(body)();
} }
export const evaluate = async (code, transpiler) => { export const evaluate = async (code, transpiler, transpilerOptions) => {
let meta = {}; let meta = {};
if (transpiler) { if (transpiler) {
// transform syntactically correct js code to semantically usable code // transform syntactically correct js code to semantically usable code
const transpiled = transpiler(code); const transpiled = transpiler(code, transpilerOptions);
code = transpiled.output; code = transpiled.output;
meta = transpiled; meta = transpiled;
} }
+13 -3
View File
@@ -94,6 +94,14 @@ export class Pattern {
return result; return result;
} }
// runs func on query state
withState(func) {
return this.withHaps((haps, state) => {
func(state);
return haps;
});
}
/** /**
* see `withValue` * see `withValue`
* @noAutocomplete * @noAutocomplete
@@ -2523,7 +2531,7 @@ export function _polymeterListSteps(steps, ...args) {
* @param {any[]} patterns one or more patterns * @param {any[]} patterns one or more patterns
* @example * @example
* // the same as "{c d, e f g}%4" * // the same as "{c d, e f g}%4"
* s_polymeterSteps(4, "c d", "e f g") * s_polymeterSteps(4, "c d", "e f g").note()
*/ */
export function s_polymeterSteps(steps, ...args) { export function s_polymeterSteps(steps, ...args) {
if (args.length == 0) { if (args.length == 0) {
@@ -2541,8 +2549,8 @@ export function s_polymeterSteps(steps, ...args) {
* *EXPERIMENTAL* - Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used. * *EXPERIMENTAL* - Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used.
* @synonyms pm * @synonyms pm
* @example * @example
* // The same as "{c eb g, c2 g2}" * // The same as note("{c eb g, c2 g2}")
* s_polymeter("c eb g", "c2 g2") * s_polymeter("c eb g", "c2 g2").note()
* *
*/ */
export function s_polymeter(...args) { export function s_polymeter(...args) {
@@ -2571,6 +2579,8 @@ export function s_polymeter(...args) {
/** Sequences patterns like `seq`, but each pattern has a length, relative to the whole. /** Sequences patterns like `seq`, but each pattern has a length, relative to the whole.
* This length can either be provided as a [length, pattern] pair, or inferred from * This length can either be provided as a [length, pattern] pair, or inferred from
* the pattern's 'tactus', generally inferred by the mininotation. Has the alias `timecat`. * the pattern's 'tactus', generally inferred by the mininotation. Has the alias `timecat`.
* @name s_cat
* @synonyms timeCat, timecat
* @return {Pattern} * @return {Pattern}
* @example * @example
* s_cat([3,"e3"],[1, "g3"]).note() * s_cat([3,"e3"],[1, "g3"]).note()
+13 -4
View File
@@ -10,6 +10,7 @@ export function repl({
defaultOutput, defaultOutput,
onEvalError, onEvalError,
beforeEval, beforeEval,
beforeStart,
afterEval, afterEval,
getTime, getTime,
transpiler, transpiler,
@@ -19,6 +20,7 @@ export function repl({
sync = false, sync = false,
setInterval, setInterval,
clearInterval, clearInterval,
id,
}) { }) {
const state = { const state = {
schedulerError: undefined, schedulerError: undefined,
@@ -32,6 +34,10 @@ export function repl({
started: false, started: false,
}; };
const transpilerOptions = {
id,
};
const updateState = (update) => { const updateState = (update) => {
Object.assign(state, update); Object.assign(state, update);
state.isDirty = state.code !== state.activeCode; state.isDirty = state.code !== state.activeCode;
@@ -48,6 +54,7 @@ export function repl({
}, },
setInterval, setInterval,
clearInterval, clearInterval,
beforeStart,
}; };
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome // NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
@@ -64,9 +71,10 @@ export function repl({
return silence; return silence;
}; };
const setPattern = (pattern, autostart = true) => { const setPattern = async (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern; pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart); await scheduler.setPattern(pattern, autostart);
return pattern;
}; };
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
@@ -139,9 +147,10 @@ export function repl({
try { try {
updateState({ code, pending: true }); updateState({ code, pending: true });
await injectPatternMethods(); await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor?
await beforeEval?.({ code }); await beforeEval?.({ code });
shouldHush && hush(); shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) { if (Object.keys(pPatterns).length) {
pattern = stack(...Object.values(pPatterns)); pattern = stack(...Object.values(pPatterns));
} }
@@ -153,7 +162,7 @@ export function repl({
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
} }
logger(`[eval] code updated`); logger(`[eval] code updated`);
setPattern(pattern, autostart); pattern = await setPattern(pattern, autostart);
updateState({ updateState({
miniLocations: meta?.miniLocations || [], miniLocations: meta?.miniLocations || [],
widgets: meta?.widgets || [], widgets: meta?.widgets || [],
+23 -13
View File
@@ -36,8 +36,8 @@ function stopAnimationFrame(id) {
delete animationFrames[id]; delete animationFrames[id];
} }
} }
function stopAllAnimations() { function stopAllAnimations(replID) {
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id)); Object.keys(animationFrames).forEach((id) => (!replID || id.startsWith(replID)) && stopAnimationFrame(id));
} }
let memory = {}; let memory = {};
@@ -72,18 +72,25 @@ Pattern.prototype.draw = function (fn, options) {
return this; return this;
}; };
export const cleanupDraw = (clearScreen = true) => { export const cleanupDraw = (clearScreen = true, id) => {
const ctx = getDrawContext(); const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
stopAllAnimations(); stopAllAnimations(id);
if (window.strudelScheduler) {
clearInterval(window.strudelScheduler);
}
}; };
Pattern.prototype.onPaint = function () { Pattern.prototype.onPaint = function (painter) {
console.warn('[draw] onPaint was not overloaded. Some drawings might not work'); return this.withState((state) => {
return this; if (!state.controls.painters) {
state.controls.painters = [];
}
state.controls.painters.push(painter);
});
};
Pattern.prototype.getPainters = function () {
let painters = [];
this.queryArc(0, 0, { painters });
return painters;
}; };
// const round = (x) => Math.round(x * 1000) / 1000; // const round = (x) => Math.round(x * 1000) / 1000;
@@ -122,6 +129,7 @@ export class Drawer {
this.visibleHaps = []; this.visibleHaps = [];
this.lastFrame = null; this.lastFrame = null;
this.drawTime = drawTime; this.drawTime = drawTime;
this.painters = [];
this.framer = new Framer( this.framer = new Framer(
() => { () => {
if (!this.scheduler) { if (!this.scheduler) {
@@ -146,7 +154,7 @@ export class Drawer {
// add new haps with onset (think right edge bars scrolling in) // add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset())); .concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead; const time = phase - lookahead;
onDraw(this.visibleHaps, time, this); onDraw(this.visibleHaps, time, this, this.painters);
}, },
(err) => { (err) => {
console.warn('draw error', err); console.warn('draw error', err);
@@ -164,11 +172,13 @@ export class Drawer {
t = t ?? scheduler.now(); t = t ?? scheduler.now();
this.scheduler = scheduler; this.scheduler = scheduler;
let [_, lookahead] = this.drawTime; let [_, lookahead] = this.drawTime;
// +0.1 = workaround for weird holes in query..
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1]; const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps // remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
this.painters = []; // will get populated by .onPaint calls attached to the pattern
// query future haps // query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query.. const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters });
// append future haps // append future haps
this.visibleHaps = this.visibleHaps.concat(futureHaps); this.visibleHaps = this.visibleHaps.concat(futureHaps);
} }
+8 -3
View File
@@ -67,6 +67,7 @@ Pattern.prototype.pianoroll = function (options = {}) {
* Displays a midi-style piano roll * Displays a midi-style piano roll
* *
* @name pianoroll * @name pianoroll
* @synonyms punchcard
* @param {Object} options Object containing all the optional following parameters as key value pairs: * @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4 * @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5 * @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
@@ -94,7 +95,11 @@ Pattern.prototype.pianoroll = function (options = {}) {
* @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default * @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default
* *
* @example * @example
* note("C2 A2 G2").euclid(5,8).s('piano').clip(1).color('salmon').pianoroll({vertical:1, labels:1}) * note("c2 a2 eb2")
* .euclid(5,8)
* .s('sawtooth')
* .lpenv(4).lpf(300)
* ._pianoroll({ labels: 1 })
*/ */
export function pianoroll({ export function pianoroll({
time, time,
@@ -113,7 +118,7 @@ export function pianoroll({
maxMidi = 90, maxMidi = 90,
autorange = 0, autorange = 0,
timeframe: timeframeProp, timeframe: timeframeProp,
fold = 0, fold = 1,
vertical = 0, vertical = 0,
labels = false, labels = false,
fill = 1, fill = 1,
@@ -266,7 +271,7 @@ export function getDrawOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime; let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind); lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind; const cycles = lookahead + lookbehind;
const playhead = lookbehind / cycles; const playhead = cycles !== 0 ? lookbehind / cycles : 0;
return { fold: 1, ...options, cycles, playhead }; return { fold: 1, ...options, cycles, playhead };
} }
+17
View File
@@ -113,6 +113,23 @@ export function pitchwheel({
return; return;
} }
/**
* Renders a pitch circle to visualize frequencies within one octave
* @name pitchwheel
* @param {number} hapcircles
* @param {number} circle
* @param {number} edo
* @param {string} root
* @param {number} thickness
* @param {number} hapRadius
* @param {string} mode
* @param {number} margin
* @example
* n("0 .. 12").scale("C:chromatic")
* .s("sawtooth")
* .lpf(500)
* ._pitchwheel()
*/
Pattern.prototype.pitchwheel = function (options = {}) { Pattern.prototype.pitchwheel = function (options = {}) {
let { ctx = getDrawContext(), id = 1 } = options; let { ctx = getDrawContext(), id = 1 } = options;
return this.tag(id).onPaint((_, time, haps) => return this.tag(id).onPaint((_, time, haps) =>
+27
View File
@@ -125,6 +125,33 @@ function drawSpiral(options) {
}); });
} }
/**
* Displays a spiral visual.
*
* @name spiral
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees
* @param {number} size the diameter of the spiral
* @param {number} thickness line thickness
* @param {string} cap style of line ends: butt (default), round, square
* @param {string} inset number of rotations before spiral starts (default 3)
* @param {string} playheadColor color of playhead, defaults to white
* @param {number} playheadLength length of playhead in rotations, defaults to 0.02
* @param {number} playheadThickness thickness of playheadrotations, defaults to thickness
* @param {number} padding space around spiral
* @param {number} steady steadyness of spiral vs playhead. 1 = spiral doesn't move, playhead does.
* @param {number} activeColor color of active segment. defaults to foreground of theme
* @param {number} inactiveColor color of inactive segments. defaults to gutterForeground of theme
* @param {boolean} colorizeInactive wether or not to colorize inactive segments, defaults to 0
* @param {boolean} fade wether or not past and future should fade out. defaults to 1
* @param {boolean} logSpiral wether or not the spiral should be logarithmic. defaults to 0
* @example
* note("c2 a2 eb2")
* .euclid(5,8)
* .s('sawtooth')
* .lpenv(4).lpf(300)
* ._spiral({ steady: .96 })
*/
Pattern.prototype.spiral = function (options = {}) { Pattern.prototype.spiral = function (options = {}) {
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options })); return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
}; };
+2 -2
View File
@@ -3,8 +3,8 @@ import { getAudioContext, registerSound } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer> export const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer> export const loadCache = {}; // string: Promise<ArrayBuffer>
export const getCachedBuffer = (url) => bufferCache[url]; export const getCachedBuffer = (url) => bufferCache[url];
+54 -1
View File
@@ -12,7 +12,7 @@ import workletsUrl from './worklets.mjs?url';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs'; import { loadBuffer, bufferCache, loadCache, onTriggerSample } from './sampler.mjs';
export const soundMap = map(); export const soundMap = map();
@@ -95,6 +95,7 @@ function loadWorklets() {
return workletsLoading; return workletsLoading;
} }
let stream;
// this function should be called on first user interaction (to avoid console warning) // this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) { export async function initAudio(options = {}) {
const { disableWorklets = false } = options; const { disableWorklets = false } = options;
@@ -112,6 +113,7 @@ export async function initAudio(options = {}) {
} catch (err) { } catch (err) {
console.warn('could not load AudioWorklet effects', err); console.warn('could not load AudioWorklet effects', err);
} }
stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
logger('[superdough] ready'); logger('[superdough] ready');
} }
let audioReady; let audioReady;
@@ -308,6 +310,48 @@ export function resetGlobalEffects() {
analysersData = {}; analysersData = {};
} }
/* async */ function record(name, begin, hapDuration) {
registerSound(
name,
() => {
console.log('trigger recording before its ready...', getAudioContext().currentTime);
},
{},
);
const ac = getAudioContext();
try {
const inputNode = ac.createMediaStreamSource(stream);
const samples = Math.round(hapDuration * ac.sampleRate);
const options = { samples, begin, end: begin + hapDuration };
const recorder = getWorklet(ac, 'recording-processor', {});
recorder.port.postMessage(options);
inputNode.connect(recorder);
/* return */ new Promise((resolve) => {
recorder.port.onmessage = async (e) => {
const audioBuffer = ac.createBuffer(1, samples, ac.sampleRate);
audioBuffer.getChannelData(0).set(e.data.buffer);
const url = `rec:${name}`;
bufferCache[url] = audioBuffer;
loadCache[url] = audioBuffer;
const value = [url];
console.log('register recording', getAudioContext().currentTime);
registerSound(name, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: undefined,
});
resolve(name);
};
});
return recorder;
} catch (err) {
console.log('err', err);
// reject(err);
}
}
export const superdough = async (value, t, hapDuration) => { export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext(); const ac = getAudioContext();
if (typeof value !== 'object') { if (typeof value !== 'object') {
@@ -330,6 +374,7 @@ export const superdough = async (value, t, hapDuration) => {
// destructure // destructure
let { let {
s = getDefaultValue('s'), s = getDefaultValue('s'),
rec,
bank, bank,
source, source,
gain = getDefaultValue('gain'), gain = getDefaultValue('gain'),
@@ -411,11 +456,19 @@ export const superdough = async (value, t, hapDuration) => {
if (bank && s) { if (bank && s) {
s = `${bank}_${s}`; s = `${bank}_${s}`;
} }
if (rec && getSound(rec)) {
s = rec;
value.s = rec;
}
// get source AudioNode // get source AudioNode
let sourceNode; let sourceNode;
if (source) { if (source) {
sourceNode = source(t, value, hapDuration); sourceNode = source(t, value, hapDuration);
} else if (rec && !getSound(rec)) {
console.log('record', rec);
const recorder = record(rec, t, hapDuration);
sourceNode = recorder;
} else if (getSound(s)) { } else if (getSound(s)) {
const { onTrigger } = getSound(s); const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, value, onended); const soundHandle = await onTrigger(t, value, onended);
+45
View File
@@ -464,3 +464,48 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
} }
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
class RecordingProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.done = false;
this.head = 0;
this.nudge = 0.1;
this.port.onmessage = (e) => {
this.begin = e.data.begin + this.nudge;
this.samples = e.data.samples;
this.buffer = new Float32Array(this.samples);
};
}
process(inputs, outputs) {
// noop if scheduled recording begin hasn't been reached
// eslint-disable-next-line no-undef
if (currentTime < this.begin) {
return true;
}
if (!this.buffer) {
console.log('buffer not ready..');
return true;
}
// stop when the buffer is full
if (!this.done && this.head >= this.samples) {
this.done = true;
this.port.postMessage({ buffer: this.buffer });
return false;
}
// so far only 1 channel
const input = inputs[0];
// const output = outputs[0];
for (let i = 0; i < input[0].length; i++) {
this.buffer[this.head] = input[0][i] * 0.25;
/* output[0][i] = input[0][i];
output[1][i] = input[0][i]; */
this.head++;
}
return true;
}
}
registerProcessor('recording-processor', RecordingProcessor);
+2 -1
View File
@@ -69,6 +69,7 @@ export function transpiler(input, options = {}) {
to: node.end, to: node.end,
index, index,
type, type,
id: options.id,
}; };
emitWidgets && widgets.push(widgetConfig); emitWidgets && widgets.push(widgetConfig);
return this.replace(widgetWithLocation(node, widgetConfig)); return this.replace(widgetWithLocation(node, widgetConfig));
@@ -162,7 +163,7 @@ export function getWidgetID(widgetConfig) {
// that means, if we use the index index of line position as id, less garbage is generated // that means, if we use the index index of line position as id, less garbage is generated
// return `widget_${widgetConfig.to}`; // more gargabe // return `widget_${widgetConfig.to}`; // more gargabe
//return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage //return `widget_${widgetConfig.index}_${widgetConfig.to}`; // also more garbage
return `widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage return `${widgetConfig.id || ''}_widget_${widgetConfig.type}_${widgetConfig.index}`; // less garbage
} }
function widgetWithLocation(node, widgetConfig) { function widgetWithLocation(node, widgetConfig) {
+1 -1
View File
@@ -130,7 +130,7 @@ Pattern.prototype.fscope = function (config = {}) {
* @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen * @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
* @param {number} trigger amplitude value that is used to align the scope. defaults to 0. * @param {number} trigger amplitude value that is used to align the scope. defaults to 0.
* @example * @example
* s("sawtooth").scope() * s("sawtooth")._scope()
*/ */
let latestColor = {}; let latestColor = {};
Pattern.prototype.tscope = function (config = {}) { Pattern.prototype.tscope = function (config = {}) {
+330 -240
View File
@@ -2717,175 +2717,175 @@ exports[`runs examples > example "floor" example index 0 1`] = `
exports[`runs examples > example "fm" example index 0 1`] = ` exports[`runs examples > example "fm" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:0 analyze:1 ]", "[ 0/1 → 1/6 | note:c fmi:0 ]",
"[ 1/6 → 1/3 | note:e fmi:0 analyze:1 ]", "[ 1/6 → 1/3 | note:e fmi:0 ]",
"[ 1/3 → 1/2 | note:g fmi:0 analyze:1 ]", "[ 1/3 → 1/2 | note:g fmi:0 ]",
"[ 1/2 → 2/3 | note:b fmi:0 analyze:1 ]", "[ 1/2 → 2/3 | note:b fmi:0 ]",
"[ 2/3 → 5/6 | note:g fmi:0 analyze:1 ]", "[ 2/3 → 5/6 | note:g fmi:0 ]",
"[ 5/6 → 1/1 | note:e fmi:0 analyze:1 ]", "[ 5/6 → 1/1 | note:e fmi:0 ]",
"[ 1/1 → 7/6 | note:c fmi:1 analyze:1 ]", "[ 1/1 → 7/6 | note:c fmi:1 ]",
"[ 7/6 → 4/3 | note:e fmi:1 analyze:1 ]", "[ 7/6 → 4/3 | note:e fmi:1 ]",
"[ 4/3 → 3/2 | note:g fmi:1 analyze:1 ]", "[ 4/3 → 3/2 | note:g fmi:1 ]",
"[ 3/2 → 5/3 | note:b fmi:1 analyze:1 ]", "[ 3/2 → 5/3 | note:b fmi:1 ]",
"[ 5/3 → 11/6 | note:g fmi:1 analyze:1 ]", "[ 5/3 → 11/6 | note:g fmi:1 ]",
"[ 11/6 → 2/1 | note:e fmi:1 analyze:1 ]", "[ 11/6 → 2/1 | note:e fmi:1 ]",
"[ 2/1 → 13/6 | note:c fmi:2 analyze:1 ]", "[ 2/1 → 13/6 | note:c fmi:2 ]",
"[ 13/6 → 7/3 | note:e fmi:2 analyze:1 ]", "[ 13/6 → 7/3 | note:e fmi:2 ]",
"[ 7/3 → 5/2 | note:g fmi:2 analyze:1 ]", "[ 7/3 → 5/2 | note:g fmi:2 ]",
"[ 5/2 → 8/3 | note:b fmi:2 analyze:1 ]", "[ 5/2 → 8/3 | note:b fmi:2 ]",
"[ 8/3 → 17/6 | note:g fmi:2 analyze:1 ]", "[ 8/3 → 17/6 | note:g fmi:2 ]",
"[ 17/6 → 3/1 | note:e fmi:2 analyze:1 ]", "[ 17/6 → 3/1 | note:e fmi:2 ]",
"[ 3/1 → 19/6 | note:c fmi:8 analyze:1 ]", "[ 3/1 → 19/6 | note:c fmi:8 ]",
"[ 19/6 → 10/3 | note:e fmi:8 analyze:1 ]", "[ 19/6 → 10/3 | note:e fmi:8 ]",
"[ 10/3 → 7/2 | note:g fmi:8 analyze:1 ]", "[ 10/3 → 7/2 | note:g fmi:8 ]",
"[ 7/2 → 11/3 | note:b fmi:8 analyze:1 ]", "[ 7/2 → 11/3 | note:b fmi:8 ]",
"[ 11/3 → 23/6 | note:g fmi:8 analyze:1 ]", "[ 11/3 → 23/6 | note:g fmi:8 ]",
"[ 23/6 → 4/1 | note:e fmi:8 analyze:1 ]", "[ 23/6 → 4/1 | note:e fmi:8 ]",
] ]
`; `;
exports[`runs examples > example "fmattack" example index 0 1`] = ` exports[`runs examples > example "fmattack" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 analyze:1 ]", "[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]",
"[ 1/6 → 1/3 | note:e fmi:4 fmattack:0 analyze:1 ]", "[ 1/6 → 1/3 | note:e fmi:4 fmattack:0 ]",
"[ 1/3 → 1/2 | note:g fmi:4 fmattack:0 analyze:1 ]", "[ 1/3 → 1/2 | note:g fmi:4 fmattack:0 ]",
"[ 1/2 → 2/3 | note:b fmi:4 fmattack:0 analyze:1 ]", "[ 1/2 → 2/3 | note:b fmi:4 fmattack:0 ]",
"[ 2/3 → 5/6 | note:g fmi:4 fmattack:0 analyze:1 ]", "[ 2/3 → 5/6 | note:g fmi:4 fmattack:0 ]",
"[ 5/6 → 1/1 | note:e fmi:4 fmattack:0 analyze:1 ]", "[ 5/6 → 1/1 | note:e fmi:4 fmattack:0 ]",
"[ 1/1 → 7/6 | note:c fmi:4 fmattack:0.05 analyze:1 ]", "[ 1/1 → 7/6 | note:c fmi:4 fmattack:0.05 ]",
"[ 7/6 → 4/3 | note:e fmi:4 fmattack:0.05 analyze:1 ]", "[ 7/6 → 4/3 | note:e fmi:4 fmattack:0.05 ]",
"[ 4/3 → 3/2 | note:g fmi:4 fmattack:0.05 analyze:1 ]", "[ 4/3 → 3/2 | note:g fmi:4 fmattack:0.05 ]",
"[ 3/2 → 5/3 | note:b fmi:4 fmattack:0.05 analyze:1 ]", "[ 3/2 → 5/3 | note:b fmi:4 fmattack:0.05 ]",
"[ 5/3 → 11/6 | note:g fmi:4 fmattack:0.05 analyze:1 ]", "[ 5/3 → 11/6 | note:g fmi:4 fmattack:0.05 ]",
"[ 11/6 → 2/1 | note:e fmi:4 fmattack:0.05 analyze:1 ]", "[ 11/6 → 2/1 | note:e fmi:4 fmattack:0.05 ]",
"[ 2/1 → 13/6 | note:c fmi:4 fmattack:0.1 analyze:1 ]", "[ 2/1 → 13/6 | note:c fmi:4 fmattack:0.1 ]",
"[ 13/6 → 7/3 | note:e fmi:4 fmattack:0.1 analyze:1 ]", "[ 13/6 → 7/3 | note:e fmi:4 fmattack:0.1 ]",
"[ 7/3 → 5/2 | note:g fmi:4 fmattack:0.1 analyze:1 ]", "[ 7/3 → 5/2 | note:g fmi:4 fmattack:0.1 ]",
"[ 5/2 → 8/3 | note:b fmi:4 fmattack:0.1 analyze:1 ]", "[ 5/2 → 8/3 | note:b fmi:4 fmattack:0.1 ]",
"[ 8/3 → 17/6 | note:g fmi:4 fmattack:0.1 analyze:1 ]", "[ 8/3 → 17/6 | note:g fmi:4 fmattack:0.1 ]",
"[ 17/6 → 3/1 | note:e fmi:4 fmattack:0.1 analyze:1 ]", "[ 17/6 → 3/1 | note:e fmi:4 fmattack:0.1 ]",
"[ 3/1 → 19/6 | note:c fmi:4 fmattack:0.2 analyze:1 ]", "[ 3/1 → 19/6 | note:c fmi:4 fmattack:0.2 ]",
"[ 19/6 → 10/3 | note:e fmi:4 fmattack:0.2 analyze:1 ]", "[ 19/6 → 10/3 | note:e fmi:4 fmattack:0.2 ]",
"[ 10/3 → 7/2 | note:g fmi:4 fmattack:0.2 analyze:1 ]", "[ 10/3 → 7/2 | note:g fmi:4 fmattack:0.2 ]",
"[ 7/2 → 11/3 | note:b fmi:4 fmattack:0.2 analyze:1 ]", "[ 7/2 → 11/3 | note:b fmi:4 fmattack:0.2 ]",
"[ 11/3 → 23/6 | note:g fmi:4 fmattack:0.2 analyze:1 ]", "[ 11/3 → 23/6 | note:g fmi:4 fmattack:0.2 ]",
"[ 23/6 → 4/1 | note:e fmi:4 fmattack:0.2 analyze:1 ]", "[ 23/6 → 4/1 | note:e fmi:4 fmattack:0.2 ]",
] ]
`; `;
exports[`runs examples > example "fmdecay" example index 0 1`] = ` exports[`runs examples > example "fmdecay" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]", "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 ]",
"[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]", "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 ]",
"[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]", "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 ]",
"[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
"[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]", "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 ]",
] ]
`; `;
exports[`runs examples > example "fmenv" example index 0 1`] = ` exports[`runs examples > example "fmenv" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]", "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp ]",
"[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
"[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin analyze:1 ]", "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:lin ]",
] ]
`; `;
exports[`runs examples > example "fmh" example index 0 1`] = ` exports[`runs examples > example "fmh" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:4 fmh:1 analyze:1 ]", "[ 0/1 → 1/6 | note:c fmi:4 fmh:1 ]",
"[ 1/6 → 1/3 | note:e fmi:4 fmh:1 analyze:1 ]", "[ 1/6 → 1/3 | note:e fmi:4 fmh:1 ]",
"[ 1/3 → 1/2 | note:g fmi:4 fmh:1 analyze:1 ]", "[ 1/3 → 1/2 | note:g fmi:4 fmh:1 ]",
"[ 1/2 → 2/3 | note:b fmi:4 fmh:1 analyze:1 ]", "[ 1/2 → 2/3 | note:b fmi:4 fmh:1 ]",
"[ 2/3 → 5/6 | note:g fmi:4 fmh:1 analyze:1 ]", "[ 2/3 → 5/6 | note:g fmi:4 fmh:1 ]",
"[ 5/6 → 1/1 | note:e fmi:4 fmh:1 analyze:1 ]", "[ 5/6 → 1/1 | note:e fmi:4 fmh:1 ]",
"[ 1/1 → 7/6 | note:c fmi:4 fmh:2 analyze:1 ]", "[ 1/1 → 7/6 | note:c fmi:4 fmh:2 ]",
"[ 7/6 → 4/3 | note:e fmi:4 fmh:2 analyze:1 ]", "[ 7/6 → 4/3 | note:e fmi:4 fmh:2 ]",
"[ 4/3 → 3/2 | note:g fmi:4 fmh:2 analyze:1 ]", "[ 4/3 → 3/2 | note:g fmi:4 fmh:2 ]",
"[ 3/2 → 5/3 | note:b fmi:4 fmh:2 analyze:1 ]", "[ 3/2 → 5/3 | note:b fmi:4 fmh:2 ]",
"[ 5/3 → 11/6 | note:g fmi:4 fmh:2 analyze:1 ]", "[ 5/3 → 11/6 | note:g fmi:4 fmh:2 ]",
"[ 11/6 → 2/1 | note:e fmi:4 fmh:2 analyze:1 ]", "[ 11/6 → 2/1 | note:e fmi:4 fmh:2 ]",
"[ 2/1 → 13/6 | note:c fmi:4 fmh:1.5 analyze:1 ]", "[ 2/1 → 13/6 | note:c fmi:4 fmh:1.5 ]",
"[ 13/6 → 7/3 | note:e fmi:4 fmh:1.5 analyze:1 ]", "[ 13/6 → 7/3 | note:e fmi:4 fmh:1.5 ]",
"[ 7/3 → 5/2 | note:g fmi:4 fmh:1.5 analyze:1 ]", "[ 7/3 → 5/2 | note:g fmi:4 fmh:1.5 ]",
"[ 5/2 → 8/3 | note:b fmi:4 fmh:1.5 analyze:1 ]", "[ 5/2 → 8/3 | note:b fmi:4 fmh:1.5 ]",
"[ 8/3 → 17/6 | note:g fmi:4 fmh:1.5 analyze:1 ]", "[ 8/3 → 17/6 | note:g fmi:4 fmh:1.5 ]",
"[ 17/6 → 3/1 | note:e fmi:4 fmh:1.5 analyze:1 ]", "[ 17/6 → 3/1 | note:e fmi:4 fmh:1.5 ]",
"[ 3/1 → 19/6 | note:c fmi:4 fmh:1.61 analyze:1 ]", "[ 3/1 → 19/6 | note:c fmi:4 fmh:1.61 ]",
"[ 19/6 → 10/3 | note:e fmi:4 fmh:1.61 analyze:1 ]", "[ 19/6 → 10/3 | note:e fmi:4 fmh:1.61 ]",
"[ 10/3 → 7/2 | note:g fmi:4 fmh:1.61 analyze:1 ]", "[ 10/3 → 7/2 | note:g fmi:4 fmh:1.61 ]",
"[ 7/2 → 11/3 | note:b fmi:4 fmh:1.61 analyze:1 ]", "[ 7/2 → 11/3 | note:b fmi:4 fmh:1.61 ]",
"[ 11/3 → 23/6 | note:g fmi:4 fmh:1.61 analyze:1 ]", "[ 11/3 → 23/6 | note:g fmi:4 fmh:1.61 ]",
"[ 23/6 → 4/1 | note:e fmi:4 fmh:1.61 analyze:1 ]", "[ 23/6 → 4/1 | note:e fmi:4 fmh:1.61 ]",
] ]
`; `;
exports[`runs examples > example "fmsustain" example index 0 1`] = ` exports[`runs examples > example "fmsustain" example index 0 1`] = `
[ [
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", "[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", "[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", "[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", "[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", "[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]", "[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.1 fmsustain:1 ]",
"[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", "[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", "[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", "[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", "[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", "[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]", "[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 ]",
"[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", "[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", "[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", "[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", "[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", "[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]", "[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 ]",
"[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", "[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", "[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", "[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", "[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", "[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0 ]",
"[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]", "[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0 ]",
] ]
`; `;
@@ -4042,19 +4042,19 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = `
exports[`runs examples > example "loopBegin" example index 0 1`] = ` exports[`runs examples > example "loopBegin" example index 0 1`] = `
[ [
"[ 0/1 → 1/1 | s:space loop:1 loopBegin:0 analyze:1 ]", "[ 0/1 → 1/1 | s:space loop:1 loopBegin:0 ]",
"[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 analyze:1 ]", "[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 ]",
"[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 analyze:1 ]", "[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 ]",
"[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 analyze:1 ]", "[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 ]",
] ]
`; `;
exports[`runs examples > example "loopEnd" example index 0 1`] = ` exports[`runs examples > example "loopEnd" example index 0 1`] = `
[ [
"[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 analyze:1 ]", "[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 ]",
"[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 analyze:1 ]", "[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 ]",
"[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 analyze:1 ]", "[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 ]",
"[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 analyze:1 ]", "[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 ]",
] ]
`; `;
@@ -5085,34 +5085,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 color:salmon ]", "[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (1/4 → 1/3) ⇝ 3/8 | note:C2 s:piano clip:1 color:salmon ]", "[ (1/4 → 1/3) ⇝ 3/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 1/4 ⇜ (1/3 → 3/8) | note:A2 s:piano clip:1 color:salmon ]", "[ 1/4 ⇜ (1/3 → 3/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 3/8 → 1/2 | note:A2 s:piano clip:1 color:salmon ]", "[ 3/8 → 1/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (5/8 → 2/3) ⇝ 3/4 | note:A2 s:piano clip:1 color:salmon ]", "[ (5/8 → 2/3) ⇝ 3/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 5/8 ⇜ (2/3 → 3/4) | note:G2 s:piano clip:1 color:salmon ]", "[ 5/8 ⇜ (2/3 → 3/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 3/4 → 7/8 | note:G2 s:piano clip:1 color:salmon ]", "[ 3/4 → 7/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 1/1 → 9/8 | note:C2 s:piano clip:1 color:salmon ]", "[ 1/1 → 9/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (5/4 → 4/3) ⇝ 11/8 | note:C2 s:piano clip:1 color:salmon ]", "[ (5/4 → 4/3) ⇝ 11/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 5/4 ⇜ (4/3 → 11/8) | note:A2 s:piano clip:1 color:salmon ]", "[ 5/4 ⇜ (4/3 → 11/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 11/8 → 3/2 | note:A2 s:piano clip:1 color:salmon ]", "[ 11/8 → 3/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (13/8 → 5/3) ⇝ 7/4 | note:A2 s:piano clip:1 color:salmon ]", "[ (13/8 → 5/3) ⇝ 7/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 13/8 ⇜ (5/3 → 7/4) | note:G2 s:piano clip:1 color:salmon ]", "[ 13/8 ⇜ (5/3 → 7/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 7/4 → 15/8 | note:G2 s:piano clip:1 color:salmon ]", "[ 7/4 → 15/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 2/1 → 17/8 | note:C2 s:piano clip:1 color:salmon ]", "[ 2/1 → 17/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (9/4 → 7/3) ⇝ 19/8 | note:C2 s:piano clip:1 color:salmon ]", "[ (9/4 → 7/3) ⇝ 19/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 9/4 ⇜ (7/3 → 19/8) | note:A2 s:piano clip:1 color:salmon ]", "[ 9/4 ⇜ (7/3 → 19/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 19/8 → 5/2 | note:A2 s:piano clip:1 color:salmon ]", "[ 19/8 → 5/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (21/8 → 8/3) ⇝ 11/4 | note:A2 s:piano clip:1 color:salmon ]", "[ (21/8 → 8/3) ⇝ 11/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 21/8 ⇜ (8/3 → 11/4) | note:G2 s:piano clip:1 color:salmon ]", "[ 21/8 ⇜ (8/3 → 11/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 11/4 → 23/8 | note:G2 s:piano clip:1 color:salmon ]", "[ 11/4 → 23/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 3/1 → 25/8 | note:C2 s:piano clip:1 color:salmon ]", "[ 3/1 → 25/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (13/4 → 10/3) ⇝ 27/8 | note:C2 s:piano clip:1 color:salmon ]", "[ (13/4 → 10/3) ⇝ 27/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 13/4 ⇜ (10/3 → 27/8) | note:A2 s:piano clip:1 color:salmon ]", "[ 13/4 ⇜ (10/3 → 27/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 27/8 → 7/2 | note:A2 s:piano clip:1 color:salmon ]", "[ 27/8 → 7/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (29/8 → 11/3) ⇝ 15/4 | note:A2 s:piano clip:1 color:salmon ]", "[ (29/8 → 11/3) ⇝ 15/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 29/8 ⇜ (11/3 → 15/4) | note:G2 s:piano clip:1 color:salmon ]", "[ 29/8 ⇜ (11/3 → 15/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 15/4 → 31/8 | note:G2 s:piano clip:1 color:salmon ]", "[ 15/4 → 31/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
] ]
`; `;
@@ -5234,6 +5234,63 @@ exports[`runs examples > example "pickF" example index 1 1`] = `
] ]
`; `;
exports[`runs examples > example "pitchwheel" example index 0 1`] = `
[
"[ 0/1 → 1/13 | note:C3 s:sawtooth cutoff:500 ]",
"[ 1/13 → 2/13 | note:Db3 s:sawtooth cutoff:500 ]",
"[ 2/13 → 3/13 | note:D3 s:sawtooth cutoff:500 ]",
"[ 3/13 → 4/13 | note:Eb3 s:sawtooth cutoff:500 ]",
"[ 4/13 → 5/13 | note:E3 s:sawtooth cutoff:500 ]",
"[ 5/13 → 6/13 | note:F3 s:sawtooth cutoff:500 ]",
"[ 6/13 → 7/13 | note:Gb3 s:sawtooth cutoff:500 ]",
"[ 7/13 → 8/13 | note:G3 s:sawtooth cutoff:500 ]",
"[ 8/13 → 9/13 | note:Ab3 s:sawtooth cutoff:500 ]",
"[ 9/13 → 10/13 | note:A3 s:sawtooth cutoff:500 ]",
"[ 10/13 → 11/13 | note:Bb3 s:sawtooth cutoff:500 ]",
"[ 11/13 → 12/13 | note:B3 s:sawtooth cutoff:500 ]",
"[ 12/13 → 1/1 | note:C4 s:sawtooth cutoff:500 ]",
"[ 1/1 → 14/13 | note:C3 s:sawtooth cutoff:500 ]",
"[ 14/13 → 15/13 | note:Db3 s:sawtooth cutoff:500 ]",
"[ 15/13 → 16/13 | note:D3 s:sawtooth cutoff:500 ]",
"[ 16/13 → 17/13 | note:Eb3 s:sawtooth cutoff:500 ]",
"[ 17/13 → 18/13 | note:E3 s:sawtooth cutoff:500 ]",
"[ 18/13 → 19/13 | note:F3 s:sawtooth cutoff:500 ]",
"[ 19/13 → 20/13 | note:Gb3 s:sawtooth cutoff:500 ]",
"[ 20/13 → 21/13 | note:G3 s:sawtooth cutoff:500 ]",
"[ 21/13 → 22/13 | note:Ab3 s:sawtooth cutoff:500 ]",
"[ 22/13 → 23/13 | note:A3 s:sawtooth cutoff:500 ]",
"[ 23/13 → 24/13 | note:Bb3 s:sawtooth cutoff:500 ]",
"[ 24/13 → 25/13 | note:B3 s:sawtooth cutoff:500 ]",
"[ 25/13 → 2/1 | note:C4 s:sawtooth cutoff:500 ]",
"[ 2/1 → 27/13 | note:C3 s:sawtooth cutoff:500 ]",
"[ 27/13 → 28/13 | note:Db3 s:sawtooth cutoff:500 ]",
"[ 28/13 → 29/13 | note:D3 s:sawtooth cutoff:500 ]",
"[ 29/13 → 30/13 | note:Eb3 s:sawtooth cutoff:500 ]",
"[ 30/13 → 31/13 | note:E3 s:sawtooth cutoff:500 ]",
"[ 31/13 → 32/13 | note:F3 s:sawtooth cutoff:500 ]",
"[ 32/13 → 33/13 | note:Gb3 s:sawtooth cutoff:500 ]",
"[ 33/13 → 34/13 | note:G3 s:sawtooth cutoff:500 ]",
"[ 34/13 → 35/13 | note:Ab3 s:sawtooth cutoff:500 ]",
"[ 35/13 → 36/13 | note:A3 s:sawtooth cutoff:500 ]",
"[ 36/13 → 37/13 | note:Bb3 s:sawtooth cutoff:500 ]",
"[ 37/13 → 38/13 | note:B3 s:sawtooth cutoff:500 ]",
"[ 38/13 → 3/1 | note:C4 s:sawtooth cutoff:500 ]",
"[ 3/1 → 40/13 | note:C3 s:sawtooth cutoff:500 ]",
"[ 40/13 → 41/13 | note:Db3 s:sawtooth cutoff:500 ]",
"[ 41/13 → 42/13 | note:D3 s:sawtooth cutoff:500 ]",
"[ 42/13 → 43/13 | note:Eb3 s:sawtooth cutoff:500 ]",
"[ 43/13 → 44/13 | note:E3 s:sawtooth cutoff:500 ]",
"[ 44/13 → 45/13 | note:F3 s:sawtooth cutoff:500 ]",
"[ 45/13 → 46/13 | note:Gb3 s:sawtooth cutoff:500 ]",
"[ 46/13 → 47/13 | note:G3 s:sawtooth cutoff:500 ]",
"[ 47/13 → 48/13 | note:Ab3 s:sawtooth cutoff:500 ]",
"[ 48/13 → 49/13 | note:A3 s:sawtooth cutoff:500 ]",
"[ 49/13 → 50/13 | note:Bb3 s:sawtooth cutoff:500 ]",
"[ 50/13 → 51/13 | note:B3 s:sawtooth cutoff:500 ]",
"[ 51/13 → 4/1 | note:C4 s:sawtooth cutoff:500 ]",
]
`;
exports[`runs examples > example "ply" example index 0 1`] = ` exports[`runs examples > example "ply" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | s:bd ]", "[ 0/1 → 1/4 | s:bd ]",
@@ -6196,67 +6253,67 @@ exports[`runs examples > example "s_cat" example index 1 1`] = `
exports[`runs examples > example "s_polymeter" example index 0 1`] = ` exports[`runs examples > example "s_polymeter" example index 0 1`] = `
[ [
"[ 0/1 → 1/3 | c ]", "[ 0/1 → 1/3 | note:c ]",
"[ 0/1 → 1/3 | c2 ]", "[ 0/1 → 1/3 | note:c2 ]",
"[ 1/3 → 2/3 | eb ]", "[ 1/3 → 2/3 | note:eb ]",
"[ 1/3 → 2/3 | g2 ]", "[ 1/3 → 2/3 | note:g2 ]",
"[ 2/3 → 1/1 | g ]", "[ 2/3 → 1/1 | note:g ]",
"[ 2/3 → 1/1 | c2 ]", "[ 2/3 → 1/1 | note:c2 ]",
"[ 1/1 → 4/3 | c ]", "[ 1/1 → 4/3 | note:c ]",
"[ 1/1 → 4/3 | g2 ]", "[ 1/1 → 4/3 | note:g2 ]",
"[ 4/3 → 5/3 | eb ]", "[ 4/3 → 5/3 | note:eb ]",
"[ 4/3 → 5/3 | c2 ]", "[ 4/3 → 5/3 | note:c2 ]",
"[ 5/3 → 2/1 | g ]", "[ 5/3 → 2/1 | note:g ]",
"[ 5/3 → 2/1 | g2 ]", "[ 5/3 → 2/1 | note:g2 ]",
"[ 2/1 → 7/3 | c ]", "[ 2/1 → 7/3 | note:c ]",
"[ 2/1 → 7/3 | c2 ]", "[ 2/1 → 7/3 | note:c2 ]",
"[ 7/3 → 8/3 | eb ]", "[ 7/3 → 8/3 | note:eb ]",
"[ 7/3 → 8/3 | g2 ]", "[ 7/3 → 8/3 | note:g2 ]",
"[ 8/3 → 3/1 | g ]", "[ 8/3 → 3/1 | note:g ]",
"[ 8/3 → 3/1 | c2 ]", "[ 8/3 → 3/1 | note:c2 ]",
"[ 3/1 → 10/3 | c ]", "[ 3/1 → 10/3 | note:c ]",
"[ 3/1 → 10/3 | g2 ]", "[ 3/1 → 10/3 | note:g2 ]",
"[ 10/3 → 11/3 | eb ]", "[ 10/3 → 11/3 | note:eb ]",
"[ 10/3 → 11/3 | c2 ]", "[ 10/3 → 11/3 | note:c2 ]",
"[ 11/3 → 4/1 | g ]", "[ 11/3 → 4/1 | note:g ]",
"[ 11/3 → 4/1 | g2 ]", "[ 11/3 → 4/1 | note:g2 ]",
] ]
`; `;
exports[`runs examples > example "s_polymeterSteps" example index 0 1`] = ` exports[`runs examples > example "s_polymeterSteps" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | c ]", "[ 0/1 → 1/4 | note:c ]",
"[ 0/1 → 1/4 | e ]", "[ 0/1 → 1/4 | note:e ]",
"[ 1/4 → 1/2 | d ]", "[ 1/4 → 1/2 | note:d ]",
"[ 1/4 → 1/2 | f ]", "[ 1/4 → 1/2 | note:f ]",
"[ 1/2 → 3/4 | c ]", "[ 1/2 → 3/4 | note:c ]",
"[ 1/2 → 3/4 | g ]", "[ 1/2 → 3/4 | note:g ]",
"[ 3/4 → 1/1 | d ]", "[ 3/4 → 1/1 | note:d ]",
"[ 3/4 → 1/1 | e ]", "[ 3/4 → 1/1 | note:e ]",
"[ 1/1 → 5/4 | c ]", "[ 1/1 → 5/4 | note:c ]",
"[ 1/1 → 5/4 | f ]", "[ 1/1 → 5/4 | note:f ]",
"[ 5/4 → 3/2 | d ]", "[ 5/4 → 3/2 | note:d ]",
"[ 5/4 → 3/2 | g ]", "[ 5/4 → 3/2 | note:g ]",
"[ 3/2 → 7/4 | c ]", "[ 3/2 → 7/4 | note:c ]",
"[ 3/2 → 7/4 | e ]", "[ 3/2 → 7/4 | note:e ]",
"[ 7/4 → 2/1 | d ]", "[ 7/4 → 2/1 | note:d ]",
"[ 7/4 → 2/1 | f ]", "[ 7/4 → 2/1 | note:f ]",
"[ 2/1 → 9/4 | c ]", "[ 2/1 → 9/4 | note:c ]",
"[ 2/1 → 9/4 | g ]", "[ 2/1 → 9/4 | note:g ]",
"[ 9/4 → 5/2 | d ]", "[ 9/4 → 5/2 | note:d ]",
"[ 9/4 → 5/2 | e ]", "[ 9/4 → 5/2 | note:e ]",
"[ 5/2 → 11/4 | c ]", "[ 5/2 → 11/4 | note:c ]",
"[ 5/2 → 11/4 | f ]", "[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | d ]", "[ 11/4 → 3/1 | note:d ]",
"[ 11/4 → 3/1 | g ]", "[ 11/4 → 3/1 | note:g ]",
"[ 3/1 → 13/4 | c ]", "[ 3/1 → 13/4 | note:c ]",
"[ 3/1 → 13/4 | e ]", "[ 3/1 → 13/4 | note:e ]",
"[ 13/4 → 7/2 | d ]", "[ 13/4 → 7/2 | note:d ]",
"[ 13/4 → 7/2 | f ]", "[ 13/4 → 7/2 | note:f ]",
"[ 7/2 → 15/4 | c ]", "[ 7/2 → 15/4 | note:c ]",
"[ 7/2 → 15/4 | g ]", "[ 7/2 → 15/4 | note:g ]",
"[ 15/4 → 4/1 | d ]", "[ 15/4 → 4/1 | note:d ]",
"[ 15/4 → 4/1 | e ]", "[ 15/4 → 4/1 | note:e ]",
] ]
`; `;
@@ -6554,10 +6611,10 @@ exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
exports[`runs examples > example "scope" example index 0 1`] = ` exports[`runs examples > example "scope" example index 0 1`] = `
[ [
"[ 0/1 → 1/1 | s:sawtooth analyze:1 ]", "[ 0/1 → 1/1 | s:sawtooth ]",
"[ 1/1 → 2/1 | s:sawtooth analyze:1 ]", "[ 1/1 → 2/1 | s:sawtooth ]",
"[ 2/1 → 3/1 | s:sawtooth analyze:1 ]", "[ 2/1 → 3/1 | s:sawtooth ]",
"[ 3/1 → 4/1 | s:sawtooth analyze:1 ]", "[ 3/1 → 4/1 | s:sawtooth ]",
] ]
`; `;
@@ -7158,6 +7215,39 @@ exports[`runs examples > example "speed" example index 1 1`] = `
] ]
`; `;
exports[`runs examples > example "spiral" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (1/4 → 1/3) ⇝ 3/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 1/4 ⇜ (1/3 → 3/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 3/8 → 1/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (5/8 → 2/3) ⇝ 3/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 5/8 ⇜ (2/3 → 3/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 3/4 → 7/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 1/1 → 9/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (5/4 → 4/3) ⇝ 11/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 5/4 ⇜ (4/3 → 11/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 11/8 → 3/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (13/8 → 5/3) ⇝ 7/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 13/8 ⇜ (5/3 → 7/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 7/4 → 15/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 2/1 → 17/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (9/4 → 7/3) ⇝ 19/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 9/4 ⇜ (7/3 → 19/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 19/8 → 5/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (21/8 → 8/3) ⇝ 11/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 21/8 ⇜ (8/3 → 11/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 11/4 → 23/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 3/1 → 25/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (13/4 → 10/3) ⇝ 27/8 | note:c2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 13/4 ⇜ (10/3 → 27/8) | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 27/8 → 7/2 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ (29/8 → 11/3) ⇝ 15/4 | note:a2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 29/8 ⇜ (11/3 → 15/4) | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
"[ 15/4 → 31/8 | note:eb2 s:sawtooth lpenv:4 cutoff:300 ]",
]
`;
exports[`runs examples > example "splice" example index 0 1`] = ` exports[`runs examples > example "splice" example index 0 1`] = `
[ [
"[ 0/1 → 1/8 | speed:1 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", "[ 0/1 → 1/8 | speed:1 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
+14
View File
@@ -122,6 +122,19 @@ strudel.Pattern.prototype.midi = function () {
return this; return this;
}; };
strudel.Pattern.prototype._scope = function () {
return this;
};
strudel.Pattern.prototype._spiral = function () {
return this;
};
strudel.Pattern.prototype._pitchwheel = function () {
return this;
};
strudel.Pattern.prototype._pianoroll = function () {
return this;
};
const uiHelpersMocked = { const uiHelpersMocked = {
backgroundImage: id, backgroundImage: id,
}; };
@@ -178,6 +191,7 @@ evalScope(
}, },
); );
// TBD: use transpiler to support labeled statements
export const queryCode = async (code, cycles = 1) => { export const queryCode = async (code, cycles = 1) => {
const { pattern } = await evaluate(code); const { pattern } = await evaluate(code);
const haps = pattern.sortHapsByPart().queryArc(0, cycles); const haps = pattern.sortHapsByPart().queryArc(0, cycles);
+1
View File
@@ -78,6 +78,7 @@ export const SIDEBAR: Sidebar = {
More: [ More: [
{ text: 'Recipes', link: 'recipes/recipes' }, { text: 'Recipes', link: 'recipes/recipes' },
{ text: 'Mini-Notation', link: 'learn/mini-notation' }, { text: 'Mini-Notation', link: 'learn/mini-notation' },
{ text: 'Visual Feedback', link: 'learn/visual-feedback' },
{ text: 'Offline', link: 'learn/pwa' }, { text: 'Offline', link: 'learn/pwa' },
{ text: 'Patterns', link: 'technical-manual/patterns' }, { text: 'Patterns', link: 'technical-manual/patterns' },
{ text: 'Music metadata', link: 'learn/metadata' }, { text: 'Music metadata', link: 'learn/metadata' },
+22 -11
View File
@@ -1,7 +1,8 @@
import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { silence, noteToMidi, _mod } from '@strudel/core'; import { silence, noteToMidi, _mod } from '@strudel/core';
import { getPunchcardPainter } from '@strudel/draw'; import { clearHydra } from '@strudel/hydra';
import { getDrawContext, 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';
@@ -28,24 +29,29 @@ export function MiniRepl({
claviature, claviature,
claviatureLabels, claviatureLabels,
maxHeight, maxHeight,
autodraw,
drawTime,
}) { }) {
const code = tunes ? tunes[0] : tune; const code = tunes ? tunes[0] : tune;
const id = useMemo(() => s4(), []); const id = useMemo(() => s4(), []);
const canvasId = useMemo(() => `canvas-${id}`, [id]);
const shouldDraw = !!punchcard || !!claviature;
const shouldShowCanvas = !!punchcard; const shouldShowCanvas = !!punchcard;
const drawTime = punchcard ? [0, 4] : [0, 0]; const canvasId = shouldShowCanvas ? useMemo(() => `canvas-${id}`, [id]) : null;
autodraw = !!punchcard || !!claviature || !!autodraw;
drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2];
if (claviature) {
drawTime = [0, 0];
}
const [activeNotes, setActiveNotes] = useState([]); const [activeNotes, setActiveNotes] = useState([]);
const init = useCallback(({ code, shouldDraw }) => { const init = useCallback(({ code, autodraw }) => {
const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null; const drawContext = canvasId ? document.querySelector('#' + canvasId)?.getContext('2d') : getDrawContext();
const editor = new StrudelMirror({ const editor = new StrudelMirror({
id, id,
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime, getTime: () => getAudioContext().currentTime,
transpiler, transpiler,
autodraw: !!shouldDraw, autodraw,
root: containerRef.current, root: containerRef.current,
initialCode: '// LOADING', initialCode: '// LOADING',
pattern: silence, pattern: silence,
@@ -56,7 +62,7 @@ export function MiniRepl({
pat = pat.onTrigger(onTrigger, false); pat = pat.onTrigger(onTrigger, false);
} }
if (claviature) { if (claviature) {
editor?.painters.push((ctx, time, haps, drawTime) => { pat = pat.onPaint((ctx, time, haps, drawTime) => {
const active = haps const active = haps
.map((hap) => hap.value.note) .map((hap) => hap.value.note)
.filter(Boolean) .filter(Boolean)
@@ -65,7 +71,7 @@ export function MiniRepl({
}); });
} }
if (punchcard) { if (punchcard) {
editor?.painters.push(getPunchcardPainter({ labels: !!punchcardLabels })); pat = pat.punchcard({ labels: !!punchcardLabels });
} }
return pat; return pat;
}, },
@@ -73,7 +79,12 @@ export function MiniRepl({
onUpdateState: (state) => { onUpdateState: (state) => {
setReplState({ ...state }); setReplState({ ...state });
}, },
beforeEval: () => audioReady, // not doing this in prebake to make sure viz is drawn onToggle: (playing) => {
if (!playing) {
// clearHydra(); // TBD: doesn't work with multiple MiniRepl's on a page
}
},
beforeStart: () => audioReady,
afterEval: ({ code }) => setVersionDefaultsFrom(code), afterEval: ({ code }) => setVersionDefaultsFrom(code),
}); });
// init settings // init settings
@@ -152,7 +163,7 @@ export function MiniRepl({
ref={(el) => { ref={(el) => {
if (!editorRef.current) { if (!editorRef.current) {
containerRef.current = el; containerRef.current = el;
init({ code, shouldDraw }); init({ code, autodraw });
} }
}} }}
></div> ></div>
+10 -10
View File
@@ -12,13 +12,13 @@ The following functions will return a pattern.
These are the equivalents used by the Mini Notation: These are the equivalents used by the Mini Notation:
| function | mini | | function | mini |
| ------------------------------ | ---------------- | | -------------------------------- | ---------------- |
| `cat(x, y)` | `"<x y>"` | | `cat(x, y)` | `"<x y>"` |
| `seq(x, y)` | `"x y"` | | `seq(x, y)` | `"x y"` |
| `stack(x, y)` | `"x,y"` | | `stack(x, y)` | `"x,y"` |
| `timeCat([3,x],[2,y])` | `"x@3 y@2"` | | `s_cat([3,x],[2,y])` | `"x@3 y@2"` |
| `polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` | | `s_polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` |
| `polymeterSteps(2, x, y, z)` | `"{x y z}%2"` | | `s_polymeterSteps(2, x, y, z)` | `"{x y z}%2"` |
| `silence` | `"~"` | | `silence` | `"~"` |
## cat ## cat
@@ -45,21 +45,21 @@ As a chained function:
<JsDoc client:idle name="Pattern.stack" h={0} hideDescription /> <JsDoc client:idle name="Pattern.stack" h={0} hideDescription />
## timeCat ## s_cat
<JsDoc client:idle name="timeCat" h={0} /> <JsDoc client:idle name="s_cat" h={0} />
## arrange ## arrange
<JsDoc client:idle name="arrange" h={0} /> <JsDoc client:idle name="arrange" h={0} />
## polymeter ## s_polymeter
<JsDoc client:idle name="polymeter" h={0} /> <JsDoc client:idle name="s_polymeter" h={0} />
## polymeterSteps ## s_polymeterSteps
<JsDoc client:idle name="polymeterSteps" h={0} /> <JsDoc client:idle name="s_polymeterSteps" h={0} />
## silence ## silence
+9 -7
View File
@@ -90,11 +90,13 @@ src(s0).kaleid(H("<4 5 6>"))
.modulateScale(osc(2,-0.25,1)) .modulateScale(osc(2,-0.25,1))
.out() .out()
// //
stack(
s("bd*4,[hh:0:<.5 1>]*8,~ rim").bank("RolandTR909").speed(.9), $: s("bd*4,[hh:0:<.5 1>]*8,~ rim").bank("RolandTR909").speed(.9)
note("[<g1!3 <bb1 <f1 d1>>>]*3").s("sawtooth")
.room(.75).sometimes(add(note(12))).clip(.3) $: note("[<g1!3 <bb1 <f1 d1>>>]\*3").s("sawtooth")
.lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db')
).fft(4) .room(.75).sometimes(add(note(12))).clip(.3)
.scope({pos:0,smear:.95})`} .lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db')
all(x=>x.fft(4).scope({pos:0,smear:.95}))`}
/> />
+3 -9
View File
@@ -20,11 +20,7 @@ Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
If no outputName is given, it uses the first midi output it finds. If no outputName is given, it uses the first midi output it finds.
<MiniRepl <MiniRepl client:idle tune={`chord("<C^7 A7 Dm7 G7>").voicing().midi()`} />
client:idle
tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()
.midi()`}
/>
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
@@ -45,10 +41,8 @@ But you can also control cc messages separately like this:
<MiniRepl <MiniRepl
client:idle client:idle
tune={`stack( tune={`$: note("c a f e").midi()
note("c a f e"), $: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
ccv(sine.segment(16).slow(4)).ccn(74)
).midi()`}
/> />
# SuperDirt API # SuperDirt API
+7 -9
View File
@@ -268,11 +268,10 @@ With it, you can enter any sample name(s) to query from [freesound.org](https://
<MiniRepl <MiniRepl
client:idle client:idle
tune={`samples('shabda:bass:4,hihat:4,rimshot:2') tune={`samples('shabda:bass:4,hihat:4,rimshot:2')
stack(
n("0 1 2 3 0 1 2 3").s('bass'), $: n("0 1 2 3 0 1 2 3").s('bass')
n("0 1*2 2 3*2").s('hihat'), $: n("0 1*2 2 3*2").s('hihat').clip(1)
n("~ 0 ~ 1 ~ 0 0 1").s('rimshot') $: n("~ 0 ~ 1 ~ 0 0 1").s('rimshot')`}
).clip(1)`}
/> />
You can also generate artificial voice samples with any text, in multiple languages. You can also generate artificial voice samples with any text, in multiple languages.
@@ -282,10 +281,9 @@ Note that the language code and the gender parameters are optional and default t
client:idle client:idle
tune={`samples('shabda/speech:the_drum,forever') tune={`samples('shabda/speech:the_drum,forever')
samples('shabda/speech/fr-FR/m:magnifique') samples('shabda/speech/fr-FR/m:magnifique')
stack(
s("the_drum*2").chop(16).speed(rand.range(0.85,1.1)), $: s("the_drum*2").chop(16).speed(rand.range(0.85,1.1))
s("forever magnifique").slow(4).late(0.125) $: s("forever magnifique").slow(4).late(0.125)`}
)`}
/> />
# Sampler Effects # Sampler Effects
+6 -21
View File
@@ -112,29 +112,14 @@ Also, samples are always loaded from a URL rather than from the disk, although [
## Evaluation ## Evaluation
The Strudel REPL does not support [block based evaluation](https://github.com/tidalcycles/strudel/issues/34) yet. The Strudel REPL does not support [block based evaluation](https://github.com/tidalcycles/strudel/issues/34) yet.
You can use the following "workaround" to create multiple patterns that can be turned on and off: You can use labeled statements and `_` to mute:
``` <MiniRepl
let a = note("c a f e") client:visible
tune={`$: n("[0 .. 8]*8/9").scale("C:minor:pentatonic")
let b = s("bd sd") \_$: s("bd\*4").bank('RolandTR909')`}
/>
stack(
a,
// b
)
```
Alternatively, you could write everything as one `stack` and use `.hush()` to silence a pattern:
```
stack(
note("c a f e"),
s("bd sd").hush()
)
```
Note that strudel will always use the last statement in your code as the pattern for querying
## Tempo ## Tempo
+12 -11
View File
@@ -18,7 +18,7 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b
client:idle client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2)) tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("<sawtooth square triangle sine>") .sound("<sawtooth square triangle sine>")
.scope()`} ._scope()`}
/> />
If you don't set a `sound` but a `note` the default value for `sound` is `triangle`! If you don't set a `sound` but a `note` the default value for `sound` is `triangle`!
@@ -28,23 +28,23 @@ If you don't set a `sound` but a `note` the default value for `sound` is `triang
You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different You can also use noise as a source by setting the waveform to: `white`, `pink` or `brown`. These are different
flavours of noise, here written from hard to soft. flavours of noise, here written from hard to soft.
<MiniRepl client:idle tune={`sound("<white pink brown>").scope()`} /> <MiniRepl client:idle tune={`sound("<white pink brown>")._scope()`} />
Here's a more musical example of how to use noise for hihats: Here's a more musical example of how to use noise for hihats:
<MiniRepl <MiniRepl
client:idle client:idle
tune={`sound("bd*2,<white pink brown>*8") tune={`sound("bd*2,<white pink brown>*8")
.decay(.04).sustain(0).scope()`} .decay(.04).sustain(0)._scope()`}
/> />
Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter: Some amount of pink noise can also be added to any oscillator by using the `noise` paremeter:
<MiniRepl client:idle tune={`note("c3").noise("<0.1 0.25 0.5>").scope()`} /> <MiniRepl client:idle tune={`note("c3").noise("<0.1 0.25 0.5>")._scope()`} />
You can also use the `crackle` type to play some subtle noise crackles. You can control noise amount by using the `density` parameter: You can also use the `crackle` type to play some subtle noise crackles. You can control noise amount by using the `density` parameter:
<MiniRepl client:idle tune={`s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(2)).scope()`} /> <MiniRepl client:idle tune={`s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(2))._scope()`} />
### Additive Synthesis ### Additive Synthesis
@@ -55,7 +55,7 @@ To tame the harsh sound of the basic waveforms, we can set the `n` control to li
tune={`note("c2 <eb2 <g2 g1>>".fast(2)) tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth") .sound("sawtooth")
.n("<32 16 8 4>") .n("<32 16 8 4>")
.scope()`} ._scope()`}
/> />
When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting.
@@ -65,7 +65,7 @@ You can also set `n` directly in mini notation with `sound`:
client:idle client:idle
tune={`note("c2 <eb2 <g2 g1>>".fast(2)) tune={`note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth:<32 16 8 4>") .sound("sawtooth:<32 16 8 4>")
.scope()`} ._scope()`}
/> />
Note for tidal users: `n` in tidal is synonymous to `note` for synths only. Note for tidal users: `n` in tidal is synonymous to `note` for synths only.
@@ -119,13 +119,14 @@ Any sample preceded by the `wt_` prefix will be loaded as a wavetable. This mean
<MiniRepl <MiniRepl
client:idle client:idle
tune={`samples('github:Bubobubobubobubo/Dough-Waveforms/main/'); tune={`samples('bubo:waveforms');
note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>") note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")
.n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9) .n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9)
.s('wt_flute').velocity(0.25).often(n => n.ply(2)) .s('wt_flute').velocity(0.25).often(n => n.ply(2))
.release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0) .release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0)
.cutoff(2000).scope({}).cutoff("<1000 2000 4000>").fast(4)`} .cutoff(2000).cutoff("<1000 2000 4000>").fast(4)
._scope()
`}
/> />
## ZZFX ## ZZFX
@@ -159,7 +160,7 @@ It has 20 parameters in total, here is a snippet that uses all:
.tremolo(0) // 0-1 lfo volume modulation amount .tremolo(0) // 0-1 lfo volume modulation amount
//.duration(.2) // overwrite strudel event duration //.duration(.2) // overwrite strudel event duration
//.gain(1) // change volume //.gain(1) // change volume
.scope() // vizualise waveform (not zzfx related) ._scope() // vizualise waveform (not zzfx related)
`} `}
/> />
-14
View File
@@ -55,20 +55,6 @@ Transposes notes inside the scale by the number of steps:
.note()`} .note()`}
/> />
### voicings(range?)
Turns chord symbols into voicings, using the smoothest voice leading possible:
<MiniRepl
client:only="react"
tune={`stack(
"<C^7 A7 Dm7 G7>".voicings('lefthand'),
"<C3 A2 D3 G2>"
).note()`}
/>
Note: This function might be removed, as `voicing` (without s) is a newer implementation.
### rootNotes(octave = 2) ### rootNotes(octave = 2)
Turns chord symbols into root notes of chords in given octave. Turns chord symbols into root notes of chords in given octave.
+100
View File
@@ -0,0 +1,100 @@
---
title: Visual Feedback
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Visual Feedback
There are several function that add visual feedback to your patterns.
## Mini Notation Highlighting
When you write mini notation with "double quotes" or \`backticks\`, the active parts of the mini notation will be highlighted:
<MiniRepl
client:idle
tune={`n("<0 2 1 3 2>*8")
.scale("<A1 D2>/4:minor:pentatonic")
.s("supersaw").lpf(300).lpenv("<4 3 2>\*4")`}
/>
You can change the color as well, even pattern it:
<MiniRepl
client:idle
tune={`n("<0 2 1 3 2>*8")
.scale("<A1 D2>/4:minor:pentatonic")
.s("supersaw").lpf(300).lpenv("<4 3 2>*4")
.color("cyan magenta")`}
/>
## Global vs Inline Visuals
The following functions all come with in 2 variants.
**Without prefix**: renders the visual to the background of the page:
<MiniRepl client:idle tune={`note("c a f e").color("white").punchcard()`} />
**With `_` prefix**: renders the visual inside the code. Allows for multiple visuals
<MiniRepl client:idle tune={`note("c a f e").color("white")._punchcard()`} />
Here we see the 2 variants for `punchcard`. The same goes for all others below.
To improve readability the following demos will all use the inline variant.
## Punchcard / Pianoroll
These 2 functions render a pianoroll style visual.
The only difference between the 2 is that `pianoroll` will render the pattern directly,
while `punchcard` will also take the transformations into account that occur afterwards:
<MiniRepl
client:idle
tune={`note("c a f e").color("white")
._punchcard()
.color("cyan")`}
autodraw
/>
Here, the `color` is still visible in the visual, even if it is applied after `_punchcard`.
On the contrary, the color is not visible when using `_pianoroll`:
<MiniRepl
client:idle
tune={`note("c a f e").color("white")
._pianoroll()
.color("cyan")`}
autodraw
/>
import Box from '@components/Box.astro';
<br />
<Box>
`punchcard` is less resource intensive because it uses the same data as used for the mini notation highlighting.
</Box>
The visual can be customized by passing options. Those options are the same for both functions.
What follows is the API doc of all the options you can pass:
<JsDoc client:idle name="pianoroll" h={0} />
## Spiral
<JsDoc client:idle name="spiral" h={0} />
## Scope
<JsDoc client:idle name="scope" h={0} />
## Pitchwheel
<JsDoc client:idle name="pitchwheel" h={0} />
+35 -46
View File
@@ -60,11 +60,10 @@ We will learn how to automate with waves later...
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: sound("hh*16").gain("[.25 1]*4")
sound("hh*16").gain("[.25 1]*4"),
sound("bd*4,[~ sd:1]*2") $: sound("bd*4,[~ sd:1]*2")`}
) `} punchcard
punchcard
/> />
<Box> <Box>
@@ -76,31 +75,21 @@ Rhythm is all about dynamics!
</Box> </Box>
**stacks within stacks**
Let's combine all of the above into a little tune: Let's combine all of the above into a little tune:
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: sound("hh*8").gain("[.25 1]*4")
stack(
sound("hh*8").gain("[.25 1]*4"), $: sound("bd*4,[~ sd:1]*2")
sound("bd*4,[~ sd:1]*2")
), $: note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>") .sound("sawtooth").lpf("200 1000 200 1000")
.sound("sawtooth").lpf("200 1000 200 1000"),
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>") $: note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
.sound("sawtooth").vowel("<a e i o>") .sound("sawtooth").vowel("<a e i o>")`}
)`}
/> />
<Box>
Try to identify the individual parts of the stacks, pay attention to where the commas are.
The 3 parts (drums, bassline, chords) are exactly as earlier, just stacked together, separated by comma.
</Box>
**shape the sound with an adsr envelope** **shape the sound with an adsr envelope**
<MiniRepl <MiniRepl
@@ -151,11 +140,10 @@ Can you guess what they do?
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") .sound("gm_electric_guitar_muted").delay(.5)
.sound("gm_electric_guitar_muted"),
sound("<bd rim>").bank("RolandTR707") $: sound("bd rim").bank("RolandTR707").delay(".5")`}
).delay(".5")`}
/> />
<Box> <Box>
@@ -199,31 +187,32 @@ Add a delay too!
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") .sound("gm_electric_guitar_muted").delay(.5)
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5), $: sound("bd rim").bank("RolandTR707").delay(.5)
n("<4 [3@3 4] [<2 0> ~@16] ~>")
$: n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2") .scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.5) .room(2).gain(.5)`}
)`}
/> />
Let's add a bass to make this complete: Let's add a bass to make this complete:
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2") .sound("gm_electric_guitar_muted").delay(.5)
.sound("gm_electric_guitar_muted").delay(.5),
sound("<bd rim>").bank("RolandTR707").delay(.5), $: sound("bd rim").bank("RolandTR707").delay(.5)
n("<4 [3@3 4] [<2 0> ~@16] ~>")
$: n("<4 [3@3 4] [<2 0> ~@16] ~>")
.scale("D4:minor").sound("gm_accordion:2") .scale("D4:minor").sound("gm_accordion:2")
.room(2).gain(.4), .room(2).gain(.4)
n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
$: n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
.scale("D2:minor") .scale("D2:minor")
.sound("sawtooth,triangle").lpf(800) .sound("sawtooth,triangle").lpf(800)`}
)`}
/> />
<Box> <Box>
@@ -266,7 +255,7 @@ By the way, inside Mini-Notation, `fast` is `*` and `slow` is `/`.
Instead of changing values stepwise, we can also control them with signals: Instead of changing values stepwise, we can also control them with signals:
<MiniRepl client:visible tune={`sound("hh*32").gain(sine)`} punchcard punchcardLabels={false} /> <MiniRepl client:visible tune={`sound("hh*16").gain(sine)`} punchcard punchcardLabels={false} />
<Box> <Box>
+21 -11
View File
@@ -315,10 +315,10 @@ Let's recap what we've learned in this chapter:
New functions: New functions:
| Name | Description | Example | | Name | Description | Example |
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- | | ----- | ----------------------------- | --------------------------------------------------------------------------------- |
| note | set pitch as number or letter | <MiniRepl client:visible tune={`note("b g e c").sound("piano")`} /> | | note | set pitch as number or letter | <MiniRepl client:visible tune={`note("b g e c").sound("piano")`} /> |
| scale | interpret `n` as scale degree | <MiniRepl client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> | | scale | interpret `n` as scale degree | <MiniRepl client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> |
| stack | play patterns in parallel (read on) | <MiniRepl client:visible tune={`stack(s("bd sd"),note("c eb g"))`} /> | | $: | play patterns in parallel | <MiniRepl client:visible tune={'$: s("bd sd")\n$: note("c eb g")'} /> |
## Examples ## Examples
@@ -356,25 +356,35 @@ New functions:
<Box> <Box>
It's called `stack` 😙 You can use `$:` 😙
</Box> </Box>
## Playing multiple patterns
If you want to play multiple patterns at the same time, make sure to write `$:` before each:
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>") .sound("gm_synth_bass_1").lpf(800)
.sound("gm_synth_bass_1").lpf(800),
n(\`< $: n(\`<
[~ 0] 2 [0 2] [~ 2] [~ 0] 2 [0 2] [~ 2]
[~ 0] 1 [0 1] [~ 1] [~ 0] 1 [0 1] [~ 1]
[~ 0] 3 [0 3] [~ 3] [~ 0] 3 [0 3] [~ 3]
[~ 0] 2 [0 2] [~ 2] [~ 0] 2 [0 2] [~ 2]
>*4\`).scale("C4:minor") >*4\`).scale("C4:minor")
.sound("gm_synth_strings_1"), .sound("gm_synth_strings_1")
sound("bd*4, [~ <sd cp>]*2, [~ hh]*4")
.bank("RolandTR909") $: sound("bd*4, [~ <sd cp>]*2, [~ hh]*4")
)`} .bank("RolandTR909")`}
/> />
<Box>
Try changing `$` to `_$` to mute a part!
</Box>
This is starting to sound like actual music! We have sounds, we have notes, now the last piece of the puzzle is missing: [effects](/workshop/first-effects) This is starting to sound like actual music! We have sounds, we have notes, now the last piece of the puzzle is missing: [effects](/workshop/first-effects)
+12 -20
View File
@@ -25,20 +25,16 @@ This is the same as:
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(0)
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(0), $: n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(1).rev()`}
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(1).rev()
)`}
/> />
Let's visualize what happens here: Let's visualize what happens here:
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(0).color("cyan")
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(0).color("cyan"), $: n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(1).color("magenta").rev()`}
n("0 1 [4 3] 2 0 2 [~ 3] 4").sound("jazz").pan(1).color("magenta").rev()
)`}
punchcard punchcard
/> />
@@ -56,11 +52,9 @@ This is like doing
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: note("c2, eb3 g3 [bb3 c4]").s("piano").slow(0.5).color('cyan')
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(0.5).color('cyan'), $: note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1).color('magenta')
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1).color('magenta'), $: note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1.5).color('yellow')`}
note("c2, eb3 g3 [bb3 c4]").s("piano").slow(1.5).color('yellow')
)`}
punchcard punchcard
/> />
@@ -110,17 +104,15 @@ We can add as often as we like:
<MiniRepl <MiniRepl
client:visible client:visible
tune={`stack( tune={`$: n("0 [2 4] <3 5> [~ <4 1>]".add("<0 [0,2,4]>"))
n("0 [2 4] <3 5> [~ <4 1>]".add("<0 [0,2,4]>"))
.scale("C5:minor") .scale("C5:minor")
.sound("gm_xylophone") .sound("gm_xylophone")
.room(.4).delay(.125), .room(.4).delay(.125)
note("c2 [eb3,g3]".add("<0 <1 -1>>")) $: note("c2 [eb3,g3]".add("<0 <1 -1>>"))
.adsr("[.1 0]:.2:[1 0]") .adsr("[.1 0]:.2:[1 0]")
.sound("gm_acoustic_bass") .sound("gm_acoustic_bass")
.room(.5), .room(.5)
n("0 1 [2 3] 2").sound("jazz").jux(rev) $: n("0 1 [2 3] 2").sound("jazz").jux(rev)`}
)`}
/> />
**ply** **ply**
+1 -1
View File
@@ -39,7 +39,7 @@ This page is just a listing of all functions covered in the workshop!
| --------- | ----------------------------- | --------------------------------------------------------------------------------- | | --------- | ----------------------------- | --------------------------------------------------------------------------------- |
| note | set pitch as number or letter | <MiniRepl client:visible tune={`note("b g e c").sound("piano")`} /> | | note | set pitch as number or letter | <MiniRepl client:visible tune={`note("b g e c").sound("piano")`} /> |
| n + scale | set note in scale | <MiniRepl client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> | | n + scale | set note in scale | <MiniRepl client:visible tune={`n("6 4 2 0").scale("C:minor").sound("piano")`} /> |
| stack | play patterns in parallel | <MiniRepl client:visible tune={`stack(s("bd sd"),note("c eb g"))`} /> | | $: | play patterns in parallel | <MiniRepl client:visible tune={'$: s("bd sd")\n$: note("c eb g")'} /> |
## Audio Effects ## Audio Effects
+2 -1
View File
@@ -75,7 +75,8 @@ export const walkFileTree = (node, fn) => {
} }
}; };
export const isAudioFile = (filename) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]); export const isAudioFile = (filename) =>
['wav', 'mp3', 'flac', 'ogg', 'm4a', 'aac'].includes(filename.split('.').slice(-1)[0]);
function uint8ArrayToDataURL(uint8Array) { function uint8ArrayToDataURL(uint8Array) {
const blob = new Blob([uint8Array], { type: 'audio/*' }); const blob = new Blob([uint8Array], { type: 'audio/*' });
+63 -34
View File
@@ -12,7 +12,7 @@ export const userSamplesDBConfig = {
}; };
// deletes all of the databases, useful for debugging // deletes all of the databases, useful for debugging
const clearIDB = () => { function clearIDB() {
window.indexedDB window.indexedDB
.databases() .databases()
.then((r) => { .then((r) => {
@@ -21,37 +21,59 @@ const clearIDB = () => {
.then(() => { .then(() => {
alert('All data cleared.'); alert('All data cleared.');
}); });
}; }
// queries the DB, and registers the sounds so they can be played // queries the DB, and registers the sounds so they can be played
export const registerSamplesFromDB = (config = userSamplesDBConfig, onComplete = () => {}) => { export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) {
openDB(config, (objectStore) => { openDB(config, (objectStore) => {
let query = objectStore.getAll(); const query = objectStore.getAll();
query.onerror = (e) => {
logger('User Samples failed to load ', 'error');
onComplete();
console.error(e?.target?.error);
};
query.onsuccess = (event) => { query.onsuccess = (event) => {
const soundFiles = event.target.result; const soundFiles = event.target.result;
if (!soundFiles?.length) { if (!soundFiles?.length) {
return; return;
} }
const sounds = new Map(); const sounds = new Map();
Promise.all(
[...soundFiles] [...soundFiles]
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' })) .sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
.forEach((soundFile) => { .map((soundFile, i) => {
const title = soundFile.title; const title = soundFile.title;
if (!isAudioFile(title)) { if (!isAudioFile(title)) {
return; return;
} }
const splitRelativePath = soundFile.id.split('/'); const splitRelativePath = soundFile.id.split('/');
const parentDirectory = let parentDirectory =
//fallback to file name before period and seperator if no parent directory //fallback to file name before period and seperator if no parent directory
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user'; splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const soundPath = soundFile.blob; const blob = soundFile.blob;
// Files used to be uploaded as base64 strings, After Jan 1 2025 this check can be safely deleted
if (typeof blob === 'string') {
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(blob);
sounds.set(parentDirectory, soundPaths);
return;
}
return blobToDataUrl(blob).then((soundPath) => {
const soundPaths = sounds.get(parentDirectory) ?? new Set(); const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath); soundPaths.add(soundPath);
sounds.set(parentDirectory, soundPaths); sounds.set(parentDirectory, soundPaths);
return;
}); });
}),
)
.then(() => {
sounds.forEach((soundPaths, key) => { sounds.forEach((soundPaths, key) => {
const value = Array.from(soundPaths); const value = Array.from(soundPaths);
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample', type: 'sample',
samples: value, samples: value,
@@ -60,15 +82,20 @@ export const registerSamplesFromDB = (config = userSamplesDBConfig, onComplete =
tag: undefined, tag: undefined,
}); });
}); });
logger('imported sounds registered!', 'success'); logger('imported sounds registered!', 'success');
onComplete(); onComplete();
})
.catch((error) => {
logger('Something went wrong while registering saved samples from the index db', 'error');
console.error(error);
});
}; };
}); });
}; }
// creates a blob from a buffer that can be read
async function bufferToDataUrl(buf) { async function blobToDataUrl(blob) {
return new Promise((resolve) => { return new Promise((resolve) => {
var blob = new Blob([buf], { type: 'application/octet-binary' });
var reader = new FileReader(); var reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
resolve(event.target.result); resolve(event.target.result);
@@ -76,8 +103,9 @@ async function bufferToDataUrl(buf) {
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
} }
//open db and initialize it if necessary //open db and initialize it if necessary
const openDB = (config, onOpened) => { function openDB(config, onOpened) {
const { dbName, version, table, columns } = config; const { dbName, version, table, columns } = config;
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; return;
@@ -102,35 +130,32 @@ const openDB = (config, onOpened) => {
dbOpen.onsuccess = () => { dbOpen.onsuccess = () => {
const db = dbOpen.result; const db = dbOpen.result;
// lock store for writing
const // lock store for writing const writeTransaction = db.transaction([table], 'readwrite');
writeTransaction = db.transaction([table], 'readwrite'),
// get object store // get object store
objectStore = writeTransaction.objectStore(table); const objectStore = writeTransaction.objectStore(table);
onOpened(objectStore, db); onOpened(objectStore, db);
}; };
}; return dbOpen;
}
const processFilesForIDB = async (files) => { async function processFilesForIDB(files) {
return await Promise.all( return Promise.all(
Array.from(files) Array.from(files)
.map(async (s) => { .map((file) => {
const title = s.name; const title = file.name;
if (!isAudioFile(title)) { if (!isAudioFile(title)) {
return; return;
} }
//create obscured url to file system that can be fetched const path = file.webkitRelativePath;
const sUrl = URL.createObjectURL(s); let id = path?.length ? path : title;
//fetch the sound and turn it into a buffer array if (id == null || title == null || file == null) {
const buf = await fetch(sUrl).then((res) => res.arrayBuffer()); return;
//create a url base64 containing all of the buffer data }
const base64 = await bufferToDataUrl(buf);
const path = s.webkitRelativePath;
const id = path?.length ? path : title;
return { return {
title, title,
blob: base64, blob: file,
id, id,
}; };
}) })
@@ -139,18 +164,22 @@ const processFilesForIDB = async (files) => {
logger('Something went wrong while processing uploaded files', 'error'); logger('Something went wrong while processing uploaded files', 'error');
console.error(error); console.error(error);
}); });
}; }
export const uploadSamplesToDB = async (config, files) => { export async function uploadSamplesToDB(config, files) {
logger('procesing user samples...');
await processFilesForIDB(files).then((files) => { await processFilesForIDB(files).then((files) => {
logger('user samples processed... opening db');
const onOpened = (objectStore, _db) => { const onOpened = (objectStore, _db) => {
logger('index db opened... writing files to db');
files.forEach((file) => { files.forEach((file) => {
if (file == null) { if (file == null) {
return; return;
} }
objectStore.put(file); objectStore.put(file);
}); });
logger('user samples written successfully');
}; };
openDB(config, onOpened); openDB(config, onOpened);
}); });
}; }
@@ -33,7 +33,7 @@ export default function ImportSoundsButton({ onComplete }) {
directory="" directory=""
webkitdirectory="" webkitdirectory=""
multiple multiple
accept="audio/*, .wav, .mp3, .m4a, .flac" accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
onChange={() => { onChange={() => {
onChange(); onChange();
}} }}