Compare commits

..

1 Commits

Author SHA1 Message Date
Karol Libardi 3daacba2c3 Added ability to mute a synth/sound after it was triggered 2024-03-04 10:27:55 +01:00
244 changed files with 11727 additions and 15687 deletions
+1 -2
View File
@@ -21,5 +21,4 @@ vite.config.js
/src-tauri/target/**/*
reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
packages/node/pattern.mjs
jsdoc-synonyms.js
-3
View File
@@ -127,6 +127,3 @@ fabric.properties
.idea/caches/build_file_checksums.ser
# END JetBrains -> BEGIN JetBrains
samples/*
!samples/README.md
-1
View File
@@ -11,4 +11,3 @@ pnpm-lock.yaml
pnpm-workspace.yaml
**/dev-dist
website/.astro
packages/node/pattern.mjs
+14 -4
View File
@@ -15,15 +15,25 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
```bash
pnpm i
pnpm dev
pnpm run setup
pnpm run repl
```
## Using Strudel In Your Project
This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel).
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
- [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`transpiler`](./packages/transpiler): user code transpiler
- [`webaudio`](./packages/webaudio): webaudio output
- [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions
- ... [and there are more](./packages/)
Click on the package names to find out more about each one.
## Contributing
+4 -3
View File
@@ -1,9 +1,10 @@
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button>
<button id="stop">stop</button>
<script>
strudel.initStrudel();
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush());
</script>
@@ -1,10 +1,10 @@
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="a">A</button>
<button id="b">B</button>
<button id="c">C</button>
<button id="stop">stop</button>
<script>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'),
});
+2 -7
View File
@@ -1,20 +1,15 @@
// this barrel export is currently only used to find undocumented exports
export * from './packages/codemirror/index.mjs';
export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/draw/index.mjs';
export * from './packages/embed/index.mjs';
export * from './packages/hydra/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs';
export * from './packages/repl/index.mjs';
export * from './packages/react/index.mjs';
export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs';
export * from './packages/superdough/index.mjs';
export * from './packages/tonal/index.mjs';
export * from './packages/transpiler/index.mjs';
export * from './packages/web/index.mjs';
export * from './packages/webaudio/index.mjs';
export * from './packages/xen/index.mjs';
+2 -3
View File
@@ -25,7 +25,6 @@
"format-check": "prettier --check .",
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
"check": "npm run format-check && npm run lint && npm run test",
"sampler": "cd samples && node ../packages/sampler/sample-server.mjs",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
},
"repository": {
@@ -54,10 +53,10 @@
"@strudel/xen": "workspace:*"
},
"devDependencies": {
"dependency-tree": "^10.0.9",
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0",
"acorn": "^8.11.3",
"dependency-tree": "^10.0.9",
"canvas": "^2.11.2",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"events": "^3.3.0",
+16 -31
View File
@@ -12,22 +12,19 @@ import {
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { Pattern, repl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel/core';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
import { persistentAtom } from '@nanostores/persistent';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
isAutoCompletionEnabled,
@@ -42,7 +39,6 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke
export const defaultSettings = {
keybindings: 'codemirror',
isBracketMatchingEnabled: false,
isBracketClosingEnabled: true,
isLineNumbersDisplayed: true,
isActiveLineHighlighted: false,
isAutoCompletionEnabled: false,
@@ -75,9 +71,9 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
...initialSettings,
javascript(),
sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
closeBrackets(),
syntaxHighlighting(defaultHighlightStyle),
history(),
EditorView.updateListener.of((v) => onChange(v)),
@@ -129,7 +125,6 @@ export class StrudelMirror {
id,
initialCode = '',
onDraw,
drawContext,
drawTime = [0, 0],
autodraw,
prebake,
@@ -142,16 +137,23 @@ export class StrudelMirror {
this.widgets = [];
this.painters = [];
this.drawTime = drawTime;
this.drawContext = drawContext;
this.onDraw = onDraw || this.draw;
this.onDraw = onDraw;
const self = this;
this.id = id || s4();
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => hap.isActive(time));
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time);
this.onDraw(haps, time, this.painters);
this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime);
// this approach does not work with multiple repls on screen
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake();
autodraw && this.drawFirstFrame();
@@ -177,14 +179,6 @@ export class StrudelMirror {
beforeEval: async () => {
cleanupDraw();
this.painters = [];
const self = this;
// this is similar to repl.mjs > injectPatternMethods
// maybe there is a solution without prototype hacking, but hey, it works
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
await this.prebaked;
await replOptions?.beforeEval?.();
},
@@ -192,10 +186,7 @@ export class StrudelMirror {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.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);
updateWidgets(this.editor, this.widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.adjustDrawTime();
@@ -239,9 +230,6 @@ export class StrudelMirror {
// when no painters are set, [0,0] is enough (just highlighting)
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
}
draw(haps, time) {
this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
}
async drawFirstFrame() {
if (!this.onDraw) {
return;
@@ -252,7 +240,7 @@ export class StrudelMirror {
await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler, -0.001);
// draw at -0.001 to avoid haps at 0 to be visualized as active
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters);
this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters);
} catch (err) {
console.warn('first frame could not be painted');
}
@@ -310,9 +298,6 @@ export class StrudelMirror {
setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
}
setBracketClosingEnabled(enabled) {
this.reconfigureExtension('isBracketClosingEnabled', enabled);
}
setTheme(theme) {
this.reconfigureExtension('theme', theme);
}
+1 -1
View File
@@ -92,7 +92,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.value?.color ?? 'var(--foreground)';
const color = hap.context.color ?? 'var(--foreground)';
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
-1
View File
@@ -3,4 +3,3 @@ export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
export * from './themes.mjs';
export * from './widget.mjs';
-2
View File
@@ -45,8 +45,6 @@
"@replit/codemirror-vim": "^6.1.0",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5"
+13 -15
View File
@@ -1,6 +1,6 @@
import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect } from '@codemirror/state';
import { StateEffect, StateField } from '@codemirror/state';
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
@@ -60,21 +60,19 @@ export class SliderWidget extends WidgetType {
}
}
export const setSliderWidgets = StateEffect.define();
export const setWidgets = StateEffect.define();
export const updateSliderWidgets = (view, widgets) => {
view.dispatch({ effects: setSliderWidgets.of(widgets) });
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setWidgets.of(widgets) });
};
function getSliders(widgetConfigs, view) {
return widgetConfigs
.filter((w) => w.type === 'slider')
.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
function getWidgets(widgetConfigs, view) {
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
@@ -101,8 +99,8 @@ export const sliderPlugin = ViewPlugin.fromClass(
}
}
for (let e of tr.effects) {
if (e.is(setSliderWidgets)) {
this.decorations = Decoration.set(getSliders(e.value, update.view));
if (e.is(setWidgets)) {
this.decorations = Decoration.set(getWidgets(e.value, update.view));
}
}
});
-2
View File
@@ -37,7 +37,6 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
import teletext, { settings as teletextSettings } from './themes/teletext';
import algoboy, { settings as algoboySettings } from './themes/algoboy';
import terminal, { settings as terminalSettings } from './themes/terminal';
import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
@@ -514,7 +513,6 @@ export function activateTheme(name) {
.map(([key, value]) => `--${key}: ${value} !important;`)
.join('\n')}
}`;
setTheme(themeSettings);
// tailwind dark mode
if (themeSettings.light) {
document.documentElement.classList.remove('dark');
-1
View File
@@ -18,7 +18,6 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: '#0f380f' },
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
-1
View File
@@ -15,7 +15,6 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
-1
View File
@@ -18,7 +18,6 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
-1
View File
@@ -15,7 +15,6 @@ export default createTheme({
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
-1
View File
@@ -27,7 +27,6 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: colorB },
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
-1
View File
@@ -14,7 +14,6 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: '#41FF00' },
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
-1
View File
@@ -16,7 +16,6 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: 'black' },
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
-135
View File
@@ -1,135 +0,0 @@
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.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
});
registerWidget('_punchcard', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
});
registerWidget('_spiral', (id, options = {}, pat) => {
let _size = options.size || 275;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).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.tag(id).scope({ ...options, ctx, id });
});
registerWidget('_pitchwheel', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pitchwheel({ ...options, ctx, id });
});
@@ -1,14 +1,11 @@
import { Pattern, silence, register, pure, createParams } from '@strudel/core';
import { getDrawContext } from './draw.mjs';
import { Pattern, getDrawContext, silence, register, pure, createParams } from './index.mjs';
let clearColor = '#22222210';
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
window.frame && cancelAnimationFrame(window.frame);
const ctx = getDrawContext();
let { clientWidth: ww, clientHeight: wh } = ctx.canvas;
ww *= window.devicePixelRatio;
wh *= window.devicePixelRatio;
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
clearColor = `#200010${smearPart}`;
-168
View File
@@ -1,168 +0,0 @@
// eslint-disable-next-line no-undef
// 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 num_seconds_at_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) => {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
const tickdeadline = phase - time;
const lastTick = time + tickdeadline;
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;
const eventLength = duration * cps;
const end = begin + eventLength;
const cycle = begin + secondsSinceLastTick * cps;
sendMessage('tick', {
begin,
end,
cps,
tickdeadline,
num_cycles_at_cps_change,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
cycle,
});
num_ticks_since_cps_change++;
};
//create clock method from zyklus
const clock = 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) {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
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;
}
}
};
self.onconnect = function (e) {
// the incoming port
const port = e.ports[0];
port.addEventListener('message', function (e) {
processMessage(e.data);
});
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
};
// used to consistently schedule events, for use in a service worker - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
function 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 };
}
+29 -89
View File
@@ -7,33 +7,26 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, register, sequence } from './pattern.mjs';
export function createParam(names) {
let isMulti = Array.isArray(names);
names = !isMulti ? [names] : names;
const name = names[0];
const name = Array.isArray(names) ? names[0] : names;
const withVal = (xs) => {
let bag;
// check if we have an object with an unnamed control (.value)
if (typeof xs === 'object' && xs.value !== undefined) {
bag = { ...xs }; // grab props that are already there
xs = xs.value; // grab the unnamed control for this one
delete bag.value;
}
if (isMulti && Array.isArray(xs)) {
const result = bag || {};
xs.forEach((x, i) => {
if (i < names.length) {
result[names[i]] = x;
}
});
return result;
} else if (bag) {
bag[name] = xs;
return bag;
} else {
return { [name]: xs };
}
};
var withVal;
if (Array.isArray(names)) {
withVal = (xs) => {
if (Array.isArray(xs)) {
const result = {};
xs.forEach((x, i) => {
if (i < names.length) {
result[names[i]] = x;
}
});
return result;
} else {
return { [name]: xs };
}
};
} else {
withVal = (x) => ({ [name]: x });
}
const func = (...pats) => sequence(...pats).withValue(withVal);
@@ -126,16 +119,6 @@ export const { note } = registerControl(['note', 'n']);
*
*/
export const { accelerate } = registerControl('accelerate');
/**
*
* Sets the velocity from 0 to 1. Is multiplied together with gain.
* @name velocity
* @example
* s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1")
* .velocity(".4 1")
*/
export const { velocity } = registerControl('velocity');
/**
* Controls the gain by an exponential amount.
*
@@ -895,37 +878,17 @@ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt',
*/
export const { lock } = registerControl('lock');
/**
* Set detune for stacked voices of supported oscillators
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
*
* @name detune
* @param {number | Pattern} amount
* @param {number | Pattern} amount between 0 and 1
* @synonyms det
* @superdirtOnly
* @example
* note("d f a a# a d3").fast(2).s("supersaw").detune("<.1 .2 .5 24.1>")
* n("0 3 7").s('superzow').octave(3).detune("<0 .25 .5 1 2>").osc()
*
*/
export const { detune, det } = registerControl('detune', 'det');
/**
* Set number of stacked voices for supported oscillators
*
* @name unison
* @param {number | Pattern} numvoices
* @example
* note("d f a a# a d3").fast(2).s("supersaw").unison("<1 2 7>")
*
*/
export const { unison } = registerControl('unison');
/**
* Set the stereo pan spread for supported oscillators
*
* @name spread
* @param {number | Pattern} spread between 0 and 1
* @example
* note("d f a a# a d3").fast(2).s("supersaw").spread("<0 .3 1>")
*
*/
export const { spread } = registerControl('spread');
/**
* Set dryness of reverb. See `room` and `size` for more information about reverb.
*
@@ -1194,9 +1157,11 @@ export const { rate } = registerControl('rate');
export const { slide } = registerControl('slide');
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
export const { semitone } = registerControl('semitone');
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['velocity'],
// TODO: synth param
export const { voice } = registerControl('voice');
// voicings // https://github.com/tidalcycles/strudel/issues/506
// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
export const { chord } = registerControl('chord');
@@ -1298,10 +1263,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
// ['sclaves'],
// ['scrash'],
/**
* (Deprecated) Wave shaping distortion. WARNING: can suddenly get unpredictably loud.
* Please use distort instead, which has a more predictable response curve
* second option in optional array syntax (ex: ".9:.5") applies a postgain to the output
*
* Wave shaping distortion. CAUTION: it might get loud
*
* @name shape
* @param {number | Pattern} distortion between 0 and 1
@@ -1309,22 +1271,7 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
* s("bd sd [~ bd] sd,hh*8").shape("<0 .2 .4 .6 .8>")
*
*/
export const { shape } = registerControl(['shape', 'shapevol']);
/**
* Wave shaping distortion. CAUTION: it can get loud.
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
*
* @name distort
* @synonyms dist
* @param {number | Pattern} distortion
* @example
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
* @example
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
*
*/
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
export const { shape } = registerControl('shape');
/**
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
@@ -1458,6 +1405,7 @@ export const { octersubsub } = registerControl('octersubsub');
export const { ring } = registerControl('ring');
export const { ringf } = registerControl('ringf');
export const { ringdf } = registerControl('ringdf');
export const { distort } = registerControl('distort');
export const { freeze } = registerControl('freeze');
export const { xsdelay } = registerControl('xsdelay');
export const { tsdelay } = registerControl('tsdelay');
@@ -1532,14 +1480,6 @@ export const { zdelay } = registerControl('zdelay');
export const { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx');
/**
* Sets the color of the hap in visualizations like pianoroll or highlighting.
* @name color
* @synonyms colour
* @param {string} color Hexadecimal or CSS color name
*/
export const { color, colour } = registerControl(['color', 'colour']);
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
export let createParams = (...names) =>
+20 -33
View File
@@ -1,5 +1,5 @@
/*
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>
cyclist.mjs - <short description TODO>
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/>.
*/
@@ -8,7 +8,7 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
@@ -16,47 +16,41 @@ export class Cyclist {
this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed
this.num_cycles_since_last_cps_change = 0;
this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset
this.clock = createClock(
getTime,
// called slightly before each cycle
(phase, duration, _, t) => {
(phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_at_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
this.num_cycles_since_last_cps_change = this.lastEnd;
}
this.num_ticks_since_cps_change++;
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
try {
const time = getTime();
const begin = this.lastEnd;
this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
if (phase < t) {
// avoid querying haps that are in the past anyway
console.log(`skip query: too late`);
return;
}
//convert ticks to cycles, so you can query the pattern for events
const eventLength = duration * this.cps;
const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
this.lastEnd = end;
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const tickdeadline = phase - time; // time left until the phase is a whole number
this.lastTick = time + tickdeadline;
haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
const duration = hap.duration / this.cps;
// the following line is dumb and only here for backwards compatibility
// see https://github.com/tidalcycles/strudel/pull/1004
const deadline = targetTime - phase;
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
onTrigger?.(hap, deadline, duration, this.cps);
}
});
} catch (e) {
@@ -65,16 +59,9 @@ export class Cyclist {
}
},
interval, // duration of each cycle
0.1,
0.1,
setInterval,
clearInterval,
);
}
now() {
if (!this.started) {
return 0;
}
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
}
@@ -84,7 +71,7 @@ export class Cyclist {
}
start() {
this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0;
this.num_cycles_since_last_cps_change = 0;
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
@@ -1,88 +1,80 @@
/*
draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/draw.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.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 { Pattern, getTime, State, TimeSpan } from '@strudel/core';
import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas', options) => {
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id);
if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = window.innerWidth * pixelRatio;
canvas.height = window.innerHeight * pixelRatio;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
pixelated && (canvas.style.imageRendering = 'pixelated');
document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth * pixelRatio;
canvas.height = window.innerHeight * pixelRatio;
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
}, 200);
});
}
return canvas.getContext(contextType);
return canvas.getContext('2d');
};
let animationFrames = {};
function stopAnimationFrame(id) {
if (animationFrames[id] !== undefined) {
cancelAnimationFrame(animationFrames[id]);
delete animationFrames[id];
}
}
function stopAllAnimations() {
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id));
}
let memory = {};
Pattern.prototype.draw = function (fn, options) {
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
if (typeof window === 'undefined') {
return this;
}
let { id = 1, lookbehind = 0, lookahead = 0 } = options;
let __t = Math.max(getTime(), 0);
stopAnimationFrame(id);
lookbehind = Math.abs(lookbehind);
// init memory, clear future haps of old pattern
memory[id] = (memory[id] || []).filter((h) => !h.isInFuture(__t));
let newFuture = this.queryArc(__t, __t + lookahead).filter((h) => h.hasOnset());
memory[id] = memory[id].concat(newFuture);
let last;
const animate = () => {
const _t = getTime();
const t = _t + lookahead;
// filter out haps that are too far in the past
memory[id] = memory[id].filter((h) => h.isInNearPast(lookbehind, _t));
// begin where we left off in last frame, but max -0.1s (inactive tab throttles to 1fps)
let begin = Math.max(last || t, t - 1 / 10);
const haps = this.queryArc(begin, t).filter((h) => h.hasOnset());
memory[id] = memory[id].concat(haps);
last = t; // makes sure no haps are missed
fn(memory[id], _t, t, this);
animationFrames[id] = requestAnimationFrame(animate);
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
const ctx = getDrawContext();
let cycle,
events = [];
const animate = (time) => {
const t = getTime();
if (from !== undefined && to !== undefined) {
const currentCycle = Math.floor(t);
if (cycle !== currentCycle) {
cycle = currentCycle;
const begin = currentCycle + from;
const end = currentCycle + to;
setTimeout(() => {
events = this.query(new State(new TimeSpan(begin, end)))
.filter(Boolean)
.filter((event) => event.part.begin.equals(event.whole.begin));
onQuery?.(events);
}, 0);
}
}
callback(ctx, events, t, time);
window.strudelAnimation = requestAnimationFrame(animate);
};
animationFrames[id] = requestAnimationFrame(animate);
requestAnimationFrame(animate);
return this;
};
export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
stopAllAnimations();
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
if (window.strudelScheduler) {
clearInterval(window.strudelScheduler);
}
};
Pattern.prototype.onPaint = function () {
console.warn('[draw] onPaint was not overloaded. Some drawings might not work');
Pattern.prototype.onPaint = function (onPaint) {
// this is evil! TODO: add pattern.context
this.context = { onPaint };
return this;
};
@@ -183,18 +175,3 @@ export class Drawer {
}
}
}
export function getComputedPropertyValue(name) {
if (typeof window === 'undefined') {
return '#fff';
}
return getComputedStyle(document.documentElement).getPropertyValue(name);
}
let theme = {};
export function getTheme() {
return theme;
}
export function setTheme(_theme) {
theme = _theme;
}
-1
View File
@@ -22,7 +22,6 @@ export const evalScope = async (...args) => {
globalThis[name] = value;
});
});
return modules;
};
function safeEval(str, options = {}) {
-43
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js';
import { TimeSpan } from './timespan.mjs';
import { removeUndefineds } from './util.mjs';
// Returns the start of the cycle.
Fraction.prototype.sam = function () {
@@ -48,39 +47,14 @@ Fraction.prototype.eq = function (other) {
return this.compare(other) == 0;
};
Fraction.prototype.ne = function (other) {
return this.compare(other) != 0;
};
Fraction.prototype.max = function (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) {
return this.lt(other) ? this : other;
};
Fraction.prototype.mulmaybe = function (other) {
return other !== undefined ? this.mul(other) : undefined;
};
Fraction.prototype.divmaybe = function (other) {
return other !== undefined ? this.div(other) : undefined;
};
Fraction.prototype.addmaybe = function (other) {
return other !== undefined ? this.add(other) : undefined;
};
Fraction.prototype.submaybe = function (other) {
return other !== undefined ? this.sub(other) : undefined;
};
Fraction.prototype.show = function (/* excludeWhole = false */) {
// return this.toFraction(excludeWhole);
return this.s * this.n + '/' + this.d;
@@ -106,26 +80,9 @@ const fraction = (n) => {
};
export const gcd = (...fractions) => {
fractions = removeUndefineds(fractions);
if (fractions.length === 0) {
return undefined;
}
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
};
export const lcm = (...fractions) => {
fractions = removeUndefineds(fractions);
if (fractions.length === 0) {
return undefined;
}
return fractions.reduce(
(lcm, fraction) => (lcm === undefined || fraction === undefined ? undefined : lcm.lcm(fraction)),
fraction(1),
);
};
fraction._original = Fraction;
export default fraction;
-25
View File
@@ -49,27 +49,6 @@ export class Hap {
return this.whole.begin.add(this.duration);
}
isActive(currentTime) {
return this.whole.begin <= currentTime && this.endClipped >= currentTime;
}
isInPast(currentTime) {
return currentTime > this.endClipped;
}
isInNearPast(margin, currentTime) {
return currentTime - margin <= this.endClipped;
}
isInFuture(currentTime) {
return currentTime < this.whole.begin;
}
isInNearFuture(margin, currentTime) {
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin;
}
isWithinTime(min, max) {
return this.whole.begin <= max && this.endClipped >= min;
}
wholeOrPart() {
return this.whole ? this.whole : this.part;
}
@@ -91,10 +70,6 @@ export class Hap {
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
}
hasTag(tag) {
return this.context.tags?.includes(tag);
}
resolveState(state) {
if (this.stateful && this.hasOnset()) {
console.log('stateful');
+5 -2
View File
@@ -7,9 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th
import * as controls from './controls.mjs'; // legacy
export * from './euclid.mjs';
import Fraction from './fraction.mjs';
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export { Fraction, controls, createClock };
export { Fraction, controls };
export * from './controls.mjs';
export * from './hap.mjs';
export * from './pattern.mjs';
@@ -23,6 +22,10 @@ export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './draw.mjs';
export * from './animate.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails)
-147
View File
@@ -1,147 +0,0 @@
/*
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.time_at_last_tick_message = 0;
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_time_dif;
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
this.worker.port.start();
this.channel = new BroadcastChannel('strudeltick');
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 = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
if (this.worker_time_dif == null) {
this.worker_time_dif = time_dif;
} else {
const w = 1; //weight of new time diff;
const new_dif =
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
if (new_dif != this.worker_time_dif) {
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
weight = 4;
}
this.worker_time_dif = new_dif;
}
weight = Math.min(weight + 1, maxWeight);
};
const tickCallback = (payload) => {
const {
num_cycles_at_cps_change,
cps,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
begin,
end,
tickdeadline,
cycle,
} = payload;
this.cps = cps;
this.cycle = cycle;
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
this.time_at_last_tick_message = this.getTime();
};
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
seconds_at_cps_change +
this.latency +
this.worker_time_dif;
const duration = hap.duration / this.cps;
onTrigger?.(hap, 0, duration, this.cps, targetTime);
}
});
};
// 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() {
this.worker_time_dif = null;
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('')}`);
}
}
+234 -777
View File
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,10 @@
/*
pianoroll.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/pianoroll.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.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 { Pattern, noteToMidi, freqToMidi } from '@strudel/core';
import { getTheme, getDrawContext } from './draw.mjs';
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => {
@@ -19,13 +18,7 @@ const getValue = (e) => {
}
note = note ?? n;
if (typeof note === 'string') {
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;
}
return noteToMidi(note);
}
if (typeof note === 'number') {
return note;
@@ -37,24 +30,25 @@ const getValue = (e) => {
};
Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options;
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to);
this.draw(
(haps, time) => {
(ctx, haps, t) => {
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
pianoroll({
...options,
time,
time: t,
ctx,
haps: haps.filter((hap) => inFrame(hap, time)),
haps: haps.filter(inFrame),
});
},
{
lookbehind: from - overscan,
lookahead: to + overscan,
id,
from: from - overscan,
to: to + overscan,
},
);
return this;
@@ -104,8 +98,11 @@ export function pianoroll({
flipTime = 0,
flipValues = 0,
hideNegative = false,
inactive = getTheme().foreground,
active = getTheme().foreground,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
@@ -124,17 +121,12 @@ export function pianoroll({
colorizeInactive = 1,
fontFamily,
ctx,
id,
} = {}) {
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead;
let to = cycles * (1 - playhead);
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
@@ -189,14 +181,13 @@ export function pianoroll({
if (hideInactive && !isActive) {
return;
}
let color = event.value?.color;
let color = event.value?.color || event.context?.color;
active = color || active;
inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color;
const { velocity = 1, gain = 1 } = event.value || {};
ctx.globalAlpha = velocity * gain;
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
@@ -272,8 +263,8 @@ export function getDrawOptions(drawTime, options = {}) {
export const getPunchcardPainter =
(options = {}) =>
(ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) });
(ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) });
Pattern.prototype.punchcard = function (options) {
return this.onPaint(getPunchcardPainter(options));
+11 -30
View File
@@ -1,4 +1,3 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
@@ -7,7 +6,9 @@ import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
export function repl({
interval,
defaultOutput,
onSchedulerError,
onEvalError,
beforeEval,
afterEval,
@@ -16,9 +17,6 @@ export function repl({
onToggle,
editPattern,
onUpdateState,
sync = false,
setInterval,
clearInterval,
}) {
const state = {
schedulerError: undefined,
@@ -39,27 +37,21 @@ export function repl({
onUpdateState?.(state);
};
const schedulerOptions = {
const scheduler = new Cyclist({
interval,
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle: (started) => {
updateState({ started });
onToggle?.(started);
},
setInterval,
clearInterval,
};
// 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 anonymousIndex = 0;
let allTransform;
const hush = function () {
pPatterns = {};
anonymousIndex = 0;
allTransform = undefined;
return silence;
};
@@ -84,15 +76,6 @@ export function repl({
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
if (id.startsWith('_') || id.endsWith('_')) {
// allows muting a pattern x with x_ or _x
return silence;
}
if (id === '$') {
// allows adding anonymous patterns with $:
id = `$${anonymousIndex}`;
anonymousIndex++;
}
pPatterns[id] = this;
return this;
};
@@ -121,7 +104,7 @@ export function repl({
const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps);
});
return evalScope({
evalScope({
all,
hush,
cpm,
@@ -138,7 +121,7 @@ export function repl({
}
try {
updateState({ code, pending: true });
await injectPatternMethods();
injectPatternMethods();
await beforeEval?.({ code });
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler);
@@ -167,7 +150,6 @@ export function repl({
return pattern;
} catch (err) {
logger(`[eval] error: ${err.message}`, 'error');
console.error(err);
updateState({ evalError: err, pending: false });
onEvalError?.(err);
}
@@ -178,15 +160,14 @@ export function repl({
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps, t) => {
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t);
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
+5 -63
View File
@@ -143,24 +143,7 @@ export const rand = signal(timeToRand);
export const rand2 = rand.toBipolar();
export const _brandBy = (p) => rand.fmap((x) => x < p);
/**
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
*
* @name brandBy
* @param {number} probability - a number between 0 and 1
* @example
* s("hh*10").pan(brandBy(0.2))
*/
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
/**
* A continuous pattern of 0 or 1 (binary random)
*
* @name brand
* @example
* s("hh*10").pan(brand)
*/
export const brand = _brandBy(0.5);
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
@@ -286,7 +269,7 @@ export const pickmodOut = register('pickmodOut', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
return _pick(lookup, pat, false).trigzeroJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
@@ -296,7 +279,7 @@ export const pickRestart = register('pickRestart', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
return _pick(lookup, pat, true).trigzeroJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
@@ -305,7 +288,7 @@ export const pickmodRestart = register('pickmodRestart', function (lookup, pat)
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
return _pick(lookup, pat, false).trigJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
@@ -315,7 +298,7 @@ export const pickReset = register('pickReset', function (lookup, pat) {
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
return _pick(lookup, pat, true).trigJoin();
});
/**
@@ -414,8 +397,6 @@ export const chooseInWith = (pat, xs) => {
* Chooses randomly from the given list of elements.
* @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(choose("sine", "triangle", "bd:6"))
*/
export const choose = (...xs) => chooseWith(rand, xs);
@@ -442,7 +423,6 @@ Pattern.prototype.choose2 = function (...xs) {
/**
* Picks one of the elements at random each cycle.
* @synonyms randcat
* @returns {Pattern}
* @example
* chooseCycles("bd", "hh", "sd").s().fast(8)
@@ -471,27 +451,9 @@ const _wchooseWith = function (pat, ...pairs) {
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
/**
* Chooses randomly from the given list of elements by giving a probability to each element
* @param {...any} pairs arrays of value and weight
* @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(wchoose(["sine",10], ["triangle",1], ["bd:6",1]))
*/
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
/**
* Picks one of the elements at random each cycle by giving a probability to each element
* @synonyms wrandcat
* @returns {Pattern}
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
* @example
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
*/
export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pairs).innerJoin();
export const wrandcat = wchooseCycles;
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
// this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => {
@@ -561,11 +523,6 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
* @returns Pattern
* @example
* s("hh*8").undegradeBy(0.2)
* @example
* s("hh*10").layer(
* x => x.degradeBy(0.2).pan(0),
* x => x.undegradeBy(0.8).pan(1)
* )
*/
export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith(
@@ -574,21 +531,6 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
);
});
/**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
*
* @name undegrade
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").undegrade()
* @example
* s("hh*10").layer(
* x => x.degrade().pan(0),
* x => x.undegrade().pan(1)
* )
*/
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/**
@@ -1,5 +1,4 @@
import { Pattern } from '@strudel/core';
import { getTheme } from './draw.mjs';
import { Pattern } from './index.mjs';
// polar coords -> xy
function fromPolar(angle, radius, cx, cy) {
@@ -20,7 +19,7 @@ function spiralSegment(options) {
cy = 100,
rotate = 0,
thickness = margin / 2,
color = getTheme().foreground,
color = '#0000ff30',
cap = 'round',
stretch = 1,
fromOpacity = 1,
@@ -50,81 +49,70 @@ function spiralSegment(options) {
ctx.stroke();
}
function drawSpiral(options) {
let {
Pattern.prototype.spiral = function (options = {}) {
const {
stretch = 1,
size = 80,
thickness = size / 2,
cap = 'butt', // round butt squar,
inset = 3, // start angl,
playheadColor = '#ffffff',
playheadColor = '#ffffff90',
playheadLength = 0.02,
playheadThickness = thickness,
padding = 0,
steady = 1,
activeColor = getTheme().foreground,
inactiveColor = getTheme().gutterForeground,
inactiveColor = '#ffffff20',
colorizeInactive = 0,
fade = true,
// logSpiral = true,
ctx,
time,
haps,
drawTime,
id,
} = options;
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
function spiral({ ctx, time, haps, drawTime }) {
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const playhead = {
...settings,
thickness: playheadThickness,
from: inset - playheadLength,
to: inset,
color: playheadColor,
};
const playhead = {
...settings,
thickness: playheadThickness,
from: inset - playheadLength,
to: inset,
color: playheadColor,
};
const [min] = drawTime;
const rotate = steady * time;
haps.forEach((hap) => {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const hapColor = hap.value?.color || activeColor;
const color = colorizeInactive || isActive ? hapColor : inactiveColor;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
const [min] = drawTime;
const rotate = steady * time;
haps.forEach((hap) => {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const { color } = hap.context;
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({
ctx,
...settings,
from,
to,
...playhead,
rotate,
color,
fromOpacity: opacity,
toOpacity: opacity,
});
});
spiralSegment({
ctx,
...playhead,
rotate,
});
}
}
Pattern.prototype.spiral = function (options = {}) {
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
};
+1 -15
View File
@@ -4,10 +4,9 @@ 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/>.
*/
import { s, pan } from '../controls.mjs';
import { s } from '../controls.mjs';
import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest';
import Fraction from '../fraction.mjs';
describe('controls', () => {
it('should support controls', () => {
@@ -26,17 +25,4 @@ describe('controls', () => {
{ s: 'sd', n: 4, gain: 0.5 },
]);
});
it('should support nested controls', () => {
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 }]);
});
it('preserves tactus of the left pattern', () => {
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3));
});
it('preserves tactus of the right pattern for .out', () => {
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4));
});
it('combines tactus of the pattern for .mix as lcm', () => {
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12));
});
});
+18 -111
View File
@@ -21,8 +21,8 @@ import {
cat,
sequence,
palindrome,
s_polymeter,
s_polymeterSteps,
polymeter,
polymeterSteps,
polyrhythm,
silence,
fast,
@@ -47,11 +47,6 @@ import {
time,
run,
pick,
stackLeft,
stackRight,
stackCentre,
s_cat,
calculateTactus,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -186,18 +181,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]);
});
it('can Reset() structure', () => {
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.reset(20, 30)
.add.trig(20, 30)
.early(2),
sequence(26, 27, 36, 37),
);
});
it('can Restart() structure', () => {
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.restart(20, 30)
.add.trigzero(20, 30)
.early(2),
sequence(21, 22, 31, 32),
);
@@ -238,18 +233,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Reset() structure', () => {
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.reset(20, 30)
.keep.trig(20, 30)
.early(2),
sequence(6, 7, 6, 7),
);
});
it('can Restart() structure', () => {
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.restart(20, 30)
.keep.trigzero(20, 30)
.early(2),
sequence(1, 2, 1, 2),
);
@@ -284,18 +279,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Reset() structure', () => {
it('can Trig() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.reset(false, true)
.keepif.trig(false, true)
.early(2),
sequence(silence, silence, 6, 7),
);
});
it('can Restart() structure', () => {
it('can Trigzero() structure', () => {
sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.restart(false, true)
.keepif.trigzero(false, true)
.early(2),
sequence(silence, silence, 1, 2),
);
@@ -608,19 +603,16 @@ describe('Pattern', () => {
);
});
});
describe('s_polymeter()', () => {
it('Can layer up cycles, stepwise, with lists', () => {
expect(s_polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
describe('polymeter()', () => {
it('Can layer up cycles, stepwise', () => {
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
expect(s_polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
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(s_polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
});
describe('firstOf()', () => {
@@ -1124,89 +1116,4 @@ describe('Pattern', () => {
);
});
});
describe('tactus', () => {
it('Is correctly preserved/calculated through transformations', () => {
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
expect(sequence({ n: 0 }, { n: 1 }, { n: 2 }).chop(4).tactus).toStrictEqual(Fraction(12));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(2)).tactus,
).toStrictEqual(Fraction(6));
expect(
pure((x) => x + 1)
.setTactus(undefined)
.appBoth(pure(1).setTactus(2)).tactus,
).toStrictEqual(Fraction(2));
expect(
pure((x) => x + 1)
.setTactus(3)
.appBoth(pure(1).setTactus(undefined)).tactus,
).toStrictEqual(Fraction(3));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(6));
expect(stack(fastcat(0, 1, 2), fastcat(3, 4).setTactus(undefined)).tactus).toStrictEqual(Fraction(3));
expect(stackLeft(fastcat(0, 1, 2, 3), fastcat(3, 4)).tactus).toStrictEqual(Fraction(4));
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
// maybe this should double when they are either all even or all odd
expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4)).tactus).toStrictEqual(Fraction(3));
expect(fastcat(0, 1).ply(3).tactus).toStrictEqual(Fraction(6));
expect(fastcat(0, 1).setTactus(undefined).ply(3).tactus).toStrictEqual(undefined);
expect(fastcat(0, 1).fast(3).tactus).toStrictEqual(Fraction(2));
expect(fastcat(0, 1).setTactus(undefined).fast(3).tactus).toStrictEqual(undefined);
});
});
describe('s_cat', () => {
it('can cat', () => {
expect(sameFirst(s_cat(fastcat(0, 1, 2, 3), fastcat(4, 5)), fastcat(0, 1, 2, 3, 4, 5)));
expect(sameFirst(s_cat(pure(1), pure(2), pure(3)), fastcat(1, 2, 3)));
});
it('calculates undefined tactuses as the average', () => {
expect(sameFirst(s_cat(pure(1), pure(2), pure(3).setTactus(undefined)), fastcat(1, 2, 3)));
});
});
describe('s_taper', () => {
it('can taper', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
});
it('can taper backwards', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_taper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)));
});
});
describe('s_add and s_sub', () => {
it('can add from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(2), sequence(0, 1)));
});
it('can sub to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(2), sequence(0, 1, 2)));
});
it('can add from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_add(-2), sequence(3, 4)));
});
it('can sub to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).s_sub(-2), sequence(2, 3, 4)));
});
it('can subtract nothing', () => {
expect(sameFirst(pure('a').s_sub(0), pure('a')));
});
it('can subtract nothing, repeatedly', () => {
expect(sameFirst(pure('a').s_sub(0, 0), fastcat('a', 'a')));
for (var i = 0; i < 100; ++i) {
expect(sameFirst(pure('a').s_sub(...Array(i).fill(0)), fastcat(...Array(i).fill('a'))));
}
});
});
});
+4
View File
@@ -22,4 +22,8 @@ describe('Value', () => {
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
expect(valued(3).mul(3).value).toEqual(9);
});
it('union bare numbers for numeral props', () => {
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
});
});
+18
View File
@@ -4,6 +4,19 @@ 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/>.
*/
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 = {}) {
const container = document.getElementById('code');
const bg = 'background-image:url(' + src + ');background-size:contain;';
@@ -22,6 +35,11 @@ export const backgroundImage = function (src, animateOptions = {}) {
if (funcOptions.length === 0) {
return;
}
frame((_, t) =>
funcOptions.forEach(([option, value]) => {
handleOption(option, value(t));
}),
);
};
export const cleanupUi = () => {
-57
View File
@@ -61,11 +61,6 @@ export const valueToMidi = (value, fallbackValue) => {
return fallbackValue;
};
// used to schedule external event like midi and osc out
export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => {
return (targetTimeSeconds - currentTimeSeconds) * 1000;
};
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
@@ -236,14 +231,6 @@ export const splitAt = function (index, value) {
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const pairs = function (xs) {
const result = [];
for (let i = 0; i < xs.length - 1; ++i) {
result.push([xs[i], xs[i + 1]]);
}
return result;
};
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
/* solmization, not used yet */
@@ -302,30 +289,6 @@ export const sol2note = (n, notation = 'letters') => {
return note + oct;
};
// Remove duplicates from list
export function uniq(a) {
var seen = {};
return a.filter(function (item) {
return seen.hasOwn(item) ? false : (seen[item] = true);
});
}
// Remove duplicates from list, sorting in the process. Mutates argument!
export function uniqsort(a) {
return a.sort().filter(function (item, pos, ary) {
return !pos || item != ary[pos - 1];
});
}
// rational version
export function uniqsortr(a) {
return a
.sort((x, y) => x.compare(y))
.filter(function (item, pos, ary) {
return !pos || item.ne(ary[pos - 1]);
});
}
// code hashing helpers
export function unicodeToBase64(text) {
@@ -360,23 +323,3 @@ export function objectMap(obj, fn) {
}
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);
// };
+6 -5
View File
@@ -5,13 +5,14 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { curry } from './util.mjs';
import { logger } from './logger.mjs';
export function unionWithObj(a, b, func) {
if (b?.value !== undefined && Object.keys(b).length === 1) {
// https://github.com/tidalcycles/strudel/issues/1026
logger(`[warn]: Can't do arithmetic on control pattern.`);
return a;
if (typeof b?.value === 'number') {
// https://github.com/tidalcycles/strudel/issues/262
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
b = Object.assign(b, numerals);
delete b.value;
}
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
+4 -9
View File
@@ -7,9 +7,6 @@ function createClock(
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
setInterval = globalThis.setInterval,
clearInterval = globalThis.clearInterval,
round = true,
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
@@ -25,8 +22,9 @@ function createClock(
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = round ? Math.round(phase * precision) / precision : phase;
callback(phase, duration, tick, t); // callback has to skip / handle phase < t!
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration
tick++;
}
@@ -37,10 +35,7 @@ function createClock(
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => {
intervalID !== undefined && clearInterval(intervalID);
intervalID = undefined;
};
const clear = () => intervalID !== undefined && clearInterval(intervalID);
const pause = () => clear();
const stop = () => {
tick = 0;
+3 -7
View File
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
instrument = instrument || 'triangle';
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
return pat.onTrigger((time, hap) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
@@ -38,11 +38,9 @@ export const csound = register('csound', (instrument, pat) => {
.join('/');
// TODO: find out how to send a precise ctx based time
// http://www.csounds.com/manual/html/i.html
const timeOffset = targetTime - currentTime; // latency ?
//const timeOffset = time_deprecate - getAudioContext().currentTime
const params = [
`"${instrument}"`, // p1: instrument name
timeOffset, // p2: starting time in arbitrary unit called beats
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
hap.duration + 0, // p3: duration in beats
// instrument specific params:
freq, //.toFixed(precision), // p4: frequency
@@ -154,14 +152,12 @@ export const csoundm = register('csoundm', (instrument, pat) => {
const p2 = tidal_time - getAudioContext().currentTime;
const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap);
let { gain = 1, velocity = 0.9 } = hap.value;
velocity = gain * velocity;
// Translate frequency to MIDI key number _without_ rounding.
const C4 = 261.62558;
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
const p4 = octave * 12.0 - 36.0;
// We prefer floating point precision, but over the MIDI range [0, 127].
const p5 = 127 * velocity;
const p5 = 127 * (hap.context?.velocity ?? 0.9);
// The Strudel controls as a string.
const p6 = Object.entries({ ...hap.value, frequency })
.flat()
+5 -7
View File
@@ -1,17 +1,15 @@
import { Invoke } from './utils.mjs';
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
import { Pattern, noteToMidi } from '@strudel/core';
const ON_MESSAGE = 0x90;
const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
const offset = getEventOffsetMs(targetTime, currentTime) + latencyMs;
velocity = Math.floor(gain * velocity * 100);
return this.onTrigger((time, hap, currentTime, cps) => {
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
const offset = (time - currentTime) * 1000;
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1;
+3 -3
View File
@@ -1,8 +1,8 @@
import { parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
import { parseNumeral, Pattern } from '@strudel/core';
import { Invoke } from './utils.mjs';
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
@@ -13,7 +13,7 @@ Pattern.prototype.osc = function () {
const params = [];
const timestamp = Math.round(Date.now() + getEventOffsetMs(targetTime, currentTime));
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
Object.keys(controls).forEach((key) => {
const val = controls[key];
-9
View File
@@ -1,9 +0,0 @@
# @strudel/canvas
Helpers for drawing with the Canvas API and Strudel
## Install
```sh
npm i @strudel/canvas --save
```
-6
View File
@@ -1,6 +0,0 @@
export * from './animate.mjs';
export * from './color.mjs';
export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './pitchwheel.mjs';
-37
View File
@@ -1,37 +0,0 @@
{
"name": "@strudel/draw",
"version": "1.0.1",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
}
}
-127
View File
@@ -1,127 +0,0 @@
import { Pattern, midiToFreq, getFrequency } from '@strudel/core';
import { getTheme, getDrawContext } from './draw.mjs';
const c = midiToFreq(36);
const circlePos = (cx, cy, radius, angle) => {
angle = angle * Math.PI * 2;
const x = Math.sin(angle) * radius + cx;
const y = Math.cos(angle) * radius + cy;
return [x, y];
};
const freq2angle = (freq, root) => {
return 0.5 - (Math.log2(freq / root) % 1);
};
export function pitchwheel({
haps,
ctx,
id,
hapcircles = 1,
circle = 0,
edo = 12,
root = c,
thickness = 3,
hapRadius = 6,
mode = 'flake',
margin = 10,
} = {}) {
const connectdots = mode === 'polygon';
const centerlines = mode === 'flake';
const w = ctx.canvas.width;
const h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const color = getTheme().foreground;
const size = Math.min(w, h);
const radius = size / 2 - thickness / 2 - hapRadius - margin;
const centerX = w / 2;
const centerY = h / 2;
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.globalAlpha = 1;
ctx.lineWidth = thickness;
if (circle) {
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.stroke();
}
if (edo) {
Array.from({ length: edo }, (_, i) => {
const angle = freq2angle(root * Math.pow(2, i / edo), root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
ctx.beginPath();
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
});
ctx.stroke();
}
let shape = [];
ctx.lineWidth = hapRadius;
haps.forEach((hap) => {
let freq;
try {
freq = getFrequency(hap);
} catch (err) {
return;
}
const angle = freq2angle(freq, root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
const hapColor = hap.value.color || color;
ctx.strokeStyle = hapColor;
ctx.fillStyle = hapColor;
const { velocity = 1, gain = 1 } = hap.value || {};
const alpha = velocity * gain;
ctx.globalAlpha = alpha;
shape.push([x, y, angle, hapColor, alpha]);
ctx.beginPath();
if (hapcircles) {
ctx.moveTo(x + hapRadius, y);
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
}
if (centerlines) {
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
}
ctx.stroke();
});
ctx.strokeStyle = color;
ctx.globalAlpha = 1;
if (connectdots && shape.length) {
shape = shape.sort((a, b) => a[2] - b[2]);
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
shape.forEach(([x, y, _, color, alpha]) => {
ctx.strokeStyle = color;
ctx.globalAlpha = alpha;
ctx.lineTo(x, y);
});
ctx.lineTo(shape[0][0], shape[0][1]);
ctx.stroke();
}
return;
}
Pattern.prototype.pitchwheel = function (options = {}) {
let { ctx = getDrawContext(), id = 1 } = options;
return this.tag(id).onPaint((_, time, haps) =>
pitchwheel({
...options,
time,
ctx,
haps: haps.filter((hap) => hap.isActive(time)),
id,
}),
);
};
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+20 -51
View File
@@ -2,63 +2,32 @@
This package contains a embeddable web component for the Strudel REPL.
## Usage via Script Tag
## Usage
Use this code in any HTML file:
Either install with `npm i @strudel/embed` or just use a cdn to import the script:
```html
<script src="https://unpkg.com/@strudel/embed@latest"></script>
<strudel-repl>
<!--
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
note(`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
-->
</strudel-repl>
```
This will load the strudel website in an iframe, using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<div id="strudel"></div>
<script>
let editor = document.createElement('strudel-repl');
editor.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(editor);
</script>
```
When you're using JSX, you could also use the `code` attribute in your markup:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<strudel-repl code={`
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
`}></strudel-repl>
```
Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
+1 -1
View File
@@ -4,7 +4,7 @@ class Strudel extends HTMLElement {
}
connectedCallback() {
setTimeout(() => {
const code = this.getAttribute('code') || (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe');
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
+15 -24
View File
@@ -1,49 +1,40 @@
import { getDrawContext } from '@strudel/draw';
import { controls } from '@strudel/core';
import { getDrawContext } from '@strudel/core';
let latestOptions;
let hydra;
function appendCanvas(c) {
const { canvas: testCanvas } = getDrawContext();
c.canvas.id = 'hydra-canvas';
c.canvas.style.position = 'fixed';
c.canvas.style.top = '0px';
testCanvas.after(c.canvas);
return testCanvas;
}
export async function initHydra(options = {}) {
// reset if options have changed since last init
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
document.getElementById('hydra-canvas')?.remove();
document.getElementById('hydra-canvas').remove();
}
latestOptions = options;
//load and init hydra
if (!document.getElementById('hydra-canvas')) {
console.log('reinit..');
const {
src = 'https://unpkg.com/hydra-synth',
feedStrudel = false,
contextType = 'webgl',
pixelRatio = 1,
pixelated = true,
...hydraConfig
} = {
detectAudio: false,
...options,
};
const { canvas } = getDrawContext('hydra-canvas', { contextType, pixelRatio, pixelated });
hydraConfig.canvas = canvas;
} = { detectAudio: false, ...options };
await import(/* @vite-ignore */ src);
hydra = new Hydra(hydraConfig);
const hydra = new Hydra(hydraConfig);
if (feedStrudel) {
const { canvas } = getDrawContext();
canvas.style.display = 'none';
hydra.synth.s0.init({ src: canvas });
}
appendCanvas(hydra);
}
}
export function clearHydra() {
if (hydra) {
hydra.hush();
}
globalThis.s0?.clear();
document.getElementById('hydra-canvas')?.remove();
globalThis.speed = controls.speed;
globalThis.shape = controls.shape;
}
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
-1
View File
@@ -34,7 +34,6 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"hydra-synth": "^1.3.29"
},
"devDependencies": {
+11 -11
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { Pattern, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -112,24 +112,24 @@ Pattern.prototype.midi = function (output) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) {
console.log('not enabled');
return;
}
const device = getDevice(output, WebMidi.outputs);
hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
// destructure value
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
velocity = gain * velocity;
const offset = (time - currentTime) * 1000;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${offset}`;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - 10;
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration });
@@ -176,7 +176,7 @@ export async function midin(input) {
}
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
if (initial || WebMidi.enabled) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
+16 -36
View File
@@ -295,15 +295,7 @@ function peg$parse(input, options) {
var peg$f6 = function(a) { return a };
var peg$f7 = function(s) { s.arguments_.alignment = 'polymeter_slowcat'; return s; };
var peg$f8 = function(a) { return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 };
var peg$f9 = function(a) { return x => {const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
x.options_['reps'] = reps;
console.log("reps: ", reps)
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
x.options_['weight'] = reps;
console.log("options: ", x.options_);
}
};
var peg$f9 = function(a) { return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 };
var peg$f10 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
var peg$f12 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
@@ -316,11 +308,11 @@ function peg$parse(input, options) {
}
return result;
};
var peg$f17 = function(tactus, s) { return new PatternStub(s, 'fastcat', undefined, !!tactus); };
var peg$f17 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; };
var peg$f21 = function(head, tail) {if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f21 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
var peg$f22 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
var peg$f23 = function(sc) { return sc; };
var peg$f24 = function(s) { return { name: "struct", args: { mini:s }}};
@@ -1485,36 +1477,24 @@ function peg$parse(input, options) {
}
function peg$parsesequence() {
var s0, s1, s2, s3;
var s0, s1, s2;
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 94) {
s1 = peg$c9;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
if (s1 === peg$FAILED) {
s1 = null;
}
s2 = [];
s3 = peg$parseslice_with_ops();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parseslice_with_ops();
s1 = [];
s2 = peg$parseslice_with_ops();
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parseslice_with_ops();
}
} else {
s2 = peg$FAILED;
s1 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f17(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
s1 = peg$f17(s1);
}
s0 = s1;
return s0;
}
@@ -2496,10 +2476,10 @@ function peg$parse(input, options) {
this.location_ = location();
}
var PatternStub = function(source, alignment, seed, tactus)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment: alignment, tactus: tactus };
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
+6 -13
View File
@@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location();
}
var PatternStub = function(source, alignment, seed, tactus)
var PatternStub = function(source, alignment, seed)
{
this.type_ = "pattern";
this.arguments_ = { alignment: alignment, tactus: tactus };
this.arguments_ = { alignment: alignment };
if (seed !== undefined) {
this.arguments_.seed = seed;
}
@@ -135,14 +135,7 @@ op_weight = ws ("@" / "_") a:number?
{ return x => x.options_['weight'] = (x.options_['weight'] ?? 1) + (a ?? 2) - 1 }
op_replicate = ws "!" a:number?
{ return x => {// A bit fiddly, to support both x!4 and x!!! as equivalent..
const reps = (x.options_['reps'] ?? 1) + (a ?? 2) - 1;
x.options_['reps'] = reps;
x.options_['ops'] = x.options_['ops'].filter(x => x.type_ !== "replicate");
x.options_['ops'].push({ type_: "replicate", arguments_ :{ amount:reps }});
x.options_['weight'] = reps;
}
}
{ return x => x.options_['reps'] = (x.options_['reps'] ?? 1) + (a ?? 2) - 1 }
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
@@ -172,8 +165,8 @@ slice_with_ops = s:slice ops:slice_op*
}
// a sequence is a combination of one or more successive slices (as an array)
sequence = tactus:'^'? s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat', undefined, !!tactus); }
sequence = s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat'); }
// a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @sequence)+
@@ -191,7 +184,7 @@ dot_tail = tail:(dot @sequence)+
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:sequence tail:(stack_tail / choose_tail / dot_tail)?
{if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
polymeter_stack = head:sequence tail:stack_tail?
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
+44 -73
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js';
import * as strudel from '@strudel/core';
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
const randOffset = 0.0003;
@@ -27,12 +26,6 @@ const applyOptions = (parent, enter) => (pat, i) => {
pat = strudel.reify(pat)[type](enter(amount));
break;
}
case 'replicate': {
const { amount } = op.arguments_;
pat = strudel.reify(pat);
pat = pat._repeatCycles(amount)._fast(amount);
break;
}
case 'bjorklund': {
if (op.arguments_.rotation) {
pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
@@ -73,88 +66,66 @@ const applyOptions = (parent, enter) => (pat, i) => {
return pat;
};
function resolveReplications(ast) {
ast.source_ = strudel.flatten(
ast.source_.map((child) => {
const { reps } = child.options_ || {};
if (!reps) {
return [child];
}
delete child.options_.reps;
return Array(reps).fill(child);
}),
);
}
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) {
onEnter?.(ast);
const enter = (node) => patternifyAST(node, code, onEnter, offset);
switch (ast.type_) {
case 'pattern': {
// resolveReplications(ast);
resolveReplications(ast);
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment;
const with_tactus = children.filter((child) => child.__tactus_source);
let pat;
switch (alignment) {
case 'stack': {
pat = strudel.stack(...children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter_slowcat': {
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter': {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
if (alignment === 'stack') {
return strudel.stack(...children);
}
if (alignment === 'polymeter_slowcat') {
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1)));
return strudel.stack(...aligned);
}
if (alignment === 'polymeter') {
// polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: 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))));
pat = strudel.stack(...aligned);
break;
}
case 'rand': {
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'feet': {
pat = strudel.fastcat(...children);
break;
}
default: {
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) {
const weightSum = ast.source_.reduce(
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
strudel.Fraction(0),
);
pat = strudel.timeCat(
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
);
pat.__weight = weightSum; // for polymeter
pat.tactus = weightSum;
if (with_tactus.length) {
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
}
} else {
pat = strudel.sequence(...children);
pat.tactus = children.length;
}
if (ast.arguments_.tactus) {
pat.__tactus_source = true;
}
}
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
return strudel.stack(...aligned);
}
if (with_tactus.length) {
pat.__tactus_source = true;
if (alignment === 'rand') {
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
}
if (alignment === 'feet') {
return strudel.fastcat(...children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
pat.__weight = weightSum;
return pat;
}
const pat = strudel.sequence(...children);
pat.__weight = children.length;
return pat;
}
case 'element': {
1;
return enter(ast.source_);
}
case 'atom': {
if (ast.source_ === '~' || ast.source_ === '-') {
if (ast.source_ === '~') {
return strudel.silence;
}
if (!ast.location_) {
@@ -190,7 +161,7 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => {
};
// takes quoted mini string, returns ast
export const mini2ast = (code, start = 0, userCode = code) => {
export const mini2ast = (code, start, userCode) => {
try {
return krill.parse(code);
} catch (error) {
-13
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
import '@strudel/core/euclid.mjs';
import { Fraction } from '@strudel/core/index.mjs';
import { describe, expect, it } from 'vitest';
describe('mini', () => {
@@ -118,9 +117,6 @@ describe('mini', () => {
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
});
it('supports the - alias for ~', () => {
expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]'));
});
it('supports the ? operator', () => {
expect(
mini('a?')
@@ -208,15 +204,6 @@ describe('mini', () => {
it('_ and @ are almost interchangeable', () => {
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
});
it('supports ^ tactus marking', () => {
expect(mini('a [^b c]').tactus).toEqual(Fraction(4));
expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8));
expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4));
expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12));
expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24));
});
});
describe('getLeafLocation', () => {
-19
View File
@@ -1,19 +0,0 @@
# @strudel/node
This is an experiment to run strudel in node.
## Usage
```sh
# Setup
# - make sure node >= 20 is installed
# - make sure pnpm is installed
cd packages/node
pnpm i
# Usage
pnpm start
```
Then run `sclang` with superdirt in another terminal.
You can now edit and save the file `pattern.mjs` to update your pattern!
-64
View File
@@ -1,64 +0,0 @@
import { createClock, evalScope } from '@strudel/core';
import { evaluate } from '@strudel/transpiler';
import watch from 'node-watch';
import fs from 'node:fs/promises';
import { AudioContext, OscillatorNode, GainNode } from 'node-web-audio-api';
const audioContext = new AudioContext();
let file = 'pattern.mjs';
let pattern;
async function evaluateFile() {
try {
console.log('// file evaluated:');
const code = await fs.readFile(file, { encoding: 'utf8' });
console.log(code);
const res = await evaluate(code);
pattern = res.pattern;
} catch (err) {
console.error(err);
}
}
// const getTime = () => performance.now() / 1000;
const getTime = () => audioContext.currentTime;
let minLatency = 50;
async function main() {
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
await evaluateFile();
watch(file, { recursive: true }, () => evaluateFile());
let lastEnd;
const clock = createClock(getTime, (phase) => {
if (!lastEnd) {
lastEnd = phase;
return;
}
const haps = pattern.queryArc(lastEnd, phase);
lastEnd = phase;
const cps = 1;
const cycle = Math.floor(phase);
haps
.filter((h) => h.hasOnset())
.forEach((hap) => {
const env = new GainNode(audioContext, { gain: 0 });
const { attack = 0.01, gain = 1 } = hap.value;
env.connect(audioContext.destination);
const now = hap.whole.begin;
const duration = hap.duration;
env.gain
.setValueAtTime(0, now)
.linearRampToValueAtTime(gain * 0.2, now + attack)
.exponentialRampToValueAtTime(0.0001, now + duration);
const frequency = hap.value.freq;
const osc = new OscillatorNode(audioContext, { frequency });
osc.connect(env);
osc.start(now);
osc.stop(now + duration);
});
});
clock.start();
}
main();
-76
View File
@@ -1,76 +0,0 @@
import { createClock, evalScope } from '@strudel/core';
import { evaluate } from '@strudel/transpiler';
import OSC from 'osc-js';
import watch from 'node-watch';
import fs from 'node:fs/promises';
const config = {
receiver: 'udp', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
open: {
host: 'localhost', // @param {string} Hostname of udp server to bind to
port: 57121, // @param {number} Port of udp client for messaging
// enabling the following line will receive tidal messages:
// port: 57120, // @param {number} Port of udp client for messaging
exclusive: false, // @param {boolean} Exclusive flag
},
send: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
},
};
const osc = new OSC({ plugin: new OSC.DatagramPlugin(config) });
osc.open(); // start a WebSocket server on port 8080
console.log('osc client running on port', config.open.port);
console.log('osc server running on port', config.send.port);
let file = 'pattern.mjs';
let pattern;
async function evaluateFile() {
try {
console.log('// file evaluated:');
const code = await fs.readFile(file, { encoding: 'utf8' });
console.log(code);
const res = await evaluate(code);
pattern = res.pattern;
} catch (err) {
console.error(err);
}
}
const getTime = () => performance.now() / 1000;
async function main() {
await evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/tonal'));
await evaluateFile();
watch(file, { recursive: true }, () => evaluateFile());
let lastEnd;
let minLatency = 50;
const clock = createClock(getTime, (phase) => {
if (!lastEnd) {
lastEnd = phase;
return;
}
const haps = pattern.queryArc(lastEnd, phase);
lastEnd = phase;
const cps = 1;
const cycle = Math.floor(phase);
haps
.filter((h) => h.hasOnset())
.forEach((hap) => {
const delta = hap.duration.valueOf();
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
const keyvals = Object.entries(controls).flat();
const ts = Math.floor(performance.timeOrigin + hap.whole.begin * 1000 + minLatency);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
osc.send(bundle);
});
});
clock.start();
}
main();
-37
View File
@@ -1,37 +0,0 @@
{
"name": "@strudel/node",
"version": "1.1.0",
"description": "Strudel running in node",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"main": "index.mjs",
"scripts": {
"osc": "node osc-superdirt.mjs",
"waa": "node node-web-audio-api.mjs"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"node-watch": "^0.7.4",
"node-web-audio-api": "^0.20.0",
"osc-js": "^2.4.0"
}
}
-2
View File
@@ -1,2 +0,0 @@
freq("110 220 [330 440,550,660]")
.attack(.1).gain(.4)
+3 -4
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import OSC from 'osc-js';
import { logger, parseNumeral, Pattern, getEventOffsetMs } from '@strudel/core';
import { logger, parseNumeral, Pattern } from '@strudel/core';
let connection; // Promise<OSC>
function connect() {
@@ -44,7 +44,7 @@ function connect() {
* @returns Pattern
*/
Pattern.prototype.osc = function () {
return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => {
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
hap.ensureObjectValue();
const osc = await connect();
const cycle = hap.wholeOrPart().begin.valueOf();
@@ -56,8 +56,7 @@ Pattern.prototype.osc = function () {
const keyvals = Object.entries(controls).flat();
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
const offset = getEventOffsetMs(targetTime, currentTime);
const offset = (time - currentTime) * 1000;
// timestamp in milliseconds used to trigger the osc bundle at a precise moment
const ts = Math.floor(Date.now() + offset);
const message = new OSC.Message('/dirt/play', ...keyvals);
+1 -1
View File
@@ -4,7 +4,7 @@ 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/>.
*/
import OSC from 'osc-js';
const OSC = require('osc-js');
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
+1 -92
View File
@@ -2,95 +2,4 @@
The Strudel REPL as a web component.
## Add Script Tag
First place this script tag once in your HTML:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
```
You can also pin the version like this:
```html
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
```
This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase.
See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions.
## Use Web Component
When you've added the script tag, you can use the `strudel-editor` web component:
```html
<strudel-editor>
<!--
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-editor>
```
This will load the Strudel REPL using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(repl);
</script>
```
## Interacting with the REPL
If you get a hold of the `strudel-editor` element, you can interact with the strudel REPL from Javascript:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<strudel-editor id="repl">
<!-- ... -->
</strudel-editor>
<script>
const repl = document.getElementById('repl');
console.log(repl.editor);
</script>
```
or
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute('code', `...`);
document.getElementById('strudel').append(repl);
console.log(repl.editor);
</script>
```
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL.
For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code.
[Usage example](https://github.com/tidalcycles/strudel/blob/main/examples/buildless/web-component-no-iframe.html)
-1
View File
@@ -35,7 +35,6 @@
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
-1
View File
@@ -6,7 +6,6 @@ export async function prebake() {
const modulesLoading = evalScope(
// import('@strudel/core'),
core,
import('@strudel/draw'),
import('@strudel/mini'),
import('@strudel/tonal'),
import('@strudel/webaudio'),
+11 -3
View File
@@ -1,5 +1,4 @@
import { silence } from '@strudel/core';
import { getDrawContext } from '@strudel/draw';
import { getDrawContext, silence } from '@strudel/core';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
@@ -41,8 +40,17 @@ if (typeof HTMLElement !== 'undefined') {
initialCode: '// LOADING',
pattern: silence,
drawTime,
drawContext,
onDraw: (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
},
prebake,
afterEval: ({ code }) => {
// window.location.hash = '#' + code2hash(code);
},
onUpdateState: (state) => {
const event = new CustomEvent('update', {
detail: state,
-22
View File
@@ -1,22 +0,0 @@
# @strudel/sampler
This package allows you to serve your samples on disk to the strudel REPL.
```sh
cd ~/your/samples/
npx @strudel/sampler
```
This will run a server on `http://localhost:5432`.
You can now load the samples via:
```js
samples('http://localhost:5432')
```
## Options
```sh
LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port
```
-19
View File
@@ -1,19 +0,0 @@
{
"name": "@strudel/sampler",
"version": "0.0.9",
"description": "",
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bin": "./sample-server.mjs",
"type": "module",
"dependencies": {
"cowsay": "^1.6.0"
}
}
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join } from 'path';
import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
async function getFilesInDirectory(directory) {
let files = [];
const dirents = await readdir(directory, { withFileTypes: true });
for (const dirent of dirents) {
const fullPath = join(directory, dirent.name);
if (dirent.isDirectory()) {
if (dirent.name.startsWith('.')) {
LOG && console.warn(`ignore hidden folder: ${fullPath}`);
continue;
}
try {
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
);
files = files.concat(subFiles);
LOG && console.log(`${dirent.name} (${subFiles.length})`);
} catch (err) {
LOG && console.warn(`skipped due to error: ${fullPath}`);
}
} else {
files.push(fullPath);
}
}
return files;
}
async function getBanks(directory) {
// const directory = resolve(__dirname, '.');
let files = await getFilesInDirectory(directory);
let banks = {};
files = files.map((url) => {
const [bank] = url.split('/').slice(-2);
banks[bank] = banks[bank] || [];
url = url.replace(directory, '');
banks[bank].push(url);
return url;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
// eslint-disable-next-line
const directory = process.cwd();
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory);
if (req.url === '/') {
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify(banks));
}
let subpath = decodeURIComponent(req.url);
if (!files.includes(subpath)) {
res.statusCode = 404;
res.end('File not found');
return;
}
const filePath = join(directory, subpath);
const readStream = createReadStream(filePath);
readStream.on('error', (err) => {
res.statusCode = 500;
res.end('Internal server error');
console.error(err);
});
readStream.pipe(res);
});
// eslint-disable-next-line
const PORT = process.env.PORT || 5432;
const IP_ADDRESS = '0.0.0.0';
let IP;
const networkInterfaces = os.networkInterfaces();
Object.keys(networkInterfaces).forEach((key) => {
networkInterfaces[key].forEach((networkInterface) => {
if (networkInterface.family === 'IPv4' && !networkInterface.internal) {
IP = networkInterface.address;
}
});
});
if (!IP) {
console.error("Unable to determine server's IP address.");
// eslint-disable-next-line
process.exit(1);
}
server.listen(PORT, IP_ADDRESS, () => {
console.log(`@strudel/sampler is now serving audio files from:
${directory}
To use them in the Strudel REPL, run:
samples('http://localhost:${PORT}')
Or on a machine in the same network:
samples('http://${IP}:${PORT}')
`);
});
+1 -1
View File
@@ -65,7 +65,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `bandf`: band pass filter cutoff
- `bandq`: band pass filter resonance
- `crush`: amplitude bit crusher using given number of bits
- `distort`: distortion effect. might get loud!
- `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
- `pan`: stereo panning from 0 (left) to 1 (right)
- `phaser`: sets the speed of the modulation
- `phaserdepth`: the amount the signal is affected by the phaser effect.
+2 -2
View File
@@ -74,6 +74,6 @@ export const dough = async (code) => {
worklet.node.connect(ac.destination);
};
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
export function doughTrigger(t, hap, currentTime, duration, cps) {
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
}
-73
View File
@@ -186,76 +186,3 @@ export function getVibratoOscillator(param, value, t) {
return vibratoOscillator;
}
}
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = audioContext.createConstantSource();
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
onComplete();
};
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
};
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = frequencyparam.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
export function applyFM(param, value, begin) {
const {
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let modulator;
let stop = () => {};
if (fmModulationIndex) {
const ac = getAudioContext();
const envGain = ac.createGain();
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform);
modulator = fmmod.node;
stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = begin + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
begin,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(param);
}
}
return { stop };
}
-3
View File
@@ -194,9 +194,6 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
if (sampleMap.startsWith('github:')) {
sampleMap = githubPath(sampleMap, 'strudel.json');
}
if (sampleMap.startsWith('local:')) {
sampleMap = `http://localhost:5432`;
}
if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:');
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
+24 -35
View File
@@ -50,8 +50,8 @@ function loadWorklets() {
return workletsLoading;
}
export function getWorklet(ac, processor, params, config) {
const node = new AudioWorkletNode(ac, processor, config);
function getWorklet(ac, processor, params) {
const node = new AudioWorkletNode(ac, processor);
Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value;
});
@@ -215,35 +215,35 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
return reverbs[orbit];
}
export let analysers = {},
analysersData = {};
export let analyser, analyserData /* s = {} */;
export function getAnalyserById(id, fftSize = 1024) {
if (!analysers[id]) {
// make sure this doesn't happen too often as it piles up garbage
export function getAnalyser(/* orbit, */ fftSize = 2048) {
if (!analyser /*s [orbit] */) {
const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize;
// getDestination().connect(analyserNode);
analysers[id] = analyserNode;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
analyser /* s[orbit] */ = analyserNode;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
}
if (analysers[id].fftSize !== fftSize) {
analysers[id].fftSize = fftSize;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
if (analyser /* s[orbit] */.fftSize !== fftSize) {
analyser /* s[orbit] */.fftSize = fftSize;
//analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
}
return analysers[id];
return analyser /* s[orbit] */;
}
export function getAnalyzerData(type = 'time', id = 1) {
export function getAnalyzerData(type = 'time') {
const getter = {
time: () => analysers[id]?.getFloatTimeDomainData(analysersData[id]),
frequency: () => analysers[id]?.getFloatFrequencyData(analysersData[id]),
time: () => analyser?.getFloatTimeDomainData(analyserData),
frequency: () => analyser?.getFloatFrequencyData(analyserData),
}[type];
if (!getter) {
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
}
getter();
return analysersData[id];
return analyserData;
}
function effectSend(input, effect, wet) {
@@ -256,11 +256,9 @@ function effectSend(input, effect, wet) {
export function resetGlobalEffects() {
delays = {};
reverbs = {};
analysers = {};
analysersData = {};
}
export const superdough = async (value, t, hapDuration) => {
export const superdough = async (value, deadline, hapDuration) => {
const ac = getAudioContext();
if (typeof value !== 'object') {
throw new Error(
@@ -272,13 +270,7 @@ export const superdough = async (value, t, hapDuration) => {
// duration is passed as value too..
value.duration = hapDuration;
// calculate absolute time
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
if (t < ac.currentTime) {
console.warn(
`[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`,
);
return;
}
let t = ac.currentTime + deadline;
// destructure
let {
s = 'triangle',
@@ -324,9 +316,6 @@ export const superdough = async (value, t, hapDuration) => {
coarse,
crush,
shape,
shapevol = 1,
distort,
distortvol = 1,
pan,
vowel,
delay = 0,
@@ -355,7 +344,7 @@ export const superdough = async (value, t, hapDuration) => {
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
gain *= velocity; // legacy fix for velocity
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
@@ -468,8 +457,7 @@ export const superdough = async (value, t, hapDuration) => {
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
compressorThreshold !== undefined &&
chain.push(
@@ -520,8 +508,8 @@ export const superdough = async (value, t, hapDuration) => {
// analyser
let analyserSend;
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, analyze);
}
// connect chain elements together
@@ -530,6 +518,7 @@ export const superdough = async (value, t, hapDuration) => {
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
return { mute: (dt) => post.gain.linearRampToValueAtTime(0, ac.currentTime + dt) };
};
export const superdoughTrigger = (t, hap, ct, cps) => superdough(hap, t - ct, hap.duration / cps, cps);
+88 -135
View File
@@ -1,138 +1,31 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, getWorklet } from './superdough.mjs';
import {
applyFM,
gainNode,
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
} from './helpers.mjs';
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value) => {
let { note, freq } = value;
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
return Number(freq);
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
};
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = osc.frequency.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() {
[...waveforms].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
let sound = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
// turn down
const g = gainNode(0.3);
const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const envEnd = holdEnd + release + 0.01;
triggerRelease?.(envEnd);
stop(envEnd);
return {
node,
stop: (releaseTime) => {},
};
},
{ type: 'synth', prebake: true },
);
});
registerSound(
'supersaw',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n, unison = 5, spread = 0.6, detune } = value;
detune = detune ?? n ?? 0.18;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
const voices = clamp(unison, 1, 100);
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
let o = getWorklet(
ac,
'supersaw-oscillator',
{
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
},
{
outputChannelCount: [2],
},
);
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
ac,
() => {
o.disconnect();
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
},
begin,
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
};
},
{ prebake: true, type: 'synth' },
);
[...noises].forEach((s) => {
[...waveforms, ...noises].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
@@ -143,9 +36,12 @@ export function registerSynthSounds() {
);
let sound;
let { density } = value;
sound = getNoiseOscillator(s, t, density);
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
let { density } = value;
sound = getNoiseOscillator(s, t, density);
}
let { node: o, stop, triggerRelease } = sound;
@@ -210,7 +106,24 @@ export function waveformN(partials, type) {
// expects one of waveforms as s
export function getOscillator(s, t, value) {
let { n: partials, duration, noise = 0 } = value;
let {
n: partials,
note,
freq,
noise = 0,
// fm
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let ac = getAudioContext();
let o;
// If no partials are given, use stock waveforms
if (!partials || s === 'sine') {
@@ -221,15 +134,55 @@ export function getOscillator(s, t, value) {
else {
o = waveformN(partials, s);
}
// get frequency from note...
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// set frequency
o.frequency.value = getFrequencyFromValue(value);
o.frequency.value = Number(freq);
o.start(t);
// FM
let stopFm;
let envGain = ac.createGain();
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = t + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
t,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(o.frequency);
}
stopFm = stop;
}
// Additional oscillator for vibrato effect
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
// pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration);
const fmModulator = applyFM(o.frequency, value, t);
let noiseMix;
if (noise) {
@@ -239,9 +192,9 @@ export function getOscillator(s, t, value) {
return {
node: noiseMix?.node || o,
stop: (time) => {
fmModulator.stop(time);
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
stopFm?.(time);
o.stop(time);
},
triggerRelease: (time) => {
+50 -218
View File
@@ -1,5 +1,6 @@
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
// all the credit goes to dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
// <3
class CoarseProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
@@ -8,27 +9,28 @@ class CoarseProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.notStarted = true;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const coarse = parameters.coarse;
const blockSize = 128;
let coarse = parameters.coarse[0] ?? 0;
coarse = Math.max(1, coarse);
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = n % coarse === 0 ? input[i][n] : output[i][n - 1];
const hasInput = !(input[0] === undefined);
if (hasInput) {
this.notStarted = false;
output[0][0] = input[0][0];
for (let n = 1; n < blockSize; n++) {
for (let o = 0; o < output.length; o++) {
output[o][n] = n % coarse == 0 ? input[0][n] : output[o][n - 1];
}
}
}
return true;
return this.notStarted || hasInput;
}
}
registerProcessor('coarse-processor', CoarseProcessor);
class CrushProcessor extends AudioWorkletProcessor {
@@ -38,239 +40,69 @@ class CrushProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.notStarted = true;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const crush = parameters.crush;
const blockSize = 128;
let crush = parameters.crush[0] ?? 8;
crush = Math.max(1, crush);
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
const x = Math.pow(2, crush - 1);
output[i][n] = Math.round(input[i][n] * x) / x;
const hasInput = !(input[0] === undefined);
if (hasInput) {
this.notStarted = false;
if (crush.length === 1) {
const x = Math.pow(2, crush[0] - 1);
for (let n = 0; n < blockSize; n++) {
const value = Math.round(input[0][n] * x) / x;
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
}
} else {
for (let n = 0; n < blockSize; n++) {
let x = Math.pow(2, crush[n] - 1);
const value = Math.round(input[0][n] * x) / x;
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
}
}
}
return true;
return this.notStarted || hasInput;
}
}
registerProcessor('crush-processor', CrushProcessor);
class ShapeProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'shape', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
return [{ name: 'shape', defaultValue: 0 }];
}
constructor() {
super();
this.notStarted = true;
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const shape0 = parameters.shape[0];
const shape1 = shape0 < 1 ? shape0 : 1.0 - 4e-10;
const shape = (2.0 * shape1) / (1.0 - shape1);
const blockSize = 128;
let shape = parameters.shape[0];
shape = shape < 1 ? shape : 1.0 - 4e-10;
shape = (2.0 * shape) / (1.0 - shape);
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
}
}
return true;
}
}
registerProcessor('shape-processor', ShapeProcessor);
class DistortProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'distort', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
}
constructor() {
super();
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const blockSize = 128;
const shape = Math.expm1(parameters.distort[0]);
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
}
}
return true;
}
}
registerProcessor('distort-processor', DistortProcessor);
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
const polyBlep = (phase, dt) => {
// 0 <= phase < 1
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
// -1 < phase < 0
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
return phase * phase + phase + phase + 1;
}
// 0 otherwise
else {
return 0;
}
};
const saw = (phase, dt) => {
const v = 2 * phase - 1;
return v - polyBlep(phase, dt);
};
function lerp(a, b, n) {
return n * (b - a) + a;
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.phase = [];
}
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'panspread',
defaultValue: 0.4,
min: 0,
max: 1,
},
{
name: 'freqspread',
defaultValue: 0.2,
min: 0,
},
{
name: 'detune',
defaultValue: 0,
min: 0,
},
{
name: 'voices',
defaultValue: 5,
min: 1,
},
];
}
process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) {
return true;
}
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0];
const voices = params.voices[0];
const freqspread = params.freqspread[0];
const panspread = params.panspread[0] * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) {
this.phase[n] = this.phase[n] ?? Math.random();
const v = saw(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR;
this.phase[n] += dt;
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
const hasInput = !(input[0] === undefined);
if (hasInput) {
this.notStarted = false;
for (let n = 0; n < blockSize; n++) {
const value = ((1 + shape) * input[0][n]) / (1 + shape * Math.abs(input[0][n]));
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
}
}
return true;
return this.notStarted || hasInput;
}
}
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
registerProcessor('shape-processor', ShapeProcessor);
-2
View File
@@ -5,5 +5,3 @@ export * from './tonal.mjs';
export * from './voicings.mjs';
import './ireal.mjs';
export const packageName = '@strudel/tonal';
+1 -33
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, n, seq, note } from '@strudel/core';
import { pure, n, seq } from '@strudel/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
@@ -44,36 +44,4 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('transposes note numbers with interval numbers', () => {
expect(
note(40, 40, 40)
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(40, 40, 40)
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note('c', 'c', 'c')
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note('c', 'c', 'c')
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
});
+48 -89
View File
@@ -96,37 +96,18 @@ function scaleOffset(scale, offset, note) {
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return pat.withHap((hap) => {
const note = hap.value.note ?? hap.value;
if (typeof note === 'number') {
// note is a number, so just add the number semitones of the interval
let semitones;
if (typeof intervalOrSemitones === 'number') {
semitones = intervalOrSemitones;
} else if (typeof intervalOrSemitones === 'string') {
semitones = Interval.semitones(intervalOrSemitones) || 0;
}
const targetNote = note + semitones;
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
return hap.withValue(() => targetNote);
}
if (typeof note !== 'string' || !isNote(note)) {
logger(`[tonal] transpose: not a note "${note}"`, 'warning');
return hap;
}
// note is a string, so we might be able to preserve harmonics if interval is a string as well
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones)
? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones);
if (typeof hap.value === 'number') {
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
return hap.withValue(() => hap.value + semitones);
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({ ...hap.value, note: Note.simplify(Note.transpose(hap.value.note, interval)) }));
// TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
// TODO: check if this is still relevant..
const targetNote = Note.simplify(Note.transpose(note, interval));
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
return hap.withValue(() => targetNote);
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
});
});
@@ -188,66 +169,44 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
* .s("piano")
*/
export const scale = register(
'scale',
function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.flat().join(' ');
}
return (
pat
.fmap((value) => {
const isObject = typeof value === 'object';
let step = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
export const scale = register('scale', function (scale, pat) {
// Supports ':' list syntax in mininotation
if (Array.isArray(scale)) {
scale = scale.flat().join(' ');
}
return (
pat
.fmap((value) => {
const isObject = typeof value === 'object';
let step = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
}
if (isNote(step)) {
// legacy..
return pure(step);
}
const asNumber = Number(step);
if (isNaN(asNumber)) {
logger(`[tonal] invalid scale step "${step}", expected number`, 'error');
return silence;
}
try {
let note;
if (value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (isNote(step)) {
// legacy..
return pure(step);
}
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
}
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
}
try {
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
}
return value;
})
.outerJoin()
// legacy:
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
);
},
true,
true, // preserve tactus
);
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
}
return value;
})
.outerJoin()
// legacy:
.withHap((hap) => hap.setContext({ ...hap.context, scale }))
);
});
+2 -17
View File
@@ -77,13 +77,8 @@ export const voicingRegistry = {
lefthand: { dictionary: lefthand, range: ['F3', 'A4'], mode: 'below', anchor: 'a4' },
triads: { dictionary: triads, mode: 'below', anchor: 'a4' },
guidetones: { dictionary: guidetones, mode: 'above', anchor: 'a4' },
legacy: { dictionary: defaultDictionary, mode: 'below', anchor: 'a4' },
default: { dictionary: defaultDictionary, mode: 'below', anchor: 'a4' },
};
let defaultDict = 'ireal';
export const setDefaultVoicings = (dict) => (defaultDict = dict);
// e.g. typeof setDefaultVoicings !== 'undefined' && setDefaultVoicings('legacy');
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
/**
@@ -198,7 +193,7 @@ export const voicing = register('voicing', function (pat) {
.fmap((value) => {
// destructure voicing controls out
value = typeof value === 'string' ? { chord: value } : value;
let { dictionary = defaultDict, chord, anchor, offset, mode, n, octaves, ...rest } = value;
let { dictionary = 'default', chord, anchor, offset, mode, n, octaves, ...rest } = value;
dictionary =
typeof dictionary === 'string' ? voicingRegistry[dictionary] : { dictionary, mode: 'below', anchor: 'c5' };
try {
@@ -235,17 +230,7 @@ Object.keys(simple).forEach((symbol) => {
let alias = symbol.replace('^', 'M');
voicingAlias(symbol, alias, [complex, simple]);
}
// add aliases for "+" === "aug"
if (symbol.includes('+')) {
let alias = symbol.replace('+', 'aug');
voicingAlias(symbol, alias, [complex, simple]);
}
});
registerVoicings('ireal', simple);
registerVoicings('ireal-ext', complex);
export function resetVoicings() {
lastVoicing = undefined;
setDefaultVoicings('ireal');
}
+4 -79
View File
@@ -3,11 +3,6 @@ import { parse } from 'acorn';
import escodegen from 'escodegen';
import { walk } from 'estree-walker';
let widgetMethods = [];
export function registerWidgetType(type) {
widgetMethods.push(type);
}
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
@@ -39,7 +34,7 @@ export function transpiler(input, options = {}) {
emitMiniLocations && collectMiniLocations(value, node);
return this.replace(miniWithLocation(value, node));
}
if (isSliderFunction(node)) {
if (isWidgetFunction(node)) {
emitWidgets &&
widgets.push({
from: node.arguments[0].start,
@@ -48,27 +43,12 @@ export function transpiler(input, options = {}) {
min: node.arguments[1]?.value ?? 0,
max: node.arguments[2]?.value ?? 1,
step: node.arguments[3]?.value,
type: 'slider',
});
return this.replace(sliderWithLocation(node));
}
if (isWidgetMethod(node)) {
const type = node.callee.property.name;
const index = widgets.filter((w) => w.type === type).length;
const widgetConfig = {
to: node.end,
index,
type,
};
emitWidgets && widgets.push(widgetConfig);
return this.replace(widgetWithLocation(node, widgetConfig));
return this.replace(widgetWithLocation(node));
}
if (isBareSamplesCall(node, parent)) {
return this.replace(withAwait(node));
}
if (isLabelStatement(node)) {
return this.replace(labelToP(node));
}
},
leave(node, parent, prop, index) {},
});
@@ -125,15 +105,11 @@ function miniWithLocation(value, node) {
// these functions are connected to @strudel/codemirror -> slider.mjs
// maybe someday there will be pluggable transpiler functions, then move this there
function isSliderFunction(node) {
function isWidgetFunction(node) {
return node.type === 'CallExpression' && node.callee.name === 'slider';
}
function isWidgetMethod(node) {
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
}
function sliderWithLocation(node) {
function widgetWithLocation(node) {
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
@@ -146,27 +122,6 @@ function sliderWithLocation(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) {
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
}
@@ -177,33 +132,3 @@ function withAwait(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}'`,
},
],
},
};
}
+8 -5
View File
@@ -7,17 +7,20 @@ This package provides an easy to use bundle of multiple strudel packages for the
Save this code as a `.html` file and double click it:
```html
<!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<!DOCTYPE html>
<button id="play">play</button>
<button id="stop">stop</button>
<script>
<script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').jux(rev).play());
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
document.getElementById('stop').addEventListener('click', () => hush());
</script>
```
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
As soon as you call `initStrudel()`, all strudel functions are made available.
In this case, we are using the `note` function to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
@@ -76,4 +79,4 @@ There will probably be an escapte hatch for that in the future.
## More Examples
Check out the examples folder for more examples, both using plain html and vite!
Check out the examples folder for more examples, both using plain html and vite!
+3 -3
View File
@@ -37,10 +37,10 @@
"@strudel/mini": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@strudel/webaudio": "workspace:*"
"@strudel/webaudio": "workspace:*",
"@rollup/plugin-replace": "^5.0.5"
},
"devDependencies": {
"vite": "^5.0.10",
"@rollup/plugin-replace": "^5.0.5"
"vite": "^5.0.10"
}
}
-1
View File
@@ -34,7 +34,6 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*"
},
"devDependencies": {
+20 -56
View File
@@ -1,37 +1,18 @@
import { Pattern, clamp } from '@strudel/core';
import { getDrawContext, getTheme } from '@strudel/draw';
import { analysers, getAnalyzerData } from 'superdough';
import { Pattern, getDrawContext, clamp } from '@strudel/core';
import { analyser, getAnalyzerData } from 'superdough';
export function drawTimeScope(
analyser,
{
align = true,
color = 'white',
thickness = 3,
scale = 0.25,
pos = 0.75,
trigger = 0,
ctx = getDrawContext(),
id = 1,
} = {},
{ align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, trigger = 0 } = {},
) {
const ctx = getDrawContext();
const dataArray = getAnalyzerData('time');
ctx.lineWidth = thickness;
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();
let canvas = ctx.canvas;
const bufferSize = analyser.frequencyBinCount;
let triggerIndex = align
@@ -57,17 +38,10 @@ export function drawTimeScope(
export function drawFrequencyScope(
analyser,
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0, ctx = getDrawContext(), id = 1 } = {},
{ color = 'white', scale = 0.25, pos = 0.75, lean = 0.5, min = -150, max = 0 } = {},
) {
if (!analyser) {
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 dataArray = getAnalyzerData('frequency');
const ctx = getDrawContext();
const canvas = ctx.canvas;
ctx.fillStyle = color;
@@ -86,7 +60,8 @@ export function drawFrequencyScope(
}
}
function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) {
function clearScreen(smear = 0, smearRGB = `0,0,0`) {
const ctx = getDrawContext();
if (!smear) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
} else {
@@ -108,14 +83,10 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`, ctx = getDrawContext()) {
* s("sawtooth").fscope()
*/
Pattern.prototype.fscope = function (config = {}) {
let id = config.id ?? 1;
return this.analyze(id).draw(
() => {
clearScreen(config.smear, '0,0,0', config.ctx);
analysers[id] && drawFrequencyScope(analysers[id], config);
},
{ id },
);
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawFrequencyScope(analyser, config);
});
};
/**
@@ -132,18 +103,11 @@ Pattern.prototype.fscope = function (config = {}) {
* @example
* s("sawtooth").scope()
*/
let latestColor = {};
Pattern.prototype.tscope = function (config = {}) {
let id = config.id ?? 1;
return this.analyze(id).draw(
(haps) => {
config.color = haps[0]?.value?.color || getTheme().foreground;
latestColor[id] = config.color;
clearScreen(config.smear, '0,0,0', config.ctx);
drawTimeScope(analysers[id], config);
},
{ id },
);
return this.analyze(1).draw(() => {
clearScreen(config.smear);
analyser && drawTimeScope(analyser, config);
});
};
Pattern.prototype.scope = Pattern.prototype.tscope;
+2 -4
View File
@@ -12,13 +12,11 @@ setLogger(logger);
const hap2value = (hap) => {
hap.ensureObjectValue();
return hap.value;
return { ...hap.value, velocity: hap.context.velocity };
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) =>
superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration);
export const webaudioOutput = (hap, deadline, hapDuration) => superdough(hap2value(hap), deadline, hapDuration);
Pattern.prototype.webaudio = function () {
return this.onTrigger(webaudioOutputTrigger);
+193 -761
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
# samples folder
1. copy any samples to this folder
2. run `npx @strudel/sampler` from this folder
3. add `samples('local:')` to your code
+115 -123
View File
@@ -1,22 +1,22 @@
use rosc::{encoder, OscTime};
use rosc::{OscBundle, OscMessage, OscPacket, OscType};
use rosc::{ encoder, OscTime };
use rosc::{ OscMessage, OscPacket, OscType, OscBundle };
use std::net::UdpSocket;
use serde::Deserialize;
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;
use tokio::sync::{ mpsc, Mutex };
use serde::Deserialize;
use std::thread::sleep;
use crate::loggerbridge::Logger;
pub struct OscMsg {
pub msg_buf: Vec<u8>,
pub timestamp: u64,
pub msg_buf: Vec<u8>,
pub timestamp: u64,
}
pub struct AsyncInputTransmit {
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
pub inner: Mutex<mpsc::Sender<Vec<OscMsg>>>,
}
const UNIX_OFFSET: u64 = 2_208_988_800; // 70 years in seconds
@@ -25,141 +25,133 @@ const NANOS_PER_SECOND: f64 = 1.0e9;
const SECONDS_PER_NANO: f64 = 1.0 / NANOS_PER_SECOND;
pub fn init(
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>,
logger: Logger,
async_input_receiver: mpsc::Receiver<Vec<OscMsg>>,
mut async_output_receiver: mpsc::Receiver<Vec<OscMsg>>,
async_output_transmitter: mpsc::Sender<Vec<OscMsg>>
) {
tauri::async_runtime::spawn(async move {
async_process_model(async_input_receiver, async_output_transmitter).await
});
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let message_queue: Arc<Mutex<Vec<OscMsg>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Listen For incoming messages and add to queue
Open OSC Ports
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
let to_addr = String::from("127.0.0.1:57120");
sock.set_nonblocking(true).unwrap();
sock.connect(to_addr).expect("could not connect to OSC address");
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
message_queue.retain(|message| {
let result = sock.send(&message.msg_buf);
if result.is_err() {
logger.log(
format!("OSC Message failed to send, the server might no longer be available"),
"error".to_string()
);
}
});
return false;
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Open OSC Ports
............................................................*/
let sock = UdpSocket::bind("127.0.0.1:57122").unwrap();
let to_addr = String::from("127.0.0.1:57120");
sock.set_nonblocking(true).unwrap();
sock.connect(to_addr)
.expect("could not connect to OSC address");
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
message_queue.retain(|message| {
let result = sock.send(&message.msg_buf);
if result.is_err() {
logger.log(
format!(
"OSC Message failed to send, the server might no longer be available"
),
"error".to_string(),
);
}
return false;
});
sleep(Duration::from_millis(1));
}
});
sleep(Duration::from_millis(1));
}
});
}
pub async fn async_process_model(
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
output_transmitter: mpsc::Sender<Vec<OscMsg>>,
mut input_reciever: mpsc::Receiver<Vec<OscMsg>>,
output_transmitter: mpsc::Sender<Vec<OscMsg>>
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
while let Some(input) = input_reciever.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct Param {
name: String,
value: String,
valueisnumber: bool,
name: String,
value: String,
valueisnumber: bool,
}
#[derive(Deserialize)]
pub struct MessageFromJS {
params: Vec<Param>,
timestamp: u64,
target: String,
params: Vec<Param>,
timestamp: u64,
target: String,
}
// Called from JS
#[tauri::command]
pub async fn sendosc(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>,
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmit>
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<OscMsg> = Vec::new();
for m in messagesfromjs {
let mut args = Vec::new();
for p in m.params {
args.push(OscType::String(p.name));
if p.valueisnumber {
args.push(OscType::Float(p.value.parse().unwrap()));
} else {
args.push(OscType::String(p.value));
}
}
let duration_since_epoch =
Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
let seconds = u32::try_from(duration_since_epoch.as_secs())
.map_err(|_| "bit conversion failed for osc message timetag")?;
let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
let timetag = OscTime::from((seconds, fractional));
let packet = OscPacket::Message(OscMessage {
addr: m.target,
args,
});
let bundle = OscBundle {
content: vec![packet],
timetag,
};
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
let message_to_process = OscMsg {
msg_buf,
timestamp: m.timestamp,
};
messages_to_process.push(message_to_process);
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<OscMsg> = Vec::new();
for m in messagesfromjs {
let mut args = Vec::new();
for p in m.params {
args.push(OscType::String(p.name));
if p.valueisnumber {
args.push(OscType::Float(p.value.parse().unwrap()));
} else {
args.push(OscType::String(p.value));
}
}
async_proc_input_tx
.send(messages_to_process)
.await
.map_err(|e| e.to_string())
let duration_since_epoch = Duration::from_millis(m.timestamp) + Duration::new(UNIX_OFFSET, 0);
let seconds = u32
::try_from(duration_since_epoch.as_secs())
.map_err(|_| "bit conversion failed for osc message timetag")?;
let nanos = duration_since_epoch.subsec_nanos() as f64;
let fractional = (nanos * SECONDS_PER_NANO * TWO_POW_32).round() as u32;
let timetag = OscTime::from((seconds, fractional));
let packet = OscPacket::Message(OscMessage {
addr: m.target,
args,
});
let bundle = OscBundle {
content: vec![packet],
timetag,
};
let msg_buf = encoder::encode(&OscPacket::Bundle(bundle)).unwrap();
let message_to_process = OscMsg {
msg_buf,
timestamp: m.timestamp,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -222,6 +222,7 @@ export const testCycles = {
randomBells: 24,
waa: 16,
waar: 16,
hyperpop: 10,
festivalOfFingers3: 16,
};
+46 -422
View File
@@ -1,86 +1,6 @@
{
"/doc.json": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/codemirror/html.mjs": [
"html",
"h"
],
"/packages/codemirror/autocomplete.mjs": [
"Autocomplete",
"strudelAutocomplete",
"isAutoCompletionEnabled"
],
"/packages/codemirror/tooltip.mjs": [
"strudelTooltip",
"isTooltipEnabled"
],
"/packages/codemirror/flash.mjs": [
"setFlash",
"flashField",
"flash",
"isFlashEnabled"
],
"/packages/codemirror/highlight.mjs": [
"setMiniLocations",
"showMiniLocations",
"updateMiniLocations",
"highlightMiniLocations",
"highlightExtension",
"isPatternHighlightingEnabled"
],
"/packages/codemirror/keybindings.mjs": [
"keybindings"
],
"/packages/codemirror/themes/strudel-theme.mjs": [],
"/packages/codemirror/themes/bluescreen.mjs": [
"settings"
],
"/packages/codemirror/themes/blackscreen.mjs": [
"settings"
],
"/packages/codemirror/themes/whitescreen.mjs": [
"settings"
],
"/packages/codemirror/themes/teletext.mjs": [
"settings"
],
"/packages/codemirror/themes/algoboy.mjs": [
"settings"
],
"/packages/codemirror/themes/terminal.mjs": [
"settings"
],
"/packages/codemirror/themes.mjs": [
"themes",
"settings",
"themeColors",
"theme",
"injectStyle",
"initTheme",
"activateTheme"
],
"/packages/codemirror/slider.mjs": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/codemirror/widget.mjs": [
"addWidget",
"updateWidgets",
"setWidget",
"BlockWidget",
"widgetPlugin",
"registerWidget"
],
"/packages/codemirror/codemirror.mjs": [
"defaultSettings",
"codemirrorSettings",
"initEditor",
"StrudelMirror"
],
"/packages/codemirror/index.mjs": [],
"/packages/core/fraction.mjs": [
"gcd",
"lcm"
"gcd"
],
"/packages/core/timespan.mjs": [
"TimeSpan"
@@ -91,10 +11,6 @@
"/packages/core/state.mjs": [
"State"
],
"/packages/core/logger.mjs": [
"logKey",
"logger"
],
"/packages/core/util.mjs": [
"isNoteWithOctave",
"isNote",
@@ -104,14 +20,13 @@
"freqToMidi",
"valueToMidi",
"_mod",
"nanFallback",
"getSoundIndex",
"getPlayableNoteValue",
"getFrequency",
"rotate",
"pipe",
"compose",
"flatten",
"id",
"constant",
"listRange",
"curry",
@@ -127,8 +42,7 @@
"unicodeToBase64",
"base64ToUnicode",
"code2hash",
"hash2code",
"objectMap"
"hash2code"
],
"/packages/core/value.mjs": [
"unionWithObj",
@@ -137,20 +51,18 @@
"map"
],
"/packages/core/drawLine.mjs": [],
"/packages/core/logger.mjs": [
"logKey",
"logger"
],
"/packages/core/pattern.mjs": [
"setStringParser",
"polyrhythm",
"pr",
"pm",
"nothing",
"isPattern",
"reify",
"stackLeft",
"stackRight",
"stackCentre",
"stackBy",
"fastcat",
"_polymeterListSteps",
"set",
"keep",
"keepif",
@@ -187,200 +99,20 @@
"stutWith",
"stutwith",
"iterback",
"slowchunk",
"slowChunk",
"chunkback",
"fastchunk",
"bypass",
"duration",
"hsla",
"hsl",
"colour",
"loopat",
"loopatcps"
],
"/packages/core/controls.mjs": [
"createParam",
"sound",
"src",
"att",
"fmi",
"fmrelease",
"fmvelocity",
"analyze",
"fft",
"dec",
"sus",
"rel",
"hold",
"bandf",
"bp",
"bandq",
"loopb",
"loope",
"ch",
"phaserrate",
"phasr",
"ph",
"phs",
"phc",
"phd",
"phasdp",
"cutoff",
"ctf",
"lp",
"lpe",
"hpe",
"bpe",
"lpa",
"hpa",
"bpa",
"lpd",
"hpd",
"bpd",
"lps",
"hps",
"bps",
"lpr",
"hpr",
"bpr",
"fanchor",
"vibrato",
"v",
"vmod",
"hcutoff",
"hp",
"hresonance",
"resonance",
"delayfb",
"dfb",
"delayt",
"dt",
"lock",
"det",
"fadeTime",
"fadeOutTime",
"fadeInTime",
"patt",
"pdec",
"psustain",
"psus",
"prel",
"gate",
"gat",
"activeLabel",
"degree",
"mtranspose",
"ctranspose",
"harmonic",
"stepsPerOctave",
"octaveR",
"nudge",
"overgain",
"overshape",
"panspan",
"pansplay",
"panwidth",
"panorient",
"rate",
"slide",
"semitone",
"voice",
"chord",
"dictionary",
"dict",
"anchor",
"offset",
"octaves",
"mode",
"rlp",
"rdim",
"rfade",
"ir",
"size",
"sz",
"rsize",
"dist",
"compressorKnee",
"compressorRatio",
"compressorAttack",
"compressorRelease",
"waveloss",
"density",
"expression",
"sustainpedal",
"tremolodepth",
"tremdp",
"tremolorate",
"tremr",
"fshift",
"fshiftnote",
"fshiftphase",
"triode",
"krush",
"kcutoff",
"octer",
"octersub",
"octersubsub",
"ring",
"ringf",
"ringdf",
"freeze",
"xsdelay",
"tsdelay",
"real",
"imag",
"enhance",
"partials",
"comb",
"smear",
"scram",
"binshift",
"hbrick",
"lbrick",
"midichan",
"control",
"ccn",
"ccv",
"polyTouch",
"midibend",
"miditouch",
"ctlNum",
"frameRate",
"frames",
"hours",
"midicmd",
"minutes",
"progNum",
"seconds",
"songPtr",
"uid",
"val",
"cps",
"legato",
"dur",
"zrand",
"curve",
"deltaSlide",
"pitchJump",
"pitchJumpTime",
"lfo",
"repeatTime",
"znoise",
"zmod",
"zcrush",
"zdelay",
"tremolo",
"zzfx",
"colour",
"createParams",
"ad",
"ds",
"ar"
],
"/packages/core/controls.mjs": [],
"/packages/core/euclid.mjs": [
"bjork",
"euclidrot"
],
"/packages/core/zyklus.mjs": [],
"/packages/core/signal.mjs": [
"steady",
"signal",
@@ -393,14 +125,16 @@
"tri2",
"time",
"_brandBy",
"brandBy",
"brand",
"_irand",
"pickSqueeze",
"pickmodSqueeze",
"__chooseWith",
"randcat",
"wrandcat",
"wchoose",
"wchooseCycles",
"perlinWith",
"degradeByWith"
"degradeByWith",
"undegrade"
],
"/packages/core/speak.mjs": [
"speak"
@@ -409,9 +143,7 @@
"evalScope",
"evaluate"
],
"/packages/core/neocyclist.mjs": [
"NeoCyclist"
],
"/packages/core/zyklus.mjs": [],
"/packages/core/cyclist.mjs": [
"Cyclist"
],
@@ -423,6 +155,31 @@
"repl",
"getTrigger"
],
"/packages/core/draw.mjs": [
"getDrawContext",
"cleanupDraw",
"Framer",
"Drawer"
],
"/packages/core/animate.mjs": [
"x",
"y",
"w",
"h",
"angle",
"r",
"fill",
"smear",
"rescale",
"moveXY",
"zoomIn"
],
"/packages/core/pianoroll.mjs": [
"getDrawOptions",
"getPunchcardPainter",
"drawPianoroll"
],
"/packages/core/spiral.mjs": [],
"/packages/core/ui.mjs": [
"backgroundImage",
"cleanupUi"
@@ -442,37 +199,6 @@
"/packages/desktopbridge/midibridge.mjs": [],
"/packages/desktopbridge/oscbridge.mjs": [],
"/packages/desktopbridge/index.mjs": [],
"/packages/draw/draw.mjs": [
"getDrawContext",
"cleanupDraw",
"Framer",
"Drawer"
],
"/packages/draw/animate.mjs": [
"x",
"y",
"w",
"h",
"angle",
"r",
"fill",
"smear",
"rescale",
"moveXY",
"zoomIn"
],
"/packages/draw/color.mjs": [
"colorMap",
"convertColorToNumber",
"convertHexToNumber"
],
"/packages/draw/pianoroll.mjs": [
"getDrawOptions",
"getPunchcardPainter",
"drawPianoroll"
],
"/packages/draw/spiral.mjs": [],
"/packages/draw/index.mjs": [],
"/packages/midi/midi.mjs": [
"WebMidi",
"enableWebMidi",
@@ -493,13 +219,6 @@
"miniAllStrings"
],
"/packages/mini/index.mjs": [],
"/packages/repl/prebake.mjs": [
"prebake"
],
"/packages/repl/repl-component.mjs": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/repl/index.mjs": [],
"/packages/soundfonts/gm.mjs": [],
"/packages/soundfonts/fontloader.mjs": [
"getFontBufferSource",
@@ -515,92 +234,7 @@
"loadSoundfont"
],
"/packages/soundfonts/index.mjs": [],
"/packages/superdough/feedbackdelay.mjs": [],
"/packages/superdough/reverbGen.mjs": [],
"/packages/superdough/reverb.mjs": [],
"/packages/superdough/vowel.mjs": [
"vowelFormant"
],
"/packages/superdough/logger.mjs": [
"logger",
"setLogger"
],
"/packages/superdough/util.mjs": [
"tokenizeNote",
"noteToMidi",
"midiToFreq",
"clamp",
"freqToMidi",
"valueToMidi",
"nanFallback",
"_mod",
"getSoundIndex"
],
"/packages/superdough/helpers.mjs": [
"gainNode",
"getParamADSR",
"getCompressor",
"getADSRValues",
"createFilter",
"drywet",
"getPitchEnvelope",
"getVibratoOscillator",
"webAudioTimeout",
"applyFM"
],
"/packages/superdough/sampler.mjs": [
"getCachedBuffer",
"getSampleBufferSource",
"loadBuffer",
"reverseBuffer",
"getLoadedBuffer",
"processSampleMap",
"registerSamplesPrefix",
"onTriggerSample"
],
"/packages/superdough/superdough.mjs": [
"soundMap",
"registerSound",
"getSound",
"resetLoadedSounds",
"setDefaultAudioContext",
"getAudioContext",
"getWorklet",
"initAudio",
"initAudioOnFirstClick",
"initializeAudioOutput",
"connectToDestination",
"panic",
"analysers",
"analysersData",
"getAnalyserById",
"getAnalyzerData",
"resetGlobalEffects",
"superdough",
"superdoughTrigger"
],
"/packages/superdough/noise.mjs": [
"getNoiseOscillator",
"getNoiseMix"
],
"/packages/superdough/synth.mjs": [
"registerSynthSounds",
"waveformN",
"getOscillator"
],
"/packages/superdough/zzfx_fork.mjs": [
"acorn parse error: SyntaxError: undefined"
],
"/packages/superdough/zzfx.mjs": [
"getZZFX",
"registerZZFXSounds"
],
"/packages/superdough/dspworklet.mjs": [
"dspWorklet",
"dough",
"doughTrigger"
],
"/packages/superdough/index.mjs": [],
"/packages/tonal/tonal.mjs": [],
"/packages/tonal/tonleiter.mjs": [
"pc2chroma",
"rotateChroma",
@@ -608,40 +242,30 @@
"tokenizeChord",
"note2pc",
"note2oct",
"note2midi",
"note2chroma",
"midi2chroma",
"pitch2chroma",
"step2semitones",
"x2midi",
"scaleStep",
"nearestNumberIndex",
"stepInNamedScale",
"renderVoicing",
"accidentalOffset",
"Step",
"Note"
],
"/packages/tonal/tonal.mjs": [],
"/packages/tonal/ireal.mjs": [
"simple",
"complex"
],
"/packages/tonal/voicings.mjs": [
"voicingRegistry",
"setDefaultVoicings",
"setVoicingRange",
"registerVoicings",
"voicingAlias",
"resetVoicings"
],
"/packages/tonal/index.mjs": [
"packageName"
"voicingAlias"
],
"/packages/tonal/index.mjs": [],
"/packages/transpiler/transpiler.mjs": [
"registerWidgetType",
"transpiler",
"getWidgetID"
"transpiler"
],
"/packages/transpiler/index.mjs": [
"evaluate"

Some files were not shown because too many files have changed in this diff Show More