Compare commits

...

38 Commits

Author SHA1 Message Date
Felix Roos b9933c610a really fix lint 2024-06-06 12:11:57 +02:00
Felix Roos 9d81a4d071 fix lint 2024-06-06 12:09:55 +02:00
Felix Roos 86cc9b87c3 looper prototype 2024-06-06 10:05:29 +02:00
Felix Roos 0df6f3e56e simplify idb import 2024-06-06 00:44:41 +02:00
Felix Roos c403a92542 Merge pull request #1126 from tidalcycles/labeled-statements-doc
Labeled statements doc
2024-06-04 00:26:48 +02:00
Felix Roos 269d6dde31 remove stack from workshop 2024-06-04 00:22:49 +02:00
Felix Roos 666c695c63 don't use labeled statements in example for now 2024-06-04 00:10:46 +02:00
Felix Roos 771eb31d3f use labels instead of stacks 2024-06-04 00:05:51 +02:00
Felix Roos c05bbf3422 don't clear hydra in minirepl for now 2024-06-03 23:53:08 +02:00
Felix Roos 364f511a7c Merge pull request #1125 from tidalcycles/viz-doc
doc: visual functions + refactor onPaint
2024-06-03 22:40:21 +02:00
Felix Roos 300022d8ac no custom line height 2024-06-03 20:34:51 +02:00
Felix Roos 357fbf8c82 snapshot 2024-06-02 23:14:32 +02:00
Felix Roos 1acb675f5a doc: visual functions 2024-06-02 23:12:31 +02:00
Jade (Rose) Rowland ab1086318c Merge pull request #1124 from daslyfe/bug_sampledb
Fix indexDB failing with large amount of files
2024-06-02 12:36:29 -04:00
Jade (Rose) Rowland 8c73ad088a support aac 2024-06-02 12:32:54 -04:00
Felix Roos 25981092b1 snapshot 2024-06-02 14:11:52 +02:00
Felix Roos e6b55baed0 fix: step based naming 2024-06-02 14:08:56 +02:00
Felix Roos 6adf1ff977 snapshot 2024-06-02 14:03:30 +02:00
Felix Roos b6e277f1ea use inline scope in examples 2024-06-02 14:03:05 +02:00
Felix Roos d0510f862a clear hydra in mini repl 2024-06-02 14:02:39 +02:00
Felix Roos 4a87df6426 small example tweaks 2024-06-02 14:00:47 +02:00
Felix Roos bf90867685 fix: mini repl punchcard + claviature flags 2024-06-02 10:49:07 +02:00
Felix Roos 3dc5fd0e7e fix: editPattern bug 2024-06-02 10:48:20 +02:00
Felix Roos a7650f1af3 add autodraw flag + scope 2024-06-02 03:08:16 +02:00
Felix Roos aadb2fee84 support onload inline viz with autodraw 2024-06-02 03:05:26 +02:00
Felix Roos bf391ad6ce simplify: painters are now handled by Drawer 2024-06-02 03:03:36 +02:00
Felix Roos faba4a4e4e implement onPaint with pattern state 2024-06-02 03:00:57 +02:00
Felix Roos 94e411aa7a add beforeStart callback 2024-06-02 02:34:33 +02:00
Jade (Rose) Rowland ffa21ddf0d remove console statement 2024-06-01 19:19:47 -04:00
Jade (Rose) Rowland 6b2d080afc remove unessecary change 2024-06-01 19:17:00 -04:00
Jade (Rose) Rowland 321a888921 add catch 2024-06-01 12:42:07 -04:00
Jade (Rose) Rowland bf7fe2bf75 fixed indexdb 2024-06-01 12:39:45 -04:00
Jade (Rose) Rowland 1fd9ba5dbf convert to blob 2024-06-01 12:11:28 -04:00
Jade (Rose) Rowland 1fca6f25b8 faster upload 2024-06-01 11:35:24 -04:00
Felix Roos 0e6a17fc77 start visual feedback article 2024-06-01 15:42:09 +02:00
Felix Roos 375c68775c make sure draw logic works with multiple repls 2024-06-01 15:41:55 +02:00
Felix Roos 827993a8c9 chore: simplify publish docs 2024-05-31 23:29:38 +02:00
Jade (Rose) Rowland e29876e7f6 debugging 2024-05-31 13:57:02 -04:00
37 changed files with 937 additions and 558 deletions
+3 -9
View File
@@ -130,20 +130,14 @@ npx lerna version --no-private
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
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!
+13 -26
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+14 -8
View File
@@ -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');
+16 -4
View File
@@ -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) {
+2 -2
View File
@@ -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;
}
+13 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+8 -3
View File
@@ -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 };
}
+17
View File
@@ -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) =>
+27
View File
@@ -125,6 +125,33 @@ function drawSpiral(options) {
});
}
/**
* Displays a spiral visual.
*
* @name spiral
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {number} stretch controls the rotations per cycle ratio, where 1 = 1 cycle / 360 degrees
* @param {number} size the diameter of the spiral
* @param {number} thickness line thickness
* @param {string} cap style of line ends: butt (default), round, square
* @param {string} inset number of rotations before spiral starts (default 3)
* @param {string} playheadColor color of playhead, defaults to white
* @param {number} playheadLength length of playhead in rotations, defaults to 0.02
* @param {number} playheadThickness thickness of playheadrotations, defaults to thickness
* @param {number} padding space around spiral
* @param {number} steady steadyness of spiral vs playhead. 1 = spiral doesn't move, playhead does.
* @param {number} activeColor color of active segment. defaults to foreground of theme
* @param {number} inactiveColor color of inactive segments. defaults to gutterForeground of theme
* @param {boolean} colorizeInactive wether or not to colorize inactive segments, defaults to 0
* @param {boolean} fade wether or not past and future should fade out. defaults to 1
* @param {boolean} logSpiral wether or not the spiral should be logarithmic. defaults to 0
* @example
* note("c2 a2 eb2")
* .euclid(5,8)
* .s('sawtooth')
* .lpenv(4).lpf(300)
* ._spiral({ steady: .96 })
*/
Pattern.prototype.spiral = function (options = {}) {
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
};
+2 -2
View File
@@ -3,8 +3,8 @@ import { getAudioContext, registerSound } from './index.mjs';
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { 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];
+54 -1
View File
@@ -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);
+45
View File
@@ -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);
+2 -1
View File
@@ -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) {
+1 -1
View File
@@ -130,7 +130,7 @@ Pattern.prototype.fscope = function (config = {}) {
* @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
* @param {number} 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 = {}) {
+330 -240
View File
@@ -2717,175 +2717,175 @@ exports[`runs examples > example "floor" example index 0 1`] = `
exports[`runs examples > example "fm" example index 0 1`] = `
[
"[ 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 ]",
+14
View File
@@ -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);
+1
View File
@@ -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' },
+22 -11
View File
@@ -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>
+15 -15
View File
@@ -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
+9 -7
View File
@@ -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}))`}
/>
+3 -9
View File
@@ -20,11 +20,7 @@ Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
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
+7 -9
View File
@@ -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
+6 -21
View File
@@ -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
+12 -11
View File
@@ -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)
`}
/>
-14
View File
@@ -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.
+100
View File
@@ -0,0 +1,100 @@
---
title: Visual Feedback
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# Visual Feedback
There are several function that add visual feedback to your patterns.
## Mini Notation Highlighting
When you write mini notation with "double quotes" or \`backticks\`, the active parts of the mini notation will be highlighted:
<MiniRepl
client:idle
tune={`n("<0 2 1 3 2>*8")
.scale("<A1 D2>/4:minor:pentatonic")
.s("supersaw").lpf(300).lpenv("<4 3 2>\*4")`}
/>
You can change the color as well, even pattern it:
<MiniRepl
client:idle
tune={`n("<0 2 1 3 2>*8")
.scale("<A1 D2>/4:minor:pentatonic")
.s("supersaw").lpf(300).lpenv("<4 3 2>*4")
.color("cyan magenta")`}
/>
## Global vs Inline Visuals
The following functions all come with in 2 variants.
**Without prefix**: renders the visual to the background of the page:
<MiniRepl client:idle tune={`note("c a f e").color("white").punchcard()`} />
**With `_` prefix**: renders the visual inside the code. Allows for multiple visuals
<MiniRepl client:idle tune={`note("c a f e").color("white")._punchcard()`} />
Here we see the 2 variants for `punchcard`. The same goes for all others below.
To improve readability the following demos will all use the inline variant.
## Punchcard / Pianoroll
These 2 functions render a pianoroll style visual.
The only difference between the 2 is that `pianoroll` will render the pattern directly,
while `punchcard` will also take the transformations into account that occur afterwards:
<MiniRepl
client:idle
tune={`note("c a f e").color("white")
._punchcard()
.color("cyan")`}
autodraw
/>
Here, the `color` is still visible in the visual, even if it is applied after `_punchcard`.
On the contrary, the color is not visible when using `_pianoroll`:
<MiniRepl
client:idle
tune={`note("c a f e").color("white")
._pianoroll()
.color("cyan")`}
autodraw
/>
import Box from '@components/Box.astro';
<br />
<Box>
`punchcard` is less resource intensive because it uses the same data as used for the mini notation highlighting.
</Box>
The visual can be customized by passing options. Those options are the same for both functions.
What follows is the API doc of all the options you can pass:
<JsDoc client:idle name="pianoroll" h={0} />
## Spiral
<JsDoc client:idle name="spiral" h={0} />
## Scope
<JsDoc client:idle name="scope" h={0} />
## Pitchwheel
<JsDoc client:idle name="pitchwheel" h={0} />
+35 -46
View File
@@ -60,11 +60,10 @@ We will learn how to automate with waves later...
<MiniRepl
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>
+24 -14
View File
@@ -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)
+13 -21
View File
@@ -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 |
+1 -1
View File
@@ -39,7 +39,7 @@ This page is just a listing of all functions covered in the workshop!
| --------- | ----------------------------- | --------------------------------------------------------------------------------- |
| note | set pitch as number or letter | <MiniRepl client:visible tune={`note("b g e c").sound("piano")`} /> |
| 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
+2 -1
View File
@@ -75,7 +75,8 @@ export const walkFileTree = (node, fn) => {
}
};
export const isAudioFile = (filename) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]);
export const isAudioFile = (filename) =>
['wav', 'mp3', 'flac', 'ogg', 'm4a', 'aac'].includes(filename.split('.').slice(-1)[0]);
function uint8ArrayToDataURL(uint8Array) {
const blob = new Blob([uint8Array], { type: 'audio/*' });
+87 -58
View File
@@ -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();
}}