mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9933c610a | |||
| 9d81a4d071 | |||
| 86cc9b87c3 | |||
| 0df6f3e56e | |||
| c403a92542 | |||
| 269d6dde31 | |||
| 666c695c63 | |||
| 771eb31d3f | |||
| c05bbf3422 | |||
| 364f511a7c | |||
| 300022d8ac | |||
| 357fbf8c82 | |||
| 1acb675f5a | |||
| ab1086318c | |||
| 8c73ad088a | |||
| 25981092b1 | |||
| e6b55baed0 | |||
| 6adf1ff977 | |||
| b6e277f1ea | |||
| d0510f862a | |||
| 4a87df6426 | |||
| bf90867685 | |||
| 3dc5fd0e7e | |||
| a7650f1af3 | |||
| aadb2fee84 | |||
| bf391ad6ce | |||
| faba4a4e4e | |||
| 94e411aa7a | |||
| ffa21ddf0d | |||
| 6b2d080afc | |||
| 321a888921 | |||
| bf7fe2bf75 | |||
| 1fd9ba5dbf | |||
| 1fca6f25b8 | |||
| 0e6a17fc77 | |||
| 375c68775c | |||
| 827993a8c9 | |||
| e29876e7f6 |
+3
-9
@@ -130,20 +130,14 @@ npx lerna version --no-private
|
||||
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
|
||||
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`.
|
||||
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
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
@@ -140,27 +140,25 @@ export class StrudelMirror {
|
||||
this.root = root;
|
||||
this.miniLocations = [];
|
||||
this.widgets = [];
|
||||
this.painters = [];
|
||||
this.drawTime = drawTime;
|
||||
this.drawContext = drawContext;
|
||||
this.onDraw = onDraw || this.draw;
|
||||
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));
|
||||
this.highlight(currentFrame, time);
|
||||
this.onDraw(haps, time, this.painters);
|
||||
this.onDraw(haps, time, painters);
|
||||
}, drawTime);
|
||||
|
||||
this.prebaked = prebake();
|
||||
autodraw && this.drawFirstFrame();
|
||||
|
||||
this.repl = repl({
|
||||
...replOptions,
|
||||
id,
|
||||
onToggle: (started) => {
|
||||
replOptions?.onToggle?.(started);
|
||||
if (started) {
|
||||
this.adjustDrawTime();
|
||||
this.drawer.start(this.repl.scheduler);
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
@@ -171,20 +169,11 @@ export class StrudelMirror {
|
||||
} else {
|
||||
this.drawer.stop();
|
||||
updateMiniLocations(this.editor, []);
|
||||
cleanupDraw(false);
|
||||
cleanupDraw(true, id);
|
||||
}
|
||||
},
|
||||
beforeEval: async () => {
|
||||
cleanupDraw();
|
||||
this.painters = [];
|
||||
const self = this;
|
||||
// this is similar to repl.mjs > injectPatternMethods
|
||||
// maybe there is a solution without prototype hacking, but hey, it works
|
||||
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
|
||||
Pattern.prototype.onPaint = function (onPaint) {
|
||||
self.painters.push(onPaint);
|
||||
return this;
|
||||
};
|
||||
cleanupDraw(true, id);
|
||||
await this.prebaked;
|
||||
await replOptions?.beforeEval?.();
|
||||
},
|
||||
@@ -198,8 +187,11 @@ export class StrudelMirror {
|
||||
updateWidgets(this.editor, widgets);
|
||||
updateMiniLocations(this.editor, this.miniLocations);
|
||||
replOptions?.afterEval?.(options);
|
||||
this.adjustDrawTime();
|
||||
this.drawer.invalidate();
|
||||
// if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
|
||||
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({
|
||||
@@ -234,13 +226,8 @@ export class StrudelMirror {
|
||||
};
|
||||
document.addEventListener('start-repl', this.onStartRepl);
|
||||
}
|
||||
// adjusts draw time depending on if there are painters
|
||||
adjustDrawTime() {
|
||||
// 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));
|
||||
draw(haps, time, painters) {
|
||||
painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
|
||||
}
|
||||
async drawFirstFrame() {
|
||||
if (!this.onDraw) {
|
||||
@@ -252,7 +239,7 @@ export class StrudelMirror {
|
||||
await this.repl.evaluate(this.code, false);
|
||||
this.drawer.invalidate(this.repl.scheduler, -0.001);
|
||||
// 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) {
|
||||
console.warn('first frame could not be painted');
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export const settings = {
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#0f380f',
|
||||
light: true,
|
||||
customStyle: '.cm-line { line-height: 1 }',
|
||||
// customStyle: '.cm-line { line-height: 1 }',
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'light',
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export const settings = {
|
||||
lineBackground: '#00000040',
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#8a919966',
|
||||
customStyle: '.cm-line { line-height: 1 }',
|
||||
// customStyle: '.cm-line { line-height: 1 }',
|
||||
};
|
||||
|
||||
let punctuation = colorD;
|
||||
|
||||
@@ -73,6 +73,8 @@ export function registerControl(names, ...aliases) {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -191,7 +193,7 @@ export const { attack, att } = registerControl('attack', 'att');
|
||||
* note("c e g b g e")
|
||||
* .fm(4)
|
||||
* .fmh("<1 2 1.5 1.61>")
|
||||
* .scope()
|
||||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
|
||||
@@ -205,7 +207,7 @@ export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
* .fm("<0 1 2 8 32>")
|
||||
* .scope()
|
||||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
||||
@@ -221,7 +223,7 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
||||
* .fmdecay(.2)
|
||||
* .fmsustain(0)
|
||||
* .fmenv("<exp lin>")
|
||||
* .scope()
|
||||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmenv } = registerControl('fmenv');
|
||||
@@ -234,7 +236,7 @@ export const { fmenv } = registerControl('fmenv');
|
||||
* note("c e g b g e")
|
||||
* .fm(4)
|
||||
* .fmattack("<0 .05 .1 .2>")
|
||||
* .scope()
|
||||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmattack } = registerControl('fmattack');
|
||||
@@ -248,7 +250,7 @@ export const { fmattack } = registerControl('fmattack');
|
||||
* .fm(4)
|
||||
* .fmdecay("<.01 .05 .1 .2>")
|
||||
* .fmsustain(.4)
|
||||
* .scope()
|
||||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmdecay } = registerControl('fmdecay');
|
||||
@@ -262,7 +264,7 @@ export const { fmdecay } = registerControl('fmdecay');
|
||||
* .fm(4)
|
||||
* .fmdecay(.1)
|
||||
* .fmsustain("<1 .75 .5 0>")
|
||||
* .scope()
|
||||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmsustain } = registerControl('fmsustain');
|
||||
@@ -392,7 +394,7 @@ export const { loop } = registerControl('loop');
|
||||
* @synonyms loopb
|
||||
* @example
|
||||
* s("space").loop(1)
|
||||
* .loopBegin("<0 .125 .25>").scope()
|
||||
* .loopBegin("<0 .125 .25>")._scope()
|
||||
*/
|
||||
export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
|
||||
/**
|
||||
@@ -405,7 +407,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
|
||||
* @synonyms loope
|
||||
* @example
|
||||
* s("space").loop(1)
|
||||
* .loopEnd("<1 .75 .5 .25>").scope()
|
||||
* .loopEnd("<1 .75 .5 .25>")._scope()
|
||||
*/
|
||||
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||
/**
|
||||
@@ -800,10 +802,12 @@ export const { fanchor } = registerControl('fanchor');
|
||||
* @example
|
||||
* note("a e")
|
||||
* .vib("<.5 1 2 4 8 16>")
|
||||
* ._scope()
|
||||
* @example
|
||||
* // change the modulation depth with ":"
|
||||
* note("a e")
|
||||
* .vib("<.5 1 2 4 8 16>:12")
|
||||
* ._scope()
|
||||
*/
|
||||
export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato', 'v');
|
||||
/**
|
||||
@@ -824,10 +828,12 @@ export const { noise } = registerControl('noise');
|
||||
* @example
|
||||
* note("a e").vib(4)
|
||||
* .vibmod("<.25 .5 1 2 12>")
|
||||
* ._scope()
|
||||
* @example
|
||||
* // change the vibrato frequency with ":"
|
||||
* note("a e")
|
||||
* .vibmod("<.25 .5 1 2 12>:8")
|
||||
* ._scope()
|
||||
*/
|
||||
export const { vibmod, vmod } = registerControl(['vibmod', 'vib'], 'vmod');
|
||||
export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp');
|
||||
|
||||
@@ -8,8 +8,19 @@ import createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
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.beforeStart = beforeStart;
|
||||
this.cps = 0.5;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
@@ -82,7 +93,8 @@ export class Cyclist {
|
||||
this.started = v;
|
||||
this.onToggle?.(v);
|
||||
}
|
||||
start() {
|
||||
async start() {
|
||||
await this.beforeStart?.();
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.num_cycles_at_cps_change = 0;
|
||||
if (!this.pattern) {
|
||||
@@ -103,10 +115,10 @@ export class Cyclist {
|
||||
this.lastEnd = 0;
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
async setPattern(pat, autostart = false) {
|
||||
this.pattern = pat;
|
||||
if (autostart && !this.started) {
|
||||
this.start();
|
||||
await this.start();
|
||||
}
|
||||
}
|
||||
setCps(cps = 0.5) {
|
||||
|
||||
@@ -37,11 +37,11 @@ function safeEval(str, options = {}) {
|
||||
return Function(body)();
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
export const evaluate = async (code, transpiler, transpilerOptions) => {
|
||||
let meta = {};
|
||||
if (transpiler) {
|
||||
// transform syntactically correct js code to semantically usable code
|
||||
const transpiled = transpiler(code);
|
||||
const transpiled = transpiler(code, transpilerOptions);
|
||||
code = transpiled.output;
|
||||
meta = transpiled;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,14 @@ export class Pattern {
|
||||
return result;
|
||||
}
|
||||
|
||||
// runs func on query state
|
||||
withState(func) {
|
||||
return this.withHaps((haps, state) => {
|
||||
func(state);
|
||||
return haps;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* see `withValue`
|
||||
* @noAutocomplete
|
||||
@@ -2523,7 +2531,7 @@ export function _polymeterListSteps(steps, ...args) {
|
||||
* @param {any[]} patterns one or more patterns
|
||||
* @example
|
||||
* // 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) {
|
||||
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.
|
||||
* @synonyms pm
|
||||
* @example
|
||||
* // The same as "{c eb g, c2 g2}"
|
||||
* s_polymeter("c eb g", "c2 g2")
|
||||
* // The same as note("{c eb g, c2 g2}")
|
||||
* s_polymeter("c eb g", "c2 g2").note()
|
||||
*
|
||||
*/
|
||||
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.
|
||||
* 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`.
|
||||
* @name s_cat
|
||||
* @synonyms timeCat, timecat
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* s_cat([3,"e3"],[1, "g3"]).note()
|
||||
|
||||
+13
-4
@@ -10,6 +10,7 @@ export function repl({
|
||||
defaultOutput,
|
||||
onEvalError,
|
||||
beforeEval,
|
||||
beforeStart,
|
||||
afterEval,
|
||||
getTime,
|
||||
transpiler,
|
||||
@@ -19,6 +20,7 @@ export function repl({
|
||||
sync = false,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
id,
|
||||
}) {
|
||||
const state = {
|
||||
schedulerError: undefined,
|
||||
@@ -32,6 +34,10 @@ export function repl({
|
||||
started: false,
|
||||
};
|
||||
|
||||
const transpilerOptions = {
|
||||
id,
|
||||
};
|
||||
|
||||
const updateState = (update) => {
|
||||
Object.assign(state, update);
|
||||
state.isDirty = state.code !== state.activeCode;
|
||||
@@ -48,6 +54,7 @@ export function repl({
|
||||
},
|
||||
setInterval,
|
||||
clearInterval,
|
||||
beforeStart,
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
const setPattern = (pattern, autostart = true) => {
|
||||
const setPattern = async (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
await scheduler.setPattern(pattern, autostart);
|
||||
return pattern;
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
|
||||
@@ -139,9 +147,10 @@ export function repl({
|
||||
try {
|
||||
updateState({ code, pending: true });
|
||||
await injectPatternMethods();
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
await beforeEval?.({ code });
|
||||
shouldHush && hush();
|
||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
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?' : '.'));
|
||||
}
|
||||
logger(`[eval] code updated`);
|
||||
setPattern(pattern, autostart);
|
||||
pattern = await setPattern(pattern, autostart);
|
||||
updateState({
|
||||
miniLocations: meta?.miniLocations || [],
|
||||
widgets: meta?.widgets || [],
|
||||
|
||||
+23
-13
@@ -36,8 +36,8 @@ function stopAnimationFrame(id) {
|
||||
delete animationFrames[id];
|
||||
}
|
||||
}
|
||||
function stopAllAnimations() {
|
||||
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id));
|
||||
function stopAllAnimations(replID) {
|
||||
Object.keys(animationFrames).forEach((id) => (!replID || id.startsWith(replID)) && stopAnimationFrame(id));
|
||||
}
|
||||
|
||||
let memory = {};
|
||||
@@ -72,18 +72,25 @@ Pattern.prototype.draw = function (fn, options) {
|
||||
return this;
|
||||
};
|
||||
|
||||
export const cleanupDraw = (clearScreen = true) => {
|
||||
export const cleanupDraw = (clearScreen = true, id) => {
|
||||
const ctx = getDrawContext();
|
||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
||||
stopAllAnimations();
|
||||
if (window.strudelScheduler) {
|
||||
clearInterval(window.strudelScheduler);
|
||||
}
|
||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
stopAllAnimations(id);
|
||||
};
|
||||
|
||||
Pattern.prototype.onPaint = function () {
|
||||
console.warn('[draw] onPaint was not overloaded. Some drawings might not work');
|
||||
return this;
|
||||
Pattern.prototype.onPaint = function (painter) {
|
||||
return this.withState((state) => {
|
||||
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;
|
||||
@@ -122,6 +129,7 @@ export class Drawer {
|
||||
this.visibleHaps = [];
|
||||
this.lastFrame = null;
|
||||
this.drawTime = drawTime;
|
||||
this.painters = [];
|
||||
this.framer = new Framer(
|
||||
() => {
|
||||
if (!this.scheduler) {
|
||||
@@ -146,7 +154,7 @@ export class Drawer {
|
||||
// add new haps with onset (think right edge bars scrolling in)
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
const time = phase - lookahead;
|
||||
onDraw(this.visibleHaps, time, this);
|
||||
onDraw(this.visibleHaps, time, this, this.painters);
|
||||
},
|
||||
(err) => {
|
||||
console.warn('draw error', err);
|
||||
@@ -164,11 +172,13 @@ export class Drawer {
|
||||
t = t ?? scheduler.now();
|
||||
this.scheduler = scheduler;
|
||||
let [_, lookahead] = this.drawTime;
|
||||
// +0.1 = workaround for weird holes in query..
|
||||
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
||||
// remove all future haps
|
||||
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
|
||||
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
|
||||
this.visibleHaps = this.visibleHaps.concat(futureHaps);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ Pattern.prototype.pianoroll = function (options = {}) {
|
||||
* Displays a midi-style piano roll
|
||||
*
|
||||
* @name pianoroll
|
||||
* @synonyms punchcard
|
||||
* @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 {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
|
||||
*
|
||||
* @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({
|
||||
time,
|
||||
@@ -113,7 +118,7 @@ export function pianoroll({
|
||||
maxMidi = 90,
|
||||
autorange = 0,
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
fold = 1,
|
||||
vertical = 0,
|
||||
labels = false,
|
||||
fill = 1,
|
||||
@@ -266,7 +271,7 @@ export function getDrawOptions(drawTime, options = {}) {
|
||||
let [lookbehind, lookahead] = drawTime;
|
||||
lookbehind = Math.abs(lookbehind);
|
||||
const cycles = lookahead + lookbehind;
|
||||
const playhead = lookbehind / cycles;
|
||||
const playhead = cycles !== 0 ? lookbehind / cycles : 0;
|
||||
return { fold: 1, ...options, cycles, playhead };
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,23 @@ export function pitchwheel({
|
||||
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 = {}) {
|
||||
let { ctx = getDrawContext(), id = 1 } = options;
|
||||
return this.tag(id).onPaint((_, time, haps) =>
|
||||
|
||||
@@ -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 = {}) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
export const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
export const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
||||
export const getCachedBuffer = (url) => bufferCache[url];
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import workletsUrl from './worklets.mjs?url';
|
||||
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
import { loadBuffer, bufferCache, loadCache, onTriggerSample } from './sampler.mjs';
|
||||
|
||||
export const soundMap = map();
|
||||
|
||||
@@ -95,6 +95,7 @@ function loadWorklets() {
|
||||
return workletsLoading;
|
||||
}
|
||||
|
||||
let stream;
|
||||
// this function should be called on first user interaction (to avoid console warning)
|
||||
export async function initAudio(options = {}) {
|
||||
const { disableWorklets = false } = options;
|
||||
@@ -112,6 +113,7 @@ export async function initAudio(options = {}) {
|
||||
} catch (err) {
|
||||
console.warn('could not load AudioWorklet effects', err);
|
||||
}
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
|
||||
logger('[superdough] ready');
|
||||
}
|
||||
let audioReady;
|
||||
@@ -308,6 +310,48 @@ export function resetGlobalEffects() {
|
||||
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) => {
|
||||
const ac = getAudioContext();
|
||||
if (typeof value !== 'object') {
|
||||
@@ -330,6 +374,7 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
// destructure
|
||||
let {
|
||||
s = getDefaultValue('s'),
|
||||
rec,
|
||||
bank,
|
||||
source,
|
||||
gain = getDefaultValue('gain'),
|
||||
@@ -411,11 +456,19 @@ export const superdough = async (value, t, hapDuration) => {
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
}
|
||||
if (rec && getSound(rec)) {
|
||||
s = rec;
|
||||
value.s = rec;
|
||||
}
|
||||
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
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)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
const soundHandle = await onTrigger(t, value, onended);
|
||||
|
||||
@@ -464,3 +464,48 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -69,6 +69,7 @@ export function transpiler(input, options = {}) {
|
||||
to: node.end,
|
||||
index,
|
||||
type,
|
||||
id: options.id,
|
||||
};
|
||||
emitWidgets && widgets.push(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
|
||||
// return `widget_${widgetConfig.to}`; // more gargabe
|
||||
//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) {
|
||||
|
||||
@@ -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} trigger amplitude value that is used to align the scope. defaults to 0.
|
||||
* @example
|
||||
* s("sawtooth").scope()
|
||||
* s("sawtooth")._scope()
|
||||
*/
|
||||
let latestColor = {};
|
||||
Pattern.prototype.tscope = function (config = {}) {
|
||||
|
||||
@@ -2717,175 +2717,175 @@ exports[`runs examples > example "floor" example index 0 1`] = `
|
||||
|
||||
exports[`runs examples > example "fm" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:0 analyze:1 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:0 analyze:1 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:0 analyze:1 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:0 analyze:1 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:0 analyze:1 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:0 analyze:1 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:1 analyze:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:1 analyze:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:1 analyze:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:1 analyze:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:1 analyze:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:1 analyze:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:2 analyze:1 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:2 analyze:1 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:2 analyze:1 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:2 analyze:1 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:2 analyze:1 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:2 analyze:1 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:8 analyze:1 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:8 analyze:1 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:8 analyze:1 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:8 analyze:1 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:8 analyze:1 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:8 analyze:1 ]",
|
||||
"[ 0/1 → 1/6 | note:c fmi:0 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:0 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:0 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:0 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:0 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:0 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:2 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:2 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:2 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:2 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:2 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:2 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:8 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:8 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:8 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:8 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:8 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:8 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fmattack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 analyze:1 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:4 fmattack:0 analyze:1 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:4 fmattack:0 analyze:1 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:4 fmattack:0 analyze:1 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:4 fmattack:0 analyze:1 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:4 fmattack:0 analyze:1 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:4 fmattack:0.05 analyze:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:4 fmattack:0.05 analyze:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:4 fmattack:0.05 analyze:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:4 fmattack:0.05 analyze:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:4 fmattack:0.05 analyze:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:4 fmattack:0.05 analyze:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:4 fmattack:0.1 analyze:1 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:4 fmattack:0.1 analyze:1 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:4 fmattack:0.1 analyze:1 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:4 fmattack:0.1 analyze:1 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:4 fmattack:0.1 analyze:1 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:4 fmattack:0.1 analyze:1 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:4 fmattack:0.2 analyze:1 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:4 fmattack:0.2 analyze:1 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:4 fmattack:0.2 analyze:1 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:4 fmattack:0.2 analyze:1 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:4 fmattack:0.2 analyze:1 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:4 fmattack:0.2 analyze:1 ]",
|
||||
"[ 0/1 → 1/6 | note:c fmi:4 fmattack:0 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:4 fmattack:0 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:4 fmattack:0 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:4 fmattack:0 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:4 fmattack:0 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:4 fmattack:0 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:4 fmattack:0.05 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:4 fmattack:0.05 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:4 fmattack:0.05 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:4 fmattack:0.05 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:4 fmattack:0.05 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:4 fmattack:0.05 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:4 fmattack:0.1 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:4 fmattack:0.1 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:4 fmattack:0.1 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:4 fmattack:0.1 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:4 fmattack:0.1 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:4 fmattack:0.1 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:4 fmattack:0.2 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:4 fmattack:0.2 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:4 fmattack:0.2 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:4 fmattack:0.2 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:4 fmattack:0.2 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:4 fmattack:0.2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
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 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.01 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.05 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.2 fmsustain:0.4 analyze:1 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.2 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
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 ]",
|
||||
"[ 1/6 → 1/3 | note:e 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 analyze:1 ]",
|
||||
"[ 1/2 → 2/3 | note:b 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 analyze:1 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.2 fmsustain:0 fmenv:exp analyze:1 ]",
|
||||
"[ 1/1 → 7/6 | note:c 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 analyze:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g 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 analyze:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g 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 analyze:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c 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 analyze:1 ]",
|
||||
"[ 7/3 → 5/2 | note:g 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 analyze:1 ]",
|
||||
"[ 8/3 → 17/6 | note:g 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 analyze:1 ]",
|
||||
"[ 3/1 → 19/6 | note:c 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 analyze:1 ]",
|
||||
"[ 10/3 → 7/2 | note:g 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 analyze:1 ]",
|
||||
"[ 11/3 → 23/6 | note:g 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 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fmh" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:4 fmh:1 analyze:1 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:4 fmh:1 analyze:1 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:4 fmh:1 analyze:1 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:4 fmh:1 analyze:1 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:4 fmh:1 analyze:1 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:4 fmh:1 analyze:1 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:4 fmh:2 analyze:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:4 fmh:2 analyze:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:4 fmh:2 analyze:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:4 fmh:2 analyze:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:4 fmh:2 analyze:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:4 fmh:2 analyze:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:4 fmh:1.5 analyze:1 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:4 fmh:1.5 analyze:1 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:4 fmh:1.5 analyze:1 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:4 fmh:1.5 analyze:1 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:4 fmh:1.5 analyze:1 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:4 fmh:1.5 analyze:1 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:4 fmh:1.61 analyze:1 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:4 fmh:1.61 analyze:1 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:4 fmh:1.61 analyze:1 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:4 fmh:1.61 analyze:1 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:4 fmh:1.61 analyze:1 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:4 fmh:1.61 analyze:1 ]",
|
||||
"[ 0/1 → 1/6 | note:c fmi:4 fmh:1 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:4 fmh:1 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:4 fmh:1 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:4 fmh:1 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:4 fmh:1 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:4 fmh:1 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:4 fmh:2 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:4 fmh:2 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:4 fmh:2 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:4 fmh:2 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:4 fmh:2 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:4 fmh:2 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:4 fmh:1.5 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:4 fmh:1.5 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:4 fmh:1.5 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:4 fmh:1.5 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:4 fmh:1.5 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:4 fmh:1.5 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:4 fmh:1.61 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:4 fmh:1.61 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:4 fmh:1.61 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:4 fmh:1.61 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:4 fmh:1.61 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:4 fmh:1.61 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "fmsustain" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
|
||||
"[ 1/6 → 1/3 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
|
||||
"[ 1/3 → 1/2 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
|
||||
"[ 1/2 → 2/3 | note:b fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
|
||||
"[ 2/3 → 5/6 | note:g fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
|
||||
"[ 5/6 → 1/1 | note:e fmi:4 fmdecay:0.1 fmsustain:1 analyze:1 ]",
|
||||
"[ 1/1 → 7/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
|
||||
"[ 7/6 → 4/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
|
||||
"[ 4/3 → 3/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
|
||||
"[ 3/2 → 5/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
|
||||
"[ 5/3 → 11/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
|
||||
"[ 11/6 → 2/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.75 analyze:1 ]",
|
||||
"[ 2/1 → 13/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
|
||||
"[ 13/6 → 7/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
|
||||
"[ 7/3 → 5/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
|
||||
"[ 5/2 → 8/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
|
||||
"[ 8/3 → 17/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
|
||||
"[ 17/6 → 3/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0.5 analyze:1 ]",
|
||||
"[ 3/1 → 19/6 | note:c fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
|
||||
"[ 19/6 → 10/3 | note:e fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
|
||||
"[ 10/3 → 7/2 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
|
||||
"[ 7/2 → 11/3 | note:b fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
|
||||
"[ 11/3 → 23/6 | note:g fmi:4 fmdecay:0.1 fmsustain:0 analyze:1 ]",
|
||||
"[ 23/6 → 4/1 | note:e fmi:4 fmdecay:0.1 fmsustain:0 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -4042,19 +4042,19 @@ exports[`runs examples > example "loopAtCps" 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 ]",
|
||||
"[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 analyze:1 ]",
|
||||
"[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 analyze:1 ]",
|
||||
"[ 3/1 → 4/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 ]",
|
||||
"[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 ]",
|
||||
"[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "loopEnd" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 analyze:1 ]",
|
||||
"[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 analyze:1 ]",
|
||||
"[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 analyze:1 ]",
|
||||
"[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 analyze:1 ]",
|
||||
"[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 ]",
|
||||
"[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 ]",
|
||||
"[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 ]",
|
||||
"[ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ (1/4 → 1/3) ⇝ 3/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ 1/4 ⇜ (1/3 → 3/8) | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 3/8 → 1/2 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ (5/8 → 2/3) ⇝ 3/4 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 5/8 ⇜ (2/3 → 3/4) | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 3/4 → 7/8 | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 1/1 → 9/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ (5/4 → 4/3) ⇝ 11/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ 5/4 ⇜ (4/3 → 11/8) | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 11/8 → 3/2 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ (13/8 → 5/3) ⇝ 7/4 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 13/8 ⇜ (5/3 → 7/4) | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 7/4 → 15/8 | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 2/1 → 17/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ (9/4 → 7/3) ⇝ 19/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ 9/4 ⇜ (7/3 → 19/8) | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 19/8 → 5/2 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ (21/8 → 8/3) ⇝ 11/4 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 21/8 ⇜ (8/3 → 11/4) | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 11/4 → 23/8 | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 3/1 → 25/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ (13/4 → 10/3) ⇝ 27/8 | note:C2 s:piano clip:1 color:salmon ]",
|
||||
"[ 13/4 ⇜ (10/3 → 27/8) | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 27/8 → 7/2 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ (29/8 → 11/3) ⇝ 15/4 | note:A2 s:piano clip:1 color:salmon ]",
|
||||
"[ 29/8 ⇜ (11/3 → 15/4) | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 15/4 → 31/8 | note:G2 s:piano clip:1 color:salmon ]",
|
||||
"[ 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 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -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`] = `
|
||||
[
|
||||
"[ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/3 | c ]",
|
||||
"[ 0/1 → 1/3 | c2 ]",
|
||||
"[ 1/3 → 2/3 | eb ]",
|
||||
"[ 1/3 → 2/3 | g2 ]",
|
||||
"[ 2/3 → 1/1 | g ]",
|
||||
"[ 2/3 → 1/1 | c2 ]",
|
||||
"[ 1/1 → 4/3 | c ]",
|
||||
"[ 1/1 → 4/3 | g2 ]",
|
||||
"[ 4/3 → 5/3 | eb ]",
|
||||
"[ 4/3 → 5/3 | c2 ]",
|
||||
"[ 5/3 → 2/1 | g ]",
|
||||
"[ 5/3 → 2/1 | g2 ]",
|
||||
"[ 2/1 → 7/3 | c ]",
|
||||
"[ 2/1 → 7/3 | c2 ]",
|
||||
"[ 7/3 → 8/3 | eb ]",
|
||||
"[ 7/3 → 8/3 | g2 ]",
|
||||
"[ 8/3 → 3/1 | g ]",
|
||||
"[ 8/3 → 3/1 | c2 ]",
|
||||
"[ 3/1 → 10/3 | c ]",
|
||||
"[ 3/1 → 10/3 | g2 ]",
|
||||
"[ 10/3 → 11/3 | eb ]",
|
||||
"[ 10/3 → 11/3 | c2 ]",
|
||||
"[ 11/3 → 4/1 | g ]",
|
||||
"[ 11/3 → 4/1 | g2 ]",
|
||||
"[ 0/1 → 1/3 | note:c ]",
|
||||
"[ 0/1 → 1/3 | note:c2 ]",
|
||||
"[ 1/3 → 2/3 | note:eb ]",
|
||||
"[ 1/3 → 2/3 | note:g2 ]",
|
||||
"[ 2/3 → 1/1 | note:g ]",
|
||||
"[ 2/3 → 1/1 | note:c2 ]",
|
||||
"[ 1/1 → 4/3 | note:c ]",
|
||||
"[ 1/1 → 4/3 | note:g2 ]",
|
||||
"[ 4/3 → 5/3 | note:eb ]",
|
||||
"[ 4/3 → 5/3 | note:c2 ]",
|
||||
"[ 5/3 → 2/1 | note:g ]",
|
||||
"[ 5/3 → 2/1 | note:g2 ]",
|
||||
"[ 2/1 → 7/3 | note:c ]",
|
||||
"[ 2/1 → 7/3 | note:c2 ]",
|
||||
"[ 7/3 → 8/3 | note:eb ]",
|
||||
"[ 7/3 → 8/3 | note:g2 ]",
|
||||
"[ 8/3 → 3/1 | note:g ]",
|
||||
"[ 8/3 → 3/1 | note:c2 ]",
|
||||
"[ 3/1 → 10/3 | note:c ]",
|
||||
"[ 3/1 → 10/3 | note:g2 ]",
|
||||
"[ 10/3 → 11/3 | note:eb ]",
|
||||
"[ 10/3 → 11/3 | note:c2 ]",
|
||||
"[ 11/3 → 4/1 | note:g ]",
|
||||
"[ 11/3 → 4/1 | note:g2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "s_polymeterSteps" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | c ]",
|
||||
"[ 0/1 → 1/4 | e ]",
|
||||
"[ 1/4 → 1/2 | d ]",
|
||||
"[ 1/4 → 1/2 | f ]",
|
||||
"[ 1/2 → 3/4 | c ]",
|
||||
"[ 1/2 → 3/4 | g ]",
|
||||
"[ 3/4 → 1/1 | d ]",
|
||||
"[ 3/4 → 1/1 | e ]",
|
||||
"[ 1/1 → 5/4 | c ]",
|
||||
"[ 1/1 → 5/4 | f ]",
|
||||
"[ 5/4 → 3/2 | d ]",
|
||||
"[ 5/4 → 3/2 | g ]",
|
||||
"[ 3/2 → 7/4 | c ]",
|
||||
"[ 3/2 → 7/4 | e ]",
|
||||
"[ 7/4 → 2/1 | d ]",
|
||||
"[ 7/4 → 2/1 | f ]",
|
||||
"[ 2/1 → 9/4 | c ]",
|
||||
"[ 2/1 → 9/4 | g ]",
|
||||
"[ 9/4 → 5/2 | d ]",
|
||||
"[ 9/4 → 5/2 | e ]",
|
||||
"[ 5/2 → 11/4 | c ]",
|
||||
"[ 5/2 → 11/4 | f ]",
|
||||
"[ 11/4 → 3/1 | d ]",
|
||||
"[ 11/4 → 3/1 | g ]",
|
||||
"[ 3/1 → 13/4 | c ]",
|
||||
"[ 3/1 → 13/4 | e ]",
|
||||
"[ 13/4 → 7/2 | d ]",
|
||||
"[ 13/4 → 7/2 | f ]",
|
||||
"[ 7/2 → 15/4 | c ]",
|
||||
"[ 7/2 → 15/4 | g ]",
|
||||
"[ 15/4 → 4/1 | d ]",
|
||||
"[ 15/4 → 4/1 | e ]",
|
||||
"[ 0/1 → 1/4 | note:c ]",
|
||||
"[ 0/1 → 1/4 | note:e ]",
|
||||
"[ 1/4 → 1/2 | note:d ]",
|
||||
"[ 1/4 → 1/2 | note:f ]",
|
||||
"[ 1/2 → 3/4 | note:c ]",
|
||||
"[ 1/2 → 3/4 | note:g ]",
|
||||
"[ 3/4 → 1/1 | note:d ]",
|
||||
"[ 3/4 → 1/1 | note:e ]",
|
||||
"[ 1/1 → 5/4 | note:c ]",
|
||||
"[ 1/1 → 5/4 | note:f ]",
|
||||
"[ 5/4 → 3/2 | note:d ]",
|
||||
"[ 5/4 → 3/2 | note:g ]",
|
||||
"[ 3/2 → 7/4 | note:c ]",
|
||||
"[ 3/2 → 7/4 | note:e ]",
|
||||
"[ 7/4 → 2/1 | note:d ]",
|
||||
"[ 7/4 → 2/1 | note:f ]",
|
||||
"[ 2/1 → 9/4 | note:c ]",
|
||||
"[ 2/1 → 9/4 | note:g ]",
|
||||
"[ 9/4 → 5/2 | note:d ]",
|
||||
"[ 9/4 → 5/2 | note:e ]",
|
||||
"[ 5/2 → 11/4 | note:c ]",
|
||||
"[ 5/2 → 11/4 | note:f ]",
|
||||
"[ 11/4 → 3/1 | note:d ]",
|
||||
"[ 11/4 → 3/1 | note:g ]",
|
||||
"[ 3/1 → 13/4 | note:c ]",
|
||||
"[ 3/1 → 13/4 | note:e ]",
|
||||
"[ 13/4 → 7/2 | note:d ]",
|
||||
"[ 13/4 → 7/2 | note:f ]",
|
||||
"[ 7/2 → 15/4 | note:c ]",
|
||||
"[ 7/2 → 15/4 | note:g ]",
|
||||
"[ 15/4 → 4/1 | note:d ]",
|
||||
"[ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:sawtooth analyze:1 ]",
|
||||
"[ 1/1 → 2/1 | s:sawtooth analyze:1 ]",
|
||||
"[ 2/1 → 3/1 | s:sawtooth analyze:1 ]",
|
||||
"[ 3/1 → 4/1 | s:sawtooth analyze:1 ]",
|
||||
"[ 0/1 → 1/1 | s:sawtooth ]",
|
||||
"[ 1/1 → 2/1 | s:sawtooth ]",
|
||||
"[ 2/1 → 3/1 | s:sawtooth ]",
|
||||
"[ 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`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | speed:1 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]",
|
||||
|
||||
@@ -122,6 +122,19 @@ strudel.Pattern.prototype.midi = function () {
|
||||
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 = {
|
||||
backgroundImage: id,
|
||||
};
|
||||
@@ -178,6 +191,7 @@ evalScope(
|
||||
},
|
||||
);
|
||||
|
||||
// TBD: use transpiler to support labeled statements
|
||||
export const queryCode = async (code, cycles = 1) => {
|
||||
const { pattern } = await evaluate(code);
|
||||
const haps = pattern.sortHapsByPart().queryArc(0, cycles);
|
||||
|
||||
@@ -78,6 +78,7 @@ export const SIDEBAR: Sidebar = {
|
||||
More: [
|
||||
{ text: 'Recipes', link: 'recipes/recipes' },
|
||||
{ text: 'Mini-Notation', link: 'learn/mini-notation' },
|
||||
{ text: 'Visual Feedback', link: 'learn/visual-feedback' },
|
||||
{ text: 'Offline', link: 'learn/pwa' },
|
||||
{ text: 'Patterns', link: 'technical-manual/patterns' },
|
||||
{ text: 'Music metadata', link: 'learn/metadata' },
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
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 { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
import { StrudelMirror } from '@strudel/codemirror';
|
||||
@@ -28,24 +29,29 @@ export function MiniRepl({
|
||||
claviature,
|
||||
claviatureLabels,
|
||||
maxHeight,
|
||||
autodraw,
|
||||
drawTime,
|
||||
}) {
|
||||
const code = tunes ? tunes[0] : tune;
|
||||
const id = useMemo(() => s4(), []);
|
||||
const canvasId = useMemo(() => `canvas-${id}`, [id]);
|
||||
const shouldDraw = !!punchcard || !!claviature;
|
||||
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 init = useCallback(({ code, shouldDraw }) => {
|
||||
const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null;
|
||||
const init = useCallback(({ code, autodraw }) => {
|
||||
const drawContext = canvasId ? document.querySelector('#' + canvasId)?.getContext('2d') : getDrawContext();
|
||||
|
||||
const editor = new StrudelMirror({
|
||||
id,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
transpiler,
|
||||
autodraw: !!shouldDraw,
|
||||
autodraw,
|
||||
root: containerRef.current,
|
||||
initialCode: '// LOADING',
|
||||
pattern: silence,
|
||||
@@ -56,7 +62,7 @@ export function MiniRepl({
|
||||
pat = pat.onTrigger(onTrigger, false);
|
||||
}
|
||||
if (claviature) {
|
||||
editor?.painters.push((ctx, time, haps, drawTime) => {
|
||||
pat = pat.onPaint((ctx, time, haps, drawTime) => {
|
||||
const active = haps
|
||||
.map((hap) => hap.value.note)
|
||||
.filter(Boolean)
|
||||
@@ -65,7 +71,7 @@ export function MiniRepl({
|
||||
});
|
||||
}
|
||||
if (punchcard) {
|
||||
editor?.painters.push(getPunchcardPainter({ labels: !!punchcardLabels }));
|
||||
pat = pat.punchcard({ labels: !!punchcardLabels });
|
||||
}
|
||||
return pat;
|
||||
},
|
||||
@@ -73,7 +79,12 @@ export function MiniRepl({
|
||||
onUpdateState: (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),
|
||||
});
|
||||
// init settings
|
||||
@@ -152,7 +163,7 @@ export function MiniRepl({
|
||||
ref={(el) => {
|
||||
if (!editorRef.current) {
|
||||
containerRef.current = el;
|
||||
init({ code, shouldDraw });
|
||||
init({ code, autodraw });
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
|
||||
@@ -11,15 +11,15 @@ import { JsDoc } from '../../docs/JsDoc';
|
||||
The following functions will return a pattern.
|
||||
These are the equivalents used by the Mini Notation:
|
||||
|
||||
| function | mini |
|
||||
| ------------------------------ | ---------------- |
|
||||
| `cat(x, y)` | `"<x y>"` |
|
||||
| `seq(x, y)` | `"x y"` |
|
||||
| `stack(x, y)` | `"x,y"` |
|
||||
| `timeCat([3,x],[2,y])` | `"x@3 y@2"` |
|
||||
| `polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` |
|
||||
| `polymeterSteps(2, x, y, z)` | `"{x y z}%2"` |
|
||||
| `silence` | `"~"` |
|
||||
| function | mini |
|
||||
| -------------------------------- | ---------------- |
|
||||
| `cat(x, y)` | `"<x y>"` |
|
||||
| `seq(x, y)` | `"x y"` |
|
||||
| `stack(x, y)` | `"x,y"` |
|
||||
| `s_cat([3,x],[2,y])` | `"x@3 y@2"` |
|
||||
| `s_polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` |
|
||||
| `s_polymeterSteps(2, x, y, z)` | `"{x y z}%2"` |
|
||||
| `silence` | `"~"` |
|
||||
|
||||
## cat
|
||||
|
||||
@@ -45,21 +45,21 @@ As a chained function:
|
||||
|
||||
<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
|
||||
|
||||
<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
|
||||
|
||||
|
||||
@@ -90,11 +90,13 @@ src(s0).kaleid(H("<4 5 6>"))
|
||||
.modulateScale(osc(2,-0.25,1))
|
||||
.out()
|
||||
//
|
||||
stack(
|
||||
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)
|
||||
.lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db')
|
||||
).fft(4)
|
||||
.scope({pos:0,smear:.95})`}
|
||||
|
||||
$: 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)
|
||||
.lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db')
|
||||
|
||||
all(x=>x.fft(4).scope({pos:0,smear:.95}))`}
|
||||
/>
|
||||
|
||||
@@ -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.
|
||||
If no outputName is given, it uses the first midi output it finds.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()
|
||||
.midi()`}
|
||||
/>
|
||||
<MiniRepl client:idle tune={`chord("<C^7 A7 Dm7 G7>").voicing().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".`
|
||||
|
||||
@@ -45,10 +41,8 @@ But you can also control cc messages separately like this:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`stack(
|
||||
note("c a f e"),
|
||||
ccv(sine.segment(16).slow(4)).ccn(74)
|
||||
).midi()`}
|
||||
tune={`$: note("c a f e").midi()
|
||||
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
|
||||
/>
|
||||
|
||||
# SuperDirt API
|
||||
|
||||
@@ -268,11 +268,10 @@ With it, you can enter any sample name(s) to query from [freesound.org](https://
|
||||
<MiniRepl
|
||||
client:idle
|
||||
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 2 3*2").s('hihat'),
|
||||
n("~ 0 ~ 1 ~ 0 0 1").s('rimshot')
|
||||
).clip(1)`}
|
||||
|
||||
$: n("0 1 2 3 0 1 2 3").s('bass')
|
||||
$: n("0 1*2 2 3*2").s('hihat').clip(1)
|
||||
$: n("~ 0 ~ 1 ~ 0 0 1").s('rimshot')`}
|
||||
/>
|
||||
|
||||
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
|
||||
tune={`samples('shabda/speech:the_drum,forever')
|
||||
samples('shabda/speech/fr-FR/m:magnifique')
|
||||
stack(
|
||||
s("the_drum*2").chop(16).speed(rand.range(0.85,1.1)),
|
||||
s("forever magnifique").slow(4).late(0.125)
|
||||
)`}
|
||||
|
||||
$: s("the_drum*2").chop(16).speed(rand.range(0.85,1.1))
|
||||
$: s("forever magnifique").slow(4).late(0.125)`}
|
||||
/>
|
||||
|
||||
# Sampler Effects
|
||||
|
||||
@@ -112,29 +112,14 @@ Also, samples are always loaded from a URL rather than from the disk, although [
|
||||
## Evaluation
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
let a = note("c a f e")
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`$: n("[0 .. 8]*8/9").scale("C:minor:pentatonic")
|
||||
|
||||
let b = s("bd sd")
|
||||
|
||||
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
|
||||
\_$: s("bd\*4").bank('RolandTR909')`}
|
||||
/>
|
||||
|
||||
## Tempo
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ The basic waveforms are `sine`, `sawtooth`, `square` and `triangle`, which can b
|
||||
client:idle
|
||||
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
||||
.sound("<sawtooth square triangle sine>")
|
||||
.scope()`}
|
||||
._scope()`}
|
||||
/>
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
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:
|
||||
|
||||
<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:
|
||||
|
||||
<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
|
||||
|
||||
@@ -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))
|
||||
.sound("sawtooth")
|
||||
.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.
|
||||
@@ -65,7 +65,7 @@ You can also set `n` directly in mini notation with `sound`:
|
||||
client:idle
|
||||
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
||||
.sound("sawtooth:<32 16 8 4>")
|
||||
.scope()`}
|
||||
._scope()`}
|
||||
/>
|
||||
|
||||
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
|
||||
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]>")
|
||||
.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))
|
||||
.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
|
||||
@@ -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
|
||||
//.duration(.2) // overwrite strudel event duration
|
||||
//.gain(1) // change volume
|
||||
.scope() // vizualise waveform (not zzfx related)
|
||||
._scope() // vizualise waveform (not zzfx related)
|
||||
`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -55,20 +55,6 @@ Transposes notes inside the scale by the number of steps:
|
||||
.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)
|
||||
|
||||
Turns chord symbols into root notes of chords in given octave.
|
||||
|
||||
@@ -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} />
|
||||
@@ -60,11 +60,10 @@ We will learn how to automate with waves later...
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
sound("hh*16").gain("[.25 1]*4"),
|
||||
sound("bd*4,[~ sd:1]*2")
|
||||
) `}
|
||||
punchcard
|
||||
tune={`$: sound("hh*16").gain("[.25 1]*4")
|
||||
|
||||
$: sound("bd*4,[~ sd:1]*2")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
<Box>
|
||||
@@ -76,31 +75,21 @@ Rhythm is all about dynamics!
|
||||
|
||||
</Box>
|
||||
|
||||
**stacks within stacks**
|
||||
|
||||
Let's combine all of the above into a little tune:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
stack(
|
||||
sound("hh*8").gain("[.25 1]*4"),
|
||||
sound("bd*4,[~ sd:1]*2")
|
||||
),
|
||||
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
|
||||
.sound("sawtooth").lpf("200 1000 200 1000"),
|
||||
note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
|
||||
.sound("sawtooth").vowel("<a e i o>")
|
||||
)`}
|
||||
tune={`$: sound("hh*8").gain("[.25 1]*4")
|
||||
|
||||
$: sound("bd*4,[~ sd:1]*2")
|
||||
|
||||
$: note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
|
||||
.sound("sawtooth").lpf("200 1000 200 1000")
|
||||
|
||||
$: note("<[c3,g3,e4] [bb2,f3,d4] [a2,f3,c4] [bb2,g3,eb4]>")
|
||||
.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**
|
||||
|
||||
<MiniRepl
|
||||
@@ -151,11 +140,10 @@ Can you guess what they do?
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted"),
|
||||
sound("<bd rim>").bank("RolandTR707")
|
||||
).delay(".5")`}
|
||||
tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5)
|
||||
|
||||
$: sound("bd rim").bank("RolandTR707").delay(".5")`}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
@@ -199,31 +187,32 @@ Add a delay too!
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5),
|
||||
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
||||
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5)
|
||||
|
||||
$: sound("bd rim").bank("RolandTR707").delay(.5)
|
||||
|
||||
$: n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
.scale("D4:minor").sound("gm_accordion:2")
|
||||
.room(2).gain(.5)
|
||||
)`}
|
||||
.room(2).gain(.5)`}
|
||||
/>
|
||||
|
||||
Let's add a bass to make this complete:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5),
|
||||
sound("<bd rim>").bank("RolandTR707").delay(.5),
|
||||
n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
tune={`$: note("[~ [<[d3,a3,f4]!2 [d3,bb3,g4]!2> ~]]*2")
|
||||
.sound("gm_electric_guitar_muted").delay(.5)
|
||||
|
||||
$: sound("bd rim").bank("RolandTR707").delay(.5)
|
||||
|
||||
$: n("<4 [3@3 4] [<2 0> ~@16] ~>")
|
||||
.scale("D4:minor").sound("gm_accordion:2")
|
||||
.room(2).gain(.4),
|
||||
n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
|
||||
.room(2).gain(.4)
|
||||
|
||||
$: n("[0 [~ 0] 4 [3 2] [0 ~] [0 ~] <0 2> ~]/2")
|
||||
.scale("D2:minor")
|
||||
.sound("sawtooth,triangle").lpf(800)
|
||||
)`}
|
||||
.sound("sawtooth,triangle").lpf(800)`}
|
||||
/>
|
||||
|
||||
<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:
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -314,11 +314,11 @@ Let's recap what we've learned in this chapter:
|
||||
|
||||
New functions:
|
||||
|
||||
| Name | Description | Example |
|
||||
| ----- | ----------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| 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")`} /> |
|
||||
| stack | play patterns in parallel (read on) | <MiniRepl client:visible tune={`stack(s("bd sd"),note("c eb g"))`} /> |
|
||||
| Name | Description | Example |
|
||||
| ----- | ----------------------------- | --------------------------------------------------------------------------------- |
|
||||
| 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")`} /> |
|
||||
| $: | play patterns in parallel | <MiniRepl client:visible tune={'$: s("bd sd")\n$: note("c eb g")'} /> |
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -356,25 +356,35 @@ New functions:
|
||||
|
||||
<Box>
|
||||
|
||||
It's called `stack` 😙
|
||||
You can use `$:` 😙
|
||||
|
||||
</Box>
|
||||
|
||||
## Playing multiple patterns
|
||||
|
||||
If you want to play multiple patterns at the same time, make sure to write `$:` before each:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
|
||||
.sound("gm_synth_bass_1").lpf(800),
|
||||
n(\`<
|
||||
tune={`$: note("<[c2 c3]*4 [bb1 bb2]*4 [f2 f3]*4 [eb2 eb3]*4>")
|
||||
.sound("gm_synth_bass_1").lpf(800)
|
||||
|
||||
$: n(\`<
|
||||
[~ 0] 2 [0 2] [~ 2]
|
||||
[~ 0] 1 [0 1] [~ 1]
|
||||
[~ 0] 3 [0 3] [~ 3]
|
||||
[~ 0] 2 [0 2] [~ 2]
|
||||
>*4\`).scale("C4:minor")
|
||||
.sound("gm_synth_strings_1"),
|
||||
sound("bd*4, [~ <sd cp>]*2, [~ hh]*4")
|
||||
.bank("RolandTR909")
|
||||
)`}
|
||||
.sound("gm_synth_strings_1")
|
||||
|
||||
$: 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)
|
||||
|
||||
@@ -25,20 +25,16 @@ This is the same as:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
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()
|
||||
)`}
|
||||
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(1).rev()`}
|
||||
/>
|
||||
|
||||
Let's visualize what happens here:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
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()
|
||||
)`}
|
||||
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(1).color("magenta").rev()`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -56,11 +52,9 @@ This is like doing
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
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.5).color('yellow')
|
||||
)`}
|
||||
tune={`$: 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.5).color('yellow')`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
@@ -110,17 +104,15 @@ We can add as often as we like:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`stack(
|
||||
n("0 [2 4] <3 5> [~ <4 1>]".add("<0 [0,2,4]>"))
|
||||
tune={`$: n("0 [2 4] <3 5> [~ <4 1>]".add("<0 [0,2,4]>"))
|
||||
.scale("C5:minor")
|
||||
.sound("gm_xylophone")
|
||||
.room(.4).delay(.125),
|
||||
note("c2 [eb3,g3]".add("<0 <1 -1>>"))
|
||||
.room(.4).delay(.125)
|
||||
$: note("c2 [eb3,g3]".add("<0 <1 -1>>"))
|
||||
.adsr("[.1 0]:.2:[1 0]")
|
||||
.sound("gm_acoustic_bass")
|
||||
.room(.5),
|
||||
n("0 1 [2 3] 2").sound("jazz").jux(rev)
|
||||
)`}
|
||||
.room(.5)
|
||||
$: n("0 1 [2 3] 2").sound("jazz").jux(rev)`}
|
||||
/>
|
||||
|
||||
**ply**
|
||||
@@ -165,7 +157,7 @@ off is also useful for modifying other sounds, and can even be nested:
|
||||
client:visible
|
||||
tune={`s("bd sd [rim bd] sd,[~ hh]*4").bank("CasioRZ1")
|
||||
.off(2/16, x=>x.speed(1.5).gain(.25)
|
||||
.off(3/16, y=>y.vowel("<a e i o>*8")))`}
|
||||
.off(3/16, y=>y.vowel("<a e i o>*8")))`}
|
||||
/>
|
||||
|
||||
| name | description | example |
|
||||
|
||||
@@ -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")`} /> |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
const blob = new Blob([uint8Array], { type: 'audio/*' });
|
||||
|
||||
@@ -12,7 +12,7 @@ export const userSamplesDBConfig = {
|
||||
};
|
||||
|
||||
// deletes all of the databases, useful for debugging
|
||||
const clearIDB = () => {
|
||||
function clearIDB() {
|
||||
window.indexedDB
|
||||
.databases()
|
||||
.then((r) => {
|
||||
@@ -21,54 +21,81 @@ const clearIDB = () => {
|
||||
.then(() => {
|
||||
alert('All data cleared.');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
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) => {
|
||||
const soundFiles = event.target.result;
|
||||
if (!soundFiles?.length) {
|
||||
return;
|
||||
}
|
||||
const sounds = new Map();
|
||||
[...soundFiles]
|
||||
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
||||
.forEach((soundFile) => {
|
||||
const title = soundFile.title;
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
}
|
||||
const splitRelativePath = soundFile.id.split('/');
|
||||
const parentDirectory =
|
||||
//fallback to file name before period and seperator if no parent directory
|
||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||
const soundPath = soundFile.blob;
|
||||
const soundPaths = sounds.get(parentDirectory) ?? new Set();
|
||||
soundPaths.add(soundPath);
|
||||
sounds.set(parentDirectory, soundPaths);
|
||||
});
|
||||
|
||||
sounds.forEach((soundPaths, key) => {
|
||||
const value = Array.from(soundPaths);
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
baseUrl: undefined,
|
||||
prebake: false,
|
||||
tag: undefined,
|
||||
Promise.all(
|
||||
[...soundFiles]
|
||||
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
||||
.map((soundFile, i) => {
|
||||
const title = soundFile.title;
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
}
|
||||
const splitRelativePath = soundFile.id.split('/');
|
||||
let parentDirectory =
|
||||
//fallback to file name before period and seperator if no parent directory
|
||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||
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();
|
||||
soundPaths.add(soundPath);
|
||||
sounds.set(parentDirectory, soundPaths);
|
||||
return;
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
sounds.forEach((soundPaths, key) => {
|
||||
const value = Array.from(soundPaths);
|
||||
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
baseUrl: undefined,
|
||||
prebake: false,
|
||||
tag: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
logger('imported sounds registered!', 'success');
|
||||
onComplete();
|
||||
})
|
||||
.catch((error) => {
|
||||
logger('Something went wrong while registering saved samples from the index db', 'error');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
logger('imported sounds registered!', 'success');
|
||||
onComplete();
|
||||
};
|
||||
});
|
||||
};
|
||||
// creates a blob from a buffer that can be read
|
||||
async function bufferToDataUrl(buf) {
|
||||
}
|
||||
|
||||
async function blobToDataUrl(blob) {
|
||||
return new Promise((resolve) => {
|
||||
var blob = new Blob([buf], { type: 'application/octet-binary' });
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
resolve(event.target.result);
|
||||
@@ -76,8 +103,9 @@ async function bufferToDataUrl(buf) {
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
//open db and initialize it if necessary
|
||||
const openDB = (config, onOpened) => {
|
||||
function openDB(config, onOpened) {
|
||||
const { dbName, version, table, columns } = config;
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -102,35 +130,32 @@ const openDB = (config, onOpened) => {
|
||||
|
||||
dbOpen.onsuccess = () => {
|
||||
const db = dbOpen.result;
|
||||
|
||||
const // lock store for writing
|
||||
writeTransaction = db.transaction([table], 'readwrite'),
|
||||
// get object store
|
||||
objectStore = writeTransaction.objectStore(table);
|
||||
// lock store for writing
|
||||
const writeTransaction = db.transaction([table], 'readwrite');
|
||||
// get object store
|
||||
const objectStore = writeTransaction.objectStore(table);
|
||||
onOpened(objectStore, db);
|
||||
};
|
||||
};
|
||||
return dbOpen;
|
||||
}
|
||||
|
||||
const processFilesForIDB = async (files) => {
|
||||
return await Promise.all(
|
||||
async function processFilesForIDB(files) {
|
||||
return Promise.all(
|
||||
Array.from(files)
|
||||
.map(async (s) => {
|
||||
const title = s.name;
|
||||
.map((file) => {
|
||||
const title = file.name;
|
||||
|
||||
if (!isAudioFile(title)) {
|
||||
return;
|
||||
}
|
||||
//create obscured url to file system that can be fetched
|
||||
const sUrl = URL.createObjectURL(s);
|
||||
//fetch the sound and turn it into a buffer array
|
||||
const buf = await fetch(sUrl).then((res) => res.arrayBuffer());
|
||||
//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;
|
||||
const path = file.webkitRelativePath;
|
||||
let id = path?.length ? path : title;
|
||||
if (id == null || title == null || file == null) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
title,
|
||||
blob: base64,
|
||||
blob: file,
|
||||
id,
|
||||
};
|
||||
})
|
||||
@@ -139,18 +164,22 @@ const processFilesForIDB = async (files) => {
|
||||
logger('Something went wrong while processing uploaded files', '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) => {
|
||||
logger('user samples processed... opening db');
|
||||
const onOpened = (objectStore, _db) => {
|
||||
logger('index db opened... writing files to db');
|
||||
files.forEach((file) => {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
objectStore.put(file);
|
||||
});
|
||||
logger('user samples written successfully');
|
||||
};
|
||||
openDB(config, onOpened);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function ImportSoundsButton({ onComplete }) {
|
||||
directory=""
|
||||
webkitdirectory=""
|
||||
multiple
|
||||
accept="audio/*, .wav, .mp3, .m4a, .flac"
|
||||
accept="audio/*, .wav, .mp3, .m4a, .flac, .aac, .ogg"
|
||||
onChange={() => {
|
||||
onChange();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user