mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Merge branch 'main' into oscillator_enhancements
This commit is contained in:
commit
ea58238453
@ -20,7 +20,8 @@ import { flash, isFlashEnabled } from './flash.mjs';
|
|||||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||||
import { keybindings } from './keybindings.mjs';
|
import { keybindings } from './keybindings.mjs';
|
||||||
import { initTheme, activateTheme, theme } from './themes.mjs';
|
import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||||
import { updateWidgets, sliderPlugin } from './slider.mjs';
|
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
|
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
|
|
||||||
const extensions = {
|
const extensions = {
|
||||||
@ -72,6 +73,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
...initialSettings,
|
...initialSettings,
|
||||||
javascript(),
|
javascript(),
|
||||||
sliderPlugin,
|
sliderPlugin,
|
||||||
|
widgetPlugin,
|
||||||
// indentOnInput(), // works without. already brought with javascript extension?
|
// indentOnInput(), // works without. already brought with javascript extension?
|
||||||
// bracketMatching(), // does not do anything
|
// bracketMatching(), // does not do anything
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
@ -187,7 +189,10 @@ export class StrudelMirror {
|
|||||||
// remember for when highlighting is toggled on
|
// remember for when highlighting is toggled on
|
||||||
this.miniLocations = options.meta?.miniLocations;
|
this.miniLocations = options.meta?.miniLocations;
|
||||||
this.widgets = options.meta?.widgets;
|
this.widgets = options.meta?.widgets;
|
||||||
updateWidgets(this.editor, this.widgets);
|
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
||||||
|
updateSliderWidgets(this.editor, sliders);
|
||||||
|
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
||||||
|
updateWidgets(this.editor, widgets);
|
||||||
updateMiniLocations(this.editor, this.miniLocations);
|
updateMiniLocations(this.editor, this.miniLocations);
|
||||||
replOptions?.afterEval?.(options);
|
replOptions?.afterEval?.(options);
|
||||||
this.adjustDrawTime();
|
this.adjustDrawTime();
|
||||||
|
|||||||
@ -3,3 +3,4 @@ export * from './highlight.mjs';
|
|||||||
export * from './flash.mjs';
|
export * from './flash.mjs';
|
||||||
export * from './slider.mjs';
|
export * from './slider.mjs';
|
||||||
export * from './themes.mjs';
|
export * from './themes.mjs';
|
||||||
|
export * from './widget.mjs';
|
||||||
|
|||||||
@ -46,6 +46,7 @@
|
|||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@uiw/codemirror-themes": "^4.21.21",
|
"@uiw/codemirror-themes": "^4.21.21",
|
||||||
"@uiw/codemirror-themes-all": "^4.21.21",
|
"@uiw/codemirror-themes-all": "^4.21.21",
|
||||||
"nanostores": "^0.9.5"
|
"nanostores": "^0.9.5"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ref, pure } from '@strudel/core';
|
import { ref, pure } from '@strudel/core';
|
||||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||||
import { StateEffect, StateField } from '@codemirror/state';
|
import { StateEffect } from '@codemirror/state';
|
||||||
|
|
||||||
export let sliderValues = {};
|
export let sliderValues = {};
|
||||||
const getSliderID = (from) => `slider_${from}`;
|
const getSliderID = (from) => `slider_${from}`;
|
||||||
@ -60,19 +60,21 @@ export class SliderWidget extends WidgetType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setWidgets = StateEffect.define();
|
export const setSliderWidgets = StateEffect.define();
|
||||||
|
|
||||||
export const updateWidgets = (view, widgets) => {
|
export const updateSliderWidgets = (view, widgets) => {
|
||||||
view.dispatch({ effects: setWidgets.of(widgets) });
|
view.dispatch({ effects: setSliderWidgets.of(widgets) });
|
||||||
};
|
};
|
||||||
|
|
||||||
function getWidgets(widgetConfigs, view) {
|
function getSliders(widgetConfigs, view) {
|
||||||
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
|
return widgetConfigs
|
||||||
return Decoration.widget({
|
.filter((w) => w.type === 'slider')
|
||||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
.map(({ from, to, value, min, max, step }) => {
|
||||||
side: 0,
|
return Decoration.widget({
|
||||||
}).range(from /* , to */);
|
widget: new SliderWidget(value, min, max, from, to, step, view),
|
||||||
});
|
side: 0,
|
||||||
|
}).range(from /* , to */);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sliderPlugin = ViewPlugin.fromClass(
|
export const sliderPlugin = ViewPlugin.fromClass(
|
||||||
@ -99,8 +101,8 @@ export const sliderPlugin = ViewPlugin.fromClass(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let e of tr.effects) {
|
for (let e of tr.effects) {
|
||||||
if (e.is(setWidgets)) {
|
if (e.is(setSliderWidgets)) {
|
||||||
this.decorations = Decoration.set(getWidgets(e.value, update.view));
|
this.decorations = Decoration.set(getSliders(e.value, update.view));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
1
packages/codemirror/themes/algoboy.mjs
vendored
1
packages/codemirror/themes/algoboy.mjs
vendored
@ -18,6 +18,7 @@ export default createTheme({
|
|||||||
theme: 'light',
|
theme: 'light',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: '#0f380f' },
|
||||||
{ tag: t.keyword, color: '#0f380f' },
|
{ tag: t.keyword, color: '#0f380f' },
|
||||||
{ tag: t.operator, color: '#0f380f' },
|
{ tag: t.operator, color: '#0f380f' },
|
||||||
{ tag: t.special(t.variableName), color: '#0f380f' },
|
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||||
|
|||||||
1
packages/codemirror/themes/blackscreen.mjs
vendored
1
packages/codemirror/themes/blackscreen.mjs
vendored
@ -15,6 +15,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: 'white' },
|
||||||
{ tag: t.keyword, color: 'white' },
|
{ tag: t.keyword, color: 'white' },
|
||||||
{ tag: t.operator, color: 'white' },
|
{ tag: t.operator, color: 'white' },
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
|||||||
1
packages/codemirror/themes/bluescreen.mjs
vendored
1
packages/codemirror/themes/bluescreen.mjs
vendored
@ -18,6 +18,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: 'white' },
|
||||||
{ tag: t.keyword, color: 'white' },
|
{ tag: t.keyword, color: 'white' },
|
||||||
{ tag: t.operator, color: 'white' },
|
{ tag: t.operator, color: 'white' },
|
||||||
{ tag: t.special(t.variableName), color: 'white' },
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
|||||||
1
packages/codemirror/themes/strudel-theme.mjs
vendored
1
packages/codemirror/themes/strudel-theme.mjs
vendored
@ -15,6 +15,7 @@ export default createTheme({
|
|||||||
gutterForeground: '#8a919966',
|
gutterForeground: '#8a919966',
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: '#89ddff' },
|
||||||
{ tag: t.keyword, color: '#c792ea' },
|
{ tag: t.keyword, color: '#c792ea' },
|
||||||
{ tag: t.operator, color: '#89ddff' },
|
{ tag: t.operator, color: '#89ddff' },
|
||||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||||
|
|||||||
1
packages/codemirror/themes/teletext.mjs
vendored
1
packages/codemirror/themes/teletext.mjs
vendored
@ -27,6 +27,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: colorB },
|
||||||
{ tag: t.keyword, color: colorA },
|
{ tag: t.keyword, color: colorA },
|
||||||
{ tag: t.operator, color: mini },
|
{ tag: t.operator, color: mini },
|
||||||
{ tag: t.special(t.variableName), color: colorA },
|
{ tag: t.special(t.variableName), color: colorA },
|
||||||
|
|||||||
1
packages/codemirror/themes/terminal.mjs
vendored
1
packages/codemirror/themes/terminal.mjs
vendored
@ -14,6 +14,7 @@ export default createTheme({
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: '#41FF00' },
|
||||||
{ tag: t.keyword, color: '#41FF00' },
|
{ tag: t.keyword, color: '#41FF00' },
|
||||||
{ tag: t.operator, color: '#41FF00' },
|
{ tag: t.operator, color: '#41FF00' },
|
||||||
{ tag: t.special(t.variableName), color: '#41FF00' },
|
{ tag: t.special(t.variableName), color: '#41FF00' },
|
||||||
|
|||||||
1
packages/codemirror/themes/whitescreen.mjs
vendored
1
packages/codemirror/themes/whitescreen.mjs
vendored
@ -16,6 +16,7 @@ export default createTheme({
|
|||||||
theme: 'light',
|
theme: 'light',
|
||||||
settings,
|
settings,
|
||||||
styles: [
|
styles: [
|
||||||
|
{ tag: t.labelName, color: 'black' },
|
||||||
{ tag: t.keyword, color: 'black' },
|
{ tag: t.keyword, color: 'black' },
|
||||||
{ tag: t.operator, color: 'black' },
|
{ tag: t.operator, color: 'black' },
|
||||||
{ tag: t.special(t.variableName), color: 'black' },
|
{ tag: t.special(t.variableName), color: 'black' },
|
||||||
|
|||||||
122
packages/codemirror/widget.mjs
Normal file
122
packages/codemirror/widget.mjs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { StateEffect, StateField } from '@codemirror/state';
|
||||||
|
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
||||||
|
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
|
||||||
|
import { Pattern } from '@strudel/core';
|
||||||
|
|
||||||
|
export const addWidget = StateEffect.define({
|
||||||
|
map: ({ from, to }, change) => {
|
||||||
|
return { from: change.mapPos(from), to: change.mapPos(to) };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateWidgets = (view, widgets) => {
|
||||||
|
view.dispatch({ effects: addWidget.of(widgets) });
|
||||||
|
};
|
||||||
|
|
||||||
|
function getWidgets(widgetConfigs) {
|
||||||
|
return (
|
||||||
|
widgetConfigs
|
||||||
|
// codemirror throws an error if we don't sort
|
||||||
|
.sort((a, b) => a.to - b.to)
|
||||||
|
.map((widgetConfig) => {
|
||||||
|
return Decoration.widget({
|
||||||
|
widget: new BlockWidget(widgetConfig),
|
||||||
|
side: 0,
|
||||||
|
block: true,
|
||||||
|
}).range(widgetConfig.to);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetField = StateField.define(
|
||||||
|
/* <DecorationSet> */ {
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(widgets, tr) {
|
||||||
|
widgets = widgets.map(tr.changes);
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(addWidget)) {
|
||||||
|
try {
|
||||||
|
widgets = widgets.update({
|
||||||
|
filter: () => false,
|
||||||
|
add: getWidgets(e.value),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('err', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widgets;
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const widgetElements = {};
|
||||||
|
export function setWidget(id, el) {
|
||||||
|
widgetElements[id] = el;
|
||||||
|
el.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BlockWidget extends WidgetType {
|
||||||
|
constructor(widgetConfig) {
|
||||||
|
super();
|
||||||
|
this.widgetConfig = widgetConfig;
|
||||||
|
}
|
||||||
|
eq() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toDOM() {
|
||||||
|
const id = getWidgetID(this.widgetConfig);
|
||||||
|
const el = widgetElements[id];
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
ignoreEvent(e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const widgetPlugin = [widgetField];
|
||||||
|
|
||||||
|
// widget implementer API to create a new widget type
|
||||||
|
export function registerWidget(type, fn) {
|
||||||
|
registerWidgetType(type);
|
||||||
|
if (fn) {
|
||||||
|
Pattern.prototype[type] = function (id, options = { fold: 1 }) {
|
||||||
|
// fn is expected to create a dom element and call setWidget(id, el);
|
||||||
|
// fn should also return the pattern
|
||||||
|
return fn(id, options, this);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wire up @strudel/draw functions
|
||||||
|
|
||||||
|
function getCanvasWidget(id, options = {}) {
|
||||||
|
const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
|
||||||
|
let canvas = document.getElementById(id) || document.createElement('canvas');
|
||||||
|
canvas.width = width * pixelRatio;
|
||||||
|
canvas.height = height * pixelRatio;
|
||||||
|
canvas.style.width = width + 'px';
|
||||||
|
canvas.style.height = height + 'px';
|
||||||
|
setWidget(id, canvas);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWidget('_pianoroll', (id, options = {}, pat) => {
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.pianoroll({ fold: 1, ...options, ctx, id });
|
||||||
|
});
|
||||||
|
|
||||||
|
/* registerWidget('_spiral', (id, options = {}, pat) => {
|
||||||
|
options = { width: 200, height: 200, size: 36, ...options };
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.spiral({ ...options, ctx, id });
|
||||||
|
}); */
|
||||||
|
|
||||||
|
registerWidget('_scope', (id, options = {}, pat) => {
|
||||||
|
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
|
||||||
|
const ctx = getCanvasWidget(id, options).getContext('2d');
|
||||||
|
return pat.scope({ ...options, ctx, id });
|
||||||
|
});
|
||||||
104
packages/core/clockworker.js
Normal file
104
packages/core/clockworker.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
importScripts('./neozyklus.js');
|
||||||
|
// TODO: swap below line with above one when firefox supports esm imports in service workers
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility
|
||||||
|
// import createClock from './zyklus.mjs';
|
||||||
|
|
||||||
|
function getTime() {
|
||||||
|
const precision = 10 ** 4;
|
||||||
|
const seconds = performance.now() / 1000;
|
||||||
|
return Math.round(seconds * precision) / precision;
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_cycles_at_cps_change = 0;
|
||||||
|
let num_ticks_since_cps_change = 0;
|
||||||
|
let cps = 0.5;
|
||||||
|
// {id: {started: boolean}}
|
||||||
|
const clients = new Map();
|
||||||
|
const duration = 0.1;
|
||||||
|
const channel = new BroadcastChannel('strudeltick');
|
||||||
|
|
||||||
|
const sendMessage = (type, payload) => {
|
||||||
|
channel.postMessage({ type, payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendTick = (phase, duration, tick, time) => {
|
||||||
|
sendMessage('tick', {
|
||||||
|
phase,
|
||||||
|
duration,
|
||||||
|
time,
|
||||||
|
cps,
|
||||||
|
num_cycles_at_cps_change,
|
||||||
|
num_ticks_since_cps_change,
|
||||||
|
});
|
||||||
|
num_ticks_since_cps_change++;
|
||||||
|
};
|
||||||
|
|
||||||
|
//create clock method from zyklus
|
||||||
|
const clock = this.createClock(getTime, sendTick, duration);
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
const startClock = (id) => {
|
||||||
|
clients.set(id, { started: true });
|
||||||
|
if (started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clock.start();
|
||||||
|
started = true;
|
||||||
|
};
|
||||||
|
const stopClock = async (id) => {
|
||||||
|
clients.set(id, { started: false });
|
||||||
|
|
||||||
|
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
|
||||||
|
//dont stop the clock if other instances are running...
|
||||||
|
if (!started || otherClientStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clock.stop();
|
||||||
|
setCycle(0);
|
||||||
|
started = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCycle = (cycle) => {
|
||||||
|
num_ticks_since_cps_change = 0;
|
||||||
|
num_cycles_at_cps_change = cycle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processMessage = (message) => {
|
||||||
|
const { type, payload } = message;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'cpschange': {
|
||||||
|
if (payload.cps !== cps) {
|
||||||
|
num_cycles_at_cps_change = num_cycles_at_cps_change + num_ticks_since_cps_change * duration * cps;
|
||||||
|
cps = payload.cps;
|
||||||
|
num_ticks_since_cps_change = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'setcycle': {
|
||||||
|
setCycle(payload.cycle);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'toggle': {
|
||||||
|
if (payload.started) {
|
||||||
|
startClock(message.id);
|
||||||
|
} else {
|
||||||
|
stopClock(message.id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.onconnect = function (e) {
|
||||||
|
// the incoming port
|
||||||
|
const port = e.ports[0];
|
||||||
|
|
||||||
|
port.addEventListener('message', function (e) {
|
||||||
|
console.log(e.data);
|
||||||
|
processMessage(e.data);
|
||||||
|
});
|
||||||
|
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
cyclist.mjs - <short description TODO>
|
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
@ -16,7 +16,7 @@ export class Cyclist {
|
|||||||
this.lastBegin = 0; // query begin of last tick
|
this.lastBegin = 0; // query begin of last tick
|
||||||
this.lastEnd = 0; // query end of last tick
|
this.lastEnd = 0; // query end of last tick
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
this.num_cycles_since_last_cps_change = 0;
|
this.num_cycles_at_cps_change = 0;
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = latency; // fixed trigger time offset
|
this.latency = latency; // fixed trigger time offset
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
@ -27,7 +27,7 @@ export class Cyclist {
|
|||||||
this.origin = phase;
|
this.origin = phase;
|
||||||
}
|
}
|
||||||
if (this.num_ticks_since_cps_change === 0) {
|
if (this.num_ticks_since_cps_change === 0) {
|
||||||
this.num_cycles_since_last_cps_change = this.lastEnd;
|
this.num_cycles_at_cps_change = this.lastEnd;
|
||||||
}
|
}
|
||||||
this.num_ticks_since_cps_change++;
|
this.num_ticks_since_cps_change++;
|
||||||
try {
|
try {
|
||||||
@ -37,7 +37,7 @@ export class Cyclist {
|
|||||||
|
|
||||||
//convert ticks to cycles, so you can query the pattern for events
|
//convert ticks to cycles, so you can query the pattern for events
|
||||||
const eventLength = duration * this.cps;
|
const eventLength = duration * this.cps;
|
||||||
const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
|
const end = this.num_cycles_at_cps_change + this.num_ticks_since_cps_change * eventLength;
|
||||||
this.lastEnd = end;
|
this.lastEnd = end;
|
||||||
|
|
||||||
// query the pattern for events
|
// query the pattern for events
|
||||||
@ -71,7 +71,7 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
this.num_ticks_since_cps_change = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
this.num_cycles_since_last_cps_change = 0;
|
this.num_cycles_at_cps_change = 0;
|
||||||
if (!this.pattern) {
|
if (!this.pattern) {
|
||||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,11 @@ Fraction.prototype.max = function (other) {
|
|||||||
return this.gt(other) ? this : other;
|
return this.gt(other) ? this : other;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Fraction.prototype.maximum = function (...others) {
|
||||||
|
others = others.map((x) => new Fraction(x));
|
||||||
|
return others.reduce((max, other) => other.max(max), this);
|
||||||
|
};
|
||||||
|
|
||||||
Fraction.prototype.min = function (other) {
|
Fraction.prototype.min = function (other) {
|
||||||
return this.lt(other) ? this : other;
|
return this.lt(other) ? this : other;
|
||||||
};
|
};
|
||||||
@ -83,6 +88,10 @@ export const gcd = (...fractions) => {
|
|||||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const lcm = (...fractions) => {
|
||||||
|
return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1));
|
||||||
|
};
|
||||||
|
|
||||||
fraction._original = Fraction;
|
fraction._original = Fraction;
|
||||||
|
|
||||||
export default fraction;
|
export default fraction;
|
||||||
|
|||||||
148
packages/core/neocyclist.mjs
Normal file
148
packages/core/neocyclist.mjs
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
export class NeoCyclist {
|
||||||
|
constructor({ onTrigger, onToggle, getTime }) {
|
||||||
|
this.started = false;
|
||||||
|
this.cps = 0.5;
|
||||||
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
|
this.getTime = getTime; // get absolute time
|
||||||
|
|
||||||
|
this.num_cycles_at_cps_change = 0;
|
||||||
|
this.onToggle = onToggle;
|
||||||
|
this.latency = 0.1; // fixed trigger time offset
|
||||||
|
this.cycle = 0;
|
||||||
|
this.id = Math.round(Date.now() * Math.random());
|
||||||
|
|
||||||
|
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
|
||||||
|
this.worker.port.start();
|
||||||
|
|
||||||
|
this.channel = new BroadcastChannel('strudeltick');
|
||||||
|
let worker_time_dif = 0; // time difference between audio context clock and worker clock
|
||||||
|
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
|
||||||
|
const maxWeight = 20;
|
||||||
|
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
|
||||||
|
|
||||||
|
// the clock of the worker and the audio context clock can drift apart over time
|
||||||
|
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
|
||||||
|
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
|
||||||
|
// in order to schedule events consistently.
|
||||||
|
const setTimeReference = (time, workertime) => {
|
||||||
|
const time_dif = workertime - time;
|
||||||
|
if (worker_time_dif === 0) {
|
||||||
|
worker_time_dif = time_dif;
|
||||||
|
} else {
|
||||||
|
const w = 1; //weight of new time diff;
|
||||||
|
const new_dif = Math.round(((worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
|
||||||
|
|
||||||
|
if (new_dif != worker_time_dif) {
|
||||||
|
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
|
||||||
|
weight = 4;
|
||||||
|
}
|
||||||
|
worker_time_dif = new_dif;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTickDeadline = (phase, time) => {
|
||||||
|
return phase - time - worker_time_dif;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tickCallback = (payload) => {
|
||||||
|
const workertime = payload.time;
|
||||||
|
const time = this.getTime();
|
||||||
|
|
||||||
|
const { duration, phase, num_ticks_since_cps_change, num_cycles_at_cps_change, cps } = payload;
|
||||||
|
setTimeReference(time, workertime);
|
||||||
|
this.cps = cps;
|
||||||
|
|
||||||
|
//calculate begin and end
|
||||||
|
const eventLength = duration * cps;
|
||||||
|
const num_cycles_since_cps_change = num_ticks_since_cps_change * eventLength;
|
||||||
|
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
|
||||||
|
const tickdeadline = getTickDeadline(phase, time);
|
||||||
|
const end = begin + eventLength;
|
||||||
|
|
||||||
|
//calculate current cycle
|
||||||
|
const lastTick = time + tickdeadline;
|
||||||
|
const secondsSinceLastTick = time - lastTick - duration;
|
||||||
|
this.cycle = begin + secondsSinceLastTick * cps;
|
||||||
|
|
||||||
|
//set the weight of average time diff and processs haps
|
||||||
|
weight = Math.min(weight + 1, maxWeight);
|
||||||
|
processHaps(begin, end, tickdeadline);
|
||||||
|
this.time_at_last_tick_message = this.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
const processHaps = (begin, end, tickdeadline) => {
|
||||||
|
if (this.started === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||||
|
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
|
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + this.latency;
|
||||||
|
const duration = hap.duration / this.cps;
|
||||||
|
onTrigger?.(hap, deadline, duration, this.cps);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// receive messages from worker clock and process them
|
||||||
|
this.channel.onmessage = (message) => {
|
||||||
|
if (!this.started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { payload, type } = message.data;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'tick': {
|
||||||
|
tickCallback(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
sendMessage(type, payload) {
|
||||||
|
this.worker.port.postMessage({ type, payload, id: this.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
now() {
|
||||||
|
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
|
||||||
|
return this.cycle + gap;
|
||||||
|
}
|
||||||
|
setCps(cps = 1) {
|
||||||
|
this.sendMessage('cpschange', { cps });
|
||||||
|
}
|
||||||
|
setCycle(cycle) {
|
||||||
|
this.sendMessage('setcycle', { cycle });
|
||||||
|
}
|
||||||
|
setStarted(started) {
|
||||||
|
this.sendMessage('toggle', { started });
|
||||||
|
this.started = started;
|
||||||
|
this.onToggle?.(started);
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
logger('[cyclist] start');
|
||||||
|
this.setStarted(true);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
logger('[cyclist] stop');
|
||||||
|
this.setStarted(false);
|
||||||
|
}
|
||||||
|
setPattern(pat, autostart = false) {
|
||||||
|
this.pattern = pat;
|
||||||
|
if (autostart && !this.started) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(begin, end, haps) {
|
||||||
|
const onsets = haps.filter((h) => h.hasOnset());
|
||||||
|
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
packages/core/neozyklus.js
Normal file
46
packages/core/neozyklus.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// used to consistently schedule events, for use in a service worker - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
|
||||||
|
this.createClock = (
|
||||||
|
getTime,
|
||||||
|
callback, // called slightly before each cycle
|
||||||
|
duration = 0.05, // duration of each cycle
|
||||||
|
interval = 0.1, // interval between callbacks
|
||||||
|
overlap = 0.1, // overlap between callbacks
|
||||||
|
) => {
|
||||||
|
let tick = 0; // counts callbacks
|
||||||
|
let phase = 0; // next callback time
|
||||||
|
let precision = 10 ** 4; // used to round phase
|
||||||
|
let minLatency = 0.01;
|
||||||
|
const setDuration = (setter) => (duration = setter(duration));
|
||||||
|
overlap = overlap || interval / 2;
|
||||||
|
const onTick = () => {
|
||||||
|
const t = getTime();
|
||||||
|
const lookahead = t + interval + overlap; // the time window for this tick
|
||||||
|
if (phase === 0) {
|
||||||
|
phase = t + minLatency;
|
||||||
|
}
|
||||||
|
// callback as long as we're inside the lookahead
|
||||||
|
while (phase < lookahead) {
|
||||||
|
phase = Math.round(phase * precision) / precision;
|
||||||
|
phase >= t && callback(phase, duration, tick, t);
|
||||||
|
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||||
|
phase += duration; // increment phase by duration
|
||||||
|
tick++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let intervalID;
|
||||||
|
const start = () => {
|
||||||
|
clear(); // just in case start was called more than once
|
||||||
|
onTick();
|
||||||
|
intervalID = setInterval(onTick, interval * 1000);
|
||||||
|
};
|
||||||
|
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||||
|
const pause = () => clear();
|
||||||
|
const stop = () => {
|
||||||
|
tick = 0;
|
||||||
|
phase = 0;
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
const getPhase = () => phase;
|
||||||
|
// setCallback
|
||||||
|
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||||
|
};
|
||||||
@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import TimeSpan from './timespan.mjs';
|
import TimeSpan from './timespan.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction, { lcm } from './fraction.mjs';
|
||||||
import Hap from './hap.mjs';
|
import Hap from './hap.mjs';
|
||||||
import State from './state.mjs';
|
import State from './state.mjs';
|
||||||
import { unionWithObj } from './value.mjs';
|
import { unionWithObj } from './value.mjs';
|
||||||
@ -29,9 +29,23 @@ export class Pattern {
|
|||||||
* @param {function} query - The function that maps a `State` to an array of `Hap`.
|
* @param {function} query - The function that maps a `State` to an array of `Hap`.
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
constructor(query) {
|
constructor(query, weight = undefined) {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
|
this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern
|
||||||
|
this.__weight = weight; // in terms of number of beats per cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
get weight() {
|
||||||
|
return this.__weight ?? Fraction(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
set weight(weight) {
|
||||||
|
this.__weight = Fraction(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
setWeight(weight) {
|
||||||
|
this.weight = weight;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@ -47,7 +61,9 @@ export class Pattern {
|
|||||||
* "0 1 2".withValue(v => v + 10).log()
|
* "0 1 2".withValue(v => v + 10).log()
|
||||||
*/
|
*/
|
||||||
withValue(func) {
|
withValue(func) {
|
||||||
return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
|
const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
|
||||||
|
result.weight = this.weight;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,6 +120,8 @@ export class Pattern {
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
appBoth(pat_val) {
|
appBoth(pat_val) {
|
||||||
|
const pat_func = this;
|
||||||
|
|
||||||
// Tidal's <*>
|
// Tidal's <*>
|
||||||
const whole_func = function (span_a, span_b) {
|
const whole_func = function (span_a, span_b) {
|
||||||
if (span_a == undefined || span_b == undefined) {
|
if (span_a == undefined || span_b == undefined) {
|
||||||
@ -111,7 +129,9 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
return span_a.intersection_e(span_b);
|
return span_a.intersection_e(span_b);
|
||||||
};
|
};
|
||||||
return this.appWhole(whole_func, pat_val);
|
const result = pat_func.appWhole(whole_func, pat_val);
|
||||||
|
result.weight = lcm(pat_val.weight, pat_func.weight);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,7 +164,9 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
return haps;
|
return haps;
|
||||||
};
|
};
|
||||||
return new Pattern(query);
|
const result = new Pattern(query);
|
||||||
|
result.weight = this.weight;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,7 +197,9 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
return haps;
|
return haps;
|
||||||
};
|
};
|
||||||
return new Pattern(query);
|
const result = new Pattern(query);
|
||||||
|
result.weight = pat_val.weight;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindWhole(choose_whole, func) {
|
bindWhole(choose_whole, func) {
|
||||||
@ -427,7 +451,9 @@ export class Pattern {
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withHaps(func) {
|
withHaps(func) {
|
||||||
return new Pattern((state) => func(this.query(state), state));
|
const result = new Pattern((state) => func(this.query(state), state));
|
||||||
|
result.weight = this.weight;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -457,7 +483,12 @@ export class Pattern {
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withContext(func) {
|
withContext(func) {
|
||||||
return this.withHap((hap) => hap.setContext(func(hap.context)));
|
const result = this.withHap((hap) => hap.setContext(func(hap.context)));
|
||||||
|
if (this.__pure !== undefined) {
|
||||||
|
result.__pure = this.__pure;
|
||||||
|
result.__pure_loc = this.__pure_loc;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -482,10 +513,15 @@ export class Pattern {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
};
|
};
|
||||||
return this.withContext((context) => {
|
const result = this.withContext((context) => {
|
||||||
const locations = (context.locations || []).concat([location]);
|
const locations = (context.locations || []).concat([location]);
|
||||||
return { ...context, locations };
|
return { ...context, locations };
|
||||||
});
|
});
|
||||||
|
if (this.__pure) {
|
||||||
|
result.__pure = this.__pure;
|
||||||
|
result.__pure_loc = location;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1123,13 +1159,25 @@ Pattern.prototype.factories = {
|
|||||||
|
|
||||||
// Elemental patterns
|
// Elemental patterns
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does absolutely nothing, with a given metrical 'weight'
|
||||||
|
* @name gap
|
||||||
|
* @param {number} weight
|
||||||
|
* @example
|
||||||
|
* gap(3) // "~@3"
|
||||||
|
*/
|
||||||
|
export const gap = (weight) => new Pattern(() => [], Fraction(weight));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does absolutely nothing..
|
* Does absolutely nothing..
|
||||||
* @name silence
|
* @name silence
|
||||||
* @example
|
* @example
|
||||||
* silence // "~"
|
* silence // "~"
|
||||||
*/
|
*/
|
||||||
export const silence = new Pattern(() => []);
|
export const silence = gap(1);
|
||||||
|
|
||||||
|
/* Like silence, but with a 'weight' (relative duration) of 0 */
|
||||||
|
export const nothing = gap(0);
|
||||||
|
|
||||||
/** A discrete value that repeats once per cycle.
|
/** A discrete value that repeats once per cycle.
|
||||||
*
|
*
|
||||||
@ -1142,7 +1190,9 @@ export function pure(value) {
|
|||||||
function query(state) {
|
function query(state) {
|
||||||
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
|
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
|
||||||
}
|
}
|
||||||
return new Pattern(query);
|
const result = new Pattern(query, 1);
|
||||||
|
result.__pure = value;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPattern(thing) {
|
export function isPattern(thing) {
|
||||||
@ -1184,7 +1234,67 @@ export function stack(...pats) {
|
|||||||
// Array test here is to avoid infinite recursions..
|
// Array test here is to avoid infinite recursions..
|
||||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||||
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
|
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
|
||||||
return new Pattern(query);
|
const result = new Pattern(query);
|
||||||
|
result.weight = lcm(...pats.map((pat) => pat.weight));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stackWith(func, pats) {
|
||||||
|
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||||
|
if (pats.length === 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
if (pats.length === 1) {
|
||||||
|
return pats[0];
|
||||||
|
}
|
||||||
|
const [left, ...right] = pats.map((pat) => pat.weight);
|
||||||
|
const weight = left.maximum(...right);
|
||||||
|
return stack(...func(weight, pats));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stackLeft(...pats) {
|
||||||
|
return _stackWith(
|
||||||
|
(weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(pat, gap(weight.sub(pat.weight))))),
|
||||||
|
pats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stackRight(...pats) {
|
||||||
|
return _stackWith(
|
||||||
|
(weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(gap(weight.sub(pat.weight)), pat))),
|
||||||
|
pats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stackCentre(...pats) {
|
||||||
|
return _stackWith(
|
||||||
|
(weight, pats) =>
|
||||||
|
pats.map((pat) => {
|
||||||
|
if (pat.weight.eq(weight)) {
|
||||||
|
return pat;
|
||||||
|
}
|
||||||
|
const g = gap(weight.sub(pat.weight).div(2));
|
||||||
|
return timeCat(g, pat, g);
|
||||||
|
}),
|
||||||
|
pats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stackBy(by, ...pats) {
|
||||||
|
const [left, ...right] = pats.map((pat) => pat.weight);
|
||||||
|
const weight = left.maximum(...right);
|
||||||
|
const lookup = {
|
||||||
|
centre: stackCentre,
|
||||||
|
left: stackLeft,
|
||||||
|
right: stackRight,
|
||||||
|
expand: stack,
|
||||||
|
beat: (...args) => polymeterSteps(weight, ...args),
|
||||||
|
};
|
||||||
|
return by
|
||||||
|
.inhabit(lookup)
|
||||||
|
.fmap((func) => func(...pats))
|
||||||
|
.innerJoin()
|
||||||
|
.setWeight(weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||||
@ -1198,7 +1308,11 @@ export function stack(...pats) {
|
|||||||
*/
|
*/
|
||||||
export function slowcat(...pats) {
|
export function slowcat(...pats) {
|
||||||
// Array test here is to avoid infinite recursions..
|
// Array test here is to avoid infinite recursions..
|
||||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));
|
||||||
|
|
||||||
|
if (pats.length == 1) {
|
||||||
|
return pats[0];
|
||||||
|
}
|
||||||
|
|
||||||
const query = function (state) {
|
const query = function (state) {
|
||||||
const span = state.span;
|
const span = state.span;
|
||||||
@ -1245,13 +1359,29 @@ export function cat(...pats) {
|
|||||||
return slowcat(...pats);
|
return slowcat(...pats);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Like `seq`, but each step has a length, relative to the whole.
|
/** Sequences patterns like `seq`, but each pattern has a length, relative to the whole.
|
||||||
|
* This length can either be provided as a [length, pattern] pair, or inferred from
|
||||||
|
* mininotation as the number of toplevel steps. The latter only works if the mininotation
|
||||||
|
* hasn't first been modified by another function.
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* timeCat([3,"e3"],[1, "g3"]).note()
|
* timeCat([3,"e3"],[1, "g3"]).note()
|
||||||
* // "e3@3 g3".note()
|
* // the same as "e3@3 g3".note()
|
||||||
|
* @example
|
||||||
|
* timeCat("bd sd cp","hh hh").sound()
|
||||||
|
* // the same as "bd sd cp hh hh".sound()
|
||||||
*/
|
*/
|
||||||
export function timeCat(...timepats) {
|
export function timeCat(...timepats) {
|
||||||
|
// Weights may either be provided explicitly in [weight, pattern] pairs, or
|
||||||
|
// where possible, inferred from the pattern.
|
||||||
|
const findWeight = (x) => (Array.isArray(x) ? x : [x.weight, x]);
|
||||||
|
timepats = timepats.map(findWeight);
|
||||||
|
if (timepats.length == 1) {
|
||||||
|
const result = reify(timepats[0][1]);
|
||||||
|
result.weight = timepats[0][0];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||||
let begin = Fraction(0);
|
let begin = Fraction(0);
|
||||||
const pats = [];
|
const pats = [];
|
||||||
@ -1260,7 +1390,9 @@ export function timeCat(...timepats) {
|
|||||||
pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
|
pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
|
||||||
begin = end;
|
begin = end;
|
||||||
}
|
}
|
||||||
return stack(...pats);
|
const result = stack(...pats);
|
||||||
|
result.weight = total;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1281,7 +1413,35 @@ export function arrange(...sections) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fastcat(...pats) {
|
export function fastcat(...pats) {
|
||||||
return slowcat(...pats)._fast(pats.length);
|
let result = slowcat(...pats);
|
||||||
|
if (pats.length > 1) {
|
||||||
|
result = result._fast(pats.length);
|
||||||
|
result.weight = pats.length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates patterns beatwise, similar to `timeCat`, but if an argument is a list, the whole pattern will be repeated for each element in the list.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* beatCat(["bd cp", "mt"], "bd").sound()
|
||||||
|
*/
|
||||||
|
export function beatCat(...groups) {
|
||||||
|
groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));
|
||||||
|
|
||||||
|
const cycles = lcm(...groups.map((x) => Fraction(x.length)));
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
for (let cycle = 0; cycle < cycles; ++cycle) {
|
||||||
|
result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length])));
|
||||||
|
}
|
||||||
|
result = result.filter((x) => x.weight > 0);
|
||||||
|
const weight = result.reduce((a, b) => a.add(b.weight), Fraction(0));
|
||||||
|
result = timeCat(...result);
|
||||||
|
result.weight = weight;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See `fastcat` */
|
/** See `fastcat` */
|
||||||
@ -1312,18 +1472,18 @@ function _sequenceCount(x) {
|
|||||||
}
|
}
|
||||||
return [reify(x), 1];
|
return [reify(x), 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aligns one or more given sequences to the given number of steps per cycle.
|
* Speeds a pattern up or down, to fit to the given metrical 'weight'.
|
||||||
*
|
|
||||||
* @name polymeterSteps
|
|
||||||
* @param {number} steps how many items are placed in one cycle
|
|
||||||
* @param {any[]} sequences one or more arrays of Patterns / values
|
|
||||||
* @example
|
* @example
|
||||||
* polymeterSteps(4, ["c", "d", "e"])
|
* s("bd sd cp").reweight(4)
|
||||||
* .note().stack(s("bd"))
|
* // The same as s("{bd sd cp}%4")
|
||||||
* // note("{c d e}%4").stack(s("bd"))
|
|
||||||
*/
|
*/
|
||||||
export function polymeterSteps(steps, ...args) {
|
export const reweight = register('reweight', function (targetWeight, pat) {
|
||||||
|
return pat.fast(Fraction(targetWeight).div(pat.weight));
|
||||||
|
});
|
||||||
|
|
||||||
|
export function _polymeterListSteps(steps, ...args) {
|
||||||
const seqs = args.map((a) => _sequenceCount(a));
|
const seqs = args.map((a) => _sequenceCount(a));
|
||||||
if (seqs.length == 0) {
|
if (seqs.length == 0) {
|
||||||
return silence;
|
return silence;
|
||||||
@ -1346,15 +1506,51 @@ export function polymeterSteps(steps, ...args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines the given lists of patterns with the same pulse. This will create so called polymeters when different sized sequences are used.
|
* Aligns one or more given patterns to the given number of steps per cycle.
|
||||||
|
* This relies on patterns having coherent number of steps per cycle,
|
||||||
|
*
|
||||||
|
* @name polymeterSteps
|
||||||
|
* @param {number} steps how many items are placed in one cycle
|
||||||
|
* @param {any[]} patterns one or more patterns
|
||||||
|
* @example
|
||||||
|
* // the same as "{c d, e f g}%4"
|
||||||
|
* polymeterSteps(4, "c d", "e f g")
|
||||||
|
*/
|
||||||
|
export function polymeterSteps(steps, ...args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
if (Array.isArray(args[0])) {
|
||||||
|
// Support old behaviour
|
||||||
|
return _polymeterListSteps(steps, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return polymeter(...args).reweight(steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines the given lists of patterns with the same pulse, creating polymeters when different sized sequences are used.
|
||||||
* @synonyms pm
|
* @synonyms pm
|
||||||
* @example
|
* @example
|
||||||
* polymeter(["c", "eb", "g"], ["c2", "g2"]).note()
|
* // The same as "{c eb g, c2 g2}"
|
||||||
* // "{c eb g, c2 g2}".note()
|
* polymeter("c eb g", "c2 g2")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function polymeter(...args) {
|
export function polymeter(...args) {
|
||||||
return polymeterSteps(0, ...args);
|
if (Array.isArray(args[0])) {
|
||||||
|
// Support old behaviour
|
||||||
|
return _polymeterListSteps(0, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length == 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
const weight = args[0].weight;
|
||||||
|
const [head, ...tail] = args;
|
||||||
|
|
||||||
|
const result = stack(head, ...tail.map((pat) => pat._slow(pat.weight.div(weight))));
|
||||||
|
result.weight = weight;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mask = curry((a, b) => reify(b).mask(a));
|
export const mask = curry((a, b) => reify(b).mask(a));
|
||||||
@ -1396,7 +1592,7 @@ export const func = curry((a, b) => reify(b).func(a));
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function register(name, func, patternify = true) {
|
export function register(name, func, patternify = true, preserveWeight = false) {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(name)) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name_item of name) {
|
for (const name_item of name) {
|
||||||
@ -1411,26 +1607,44 @@ export function register(name, func, patternify = true) {
|
|||||||
pfunc = function (...args) {
|
pfunc = function (...args) {
|
||||||
args = args.map(reify);
|
args = args.map(reify);
|
||||||
const pat = args[args.length - 1];
|
const pat = args[args.length - 1];
|
||||||
|
let result;
|
||||||
|
|
||||||
if (arity === 1) {
|
if (arity === 1) {
|
||||||
return func(pat);
|
result = func(pat);
|
||||||
|
} else {
|
||||||
|
const firstArgs = args.slice(0, -1);
|
||||||
|
|
||||||
|
if (firstArgs.every((arg) => arg.__pure != undefined)) {
|
||||||
|
const pureArgs = firstArgs.map((arg) => arg.__pure);
|
||||||
|
const pureLocs = firstArgs.filter((arg) => arg.__pure_loc).map((arg) => arg.__pure_loc);
|
||||||
|
result = func(...pureArgs, pat);
|
||||||
|
result = result.withContext((context) => {
|
||||||
|
const locations = (context.locations || []).concat(pureLocs);
|
||||||
|
return { ...context, locations };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const [left, ...right] = firstArgs;
|
||||||
|
|
||||||
|
let mapFn = (...args) => {
|
||||||
|
return func(...args, pat);
|
||||||
|
};
|
||||||
|
mapFn = curry(mapFn, null, arity - 1);
|
||||||
|
result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const [left, ...right] = args.slice(0, -1);
|
if (preserveWeight) {
|
||||||
let mapFn = (...args) => {
|
result.weight = pat.weight;
|
||||||
// make sure to call func with the correct argument count
|
}
|
||||||
// args.length is expected to be <= arity-1
|
return result;
|
||||||
// so we set undefined args explicitly undefined
|
|
||||||
Array(arity - 1)
|
|
||||||
.fill()
|
|
||||||
.map((_, i) => args[i] ?? undefined);
|
|
||||||
return func(...args, pat);
|
|
||||||
};
|
|
||||||
mapFn = curry(mapFn, null, arity - 1);
|
|
||||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
pfunc = function (...args) {
|
pfunc = function (...args) {
|
||||||
args = args.map(reify);
|
args = args.map(reify);
|
||||||
return func(...args);
|
const result = func(...args);
|
||||||
|
if (preserveWeight) {
|
||||||
|
result.weight = args[args.length - 1].weight;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1665,7 +1879,9 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun
|
|||||||
* s("bd ~ sd cp").ply("<1 2 3>")
|
* s("bd ~ sd cp").ply("<1 2 3>")
|
||||||
*/
|
*/
|
||||||
export const ply = register('ply', function (factor, pat) {
|
export const ply = register('ply', function (factor, pat) {
|
||||||
return pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
|
const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
|
||||||
|
result.weight = pat.weight.mul(factor);
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1685,7 +1901,9 @@ export const { fast, density } = register(['fast', 'density'], function (factor,
|
|||||||
}
|
}
|
||||||
factor = Fraction(factor);
|
factor = Fraction(factor);
|
||||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||||
return fastQuery.withHapTime((t) => t.div(factor));
|
const result = fastQuery.withHapTime((t) => t.div(factor));
|
||||||
|
result.weight = factor.mul(pat.weight);
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1810,10 +2028,15 @@ export const cpm = register('cpm', function (cpm, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* "bd ~".stack("hh ~".early(.1)).s()
|
* "bd ~".stack("hh ~".early(.1)).s()
|
||||||
*/
|
*/
|
||||||
export const early = register('early', function (offset, pat) {
|
export const early = register(
|
||||||
offset = Fraction(offset);
|
'early',
|
||||||
return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
|
function (offset, pat) {
|
||||||
});
|
offset = Fraction(offset);
|
||||||
|
return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator
|
* Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator
|
||||||
@ -1825,10 +2048,15 @@ export const early = register('early', function (offset, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* "bd ~".stack("hh ~".late(.1)).s()
|
* "bd ~".stack("hh ~".late(.1)).s()
|
||||||
*/
|
*/
|
||||||
export const late = register('late', function (offset, pat) {
|
export const late = register(
|
||||||
offset = Fraction(offset);
|
'late',
|
||||||
return pat._early(Fraction(0).sub(offset));
|
function (offset, pat) {
|
||||||
});
|
offset = Fraction(offset);
|
||||||
|
return pat._early(Fraction(0).sub(offset));
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern:
|
* Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern:
|
||||||
@ -1857,14 +2085,19 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
|
|||||||
* @example
|
* @example
|
||||||
* s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>")
|
* s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>")
|
||||||
*/
|
*/
|
||||||
export const linger = register('linger', function (t, pat) {
|
export const linger = register(
|
||||||
if (t == 0) {
|
'linger',
|
||||||
return silence;
|
function (t, pat) {
|
||||||
} else if (t < 0) {
|
if (t == 0) {
|
||||||
return pat._zoom(t.add(1), 1)._slow(t);
|
return silence;
|
||||||
}
|
} else if (t < 0) {
|
||||||
return pat._zoom(0, t)._slow(t);
|
return pat._zoom(t.add(1), 1)._slow(t);
|
||||||
});
|
}
|
||||||
|
return pat._zoom(0, t)._slow(t);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
|
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
|
||||||
@ -1873,7 +2106,7 @@ export const linger = register('linger', function (t, pat) {
|
|||||||
* note(saw.range(40,52).segment(24))
|
* note(saw.range(40,52).segment(24))
|
||||||
*/
|
*/
|
||||||
export const segment = register('segment', function (rate, pat) {
|
export const segment = register('segment', function (rate, pat) {
|
||||||
return pat.struct(pure(true)._fast(rate));
|
return pat.struct(pure(true)._fast(rate)).setWeight(rate);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1883,10 +2116,15 @@ export const segment = register('segment', function (rate, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert))
|
* s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert))
|
||||||
*/
|
*/
|
||||||
export const { invert, inv } = register(['invert', 'inv'], function (pat) {
|
export const { invert, inv } = register(
|
||||||
// Swap true/false in a binary pattern
|
['invert', 'inv'],
|
||||||
return pat.fmap((x) => !x);
|
function (pat) {
|
||||||
});
|
// Swap true/false in a binary pattern
|
||||||
|
return pat.fmap((x) => !x);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the given function whenever the given pattern is in a true state.
|
* Applies the given function whenever the given pattern is in a true state.
|
||||||
@ -1935,24 +2173,29 @@ export const brak = register('brak', function (pat) {
|
|||||||
* @example
|
* @example
|
||||||
* note("c d e g").rev()
|
* note("c d e g").rev()
|
||||||
*/
|
*/
|
||||||
export const rev = register('rev', function (pat) {
|
export const rev = register(
|
||||||
const query = function (state) {
|
'rev',
|
||||||
const span = state.span;
|
function (pat) {
|
||||||
const cycle = span.begin.sam();
|
const query = function (state) {
|
||||||
const next_cycle = span.begin.nextSam();
|
const span = state.span;
|
||||||
const reflect = function (to_reflect) {
|
const cycle = span.begin.sam();
|
||||||
const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time)));
|
const next_cycle = span.begin.nextSam();
|
||||||
// [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work
|
const reflect = function (to_reflect) {
|
||||||
const tmp = reflected.begin;
|
const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time)));
|
||||||
reflected.begin = reflected.end;
|
// [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work
|
||||||
reflected.end = tmp;
|
const tmp = reflected.begin;
|
||||||
return reflected;
|
reflected.begin = reflected.end;
|
||||||
|
reflected.end = tmp;
|
||||||
|
return reflected;
|
||||||
|
};
|
||||||
|
const haps = pat.query(state.setSpan(reflect(span)));
|
||||||
|
return haps.map((hap) => hap.withSpan(reflect));
|
||||||
};
|
};
|
||||||
const haps = pat.query(state.setSpan(reflect(span)));
|
return new Pattern(query).splitQueries();
|
||||||
return haps.map((hap) => hap.withSpan(reflect));
|
},
|
||||||
};
|
false,
|
||||||
return new Pattern(query).splitQueries();
|
true,
|
||||||
});
|
);
|
||||||
|
|
||||||
/** Like press, but allows you to specify the amount by which each
|
/** Like press, but allows you to specify the amount by which each
|
||||||
* event is shifted. pressBy(0.5) is the same as press, while
|
* event is shifted. pressBy(0.5) is the same as press, while
|
||||||
@ -1994,9 +2237,14 @@ Pattern.prototype.hush = function () {
|
|||||||
* @example
|
* @example
|
||||||
* note("c d e g").palindrome()
|
* note("c d e g").palindrome()
|
||||||
*/
|
*/
|
||||||
export const palindrome = register('palindrome', function (pat) {
|
export const palindrome = register(
|
||||||
return pat.lastOf(2, rev);
|
'palindrome',
|
||||||
});
|
function (pat) {
|
||||||
|
return pat.lastOf(2, rev);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jux with adjustable stereo width. 0 = mono, 1 = full stereo.
|
* Jux with adjustable stereo width. 0 = mono, 1 = full stereo.
|
||||||
@ -2014,9 +2262,9 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
|
|||||||
return dflt;
|
return dflt;
|
||||||
};
|
};
|
||||||
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
|
const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
|
||||||
const right = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }));
|
const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by })));
|
||||||
|
|
||||||
return stack(left, func(right));
|
return stack(left, right).setWeight(lcm(left.weight, right.weight));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2097,9 +2345,14 @@ const _iter = function (times, pat, back = false) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const iter = register('iter', function (times, pat) {
|
export const iter = register(
|
||||||
return _iter(times, pat, false);
|
'iter',
|
||||||
});
|
function (times, pat) {
|
||||||
|
return _iter(times, pat, false);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
|
* Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
|
||||||
@ -2110,9 +2363,14 @@ export const iter = register('iter', function (times, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* note("0 1 2 3".scale('A minor')).iterBack(4)
|
* note("0 1 2 3".scale('A minor')).iterBack(4)
|
||||||
*/
|
*/
|
||||||
export const { iterBack, iterback } = register(['iterBack', 'iterback'], function (times, pat) {
|
export const { iterBack, iterback } = register(
|
||||||
return _iter(times, pat, true);
|
['iterBack', 'iterback'],
|
||||||
});
|
function (times, pat) {
|
||||||
|
return _iter(times, pat, true);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repeats each cycle the given number of times.
|
* Repeats each cycle the given number of times.
|
||||||
@ -2122,11 +2380,14 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio
|
|||||||
* @example
|
* @example
|
||||||
* note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
|
* note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
|
||||||
*/
|
*/
|
||||||
const _repeatCycles = function (n, pat) {
|
export const { repeatCycles } = register(
|
||||||
return slowcat(...Array(n).fill(pat));
|
'repeatCycles',
|
||||||
};
|
function (n, pat) {
|
||||||
|
return slowcat(...Array(n).fill(pat));
|
||||||
const { repeatCycles } = register('repeatCycles', _repeatCycles);
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
||||||
@ -2150,7 +2411,7 @@ const _chunk = function (n, func, pat, back = false, fast = false) {
|
|||||||
return pat.when(binary_pat, func);
|
return pat.when(binary_pat, func);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
|
export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
|
||||||
return _chunk(n, func, pat, false, false);
|
return _chunk(n, func, pat, false, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2185,10 +2446,15 @@ export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], fun
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO - redefine elsewhere in terms of mask
|
// TODO - redefine elsewhere in terms of mask
|
||||||
export const bypass = register('bypass', function (on, pat) {
|
export const bypass = register(
|
||||||
on = Boolean(parseInt(on));
|
'bypass',
|
||||||
return on ? silence : pat;
|
function (on, pat) {
|
||||||
});
|
on = Boolean(parseInt(on));
|
||||||
|
return on ? silence : pat;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loops the pattern inside at `offset` for `cycles`.
|
* Loops the pattern inside at `offset` for `cycles`.
|
||||||
@ -2245,7 +2511,7 @@ export const chop = register('chop', function (n, pat) {
|
|||||||
const func = function (o) {
|
const func = function (o) {
|
||||||
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
|
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
|
||||||
};
|
};
|
||||||
return pat.squeezeBind(func);
|
return pat.squeezeBind(func).setWeight(pat.weight.mul(n));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2297,17 +2563,19 @@ const _loopAt = function (factor, pat, cps = 0.5) {
|
|||||||
export const slice = register(
|
export const slice = register(
|
||||||
'slice',
|
'slice',
|
||||||
function (npat, ipat, opat) {
|
function (npat, ipat, opat) {
|
||||||
return npat.innerBind((n) =>
|
return npat
|
||||||
ipat.outerBind((i) =>
|
.innerBind((n) =>
|
||||||
opat.outerBind((o) => {
|
ipat.outerBind((i) =>
|
||||||
// If it's not an object, assume it's a string and make it a 's' control parameter
|
opat.outerBind((o) => {
|
||||||
o = o instanceof Object ? o : { s: o };
|
// If it's not an object, assume it's a string and make it a 's' control parameter
|
||||||
const begin = Array.isArray(n) ? n[i] : i / n;
|
o = o instanceof Object ? o : { s: o };
|
||||||
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
|
const begin = Array.isArray(n) ? n[i] : i / n;
|
||||||
return pure({ begin, end, _slices: n, ...o });
|
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
|
||||||
}),
|
return pure({ begin, end, _slices: n, ...o });
|
||||||
),
|
}),
|
||||||
);
|
),
|
||||||
|
)
|
||||||
|
.setWeight(ipat.weight);
|
||||||
},
|
},
|
||||||
false, // turns off auto-patternification
|
false, // turns off auto-patternification
|
||||||
);
|
);
|
||||||
@ -2338,7 +2606,7 @@ export const splice = register(
|
|||||||
...v,
|
...v,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
});
|
}).setWeight(ipat.weight);
|
||||||
},
|
},
|
||||||
false, // turns off auto-patternification
|
false, // turns off auto-patternification
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { NeoCyclist } from './neocyclist.mjs';
|
||||||
import { Cyclist } from './cyclist.mjs';
|
import { Cyclist } from './cyclist.mjs';
|
||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
@ -6,9 +7,7 @@ import { evalScope } from './evaluate.mjs';
|
|||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
interval,
|
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
onSchedulerError,
|
|
||||||
onEvalError,
|
onEvalError,
|
||||||
beforeEval,
|
beforeEval,
|
||||||
afterEval,
|
afterEval,
|
||||||
@ -17,6 +16,7 @@ export function repl({
|
|||||||
onToggle,
|
onToggle,
|
||||||
editPattern,
|
editPattern,
|
||||||
onUpdateState,
|
onUpdateState,
|
||||||
|
sync = false,
|
||||||
}) {
|
}) {
|
||||||
const state = {
|
const state = {
|
||||||
schedulerError: undefined,
|
schedulerError: undefined,
|
||||||
@ -37,16 +37,18 @@ export function repl({
|
|||||||
onUpdateState?.(state);
|
onUpdateState?.(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheduler = new Cyclist({
|
const schedulerOptions = {
|
||||||
interval,
|
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
onError: onSchedulerError,
|
|
||||||
getTime,
|
getTime,
|
||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
onToggle?.(started);
|
onToggle?.(started);
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
|
||||||
|
const scheduler =
|
||||||
|
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
|
||||||
let pPatterns = {};
|
let pPatterns = {};
|
||||||
let allTransform;
|
let allTransform;
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,10 @@ Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { s } from '../controls.mjs';
|
import { s, pan } from '../controls.mjs';
|
||||||
import { mini } from '../../mini/mini.mjs';
|
import { mini } from '../../mini/mini.mjs';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import Fraction from '../fraction.mjs';
|
||||||
|
|
||||||
describe('controls', () => {
|
describe('controls', () => {
|
||||||
it('should support controls', () => {
|
it('should support controls', () => {
|
||||||
@ -29,4 +30,13 @@ describe('controls', () => {
|
|||||||
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
|
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
|
||||||
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
|
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
|
||||||
});
|
});
|
||||||
|
it('preserves weight of the left pattern', () => {
|
||||||
|
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).weight).toEqual(Fraction(3));
|
||||||
|
});
|
||||||
|
it('preserves weight of the right pattern for .out', () => {
|
||||||
|
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(4));
|
||||||
|
});
|
||||||
|
it('combines weight of the pattern for .mix as lcm', () => {
|
||||||
|
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(12));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -604,7 +604,7 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('polymeter()', () => {
|
describe('polymeter()', () => {
|
||||||
it('Can layer up cycles, stepwise', () => {
|
it('Can layer up cycles, stepwise, with lists', () => {
|
||||||
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||||
);
|
);
|
||||||
@ -613,6 +613,9 @@ describe('Pattern', () => {
|
|||||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('Can layer up cycles, stepwise, with weighted patterns', () => {
|
||||||
|
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('firstOf()', () => {
|
describe('firstOf()', () => {
|
||||||
@ -1116,4 +1119,23 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('weight', () => {
|
||||||
|
it('Is correctly preserved/calculated through transformations', () => {
|
||||||
|
expect(sequence(0, 1, 2, 3).linger(4).weight).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(0, 1, 2, 3).iter(4).weight).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(0, 1, 2, 3).fast(4).weight).toStrictEqual(Fraction(16));
|
||||||
|
expect(sequence(0, 1, 2, 3).hurry(4).weight).toStrictEqual(Fraction(16));
|
||||||
|
expect(sequence(0, 1, 2, 3).rev().weight).toStrictEqual(Fraction(4));
|
||||||
|
expect(sequence(1).segment(10).weight).toStrictEqual(Fraction(10));
|
||||||
|
expect(sequence(1, 0, 1).invert().weight).toStrictEqual(Fraction(3));
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).weight).toStrictEqual(Fraction(8));
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).weight).toStrictEqual(Fraction(8));
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual(
|
||||||
|
Fraction(4),
|
||||||
|
);
|
||||||
|
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual(
|
||||||
|
Fraction(4),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,19 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getTime } from './time.mjs';
|
|
||||||
|
|
||||||
function frame(callback) {
|
|
||||||
if (window.strudelAnimation) {
|
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
|
||||||
}
|
|
||||||
const animate = (animationTime) => {
|
|
||||||
callback(animationTime, getTime());
|
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const backgroundImage = function (src, animateOptions = {}) {
|
export const backgroundImage = function (src, animateOptions = {}) {
|
||||||
const container = document.getElementById('code');
|
const container = document.getElementById('code');
|
||||||
const bg = 'background-image:url(' + src + ');background-size:contain;';
|
const bg = 'background-image:url(' + src + ');background-size:contain;';
|
||||||
@ -35,11 +22,6 @@ export const backgroundImage = function (src, animateOptions = {}) {
|
|||||||
if (funcOptions.length === 0) {
|
if (funcOptions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frame((_, t) =>
|
|
||||||
funcOptions.forEach(([option, value]) => {
|
|
||||||
handleOption(option, value(t));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanupUi = () => {
|
export const cleanupUi = () => {
|
||||||
|
|||||||
@ -323,3 +323,23 @@ export function objectMap(obj, fn) {
|
|||||||
}
|
}
|
||||||
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Floating point versions, see Fraction for rational versions
|
||||||
|
// // greatest common divisor
|
||||||
|
// export const gcd = function (x, y, ...z) {
|
||||||
|
// if (!y && z.length > 0) {
|
||||||
|
// return gcd(x, ...z);
|
||||||
|
// }
|
||||||
|
// if (!y) {
|
||||||
|
// return x;
|
||||||
|
// }
|
||||||
|
// return gcd(y, x % y, ...z);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // lowest common multiple
|
||||||
|
// export const lcm = function (x, y, ...z) {
|
||||||
|
// if (z.length == 0) {
|
||||||
|
// return (x * y) / gcd(x, y);
|
||||||
|
// }
|
||||||
|
// return lcm((x * y) / gcd(x, y), ...z);
|
||||||
|
// };
|
||||||
|
|||||||
@ -23,7 +23,7 @@ function createClock(
|
|||||||
// callback as long as we're inside the lookahead
|
// callback as long as we're inside the lookahead
|
||||||
while (phase < lookahead) {
|
while (phase < lookahead) {
|
||||||
phase = Math.round(phase * precision) / precision;
|
phase = Math.round(phase * precision) / precision;
|
||||||
phase >= t && callback(phase, duration, tick);
|
phase >= t && callback(phase, duration, tick, t);
|
||||||
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||||
phase += duration; // increment phase by duration
|
phase += duration; // increment phase by duration
|
||||||
tick++;
|
tick++;
|
||||||
|
|||||||
@ -29,14 +29,22 @@ export const getDrawContext = (id = 'test-canvas', options) => {
|
|||||||
return canvas.getContext(contextType);
|
return canvas.getContext(contextType);
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
let animationFrames = {};
|
||||||
|
function stopAnimationFrame(id) {
|
||||||
|
if (animationFrames[id] !== undefined) {
|
||||||
|
cancelAnimationFrame(animationFrames[id]);
|
||||||
|
delete animationFrames[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function stopAllAnimations() {
|
||||||
|
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id));
|
||||||
|
}
|
||||||
|
Pattern.prototype.draw = function (callback, { id = 'std', from, to, onQuery, ctx } = {}) {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
if (window.strudelAnimation) {
|
stopAnimationFrame(id);
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
ctx = ctx || getDrawContext();
|
||||||
}
|
|
||||||
const ctx = getDrawContext();
|
|
||||||
let cycle,
|
let cycle,
|
||||||
events = [];
|
events = [];
|
||||||
const animate = (time) => {
|
const animate = (time) => {
|
||||||
@ -56,7 +64,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback(ctx, events, t, time);
|
callback(ctx, events, t, time);
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
animationFrames[id] = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
return this;
|
return this;
|
||||||
@ -64,18 +72,16 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
|||||||
|
|
||||||
// this is a more generic helper to get a rendering callback for the currently active haps
|
// this is a more generic helper to get a rendering callback for the currently active haps
|
||||||
// TODO: this misses events that are prolonged with clip or duration (would need state)
|
// TODO: this misses events that are prolonged with clip or duration (would need state)
|
||||||
Pattern.prototype.onFrame = function (fn, offset = 0) {
|
Pattern.prototype.onFrame = function (id, fn, offset = 0) {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
if (window.strudelAnimation) {
|
stopAnimationFrame(id);
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
|
||||||
}
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
const t = getTime() + offset;
|
const t = getTime() + offset;
|
||||||
const haps = this.queryArc(t, t);
|
const haps = this.queryArc(t, t);
|
||||||
fn(haps, t, this);
|
fn(haps, t, this);
|
||||||
window.strudelAnimation = requestAnimationFrame(animate);
|
animationFrames[id] = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
return this;
|
return this;
|
||||||
@ -84,9 +90,7 @@ Pattern.prototype.onFrame = function (fn, offset = 0) {
|
|||||||
export const cleanupDraw = (clearScreen = true) => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
||||||
if (window.strudelAnimation) {
|
stopAllAnimations();
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
|
||||||
}
|
|
||||||
if (window.strudelScheduler) {
|
if (window.strudelScheduler) {
|
||||||
clearInterval(window.strudelScheduler);
|
clearInterval(window.strudelScheduler);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,13 @@ const getValue = (e) => {
|
|||||||
}
|
}
|
||||||
note = note ?? n;
|
note = note ?? n;
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
return noteToMidi(note);
|
try {
|
||||||
|
// TODO: n(run(32)).scale("D:minor") fails when trying to query negative time..
|
||||||
|
return noteToMidi(note);
|
||||||
|
} catch (err) {
|
||||||
|
// console.warn(`error converting note to midi: ${err}`); // this spams to crazy
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@ -30,7 +36,7 @@ const getValue = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.pianoroll = function (options = {}) {
|
Pattern.prototype.pianoroll = function (options = {}) {
|
||||||
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false, ctx, id } = options;
|
||||||
|
|
||||||
let from = -cycles * playhead;
|
let from = -cycles * playhead;
|
||||||
let to = cycles * (1 - playhead);
|
let to = cycles * (1 - playhead);
|
||||||
@ -49,6 +55,8 @@ Pattern.prototype.pianoroll = function (options = {}) {
|
|||||||
{
|
{
|
||||||
from: from - overscan,
|
from: from - overscan,
|
||||||
to: to + overscan,
|
to: to + overscan,
|
||||||
|
ctx,
|
||||||
|
id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@ -49,7 +49,7 @@ function spiralSegment(options) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.spiral = function (options = {}) {
|
function drawSpiral(options) {
|
||||||
const {
|
const {
|
||||||
stretch = 1,
|
stretch = 1,
|
||||||
size = 80,
|
size = 80,
|
||||||
@ -65,54 +65,58 @@ Pattern.prototype.spiral = function (options = {}) {
|
|||||||
colorizeInactive = 0,
|
colorizeInactive = 0,
|
||||||
fade = true,
|
fade = true,
|
||||||
// logSpiral = true,
|
// logSpiral = true,
|
||||||
|
ctx,
|
||||||
|
time,
|
||||||
|
haps,
|
||||||
|
drawTime,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
function spiral({ ctx, time, haps, drawTime }) {
|
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
||||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
ctx.clearRect(0, 0, w * 2, h * 2);
|
||||||
ctx.clearRect(0, 0, w * 2, h * 2);
|
const [cx, cy] = [w / 2, h / 2];
|
||||||
const [cx, cy] = [w / 2, h / 2];
|
const settings = {
|
||||||
const settings = {
|
margin: size / stretch,
|
||||||
margin: size / stretch,
|
cx,
|
||||||
cx,
|
cy,
|
||||||
cy,
|
stretch,
|
||||||
stretch,
|
cap,
|
||||||
cap,
|
thickness,
|
||||||
thickness,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const playhead = {
|
const playhead = {
|
||||||
...settings,
|
...settings,
|
||||||
thickness: playheadThickness,
|
thickness: playheadThickness,
|
||||||
from: inset - playheadLength,
|
from: inset - playheadLength,
|
||||||
to: inset,
|
to: inset,
|
||||||
color: playheadColor,
|
color: playheadColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [min] = drawTime;
|
const [min] = drawTime;
|
||||||
const rotate = steady * time;
|
const rotate = steady * time;
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
||||||
const from = hap.whole.begin - time + inset;
|
const from = hap.whole.begin - time + inset;
|
||||||
const to = hap.endClipped - time + inset - padding;
|
const to = hap.endClipped - time + inset - padding;
|
||||||
const { color } = hap.context;
|
const { color } = hap.context;
|
||||||
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
||||||
spiralSegment({
|
|
||||||
ctx,
|
|
||||||
...settings,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
rotate,
|
|
||||||
color: colorizeInactive || isActive ? color : inactiveColor,
|
|
||||||
fromOpacity: opacity,
|
|
||||||
toOpacity: opacity,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
spiralSegment({
|
spiralSegment({
|
||||||
ctx,
|
ctx,
|
||||||
...playhead,
|
...settings,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
rotate,
|
rotate,
|
||||||
|
color: colorizeInactive || isActive ? color : inactiveColor,
|
||||||
|
fromOpacity: opacity,
|
||||||
|
toOpacity: opacity,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
spiralSegment({
|
||||||
|
ctx,
|
||||||
|
...playhead,
|
||||||
|
rotate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
|
Pattern.prototype.spiral = function (options = {}) {
|
||||||
|
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,16 +92,16 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
|
|||||||
return strudel.stack(...children);
|
return strudel.stack(...children);
|
||||||
}
|
}
|
||||||
if (alignment === 'polymeter_slowcat') {
|
if (alignment === 'polymeter_slowcat') {
|
||||||
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1)));
|
const aligned = children.map((child) => child._slow(child.weight));
|
||||||
return strudel.stack(...aligned);
|
return strudel.stack(...aligned);
|
||||||
}
|
}
|
||||||
if (alignment === 'polymeter') {
|
if (alignment === 'polymeter') {
|
||||||
// polymeter
|
// polymeter
|
||||||
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||||
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
|
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
|
||||||
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].weight : 1));
|
||||||
|
|
||||||
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.weight))));
|
||||||
return strudel.stack(...aligned);
|
return strudel.stack(...aligned);
|
||||||
}
|
}
|
||||||
if (alignment === 'rand') {
|
if (alignment === 'rand') {
|
||||||
@ -112,13 +112,18 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
|
|||||||
}
|
}
|
||||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||||
if (weightedChildren) {
|
if (weightedChildren) {
|
||||||
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
|
const weightSum = ast.source_.reduce(
|
||||||
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
|
||||||
pat.__weight = weightSum;
|
strudel.Fraction(0),
|
||||||
|
);
|
||||||
|
const pat = strudel.timeCat(
|
||||||
|
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
|
||||||
|
);
|
||||||
|
pat.weight = weightSum;
|
||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
const pat = strudel.sequence(...children);
|
const pat = strudel.sequence(...children);
|
||||||
pat.__weight = children.length;
|
pat.weight = children.length;
|
||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
case 'element': {
|
case 'element': {
|
||||||
|
|||||||
@ -215,35 +215,35 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
|
|||||||
return reverbs[orbit];
|
return reverbs[orbit];
|
||||||
}
|
}
|
||||||
|
|
||||||
export let analyser, analyserData /* s = {} */;
|
export let analysers = {},
|
||||||
|
analysersData = {};
|
||||||
|
|
||||||
export function getAnalyser(/* orbit, */ fftSize = 2048) {
|
export function getAnalyserById(id, fftSize = 1024) {
|
||||||
if (!analyser /*s [orbit] */) {
|
if (!analysers[id]) {
|
||||||
|
// make sure this doesn't happen too often as it piles up garbage
|
||||||
const analyserNode = getAudioContext().createAnalyser();
|
const analyserNode = getAudioContext().createAnalyser();
|
||||||
analyserNode.fftSize = fftSize;
|
analyserNode.fftSize = fftSize;
|
||||||
// getDestination().connect(analyserNode);
|
// getDestination().connect(analyserNode);
|
||||||
analyser /* s[orbit] */ = analyserNode;
|
analysers[id] = analyserNode;
|
||||||
//analyserData = new Uint8Array(analyser.frequencyBinCount);
|
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
|
||||||
analyserData = new Float32Array(analyser.frequencyBinCount);
|
|
||||||
}
|
}
|
||||||
if (analyser /* s[orbit] */.fftSize !== fftSize) {
|
if (analysers[id].fftSize !== fftSize) {
|
||||||
analyser /* s[orbit] */.fftSize = fftSize;
|
analysers[id].fftSize = fftSize;
|
||||||
//analyserData = new Uint8Array(analyser.frequencyBinCount);
|
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
|
||||||
analyserData = new Float32Array(analyser.frequencyBinCount);
|
|
||||||
}
|
}
|
||||||
return analyser /* s[orbit] */;
|
return analysers[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAnalyzerData(type = 'time') {
|
export function getAnalyzerData(type = 'time', id = 1) {
|
||||||
const getter = {
|
const getter = {
|
||||||
time: () => analyser?.getFloatTimeDomainData(analyserData),
|
time: () => analysers[id]?.getFloatTimeDomainData(analysersData[id]),
|
||||||
frequency: () => analyser?.getFloatFrequencyData(analyserData),
|
frequency: () => analysers[id]?.getFloatFrequencyData(analysersData[id]),
|
||||||
}[type];
|
}[type];
|
||||||
if (!getter) {
|
if (!getter) {
|
||||||
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
|
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
|
||||||
}
|
}
|
||||||
getter();
|
getter();
|
||||||
return analyserData;
|
return analysersData[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectSend(input, effect, wet) {
|
function effectSend(input, effect, wet) {
|
||||||
@ -256,6 +256,8 @@ function effectSend(input, effect, wet) {
|
|||||||
export function resetGlobalEffects() {
|
export function resetGlobalEffects() {
|
||||||
delays = {};
|
delays = {};
|
||||||
reverbs = {};
|
reverbs = {};
|
||||||
|
analysers = {};
|
||||||
|
analysersData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const superdough = async (value, deadline, hapDuration) => {
|
export const superdough = async (value, deadline, hapDuration) => {
|
||||||
@ -512,8 +514,8 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||||||
// analyser
|
// analyser
|
||||||
let analyserSend;
|
let analyserSend;
|
||||||
if (analyze) {
|
if (analyze) {
|
||||||
const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5));
|
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||||
analyserSend = effectSend(post, analyserNode, analyze);
|
analyserSend = effectSend(post, analyserNode, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect chain elements together
|
// connect chain elements together
|
||||||
|
|||||||
@ -3,6 +3,11 @@ import { parse } from 'acorn';
|
|||||||
import escodegen from 'escodegen';
|
import escodegen from 'escodegen';
|
||||||
import { walk } from 'estree-walker';
|
import { walk } from 'estree-walker';
|
||||||
|
|
||||||
|
let widgetMethods = [];
|
||||||
|
export function registerWidgetType(type) {
|
||||||
|
widgetMethods.push(type);
|
||||||
|
}
|
||||||
|
|
||||||
export function transpiler(input, options = {}) {
|
export function transpiler(input, options = {}) {
|
||||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||||
|
|
||||||
@ -34,7 +39,7 @@ export function transpiler(input, options = {}) {
|
|||||||
emitMiniLocations && collectMiniLocations(value, node);
|
emitMiniLocations && collectMiniLocations(value, node);
|
||||||
return this.replace(miniWithLocation(value, node));
|
return this.replace(miniWithLocation(value, node));
|
||||||
}
|
}
|
||||||
if (isWidgetFunction(node)) {
|
if (isSliderFunction(node)) {
|
||||||
emitWidgets &&
|
emitWidgets &&
|
||||||
widgets.push({
|
widgets.push({
|
||||||
from: node.arguments[0].start,
|
from: node.arguments[0].start,
|
||||||
@ -43,12 +48,25 @@ export function transpiler(input, options = {}) {
|
|||||||
min: node.arguments[1]?.value ?? 0,
|
min: node.arguments[1]?.value ?? 0,
|
||||||
max: node.arguments[2]?.value ?? 1,
|
max: node.arguments[2]?.value ?? 1,
|
||||||
step: node.arguments[3]?.value,
|
step: node.arguments[3]?.value,
|
||||||
|
type: 'slider',
|
||||||
});
|
});
|
||||||
return this.replace(widgetWithLocation(node));
|
return this.replace(sliderWithLocation(node));
|
||||||
|
}
|
||||||
|
if (isWidgetMethod(node)) {
|
||||||
|
const widgetConfig = {
|
||||||
|
to: node.end,
|
||||||
|
index: widgets.length,
|
||||||
|
type: node.callee.property.name,
|
||||||
|
};
|
||||||
|
emitWidgets && widgets.push(widgetConfig);
|
||||||
|
return this.replace(widgetWithLocation(node, widgetConfig));
|
||||||
}
|
}
|
||||||
if (isBareSamplesCall(node, parent)) {
|
if (isBareSamplesCall(node, parent)) {
|
||||||
return this.replace(withAwait(node));
|
return this.replace(withAwait(node));
|
||||||
}
|
}
|
||||||
|
if (isLabelStatement(node)) {
|
||||||
|
return this.replace(labelToP(node));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
leave(node, parent, prop, index) {},
|
leave(node, parent, prop, index) {},
|
||||||
});
|
});
|
||||||
@ -105,11 +123,15 @@ function miniWithLocation(value, node) {
|
|||||||
|
|
||||||
// these functions are connected to @strudel/codemirror -> slider.mjs
|
// these functions are connected to @strudel/codemirror -> slider.mjs
|
||||||
// maybe someday there will be pluggable transpiler functions, then move this there
|
// maybe someday there will be pluggable transpiler functions, then move this there
|
||||||
function isWidgetFunction(node) {
|
function isSliderFunction(node) {
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'slider';
|
return node.type === 'CallExpression' && node.callee.name === 'slider';
|
||||||
}
|
}
|
||||||
|
|
||||||
function widgetWithLocation(node) {
|
function isWidgetMethod(node) {
|
||||||
|
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliderWithLocation(node) {
|
||||||
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
|
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
|
||||||
// add loc as identifier to first argument
|
// add loc as identifier to first argument
|
||||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
@ -122,6 +144,27 @@ function widgetWithLocation(node) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWidgetID(widgetConfig) {
|
||||||
|
// the widget id is used as id for the dom element + as key for eventual resources
|
||||||
|
// for example, for each scope widget, a new analyser + buffer (large) is created
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetWithLocation(node, widgetConfig) {
|
||||||
|
const id = getWidgetID(widgetConfig);
|
||||||
|
// add loc as identifier to first argument
|
||||||
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
|
node.arguments.unshift({
|
||||||
|
type: 'Literal',
|
||||||
|
value: id,
|
||||||
|
raw: id,
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
function isBareSamplesCall(node, parent) {
|
function isBareSamplesCall(node, parent) {
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
||||||
}
|
}
|
||||||
@ -132,3 +175,33 @@ function withAwait(node) {
|
|||||||
argument: node,
|
argument: node,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLabelStatement(node) {
|
||||||
|
return node.type === 'LabeledStatement';
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts label expressions to p calls: "x: y" to "y.p('x')"
|
||||||
|
// see https://github.com/tidalcycles/strudel/issues/990
|
||||||
|
function labelToP(node) {
|
||||||
|
return {
|
||||||
|
type: 'ExpressionStatement',
|
||||||
|
expression: {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: node.body.expression,
|
||||||
|
property: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'p',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
type: 'Literal',
|
||||||
|
value: node.label.name,
|
||||||
|
raw: `'${node.label.name}'`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -1,19 +1,37 @@
|
|||||||
import { Pattern, clamp } from '@strudel/core';
|
import { Pattern, clamp } from '@strudel/core';
|
||||||
import { getDrawContext } from '../draw/index.mjs';
|
import { getDrawContext } from '../draw/index.mjs';
|
||||||
import { analyser, getAnalyzerData } from 'superdough';
|
import { analysers, getAnalyzerData } from 'superdough';
|
||||||
|
|
||||||
export function drawTimeScope(
|
export function drawTimeScope(
|
||||||
analyser,
|
analyser,
|
||||||
{ align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, trigger = 0 } = {},
|
{
|
||||||
|
align = true,
|
||||||
|
color = 'white',
|
||||||
|
thickness = 3,
|
||||||
|
scale = 0.25,
|
||||||
|
pos = 0.75,
|
||||||
|
trigger = 0,
|
||||||
|
ctx = getDrawContext(),
|
||||||
|
id = 1,
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
const ctx = getDrawContext();
|
|
||||||
const dataArray = getAnalyzerData('time');
|
|
||||||
|
|
||||||
ctx.lineWidth = thickness;
|
ctx.lineWidth = thickness;
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
|
let canvas = ctx.canvas;
|
||||||
|
|
||||||
|
if (!analyser) {
|
||||||
|
// if analyser is undefined, draw straight line
|
||||||
|
// it may be undefined when no sound has been played yet
|
||||||
|
ctx.beginPath();
|
||||||
|
let y = pos * canvas.height;
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(canvas.width, y);
|
||||||
|
ctx.stroke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dataArray = getAnalyzerData('time', id);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
let canvas = ctx.canvas;
|
|
||||||
|
|
||||||
const bufferSize = analyser.frequencyBinCount;
|
const bufferSize = analyser.frequencyBinCount;
|
||||||
let triggerIndex = align
|
let triggerIndex = align
|
||||||
@ -39,10 +57,17 @@ export function drawTimeScope(
|
|||||||
|
|
||||||
export function drawFrequencyScope(
|
export function drawFrequencyScope(
|
||||||
analyser,
|
analyser,
|
||||||
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {},
|
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0, ctx = getDrawContext(), id = 1 } = {},
|
||||||
) {
|
) {
|
||||||
const dataArray = getAnalyzerData('frequency');
|
if (!analyser) {
|
||||||
const ctx = getDrawContext();
|
ctx.beginPath();
|
||||||
|
let y = pos * canvas.height;
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(canvas.width, y);
|
||||||
|
ctx.stroke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dataArray = getAnalyzerData('frequency', id);
|
||||||
const canvas = ctx.canvas;
|
const canvas = ctx.canvas;
|
||||||
|
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
@ -61,8 +86,7 @@ export function drawFrequencyScope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearScreen(smear = 0, smearRGB = `0,0,0`) {
|
function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) {
|
||||||
const ctx = getDrawContext();
|
|
||||||
if (!smear) {
|
if (!smear) {
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
} else {
|
} else {
|
||||||
@ -84,10 +108,14 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`) {
|
|||||||
* s("sawtooth").fscope()
|
* s("sawtooth").fscope()
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.fscope = function (config = {}) {
|
Pattern.prototype.fscope = function (config = {}) {
|
||||||
return this.analyze(1).draw(() => {
|
let id = config.id ?? 1;
|
||||||
clearScreen(config.smear);
|
return this.analyze(id).draw(
|
||||||
analyser && drawFrequencyScope(analyser, config);
|
() => {
|
||||||
});
|
clearScreen(config.smear, '0,0,0', config.ctx);
|
||||||
|
analysers[id] && drawFrequencyScope(analysers[id], config);
|
||||||
|
},
|
||||||
|
{ id },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,10 +133,14 @@ Pattern.prototype.fscope = function (config = {}) {
|
|||||||
* s("sawtooth").scope()
|
* s("sawtooth").scope()
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.tscope = function (config = {}) {
|
Pattern.prototype.tscope = function (config = {}) {
|
||||||
return this.analyze(1).draw(() => {
|
let id = config.id ?? 1;
|
||||||
clearScreen(config.smear);
|
return this.analyze(id).draw(
|
||||||
analyser && drawTimeScope(analyser, config);
|
() => {
|
||||||
});
|
clearScreen(config.smear, '0,0,0', config.ctx);
|
||||||
|
drawTimeScope(analysers[id], config);
|
||||||
|
},
|
||||||
|
{ id },
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.scope = Pattern.prototype.tscope;
|
Pattern.prototype.scope = Pattern.prototype.tscope;
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -184,6 +184,9 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../draw
|
version: link:../draw
|
||||||
|
'@strudel/transpiler':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../transpiler
|
||||||
'@uiw/codemirror-themes':
|
'@uiw/codemirror-themes':
|
||||||
specifier: ^4.21.21
|
specifier: ^4.21.21
|
||||||
version: 4.21.21(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)
|
version: 4.21.21(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)
|
||||||
@ -826,7 +829,7 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/gen-mapping': 0.1.1
|
'@jridgewell/gen-mapping': 0.1.1
|
||||||
'@jridgewell/trace-mapping': 0.3.17
|
'@jridgewell/trace-mapping': 0.3.20
|
||||||
|
|
||||||
/@apideck/better-ajv-errors@0.3.6(ajv@8.12.0):
|
/@apideck/better-ajv-errors@0.3.6(ajv@8.12.0):
|
||||||
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
|
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
|
||||||
@ -3061,7 +3064,6 @@ packages:
|
|||||||
/@jridgewell/resolve-uri@3.1.1:
|
/@jridgewell/resolve-uri@3.1.1:
|
||||||
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
|
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@jridgewell/set-array@1.1.2:
|
/@jridgewell/set-array@1.1.2:
|
||||||
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
|
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
|
||||||
@ -3091,7 +3093,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/resolve-uri': 3.1.1
|
'@jridgewell/resolve-uri': 3.1.1
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@jsdoc/salty@0.2.3:
|
/@jsdoc/salty@0.2.3:
|
||||||
resolution: {integrity: sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==}
|
resolution: {integrity: sha512-bbtCxCkxcnWhi50I+4Lj6mdz9w3pOXOgEQrID8TCZ/DF51fW7M9GCQW2y45SpBDdHd1Eirm1X/Cf6CkAAe8HPg==}
|
||||||
|
|||||||
@ -567,10 +567,8 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "accelerate" example index 0 1`] = `
|
exports[`runs examples > example "accelerate" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ (0/1 → 1/1) ⇝ 2/1 | s:sax accelerate:0 ]",
|
"[ 0/1 → 2/1 | s:sax accelerate:0 ]",
|
||||||
"[ 0/1 ⇜ (1/1 → 2/1) | s:sax accelerate:0 ]",
|
"[ 2/1 → 4/1 | s:sax accelerate:1 ]",
|
||||||
"[ (2/1 → 3/1) ⇝ 4/1 | s:sax accelerate:1 ]",
|
|
||||||
"[ 2/1 ⇜ (3/1 → 4/1) | s:sax accelerate:1 ]",
|
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -956,6 +954,31 @@ exports[`runs examples > example "bank" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "beatCat" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/5 | s:bd ]",
|
||||||
|
"[ 1/5 → 2/5 | s:cp ]",
|
||||||
|
"[ 2/5 → 3/5 | s:bd ]",
|
||||||
|
"[ 3/5 → 4/5 | s:mt ]",
|
||||||
|
"[ 4/5 → 1/1 | s:bd ]",
|
||||||
|
"[ 1/1 → 6/5 | s:bd ]",
|
||||||
|
"[ 6/5 → 7/5 | s:cp ]",
|
||||||
|
"[ 7/5 → 8/5 | s:bd ]",
|
||||||
|
"[ 8/5 → 9/5 | s:mt ]",
|
||||||
|
"[ 9/5 → 2/1 | s:bd ]",
|
||||||
|
"[ 2/1 → 11/5 | s:bd ]",
|
||||||
|
"[ 11/5 → 12/5 | s:cp ]",
|
||||||
|
"[ 12/5 → 13/5 | s:bd ]",
|
||||||
|
"[ 13/5 → 14/5 | s:mt ]",
|
||||||
|
"[ 14/5 → 3/1 | s:bd ]",
|
||||||
|
"[ 3/1 → 16/5 | s:bd ]",
|
||||||
|
"[ 16/5 → 17/5 | s:cp ]",
|
||||||
|
"[ 17/5 → 18/5 | s:bd ]",
|
||||||
|
"[ 18/5 → 19/5 | s:mt ]",
|
||||||
|
"[ 19/5 → 4/1 | s:bd ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "begin" example index 0 1`] = `
|
exports[`runs examples > example "begin" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/2 | s:rave begin:0 ]",
|
"[ 0/1 → 1/2 | s:rave begin:0 ]",
|
||||||
@ -1652,8 +1675,7 @@ exports[`runs examples > example "cpm" example index 0 1`] = `
|
|||||||
"[ 0/1 → 2/3 | s:bd ]",
|
"[ 0/1 → 2/3 | s:bd ]",
|
||||||
"[ 1/3 → 2/3 | s:hh ]",
|
"[ 1/3 → 2/3 | s:hh ]",
|
||||||
"[ 2/3 → 1/1 | s:hh ]",
|
"[ 2/3 → 1/1 | s:hh ]",
|
||||||
"[ (2/3 → 1/1) ⇝ 4/3 | s:sd ]",
|
"[ 2/3 → 4/3 | s:sd ]",
|
||||||
"[ 2/3 ⇜ (1/1 → 4/3) | s:sd ]",
|
|
||||||
"[ 1/1 → 4/3 | s:hh ]",
|
"[ 1/1 → 4/3 | s:hh ]",
|
||||||
"[ 4/3 → 5/3 | s:hh ]",
|
"[ 4/3 → 5/3 | s:hh ]",
|
||||||
"[ 4/3 → 2/1 | s:bd ]",
|
"[ 4/3 → 2/1 | s:bd ]",
|
||||||
@ -1662,8 +1684,7 @@ exports[`runs examples > example "cpm" example index 0 1`] = `
|
|||||||
"[ 2/1 → 8/3 | s:sd ]",
|
"[ 2/1 → 8/3 | s:sd ]",
|
||||||
"[ 7/3 → 8/3 | s:hh ]",
|
"[ 7/3 → 8/3 | s:hh ]",
|
||||||
"[ 8/3 → 3/1 | s:hh ]",
|
"[ 8/3 → 3/1 | s:hh ]",
|
||||||
"[ (8/3 → 3/1) ⇝ 10/3 | s:bd ]",
|
"[ 8/3 → 10/3 | s:bd ]",
|
||||||
"[ 8/3 ⇜ (3/1 → 10/3) | s:bd ]",
|
|
||||||
"[ 3/1 → 10/3 | s:hh ]",
|
"[ 3/1 → 10/3 | s:hh ]",
|
||||||
"[ 10/3 → 11/3 | s:hh ]",
|
"[ 10/3 → 11/3 | s:hh ]",
|
||||||
"[ 10/3 → 4/1 | s:sd ]",
|
"[ 10/3 → 4/1 | s:sd ]",
|
||||||
@ -2152,14 +2173,11 @@ exports[`runs examples > example "early" example index 0 1`] = `
|
|||||||
[
|
[
|
||||||
"[ -1/10 ⇜ (0/1 → 2/5) | s:hh ]",
|
"[ -1/10 ⇜ (0/1 → 2/5) | s:hh ]",
|
||||||
"[ 0/1 → 1/2 | s:bd ]",
|
"[ 0/1 → 1/2 | s:bd ]",
|
||||||
"[ (9/10 → 1/1) ⇝ 7/5 | s:hh ]",
|
"[ 9/10 → 7/5 | s:hh ]",
|
||||||
"[ 9/10 ⇜ (1/1 → 7/5) | s:hh ]",
|
|
||||||
"[ 1/1 → 3/2 | s:bd ]",
|
"[ 1/1 → 3/2 | s:bd ]",
|
||||||
"[ (19/10 → 2/1) ⇝ 12/5 | s:hh ]",
|
"[ 19/10 → 12/5 | s:hh ]",
|
||||||
"[ 19/10 ⇜ (2/1 → 12/5) | s:hh ]",
|
|
||||||
"[ 2/1 → 5/2 | s:bd ]",
|
"[ 2/1 → 5/2 | s:bd ]",
|
||||||
"[ (29/10 → 3/1) ⇝ 17/5 | s:hh ]",
|
"[ 29/10 → 17/5 | s:hh ]",
|
||||||
"[ 29/10 ⇜ (3/1 → 17/5) | s:hh ]",
|
|
||||||
"[ 3/1 → 7/2 | s:bd ]",
|
"[ 3/1 → 7/2 | s:bd ]",
|
||||||
"[ (39/10 → 4/1) ⇝ 22/5 | s:hh ]",
|
"[ (39/10 → 4/1) ⇝ 22/5 | s:hh ]",
|
||||||
]
|
]
|
||||||
@ -2479,10 +2497,8 @@ exports[`runs examples > example "firstOf" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "fit" example index 0 1`] = `
|
exports[`runs examples > example "fit" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ (0/1 → 1/1) ⇝ 2/1 | s:rhodes speed:0.5 unit:c ]",
|
"[ 0/1 → 2/1 | s:rhodes speed:0.5 unit:c ]",
|
||||||
"[ 0/1 ⇜ (1/1 → 2/1) | s:rhodes speed:0.5 unit:c ]",
|
"[ 2/1 → 4/1 | s:rhodes speed:0.5 unit:c ]",
|
||||||
"[ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.5 unit:c ]",
|
|
||||||
"[ 2/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.5 unit:c ]",
|
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -2863,6 +2879,8 @@ exports[`runs examples > example "gain" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "hpattack" example index 0 1`] = `
|
exports[`runs examples > example "hpattack" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
|
"[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]",
|
||||||
@ -3130,11 +3148,9 @@ exports[`runs examples > example "hpsustain" example index 0 1`] = `
|
|||||||
exports[`runs examples > example "hurry" example index 0 1`] = `
|
exports[`runs examples > example "hurry" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 3/4 | s:bd speed:1 ]",
|
"[ 0/1 → 3/4 | s:bd speed:1 ]",
|
||||||
"[ (3/4 → 1/1) ⇝ 3/2 | s:sd n:2 speed:1 ]",
|
"[ 3/4 → 3/2 | s:sd n:2 speed:1 ]",
|
||||||
"[ 3/4 ⇜ (1/1 → 3/2) | s:sd n:2 speed:1 ]",
|
|
||||||
"[ 3/2 → 15/8 | s:bd speed:2 ]",
|
"[ 3/2 → 15/8 | s:bd speed:2 ]",
|
||||||
"[ (15/8 → 2/1) ⇝ 9/4 | s:sd n:2 speed:2 ]",
|
"[ 15/8 → 9/4 | s:sd n:2 speed:2 ]",
|
||||||
"[ 15/8 ⇜ (2/1 → 9/4) | s:sd n:2 speed:2 ]",
|
|
||||||
"[ 9/4 → 21/8 | s:bd speed:2 ]",
|
"[ 9/4 → 21/8 | s:bd speed:2 ]",
|
||||||
"[ 21/8 → 3/1 | s:sd n:2 speed:2 ]",
|
"[ 21/8 → 3/1 | s:sd n:2 speed:2 ]",
|
||||||
"[ 3/1 → 51/16 | s:bd speed:4 ]",
|
"[ 3/1 → 51/16 | s:bd speed:4 ]",
|
||||||
@ -3741,10 +3757,8 @@ exports[`runs examples > example "loop" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "loopAt" example index 0 1`] = `
|
exports[`runs examples > example "loopAt" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ (0/1 → 1/1) ⇝ 2/1 | s:rhodes speed:0.25 unit:c ]",
|
"[ 0/1 → 2/1 | s:rhodes speed:0.25 unit:c ]",
|
||||||
"[ 0/1 ⇜ (1/1 → 2/1) | s:rhodes speed:0.25 unit:c ]",
|
"[ 2/1 → 4/1 | s:rhodes speed:0.25 unit:c ]",
|
||||||
"[ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]",
|
|
||||||
"[ 2/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.25 unit:c ]",
|
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -4214,10 +4228,8 @@ exports[`runs examples > example "never" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "noise" example index 0 1`] = `
|
exports[`runs examples > example "noise" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ (0/1 → 1/1) ⇝ 2/1 | s:white ]",
|
"[ 0/1 → 2/1 | s:white ]",
|
||||||
"[ 0/1 ⇜ (1/1 → 2/1) | s:white ]",
|
"[ 2/1 → 4/1 | s:pink ]",
|
||||||
"[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]",
|
|
||||||
"[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]",
|
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -4939,55 +4951,67 @@ exports[`runs examples > example "ply" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/3 | note:c ]",
|
"[ 0/1 → 1/3 | c ]",
|
||||||
"[ 0/1 → 1/3 | note:c2 ]",
|
"[ 0/1 → 1/3 | c2 ]",
|
||||||
"[ 1/3 → 2/3 | note:eb ]",
|
"[ 1/3 → 2/3 | eb ]",
|
||||||
"[ 1/3 → 2/3 | note:g2 ]",
|
"[ 1/3 → 2/3 | g2 ]",
|
||||||
"[ 2/3 → 1/1 | note:g ]",
|
"[ 2/3 → 1/1 | g ]",
|
||||||
"[ 2/3 → 1/1 | note:c2 ]",
|
"[ 2/3 → 1/1 | c2 ]",
|
||||||
"[ 1/1 → 4/3 | note:c ]",
|
"[ 1/1 → 4/3 | c ]",
|
||||||
"[ 1/1 → 4/3 | note:g2 ]",
|
"[ 1/1 → 4/3 | g2 ]",
|
||||||
"[ 4/3 → 5/3 | note:eb ]",
|
"[ 4/3 → 5/3 | eb ]",
|
||||||
"[ 4/3 → 5/3 | note:c2 ]",
|
"[ 4/3 → 5/3 | c2 ]",
|
||||||
"[ 5/3 → 2/1 | note:g ]",
|
"[ 5/3 → 2/1 | g ]",
|
||||||
"[ 5/3 → 2/1 | note:g2 ]",
|
"[ 5/3 → 2/1 | g2 ]",
|
||||||
"[ 2/1 → 7/3 | note:c ]",
|
"[ 2/1 → 7/3 | c ]",
|
||||||
"[ 2/1 → 7/3 | note:c2 ]",
|
"[ 2/1 → 7/3 | c2 ]",
|
||||||
"[ 7/3 → 8/3 | note:eb ]",
|
"[ 7/3 → 8/3 | eb ]",
|
||||||
"[ 7/3 → 8/3 | note:g2 ]",
|
"[ 7/3 → 8/3 | g2 ]",
|
||||||
"[ 8/3 → 3/1 | note:g ]",
|
"[ 8/3 → 3/1 | g ]",
|
||||||
"[ 8/3 → 3/1 | note:c2 ]",
|
"[ 8/3 → 3/1 | c2 ]",
|
||||||
"[ 3/1 → 10/3 | note:c ]",
|
"[ 3/1 → 10/3 | c ]",
|
||||||
"[ 3/1 → 10/3 | note:g2 ]",
|
"[ 3/1 → 10/3 | g2 ]",
|
||||||
"[ 10/3 → 11/3 | note:eb ]",
|
"[ 10/3 → 11/3 | eb ]",
|
||||||
"[ 10/3 → 11/3 | note:c2 ]",
|
"[ 10/3 → 11/3 | c2 ]",
|
||||||
"[ 11/3 → 4/1 | note:g ]",
|
"[ 11/3 → 4/1 | g ]",
|
||||||
"[ 11/3 → 4/1 | note:g2 ]",
|
"[ 11/3 → 4/1 | g2 ]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
|
exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c ]",
|
"[ 0/1 → 1/4 | c ]",
|
||||||
"[ 0/1 → 1/1 | s:bd ]",
|
"[ 0/1 → 1/4 | e ]",
|
||||||
"[ 1/4 → 1/2 | note:d ]",
|
"[ 1/4 → 1/2 | d ]",
|
||||||
"[ 1/2 → 3/4 | note:e ]",
|
"[ 1/4 → 1/2 | f ]",
|
||||||
"[ 3/4 → 1/1 | note:c ]",
|
"[ 1/2 → 3/4 | c ]",
|
||||||
"[ 1/1 → 5/4 | note:d ]",
|
"[ 1/2 → 3/4 | g ]",
|
||||||
"[ 1/1 → 2/1 | s:bd ]",
|
"[ 3/4 → 1/1 | d ]",
|
||||||
"[ 5/4 → 3/2 | note:e ]",
|
"[ 3/4 → 1/1 | e ]",
|
||||||
"[ 3/2 → 7/4 | note:c ]",
|
"[ 1/1 → 5/4 | c ]",
|
||||||
"[ 7/4 → 2/1 | note:d ]",
|
"[ 1/1 → 5/4 | f ]",
|
||||||
"[ 2/1 → 9/4 | note:e ]",
|
"[ 5/4 → 3/2 | d ]",
|
||||||
"[ 2/1 → 3/1 | s:bd ]",
|
"[ 5/4 → 3/2 | g ]",
|
||||||
"[ 9/4 → 5/2 | note:c ]",
|
"[ 3/2 → 7/4 | c ]",
|
||||||
"[ 5/2 → 11/4 | note:d ]",
|
"[ 3/2 → 7/4 | e ]",
|
||||||
"[ 11/4 → 3/1 | note:e ]",
|
"[ 7/4 → 2/1 | d ]",
|
||||||
"[ 3/1 → 13/4 | note:c ]",
|
"[ 7/4 → 2/1 | f ]",
|
||||||
"[ 3/1 → 4/1 | s:bd ]",
|
"[ 2/1 → 9/4 | c ]",
|
||||||
"[ 13/4 → 7/2 | note:d ]",
|
"[ 2/1 → 9/4 | g ]",
|
||||||
"[ 7/2 → 15/4 | note:e ]",
|
"[ 9/4 → 5/2 | d ]",
|
||||||
"[ 15/4 → 4/1 | note:c ]",
|
"[ 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 ]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -5537,6 +5561,27 @@ exports[`runs examples > example "rev" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "reweight" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd ]",
|
||||||
|
"[ 1/4 → 1/2 | s:sd ]",
|
||||||
|
"[ 1/2 → 3/4 | s:cp ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd ]",
|
||||||
|
"[ 1/1 → 5/4 | s:sd ]",
|
||||||
|
"[ 5/4 → 3/2 | s:cp ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd ]",
|
||||||
|
"[ 7/4 → 2/1 | s:sd ]",
|
||||||
|
"[ 2/1 → 9/4 | s:cp ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd ]",
|
||||||
|
"[ 5/2 → 11/4 | s:sd ]",
|
||||||
|
"[ 11/4 → 3/1 | s:cp ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd ]",
|
||||||
|
"[ 13/4 → 7/2 | s:sd ]",
|
||||||
|
"[ 7/2 → 15/4 | s:cp ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "ribbon" example index 0 1`] = `
|
exports[`runs examples > example "ribbon" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/2 | note:d ]",
|
"[ 0/1 → 1/2 | note:d ]",
|
||||||
@ -6464,8 +6509,7 @@ exports[`runs examples > example "slice" example index 0 1`] = `
|
|||||||
"[ 3/4 → 27/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
|
"[ 3/4 → 27/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
|
||||||
"[ 27/32 → 15/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
|
"[ 27/32 → 15/16 | begin:0.125 end:0.25 _slices:8 s:breaks165 ]",
|
||||||
"[ 15/16 → 63/64 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
|
"[ 15/16 → 63/64 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
|
||||||
"[ (63/64 → 1/1) ⇝ 33/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
|
"[ 63/64 → 33/32 | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
|
||||||
"[ 63/64 ⇜ (1/1 → 33/32) | begin:0.25 end:0.375 _slices:8 s:breaks165 ]",
|
|
||||||
"[ 33/32 → 9/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
|
"[ 33/32 → 9/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
|
||||||
"[ 9/8 → 75/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
|
"[ 9/8 → 75/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
|
||||||
"[ 75/64 → 39/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
|
"[ 75/64 → 39/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
|
||||||
@ -6478,8 +6522,7 @@ exports[`runs examples > example "slice" example index 0 1`] = `
|
|||||||
"[ 57/32 → 15/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
|
"[ 57/32 → 15/8 | begin:0.375 end:0.5 _slices:8 s:breaks165 ]",
|
||||||
"[ 15/8 → 123/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
|
"[ 15/8 → 123/64 | begin:0.5 end:0.625 _slices:8 s:breaks165 ]",
|
||||||
"[ 123/64 → 63/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
|
"[ 123/64 → 63/32 | begin:0 end:0.125 _slices:8 s:breaks165 ]",
|
||||||
"[ (63/32 → 2/1) ⇝ 33/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
|
"[ 63/32 → 33/16 | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
|
||||||
"[ 63/32 ⇜ (2/1 → 33/16) | begin:0.625 end:0.75 _slices:8 s:breaks165 ]",
|
|
||||||
"[ 33/16 → 69/32 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]",
|
"[ 33/16 → 69/32 | begin:0.75 end:0.875 _slices:8 s:breaks165 ]",
|
||||||
"[ 69/32 → 9/4 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
|
"[ 69/32 → 9/4 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
|
||||||
"[ 9/4 → 75/32 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
|
"[ 9/4 → 75/32 | begin:0.875 end:1 _slices:8 s:breaks165 ]",
|
||||||
@ -7178,6 +7221,31 @@ exports[`runs examples > example "timeCat" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "timeCat" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/5 | s:bd ]",
|
||||||
|
"[ 1/5 → 2/5 | s:sd ]",
|
||||||
|
"[ 2/5 → 3/5 | s:cp ]",
|
||||||
|
"[ 3/5 → 4/5 | s:hh ]",
|
||||||
|
"[ 4/5 → 1/1 | s:hh ]",
|
||||||
|
"[ 1/1 → 6/5 | s:bd ]",
|
||||||
|
"[ 6/5 → 7/5 | s:sd ]",
|
||||||
|
"[ 7/5 → 8/5 | s:cp ]",
|
||||||
|
"[ 8/5 → 9/5 | s:hh ]",
|
||||||
|
"[ 9/5 → 2/1 | s:hh ]",
|
||||||
|
"[ 2/1 → 11/5 | s:bd ]",
|
||||||
|
"[ 11/5 → 12/5 | s:sd ]",
|
||||||
|
"[ 12/5 → 13/5 | s:cp ]",
|
||||||
|
"[ 13/5 → 14/5 | s:hh ]",
|
||||||
|
"[ 14/5 → 3/1 | s:hh ]",
|
||||||
|
"[ 3/1 → 16/5 | s:bd ]",
|
||||||
|
"[ 16/5 → 17/5 | s:sd ]",
|
||||||
|
"[ 17/5 → 18/5 | s:cp ]",
|
||||||
|
"[ 18/5 → 19/5 | s:hh ]",
|
||||||
|
"[ 19/5 → 4/1 | s:hh ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "transpose" example index 0 1`] = `
|
exports[`runs examples > example "transpose" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:C2 ]",
|
"[ 0/1 → 1/4 | note:C2 ]",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -72,6 +72,7 @@ export function Repl({ embedded = false }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const editor = new StrudelMirror({
|
const editor = new StrudelMirror({
|
||||||
|
sync: false,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime: () => getAudioContext().currentTime,
|
getTime: () => getAudioContext().currentTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user