Merge branch 'main' of https://codeberg.org/uzu/strudel into fix/midi-init-bug

This commit is contained in:
Chandler Abraham 2025-07-22 21:52:03 -07:00
commit 2eda047108
213 changed files with 6685 additions and 2331 deletions

View file

@ -2,4 +2,4 @@
Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel).
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/wiki/Technical-Manual) or the individual READMEs.

View file

@ -1,7 +1,7 @@
import { closeBrackets } from '@codemirror/autocomplete';
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { history, indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
@ -37,6 +37,14 @@ const extensions = {
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
isFlashEnabled,
keybindings,
isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []),
isMultiCursorEnabled: (on) =>
on
? [
EditorState.allowMultipleSelections.of(true),
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
]
: [],
};
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
@ -51,6 +59,8 @@ export const defaultSettings = {
isFlashEnabled: true,
isTooltipEnabled: false,
isLineWrappingEnabled: false,
isTabIndentationEnabled: false,
isMultiCursorEnabled: false,
theme: 'strudelTheme',
fontFamily: 'monospace',
fontSize: 18,
@ -62,7 +72,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
});
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
const settings = codemirrorSettings.get();
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
@ -75,7 +85,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
/* search(),
highlightSelectionMatches(), */
...initialSettings,
javascript(),
mondo ? [] : javascript(),
sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
@ -209,6 +219,7 @@ export class StrudelMirror {
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
mondo: replOptions.mondo,
});
const cmEditor = this.root.querySelector('.cm-editor');
if (cmEditor) {

View file

@ -1,4 +1,4 @@
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setMiniLocations = StateEffect.define();
@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => {
setTimeout(() => {
updateMiniLocations(config.editor, config.miniLocations);
}, 100);
return on ? highlightExtension : [];
return on ? Prec.highest(highlightExtension) : [];
};

View file

@ -21,11 +21,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const keymaps = {
vim,
emacs,
codemirror: () => keymap.of(defaultKeymap),
vscode: vscodeExtension,
};
export function keybindings(name) {
const active = keymaps[name];
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
// keymap.of(searchKeymap),
return [active ? active() : [], keymap.of(historyKeymap)];
}

View file

@ -13,7 +13,7 @@
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -28,9 +28,9 @@
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.18.4",
"@codemirror/commands": "^6.8.0",

View file

@ -8,6 +8,11 @@ import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs';
import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs';
import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs';
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
import aura, { settings as auraSettings } from './themes/aura.mjs';
@ -39,17 +44,20 @@ import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
algoboy,
archBtw,
androidstudio,
atomone,
aura,
bbedit,
blackscreen,
bluescreen,
bluescreenlight,
CutiePi,
darcula,
dracula,
duotoneDark,
eclipse,
fruitDaw,
githubDark,
githubLight,
greenText,
@ -78,10 +86,12 @@ export const themes = {
export const settings = {
strudelTheme: strudelThemeSettings,
bluescreen: bluescreenSettings,
bluescreenlight: bluescreenlightsettings,
blackscreen: blackscreenSettings,
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
archBtw: archBtwSettings,
androidstudio: androidstudioSettings,
atomone: atomOneSettings,
aura: auraSettings,
@ -92,9 +102,11 @@ export const settings = {
eclipse: eclipseSettings,
CutiePi: CutiePiSettings,
sonicPink: sonicPinkSettings,
fruitDaw: fruitDawSettings,
githubLight: githubLightSettings,
githubDark: githubDarkSettings,
greenText: greenTextSettings,
gruvboxDark: gruvboxDarkSettings,
gruvboxLight: gruvboxLightSettings,
materialDark: materialDarkSettings,

38
packages/codemirror/themes/archBtw.mjs vendored Normal file
View file

@ -0,0 +1,38 @@
/*
* Arch Btw
* Modern terminal inspired theme
* made by Jade
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[1],
selection: hex[2],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[2],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[1],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[1] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});

View file

@ -0,0 +1,37 @@
/*
* A lighter blue screen theme
* made by Jade
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[3],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[2],
gutterBorder: 'transparent',
lineHighlight: hex[1],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] },
{ tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[2] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
],
});

50
packages/codemirror/themes/fruitDaw.mjs vendored Normal file
View file

@ -0,0 +1,50 @@
/*
* Fruit Daw
* made by Jade
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = [
'rgb(84, 93, 98)',
'rgb(255, 255, 255)',
'rgba(255, 255, 255, .25)',
'rgb(67, 76, 81)',
'rgb(186, 230, 115)',
'rgb(252, 184, 67)',
'rgb(124, 206, 254)',
'rgb(83, 101, 102)',
'rgba(46, 62, 72,.5)',
'rgb(94, 100, 108)',
'rgb(167, 216, 177)',
];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[10],
selection: hex[8],
selectionMatch: hex[0],
gutterBackground: hex[3],
gutterForeground: hex[2],
gutterBorder: 'transparent',
lineHighlight: hex[3],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[1],
},
{ tag: [t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
{ tag: [t.variableName], color: hex[1] },
{ tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] },
{ tag: [t.attributeName, t.number], color: hex[6] },
{ tag: t.keyword, color: hex[5] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] },
],
});

View file

@ -113,7 +113,7 @@ self.onconnect = function (e) {
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>
// used to consistently schedule events, for use in a service worker - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/clockworker.mjs>
function createClock(
getTime,
callback, // called slightly before each cycle

View file

@ -1,16 +1,17 @@
/*
controls.mjs - Registers audio controls for pattern manipulation and effects.
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/controls.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/controls.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, register, sequence } from './pattern.mjs';
import { Pattern, register, reify } from './pattern.mjs';
export function createParam(names) {
let isMulti = Array.isArray(names);
names = !isMulti ? [names] : names;
const name = names[0];
// todo: make this less confusing
const withVal = (xs) => {
let bag;
// check if we have an object with an unnamed control (.value)
@ -35,25 +36,34 @@ export function createParam(names) {
}
};
const func = (...pats) => sequence(...pats).withValue(withVal);
const setter = function (...pats) {
if (!pats.length) {
return this.fmap(withVal);
// todo: make this less confusing
const func = function (value, pat) {
if (!pat) {
return reify(value).withValue(withVal);
}
return this.set(func(...pats));
if (typeof value === 'undefined') {
return pat.fmap(withVal);
}
return pat.set(reify(value).withValue(withVal));
};
Pattern.prototype[name] = function (value) {
return func(value, this);
};
Pattern.prototype[name] = setter;
return func;
}
// maps control alias names to the "main" control name
const controlAlias = new Map();
export function isControlName(name) {
return controlAlias.has(name);
}
export function registerControl(names, ...aliases) {
const name = Array.isArray(names) ? names[0] : names;
let bag = {};
bag[name] = createParam(names);
controlAlias.set(name, name);
aliases.forEach((alias) => {
bag[alias] = bag[name];
controlAlias.set(alias, name);
@ -96,7 +106,7 @@ export const { source, src } = registerControl('source', 'src');
* @example
* s("bd sd [~ bd] sd,hh*6").n("<0 1>")
*/
// also see https://github.com/tidalcycles/strudel/pull/63
// also see https://codeberg.org/uzu/strudel/pulls/63
export const { n } = registerControl('n');
/**
* Plays the given note name or midi number. A note name consists of
@ -348,7 +358,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
* s("bd sd [~ bd] sd").bpf(500).bpq("<0 1 2 3>")
*
*/
// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496
// currently an alias of 'bandq' https://codeberg.org/uzu/strudel/issues/496
// ['bpq'],
export const { bandq, bpq } = registerControl('bandq', 'bpq');
/**
@ -434,6 +444,78 @@ export const { crush } = registerControl('crush');
*/
export const { coarse } = registerControl('coarse');
/**
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
* @synonyms trem
* @param {number | Pattern} speed modulation speed in HZ
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
*
*/
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
/**
* modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
* @synonyms tremsync
* @param {number | Pattern} cycles modulation speed in cycles
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>")
*
*/
export const { tremolosync } = registerControl(
['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'],
'tremsync',
);
/**
* depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
* @param {number | Pattern} depth
* @example
* note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>")
*
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>")
*
*/
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
* @param {number | Pattern} offset the offset in cycles of the modulation
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>")
*
*/
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* shape of amplitude modulation
*
* @name tremoloshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
*
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* filter overdrive for supported filter types
*
@ -443,6 +525,30 @@ export const { coarse } = registerControl('coarse');
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
*
*/
// TODO: SUPRADOUGH implement post orbit "pump" sidechain effect
// /**
// * modulate the amplitude of an orbit to create a "sidechain" like effect
// *
// * @name pump
// * @param {number | Pattern} speed modulation speed in cycles
// * @example
// * note("{f g c d}%16").s("sawtooth").pump(".25:.75")
// *
// */
// export const { pump } = registerControl(['pump', 'pumpdepth']);
// /**
// * modulate the amplitude of an orbit to create a "sidechain" like effect
// *
// * @name pumpdepth
// * @param {number | Pattern} depth depth of modulation from 0 to 1
// * @example
// * note("{f g c d}%16").s("sawtooth").pump(".25").depth("<.25 .5 .75 1>")
// *
// */
// export const { pumpdepth } = registerControl('pumpdepth');
export const { drive } = registerControl('drive');
/**
@ -855,7 +961,7 @@ export const { fanchor } = registerControl('fanchor');
* s("bd sd [~ bd] sd,hh*8").hpf("<2000 2000:25>")
*
*/
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496
// ['hpf'],
/**
* Applies a vibrato to the frequency of the oscillator.
@ -922,7 +1028,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq');
* s("bd sd [~ bd] sd,hh*8").lpf(2000).lpq("<0 10 20 30>")
*
*/
// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496
// currently an alias of 'resonance' https://codeberg.org/uzu/strudel/issues/496
export const { resonance, lpq } = registerControl('resonance', 'lpq');
/**
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
@ -975,15 +1081,31 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
*
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
/* // TODO: test
/**
* Sets the time of the delay effect in cycles.
*
* @name delaysync
* @param {number | Pattern} cycles delay length in cycles
* @synonyms delayt, dt
* @example
* s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8))
*
*/
export const { delaysync } = registerControl('delaysync');
/**
* Specifies whether delaytime is calculated relative to cps.
*
* @name lock
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
* @superdirtOnly
* @example
* s("sd").delay().lock(1).osc()
*
*
*/
export const { lock } = registerControl('lock');
/**
* Set detune for stacked voices of supported oscillators
@ -1288,7 +1410,7 @@ export const { semitone } = registerControl('semitone');
// TODO: synth param
export const { voice } = registerControl('voice');
// voicings // https://github.com/tidalcycles/strudel/issues/506
// voicings // https://codeberg.org/uzu/strudel/issues/506
// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
export const { chord } = registerControl('chord');
// which dictionary to use for the voicings
@ -1535,18 +1657,6 @@ export const { density } = registerControl('density');
// ['modwheel'],
export const { expression } = registerControl('expression');
export const { sustainpedal } = registerControl('sustainpedal');
/* // TODO: doesn't seem to do anything
*
* Tremolo Audio DSP effect
*
* @name tremolodepth
* @param {number | Pattern} depth between 0 and 1
* @example
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
*
*/
export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp');
export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr');
export const { fshift } = registerControl('fshift');
export const { fshiftnote } = registerControl('fshiftnote');
@ -1623,7 +1733,6 @@ export const { zmod } = registerControl('zmod');
// like crush but scaled differently
export const { zcrush } = registerControl('zcrush');
export const { zdelay } = registerControl('zdelay');
export const { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx');
/**

View file

@ -1,11 +1,11 @@
/*
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/neocyclist.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/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/>.
*/
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
import { errorLogger, logger } from './logger.mjs';
export class Cyclist {
constructor({
@ -65,8 +65,9 @@ export class Cyclist {
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + 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
// see https://codeberg.org/uzu/strudel/pulls/1004
const deadline = targetTime - phase;
// this onTrigger has another signature
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
this.cps = hap.value.cps;
@ -75,7 +76,7 @@ export class Cyclist {
}
});
} catch (e) {
logger(`[cyclist] error: ${e.message}`);
errorLogger(e);
onError?.(e);
}
},

View file

@ -1,6 +1,6 @@
/*
drawLine.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/drawLine.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/drawLine.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/>.
*/

View file

@ -2,7 +2,7 @@
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
Copyright (C) 2023 Rohan Drape and strudel contributors
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
See <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/euclid.mjs> for authors of this file.
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
https://rohandrape.net/?t=hmt
@ -10,9 +10,9 @@ https://rohandrape.net/?t=hmt
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, timeCat, register, silence } from './pattern.mjs';
import { timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction from './fraction.mjs';
import Fraction, { lcm } from './fraction.mjs';
const left = function (n, x) {
const [ons, offs] = n;
@ -42,29 +42,26 @@ const _bjork = function (n, x) {
export const bjork = function (ons, steps) {
const inverted = ons < 0;
ons = Math.abs(ons);
const offs = steps - ons;
const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]);
const p = flatten(result[1][0]).concat(flatten(result[1][1]));
if (inverted) {
return p.map((x) => (x === 0 ? 1 : 0));
}
return p;
const absOns = Math.abs(ons);
const offs = steps - absOns;
const ones = Array(absOns).fill([1]);
const zeros = Array(offs).fill([0]);
const result = _bjork([absOns, offs], [ones, zeros]);
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
return inverted ? pattern.map((x) => 1 - x) : pattern;
};
/**
* Changes the structure of the pattern to form an euclidean rhythm.
* Euclidian rhythms are rhythms obtained using the greatest common
* Changes the structure of the pattern to form an Euclidean rhythm.
* Euclidean rhythms are rhythms obtained using the greatest common
* divisor of two numbers. They were described in 2004 by Godfried
* Toussaint, a canadian computer scientist. Euclidian rhythms are
* Toussaint, a Canadian computer scientist. Euclidean rhythms are
* really useful for computer/algorithmic music because they can
* describe a large number of rhythms with a couple of numbers.
*
* @memberof Pattern
* @name euclid
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @returns Pattern
* @example
@ -76,7 +73,7 @@ export const bjork = function (ons, steps) {
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
* @memberof Pattern
* @name euclidRot
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
* @returns Pattern
@ -86,13 +83,13 @@ export const bjork = function (ons, steps) {
*/
/**
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
* @example // A thirteenth-century Persian rhythm called Khafif-e-ramal.
* note("c3").euclid(2,5)
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
* note("c3").euclid(3,4)
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
* note("c3").euclidRot(3,5,2)
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
* @example // A Ruchenitza rhythm used in a Bulgarian folk dance.
* note("c3").euclid(3,7)
* @example // The Cuban tresillo pattern.
* note("c3").euclid(3,8)
@ -142,6 +139,14 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0));
});
export const e = register('e', function (euc, pat) {
if (!Array.isArray(euc)) {
euc = [euc];
}
const [pulses, steps = pulses, rot = 0] = euc;
return pat.struct(_euclidRot(pulses, steps, rot));
});
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
return pat.struct(_euclidRot(pulses, steps, rotation));
});
@ -151,8 +156,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param rotation offset in steps
* @param pat
* @example
* note("c3").euclidLegato(3,8)
*/
@ -161,13 +168,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) {
if (pulses < 1) {
return silence;
}
const bin_pat = _euclidRot(pulses, steps, rotation);
const bin_pat = _euclidRot(pulses, steps, 0);
const gapless = bin_pat
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return pat.struct(timeCat(...gapless));
return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps));
};
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
@ -180,7 +187,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
* the resulting sequence
* @name euclidLegatoRot
* @memberof Pattern
* @param {number} pulses the number of onsets / beats
* @param {number} pulses the number of onsets/beats
* @param {number} steps the number of steps to fill
* @param {number} rotation offset in steps
* @example

View file

@ -1,9 +1,11 @@
/*
evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/evaluate.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/>.
*/
export const strudelScope = {};
export const evalScope = async (...args) => {
const results = await Promise.allSettled(args);
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
@ -18,6 +20,7 @@ export const evalScope = async (...args) => {
modules.forEach((module) => {
Object.entries(module).forEach(([name, value]) => {
globalThis[name] = value;
strudelScope[name] = value;
});
});
return modules;

View file

@ -1,6 +1,6 @@
/*
fraction.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/fraction.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/fraction.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/>.
*/

View file

@ -1,6 +1,6 @@
/*
hap.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/hap.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 Fraction from './fraction.mjs';

View file

@ -1,6 +1,6 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/index.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.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/>.
*/

View file

@ -4,6 +4,12 @@ let debounce = 1000,
lastMessage,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e)
logger(`[${origin}] error: ${e.message}`);
}
export function logger(message, type, data = {}) {
let t = performance.now();
if (lastMessage === message && t - lastTime < debounce) {

View file

@ -1,6 +1,6 @@
/*
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>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/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/>.
*/
@ -11,7 +11,6 @@ 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;
// the clock of the worker and the audio context clock can drift apart over time

View file

@ -15,7 +15,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -27,7 +27,7 @@
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://strudel.cc",
"dependencies": {

View file

@ -1,6 +1,6 @@
/*
pattern.mjs - Core pattern representation for strudel
Copyright (C) 2025 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pattern.mjs>
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/pattern.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/>.
*/
@ -869,6 +869,31 @@ export class Pattern {
console.log(drawLine(this));
return this;
}
//////////////////////////////////////////////////////////////////////
// methods relating to breaking patterns into subcycles
// Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern.
unjoin(pieces, func = id) {
return pieces.withHap((hap) =>
hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)),
);
}
/**
* Breaks a pattern into pieces according to the structure of a given pattern.
* True values in the given pattern cause the corresponding subcycle of the
* source pattern to be looped, and for an (optional) given function to be
* applied. False values result in the corresponding part of the source pattern
* to be played unchanged.
* @name into
* @memberof Pattern
* @example
* sound("bd sd ht lt").into("1 0", hurry(2))
*/
into(pieces, func) {
return this.unjoin(pieces, func).innerJoin();
}
}
//////////////////////////////////////////////////////////////////////
@ -904,12 +929,13 @@ Pattern.prototype.collect = function () {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arpWith(haps => haps[2])
* */
Pattern.prototype.arpWith = function (func) {
return this.collect()
export const arpWith = register('arpWith', (func, pat) => {
return pat
.collect()
.fmap((v) => reify(func(v)))
.innerJoin()
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
};
});
/**
* Selects indices in in stacked notes.
@ -917,9 +943,11 @@ Pattern.prototype.arpWith = function (func) {
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]")
* */
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
};
export const arp = register(
'arp',
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
false,
);
/*
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
@ -1981,6 +2009,7 @@ export const apply = register('apply', function (func, pat) {
/**
* Plays the pattern at the given cycles per minute.
* @deprecated
* @example
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
*/
@ -2490,6 +2519,37 @@ export const { fastchunk, fastChunk } = register(
true,
);
/**
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
* @name chunkInto
* @synonyms chunkinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
* .bank("tr909")
*/
export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) {
return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func);
});
/**
* Like `chunkInto`, but moves backwards through the chunks.
* @name chunkBackInto
* @synonyms chunkbackinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
* .bank("tr909")
*/
export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) {
return pat.into(
fastcat(true, ...Array(n - 1).fill(false))
._iter(n)
._early(1),
func,
);
});
// TODO - redefine elsewhere in terms of mask
export const bypass = register(
'bypass',
@ -2505,7 +2565,7 @@ export const bypass = register(
* Loops the pattern inside an `offset` for `cycles`.
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
* @name ribbon
* @synonym rib
* @synonyms rib
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
@ -2719,7 +2779,7 @@ export function stepcat(...timepats) {
if (timepats.length === 0) {
return nothing;
}
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]);
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]);
timepats = timepats.map(findsteps);
if (timepats.find((x) => x[0] === undefined)) {
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
@ -3192,6 +3252,25 @@ export const slice = register(
false, // turns off auto-patternification
);
/**
*
* make something happen on event time
* uses browser timeout which is innacurate for audio tasks
* @name onTriggerTime
* @memberof Pattern
* @returns Pattern
* @example
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
*/
Pattern.prototype.onTriggerTime = function (func) {
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
const diff = targetTime - currentTime;
window.setTimeout(() => {
func(hap);
}, diff * 1000);
}, false);
};
/**
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice

View file

@ -1,6 +1,6 @@
/*
pick.mjs - methods that use one pattern to pick events from other patterns.
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
Copyright (C) 2024 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/signal.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/>.
*/

View file

@ -1,7 +1,7 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs';
import { errorLogger, logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
@ -21,6 +21,7 @@ export function repl({
setInterval,
clearInterval,
id,
mondo = false,
}) {
const state = {
schedulerError: undefined,
@ -85,6 +86,17 @@ export function repl({
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
/**
* Changes the global tempo to the given cycles per minute
*
* @name setcpm
* @alias setCpm
* @param {number} cpm cycles per minute
* @example
* setcpm(140/4) // =140 bpm in 4/4
* $: s("bd*4,[- sd]*2").bank('tr707')
*/
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
@ -102,8 +114,9 @@ export function repl({
* all(x => x.pianoroll())
* ```
*/
let allTransforms = [];
const all = function (transform) {
allTransform = transform;
allTransforms.push(transform);
return silence;
};
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
@ -179,7 +192,12 @@ export function repl({
await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor?
await beforeEval?.({ code });
allTransforms = []; // reset all transforms
shouldHush && hush();
if (mondo) {
code = `mondolang\`${code}\``;
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) {
let patterns = Object.values(pPatterns);
@ -191,8 +209,10 @@ export function repl({
} else if (eachTransform) {
pattern = eachTransform(pattern);
}
if (allTransform) {
pattern = allTransform(pattern);
if (allTransforms.length) {
for (let i in allTransforms) {
pattern = allTransforms[i](pattern);
}
}
if (!isPattern(pattern)) {
const message = `got "${typeof evaluated}" instead of pattern`;
@ -225,16 +245,17 @@ 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
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t);
}
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(hap, getTime(), cps, t);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
errorLogger(err, 'getTrigger');
}
};

View file

@ -1,6 +1,6 @@
/*
signal.mjs - continuous patterns
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
Copyright (C) 2024 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/signal.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/>.
*/
@ -71,7 +71,6 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
/**
* A sine signal between 0 and 1.
*
* @return {Pattern}
* @example
* n(sine.segment(16).range(0,15))
@ -100,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
/**
* A square signal between 0 and 1.
*
* @return {Pattern}
* @example
* n(square.segment(4).range(0,7)).scale("C:minor")
@ -266,7 +264,7 @@ export const randrun = (n) => {
const rands = timeToRands(t.floor().add(0.5), n);
const nums = rands
.map((n, i) => [n, i])
.sort((a, b) => a[0] > b[0] - a[0] < b[0])
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
.map((x) => x[1]);
const i = t.cyclePos().mul(n).floor() % n;
return nums[i];
@ -285,7 +283,7 @@ const _rearrangeWith = (ipat, n, pat) => {
* @example
* note("c d e f").sound("piano").shuffle(4)
* @example
* note("c d e f".shuffle(4), "g").sound("piano")
* seq("c d e f".shuffle(4), "g").note().sound("piano")
*/
export const shuffle = register('shuffle', (n, pat) => {
return _rearrangeWith(randrun(n), n, pat);
@ -298,7 +296,7 @@ export const shuffle = register('shuffle', (n, pat) => {
* @example
* note("c d e f").sound("piano").scramble(4)
* @example
* note("c d e f".scramble(4), "g").sound("piano")
* seq("c d e f".scramble(4), "g").note().sound("piano")
*/
export const scramble = register('scramble', (n, pat) => {
return _rearrangeWith(_irand(n)._segment(n), n, pat);
@ -398,6 +396,10 @@ export const chooseInWith = (pat, xs) => {
*/
export const choose = (...xs) => chooseWith(rand, xs);
// todo: doc
export const chooseIn = (...xs) => chooseInWith(rand, xs);
export const chooseOut = choose;
/**
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
@ -486,13 +488,34 @@ export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pair
export const wrandcat = wchooseCycles;
// this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor);
const patb = pat.fmap((t) => Math.floor(t) + 1);
function _perlin(t) {
let ta = Math.floor(t);
let tb = ta + 1;
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb));
return v;
}
export const perlinWith = (tpat) => {
return tpat.fmap(_perlin);
};
function _berlin(t) {
const prevRidgeStartIndex = Math.floor(t);
const nextRidgeStartIndex = prevRidgeStartIndex + 1;
const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex);
const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint;
const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex);
const interp = (a, b, t) => {
return a + (b - a) * t;
};
return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2;
}
export const berlinWith = (tpat) => {
return tpat.fmap(_berlin);
};
/**
@ -506,6 +529,18 @@ export const perlinWith = (pat) => {
*/
export const perlin = perlinWith(time.fmap((v) => Number(v)));
/**
* Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful,
* like perlin noise but with sawtooth waves), in the range 0..1.
*
* @name berlin
* @example
* // ascending arpeggios
* n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor")
*
*/
export const berlin = berlinWith(time.fmap((v) => Number(v)));
export const degradeByWith = register(
'degradeByWith',
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),

View file

@ -1,6 +1,6 @@
/*
speak.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/speak.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/speak.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/>.
*/
@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
}
export const speak = register('speak', function (lang, voice, pat) {
return pat.onTrigger((_, hap) => {
return pat.onTrigger((hap) => {
triggerSpeech(hap.value, lang, voice);
});
});

View file

@ -1,6 +1,6 @@
/*
state.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/state.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/state.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/>.
*/

View file

@ -1,6 +1,6 @@
/*
controls.test.mjs - <short description TODO>
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/controls.test.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/>.
*/

View file

@ -1,6 +1,6 @@
/*
drawLine.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/drawLine.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/drawLine.test.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/>.
*/

View file

@ -0,0 +1,89 @@
import { bjork } from '../euclid.mjs';
import { describe, expect, it } from 'vitest';
import { fastcat } from '../pattern.mjs';
describe('bjork', () => {
it('should apply bjorklund to ons and steps', () => {
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
});
});
describe('euclid', () => {
it('Can create euclid', () => {
expect(
fastcat('a')
.euclid(3, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/8: a', '3/8 → 1/2: a', '3/4 → 7/8: a']);
expect(
fastcat('a')
.euclid(5, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']);
});
});
describe('euclidRot', () => {
it('Can create euclidRot', () => {
expect(
fastcat('a')
.euclidRot(3, 8, 2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '5/8 → 3/4: a']);
expect(
fastcat('a')
.euclidRot(5, 8, 2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '1/2 → 5/8: a', '5/8 → 3/4: a', '7/8 → 1/1: a']);
});
});
describe('euclidLegato', () => {
it('Can create euclidLegato', () => {
expect(
fastcat('a')
.euclidLegato(3, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 3/8: a', '3/8 → 3/4: a', '3/4 → 1/1: a']);
expect(
fastcat('a')
.euclidLegato(5, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']);
});
});
describe('euclidLegatoRot', () => {
it('Can create euclidLegatoRot', () => {
expect(
fastcat('a')
.euclidLegatoRot(3, 8, 2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 5/8: a', '5/8 → 1/1: a']);
expect(
fastcat('a')
.euclidLegatoRot(5, 8, 2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 1/2: a', '1/2 → 5/8: a', '5/8 → 7/8: a', '7/8 → 1/1: a']);
});
});

View file

@ -1,6 +1,6 @@
/*
fraction.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/fraction.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/fraction.test.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/>.
*/

View file

@ -1,6 +1,6 @@
/*
pattern.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/pattern.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.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/>.
*/
@ -52,6 +52,7 @@ import {
stackCentre,
stepcat,
sometimes,
expand,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@ -884,7 +885,7 @@ describe('Pattern', () => {
);
});
it('Doesnt drop haps in the 9th cycle', () => {
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
// fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
});
});
@ -1001,7 +1002,7 @@ describe('Pattern', () => {
});
describe('hurry', () => {
it('Can speed up patterns and sounds', () => {
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2));
});
});
/*describe('composable functions', () => {
@ -1179,6 +1180,9 @@ describe('Pattern', () => {
it('calculates undefined steps as the average', () => {
expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3)));
});
it('works with auto-reified values', () => {
expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim'))));
});
});
describe('shrink', () => {
it('can shrink', () => {
@ -1267,4 +1271,39 @@ describe('Pattern', () => {
);
});
});
describe('unjoin', () => {
it('destructures a pattern into subcycles', () => {
sameFirst(
fastcat('a', 'b', 'c', 'd')
.unjoin(fastcat(true, fastcat(true, true)))
.fmap(fast(2))
.join(),
fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'),
);
});
});
describe('into', () => {
it('applies a function to subcycles of a pattern', () => {
sameFirst(
fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)),
fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'),
);
});
});
describe('chunkinto', () => {
it('chunks into subcycles', () => {
sameFirst(
fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3),
fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')),
);
});
});
describe('chunkbackinto', () => {
it('chunks into subcycles backwards', () => {
sameFirst(
fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3),
fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'),
);
});
});
});

View file

@ -1,26 +1,30 @@
/*
util.test.mjs - Tests for the core 'util' module
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/util.test.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 { pure } from '../pattern.mjs';
import {
isNote,
tokenizeNote,
noteToMidi,
midiToFreq,
freqToMidi,
_mod,
compose,
flatten,
fractionalArgs,
freqToMidi,
getFrequency,
getPlayableNoteValue,
parseNumeral,
parseFractional,
isNote,
midiToFreq,
noteToMidi,
numeralArgs,
fractionalArgs,
parseFractional,
parseNumeral,
rotate,
splitAt,
tokenizeNote,
zipWith,
} from '../util.mjs';
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
describe('isNote', () => {
it('should recognize notes without accidentals', () => {
@ -233,3 +237,46 @@ describe('fractionalArgs', () => {
expect(add('q', 2)).toBe(2.25);
});
});
describe('rotate', () => {
it('should rotate array to the left', () => {
expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]);
expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]);
expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]);
expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]);
expect(rotate([0], 3)).toStrictEqual([0]);
expect(rotate([], 3)).toStrictEqual([]);
});
});
describe('flatten', () => {
it('should flatten array by one level', () => {
expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]);
expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]);
expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]);
expect(flatten([0])).toStrictEqual([0]);
expect(flatten([])).toStrictEqual([]);
});
});
describe('splitAt', () => {
it('should split array into two', () => {
expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([
[0, 1],
[2, 3],
]);
expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]);
expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]);
expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]);
});
});
describe('zipWith', () => {
it('should use the function to combine the two arrays element-wise', () => {
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]);
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]);
expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]);
expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]);
expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]);
});
});

View file

@ -1,6 +1,6 @@
/*
value.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/value.test.mjs>
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
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/>.
*/

View file

@ -1,6 +1,6 @@
/*
timespan.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/timespan.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/timespan.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/>.
*/

View file

@ -1,6 +1,6 @@
/*
ui.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/ui.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/>.
*/

View file

@ -1,6 +1,6 @@
/*
util.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/util.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/util.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/>.
*/
@ -162,6 +162,7 @@ export const compose = (...funcs) => pipe(...funcs.reverse());
// Removes 'None' values from given list
export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
// flattens by one level
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
@ -237,6 +238,7 @@ export const splitAt = function (index, value) {
return [value.slice(0, index), value.slice(index)];
};
// Uses the function f to combine the arrays xs, ys element-wise
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const pairs = function (xs) {

View file

@ -1,6 +1,6 @@
/*
value.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/value.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/value.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/>.
*/
@ -9,7 +9,7 @@ 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
// https://codeberg.org/uzu/strudel/issues/1026
logger(`[warn]: Can't do arithmetic on control pattern.`);
return a;
}

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((hap, currentTime, _cps, targetTime) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
p1 = `"${instrument}"`;
}
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
return pat.onTrigger((tidal_time, hap) => {
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
if (!_csound) {
logger('[csound] not loaded yet', 'warning');
return;
@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
throw new Error('csound only support objects as hap values');
}
// Time in seconds counting from now.
const p2 = tidal_time - getAudioContext().currentTime;
const p2 = targetTime - currentTime;
const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap);
let { gain = 1, velocity = 0.9 } = hap.value;

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -28,9 +28,9 @@
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@csound/browser": "6.18.7",
"@strudel/core": "workspace:*",

View file

@ -1,6 +1,6 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/desktopbridge/index.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/desktopbridge/index.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/>.
*/

View file

@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return this.onTrigger((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;

View file

@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs';
const collator = new ClockCollator({});
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
const controls = parseControlsFromHap(hap, cps);
const params = [];
const timestamp = collator.calculateTimestamp(currentTime, targetTime);

View file

@ -7,7 +7,7 @@
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -19,11 +19,11 @@
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"dependencies": {
"@strudel/core": "workspace:*",
"@tauri-apps/api": "^2.2.0"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}
"homepage": "https://codeberg.org/uzu/strudel#readme"
}

View file

@ -1,6 +1,6 @@
/*
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://codeberg.org/uzu/strudel/src/branch/main/packages/canvas/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/>.
*/
@ -150,7 +150,7 @@ export class Drawer {
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.endClipped >= phase - lookbehind - lookahead)
.filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
@ -175,7 +175,7 @@ export class Drawer {
// +0.1 = workaround for weird holes in query..
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t);
this.painters = []; // will get populated by .onPaint calls attached to the pattern
// query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters });

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"titdalcycles",
@ -25,9 +25,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},

View file

@ -1,6 +1,6 @@
/*
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://codeberg.org/uzu/strudel/src/branch/main/packages/canvas/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/>.
*/

View file

@ -6,7 +6,7 @@
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -18,7 +18,7 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
"homepage": "https://codeberg.org/uzu/strudel#readme"
}

View file

@ -56,14 +56,15 @@ You can use button inputs to control different aspects of your music, such as ga
<MiniRepl
client:idle
tune={`const gp = gamepad(0)
// Use button values to control amplitude
setcpm(120)
// Use button values to control amplitude
$: stack(
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
).cpm(120)
)
`}
/>
@ -74,14 +75,13 @@ Analog sticks can be used for continuous control, such as pitch shifting or pann
<MiniRepl
client:idle
tune={`const gp = gamepad(0)
// Use analog stick for continuous control
setcpm(120)
// Use analog stick for continuous control
$: note("c4 d3 a3 e3").sound("sawtooth")
.lpf(gp.x1.range(100,4000))
.lpq(gp.y1.range(5,30))
.decay(gp.y2.range(0.1,2))
.lpenv(gp.x2.range(-5,5))
.cpm(120)
`}
.lpenv(gp.x2.range(-5,5))`}
/>
### Button Sequences
@ -89,6 +89,7 @@ $: note("c4 d3 a3 e3").sound("sawtooth")
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
<MiniRepl client:idle tune={`const gp = gamepad(0)
setcpm(120)
// Define button sequences
const HADOUKEN = [
'd', // Down
@ -99,7 +100,7 @@ const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
$: s("free_hadouken -").slow(2)
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
.mask(gp.btnSequence(HADOUKEN)).room(1)
// hadouken.wav by Syna-Max
//https://freesound.org/people/Syna-Max/sounds/67674/

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"titdalcycles",
@ -25,9 +25,9 @@
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},

View file

@ -2,7 +2,7 @@
Experimental haskell in javascript interpreter. Many haskell features are not implemented.
This projects mainly exists to be able to write and interpret [Tidal Cycles](https://tidalcycles.org/) code in the browser,
as part of [Strudel](https://github.com/tidalcycles/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell).
as part of [Strudel](https://codeberg.org/uzu/strudel). This project could only exist thanks to [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell).
## Installation

View file

@ -16,7 +16,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"haskell",
@ -25,9 +25,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel",
"homepage": "https://codeberg.org/uzu/strudel/",
"dependencies": {
"web-tree-sitter": "^0.24.7"
},

View file

@ -17,7 +17,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -29,9 +29,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",

View file

@ -1,6 +1,6 @@
/*
midi.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/midi/midi.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/midi/midi.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/>.
*/
@ -333,7 +333,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
return this.onTrigger((hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
return;

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"titdalcycles",
@ -25,9 +25,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",

View file

@ -1,6 +1,6 @@
/*
krill.pegjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/krill.pegjs>
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/>.
*/

View file

@ -1,6 +1,6 @@
/*
mini.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/mini.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/>.
*/

View file

@ -16,7 +16,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -28,9 +28,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},

View file

@ -1,6 +1,6 @@
/*
mini.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/test/mini.test.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/>.
*/

39
packages/mondo/README.md Normal file
View file

@ -0,0 +1,39 @@
# mondo
a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet.
see the `test` folder for usage examples
more info:
- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html)
- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html)
## Example Usage
```js
import { MondoRunner } from 'mondolang'
// define our library of functions and variables
let lib = {
add: (a, b) => a + b,
mul: (a, b) => a * b,
PI: Math.PI,
};
// this function will evaluate nodes in the syntax tree
function evaluator(node) {
// check if node is a leaf node (!= list)
if (node.type !== 'list') {
// check lib if we find a match in the lib, otherwise return value
return lib[node.value] ?? node.value;
}
// now it can only be a list..
const [fn, ...args] = node.children;
// children in a list will already be evaluated
// the first child is expected to be a function
if (typeof fn !== 'function') {
throw new Error(`"${fn}" is not a function`);
}
return fn(...args);
}
const runner = new MondoRunner({ evaluator });
const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586
```

484
packages/mondo/mondo.mjs Normal file
View file

@ -0,0 +1,484 @@
/*
mondo.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.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/>.
*/
// evolved from https://garten.salat.dev/lisp/parser.html
export class MondoParser {
// these are the tokens we expect
token_types = {
comment: /^\/\/(.*?)(?=\n|$)/,
quotes_double: /^"(.*?)"/,
quotes_single: /^'(.*?)'/,
open_list: /^\(/,
close_list: /^\)/,
open_angle: /^</,
close_angle: /^>/,
open_square: /^\[/,
close_square: /^\]/,
open_curly: /^\{/,
close_curly: /^\}/,
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
// dollar: /^\$/,
pipe: /^#/,
stack: /^[,$]/,
or: /^[|]/,
plain: /^[a-zA-Z0-9-~_^#]+/,
};
// matches next token
next_token(code, offset = 0) {
for (let type in this.token_types) {
const match = code.match(this.token_types[type]);
if (match) {
let token = { type, value: match[0] };
if (offset !== -1) {
// add location
token.loc = [offset, offset + match[0].length];
}
return token;
}
}
throw new Error(`mondo: could not match '${code}'`);
}
// takes code string, returns list of matched tokens (if valid)
tokenize(code, offset = 0) {
let tokens = [];
let locEnabled = offset !== -1;
let trim = () => {
// trim whitespace at start, update offset
offset += code.length - code.trimStart().length;
// trim start and end to not confuse parser
return code.trim();
};
code = trim();
while (code.length > 0) {
code = trim();
const token = this.next_token(code, locEnabled ? offset : -1);
code = code.slice(token.value.length);
offset += token.value.length;
tokens.push(token);
}
return tokens;
}
// take code, return abstract syntax tree
parse(code, offset) {
this.code = code;
this.offset = offset;
this.tokens = this.tokenize(code, offset);
const expressions = [];
while (this.tokens.length) {
expressions.push(this.parse_expr());
}
if (expressions.length === 0) {
// empty case
return { type: 'list', children: [] };
}
// do we have multiple top level expressions or a single non list?
if (expressions.length > 1 || expressions[0].type !== 'list') {
return {
type: 'list',
children: this.desugar(expressions),
};
}
// we have a single list
return expressions[0];
}
// parses any valid expression
parse_expr() {
if (!this.tokens[0]) {
throw new Error(`unexpected end of file`);
// TODO: could we allow that? like (((((((( s bd
// return { type: 'list', children: [] };
}
let next = this.tokens[0]?.type;
if (next === 'open_list') {
return this.parse_list();
}
if (next === 'open_angle') {
return this.parse_angle();
}
if (next === 'open_square') {
return this.parse_square();
}
if (next === 'open_curly') {
return this.parse_curly();
}
return this.consume(next);
}
// Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']]
split_children(children, split_type) {
const chunks = [];
while (true) {
let splitIndex = children.findIndex((child) => child.type === split_type);
if (splitIndex === -1) break;
const chunk = children.slice(0, splitIndex);
chunks.push(chunk);
children = children.slice(splitIndex + 1);
}
chunks.push(children);
return chunks;
}
desugar_split(children, split_type, next) {
const chunks = this.split_children(children, split_type);
if (chunks.length === 1) {
return next(children);
}
// collect args of stack function
const args = chunks
.map((chunk) => {
if (!chunk.length) {
return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty)
}
if (chunk.length === 1) {
// chunks of one element can be added to the stack as is
return chunk[0];
}
// chunks of multiple args
chunk = next(chunk);
return { type: 'list', children: chunk };
})
.filter(Boolean); // ignore empty chunks
return [{ type: 'plain', value: split_type }, ...args];
}
// prevents to get a list, e.g. ((x y)) => (x y)
unwrap_children(children) {
if (children.length === 1) {
return children[0].children;
}
return children;
}
desugar_ops(children) {
while (true) {
let opIndex = children.findIndex((child) => child.type === 'op');
if (opIndex === -1) break;
const op = { type: 'plain', value: children[opIndex].value };
if (opIndex === children.length - 1) {
//throw new Error(`cannot use operator as last child.`);
children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]"
continue;
}
if (opIndex === 0) {
// regular function call (assuming each operator exists as function)
children[opIndex] = op;
continue;
}
// convert infix to prefix notation
const left = children[opIndex - 1];
const right = children[opIndex + 1];
if (left.type === 'pipe') {
// "x !* 2" => (* 2 x)
children[opIndex] = op;
continue;
}
// some careful error handling
if (left.type === 'op') {
throw new Error(`got 2 ops in a row: "${left.value}${op.value}"`);
}
if (right.type === 'op') {
let err = `got 2 ops in a row: "${op.value}${right.value}"`;
if (op.value === '-') {
// yes i know this file is not supposed to know about rests x.X
err += '. you probably want a rest, which is "_" in mondo!';
}
throw new Error(err);
}
const call = { type: 'list', children: [op, right, left] };
// insert call while keeping other siblings
children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)];
children = this.unwrap_children(children);
}
return children;
}
get_lambda(args, children) {
// (.fast 2) = (fn (_) (fast _ 2))
children = this.desugar(children);
const body = children.length === 1 ? children[0] : { type: 'list', children };
return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body];
}
// returns location range of given ast (even if desugared)
get_range(ast, range = [Infinity, 0]) {
let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])];
if (ast.loc) {
return union(range, ast.loc);
}
if (ast.type !== 'list') {
return range;
}
return ast.children.reduce((range, child) => {
const childrange = this.get_range(child, range);
return union(range, childrange);
}, range);
}
errorhead(ast) {
return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`;
}
// returns original user code where the given ast originates (even if desugared)
get_code_snippet(ast) {
const [min, max] = this.get_range(ast);
return this.code.slice(min - this.offset, max - this.offset);
}
desugar_pipes(children) {
let chunks = this.split_children(children, 'pipe');
while (chunks.length > 1) {
let [left, right, ...rest] = chunks;
if (!left.length) {
const arg = { type: 'plain', value: '_' };
return this.get_lambda([arg], [arg, ...children]);
}
// s jazz hh.fast 2 => (fast 2 (s jazz hh))
const call = left.length > 1 ? { type: 'list', children: left } : left[0];
chunks = [[...right, call], ...rest];
}
// return next(chunks[0]);
return chunks[0];
}
parse_pair(open_type, close_type) {
const begin = this.tokens[0].loc?.[0];
this.consume(open_type);
const children = [];
while (this.tokens[0]?.type !== close_type) {
children.push(this.parse_expr());
}
const end = this.tokens[0].loc?.[1];
this.consume(close_type);
const node = { type: 'list', children };
if (begin !== undefined) {
node.loc = [begin, end];
node.raw = this.code.slice(begin, end);
}
return node;
}
desugar(children, type) {
// if type is given, the first element is expected to contain it as plain value
// e.g. with (square a b, c), we want to split (a b, c) and ignore "square"
children = type ? children.slice(1) : children;
children = this.desugar_split(children, 'stack', (children) =>
this.desugar_split(children, 'or', (children) => {
// chunks of multiple args
if (type) {
// the type we've removed before splitting needs to be added back
children = [{ type: 'plain', value: type }, ...children];
}
children = this.desugar_ops(children);
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
children = this.desugar_pipes(children);
return children;
}),
);
return children;
}
parse_list() {
let node = this.parse_pair('open_list', 'close_list');
node.children = this.desugar(node.children);
return node;
}
parse_angle() {
let node = this.parse_pair('open_angle', 'close_angle');
node.children.unshift({ type: 'plain', value: 'angle' });
node.children = this.desugar(node.children, 'angle');
return node;
}
parse_square() {
let node = this.parse_pair('open_square', 'close_square');
node.children.unshift({ type: 'plain', value: 'square' });
node.children = this.desugar(node.children, 'square');
return node;
}
parse_curly() {
let node = this.parse_pair('open_curly', 'close_curly');
node.children.unshift({ type: 'plain', value: 'curly' });
node.children = this.desugar(node.children, 'curly');
return node;
}
consume(type) {
// shift removes first element and returns it
const token = this.tokens.shift();
if (token.type !== type) {
throw new Error(`expected token type ${type}, got ${token.type}`);
}
return token;
}
get_locations(code, offset = 0) {
let walk = (ast, locations = []) => {
if (ast.type === 'list') {
return ast.children.forEach((child) => walk(child, locations));
}
if (ast.loc) {
locations.push(ast.loc);
}
};
const ast = this.parse(code, offset);
let locations = [];
walk(ast, locations);
return locations;
}
}
export function printAst(ast, compact = false, lvl = 0) {
const br = compact ? '' : '\n';
const spaces = compact ? '' : Array(lvl).fill(' ').join('');
if (ast.type === 'list') {
return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${
ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')'
}`;
}
return `${ast.value}`;
}
// lisp runner
export class MondoRunner {
constructor({ evaluator } = {}) {
this.parser = new MondoParser();
this.evaluator = evaluator;
this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`);
}
// a helper to check conditions and throw if they are not met
assert(condition, error) {
if (!condition) {
throw new Error(error);
}
}
run(code, scope, offset = 0) {
const ast = this.parser.parse(code, offset);
//console.log(printAst(ast));
return this.evaluate(ast, scope);
}
evaluate_let(ast, scope) {
// (let ((x 3) (y 4)) ...body)
// = ((fn (x y) ...body) 3 4)
const defs = ast.children[1].children;
const args = defs.map((pair) => pair.children[0]);
const vals = defs.map((pair) => pair.children[1]);
const body = ast.children.slice(2);
const lambda = {
type: 'list',
children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body],
};
return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope);
}
evaluate_def(ast, scope) {
// function definition special form?
if (ast.children[1].type === 'list') {
// (def (add a b) (+ a b))
// => (def add (fn (a b) (+ a b)) )
const args = ast.children[1].children.slice(1);
const lambda = {
// lambda
type: 'list',
children: [
{ type: 'plain', value: 'fn' },
{ type: 'list', children: args },
...ast.children.slice(2), // body
],
};
// we mutate to make sure the old ast wont make a mess later
ast.children[1] = ast.children[1].children[0];
ast.children[2] = lambda;
ast.children = ast.children.slice(0, 3); // throw away rest
}
// (def name body)
if (ast.children.length !== 3) {
throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`);
}
const name = ast.children[1].value;
const body = this.evaluate(ast.children[2], scope);
scope[name] = body;
// def with fall through
}
evaluate_match(ast, scope) {
// (match (p1 e1) (p2 e2) ... (pn en))
// = cond in lisp
if (ast.children.length < 2) {
return;
}
const [_, ...body] = ast.children;
for (let i = 0; i < body.length; ++i) {
const [predicate, exp] = body[i].children;
if (predicate.value === 'else') {
return this.evaluate(exp, scope);
}
const outcome = this.evaluate(predicate, scope);
if (outcome) {
return this.evaluate(exp, scope);
}
}
return undefined; // nothing was matched
}
evaluate_if(ast, scope) {
// if is a special case of match
if (ast.children.length !== 4) {
return;
}
// (if predicate consequent alternative)
const [_, predicate, consequent, alternative] = ast.children;
// (match (predicate consequent) (else alternative))
const matcher = {
type: 'list',
children: [
{ type: 'plain', value: 'match' },
{ type: 'list', children: [predicate, consequent] },
{ type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] },
],
};
return this.evaluate_match(matcher, scope);
}
evaluate_lambda(ast, scope) {
// (fn (_) (ply 2 _)
// ^args ^ body
const [_, formalArgs, ...body] = ast.children;
return (...args) => {
const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]]));
const closure = {
...scope,
...params,
};
// body can have multiple expressions
const res = body.map((exp) => this.evaluate(exp, closure));
// last expression is the return value
return res[res.length - 1];
};
}
evaluate_list(ast, scope) {
// evaluate all children before evaluating list (dont mutate!!!)
const args = ast.children
.filter((child) => child.type !== 'comment') // ignore comments
.map((arg) => this.evaluate(arg, scope));
const node = { type: 'list', children: args };
return this.evaluator(node, scope);
}
evaluate_leaf(ast, scope) {
if (ast.type === 'number') {
ast.value = Number(ast.value);
} else if (['quotes_double', 'quotes_single'].includes(ast.type)) {
ast.value = ast.value.slice(1, -1);
ast.type = 'string';
}
return this.evaluator(ast, scope);
}
evaluate(ast, scope = {}) {
if (ast.type !== 'list') {
return this.evaluate_leaf(ast, scope);
}
const name = ast.children[0]?.value;
if (name === 'fn') {
return this.evaluate_lambda(ast, scope);
}
if (name === 'match') {
return this.evaluate_match(ast, scope);
}
if (name === 'if') {
return this.evaluate_if(ast, scope);
}
if (name === 'let') {
return this.evaluate_let(ast, scope);
}
if (name === 'def') {
this.evaluate_def(ast, scope);
}
return this.evaluate_list(ast, scope);
}
}

View file

@ -0,0 +1,37 @@
{
"name": "mondolang",
"version": "1.1.0",
"description": "a language for functional composition that translates to js",
"main": "mondo.mjs",
"type": "module",
"publishConfig": {
"main": "dist/mondo.mjs"
},
"scripts": {
"test": "vitest run",
"bench": "vitest bench",
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"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/blob/main/packages/mondo/README.md",
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
}
}

View file

@ -0,0 +1,978 @@
/*
mondo.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.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 { describe, expect, it } from 'vitest';
import { MondoParser, printAst, MondoRunner } from '../mondo.mjs';
const parser = new MondoParser();
const p = (code) => parser.parse(code, -1);
describe('mondo tokenizer', () => {
const parser = new MondoParser();
it('should tokenize with locations', () =>
expect(
parser
.tokenize('(one two three)')
.map((t) => t.value + '=' + t.loc.join('-'))
.join(' '),
).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15'));
// it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual());
it('should get loangleions', () =>
expect(parser.get_locations('s bd rim')).toEqual([
[0, 1],
[2, 4],
[5, 8],
]));
});
describe('mondo s-expressions parser', () => {
it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] }));
it('should parse a single item', () =>
expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] }));
it('should parse a list with 1 item', () =>
expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
it('should parse a list with 2 items', () =>
expect(p('(a b)')).toEqual({
type: 'list',
children: [
{ type: 'plain', value: 'a' },
{ type: 'plain', value: 'b' },
],
}));
it('should parse a list with 2 items', () =>
expect(p('(a (b c))')).toEqual({
type: 'list',
children: [
{ type: 'plain', value: 'a' },
{
type: 'list',
children: [
{ type: 'plain', value: 'b' },
{ type: 'plain', value: 'c' },
],
},
],
}));
it('should parse numbers', () =>
expect(p('(1 .2 1.2 10 22.3)')).toEqual({
type: 'list',
children: [
{ type: 'number', value: '1' },
{ type: 'number', value: '.2' },
{ type: 'number', value: '1.2' },
{ type: 'number', value: '10' },
{ type: 'number', value: '22.3' },
],
}));
it('should parse comments', () =>
expect(p('a // hello')).toEqual({
type: 'list',
children: [
{ type: 'plain', value: 'a' },
{ type: 'comment', value: '// hello' },
],
}));
});
let desguar = (a) => {
return printAst(parser.parse(a), true);
};
describe('mondo sugar', () => {
it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)'));
it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)'));
it('should desugar <>', () => expect(desguar('<a b c>')).toEqual('(angle a b c)'));
it('should desugar <> nested', () => expect(desguar('<a <b c> d>')).toEqual('(angle a (angle b c) d)'));
it('should desugar mixed [] <>', () => expect(desguar('[a <b c>]')).toEqual('(square a (angle b c))'));
it('should desugar mixed <> []', () => expect(desguar('<a [b c]>')).toEqual('(angle a (square b c))'));
it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))'));
it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))'));
it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))'));
it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
it('should desugar # within , within []', () =>
expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)'));
// it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))'));
it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))'));
it('should desugar , | of []', () =>
expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))'));
it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)'));
it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))'));
it('should desugar , square 3', () =>
expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))'));
it('should desugar , angle', () => expect(desguar('<bd, hh>')).toEqual('(stack bd hh)'));
it('should desugar , angle 2', () => expect(desguar('<bd, hh oh>')).toEqual('(stack bd (angle hh oh))'));
it('should desugar , angle 3', () =>
expect(desguar('<bd cp, hh oh>')).toEqual('(stack (angle bd cp) (angle hh oh))'));
it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))'));
it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)'));
it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))'));
it('should desugar []*<x y>', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)'));
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */
it('should desugar README example', () =>
expect(desguar('s [bd hh*2 (cp # crush 4) <mt ht lt>] # speed .8')).toEqual(
'(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))',
));
it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)'));
it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))'));
it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)'));
it('should desugar lambda with pipe', () =>
expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))'));
/* const lambda = parser.parse('(lambda (_) (fast 2 _))');
const target = { type: 'plain', value: 'xyz' };
it('should desugar_lambda', () =>
expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */
});
describe('mondo arithmetic', () => {
let multi =
(op) =>
(init, ...rest) =>
rest.reduce((acc, arg) => op(acc, arg), init);
let lib = {
'+': multi((a, b) => a + b),
add: multi((a, b) => a + b),
'-': multi((a, b) => a - b),
sub: multi((a, b) => a - b),
'*': multi((a, b) => a * b),
'/': multi((a, b) => a / b),
mod: multi((a, b) => a % b),
eq: (a, b) => a === b,
lt: (a, b) => a < b,
gt: (a, b) => a > b,
and: (a, b) => a && b,
or: (a, b) => a || b,
not: (a) => !a,
run: (...args) => args[args.length - 1],
def: () => 0,
sin: Math.sin,
cos: Math.cos,
PI: Math.PI,
cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])],
car: (pair) => pair[0],
cdr: (pair) => pair.slice(1),
list: (...items) => items,
nil: [],
isnull: (items) => items.length === 0,
concat: (...msgs) => msgs.join(''),
error: (...msgs) => {
throw new Error(msgs.join(' '));
},
};
function evaluator(node, scope) {
if (node.type !== 'list') {
// is leaf
return scope[node.value] ?? lib[node.value] ?? node.value;
}
// is list
const [fn, ...args] = node.children;
if (typeof fn !== 'function') {
throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`);
}
return fn(...args);
}
const runner = new MondoRunner({ evaluator });
let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope);
let pretty = (exp) => printAst(runner.parser.parse(exp), false);
//it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28'));
it('eval number', () => expect(evaluate('2')).toEqual(2));
it('eval string', () => expect(evaluate('abc')).toEqual('abc'));
it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3));
it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6));
it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2));
it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4));
it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9));
// sicp
it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486));
it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666));
it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495));
it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2));
it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7));
it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75));
it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200));
it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19));
it('sicp 9.4', () =>
expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+
(* 3
(+
(* 2 4)
(+ 3 5)
)
)
(+
(- 10 7) 6
)
)`)); // this is not exactly pretty printing by convention..
let scope = {};
it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10));
it('sicp 11.2', () =>
expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159));
it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0));
it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318));
it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390));
it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
// it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441));
it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49));
it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81));
it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0));
it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25));
it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136));
it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136));
it('sicp 22.1', () =>
expect(
evaluate(
`(def (abs x)
(match
((gt x 0) x)
((eq x 0) 0)
((lt x 0) (- 0 x))
))`, // sicp was doing (- x), which doesnt work with our -
scope,
),
).toEqual(0));
it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false));
it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true));
it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true));
it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false));
it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3));
it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3));
it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12));
it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0));
it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13));
it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true));
it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false));
it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0));
it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0));
it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19));
it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4));
it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16));
it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6));
it('sicp ex1.1.7', () =>
expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16));
// .. cant use "+" and "-" as standalone expressions, because they are parsed as operators...
it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0));
it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4));
it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4));
// 1.1.7 Example: Square Roots by Newtons Method
it('sicp 30.1', () =>
expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0));
it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true));
it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false));
it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0));
it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19));
it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0));
it('sicp 31.1', () =>
expect(
evaluate(
`(def (sqrtiter guess x) (if (goodenuf guess x)
guess
(sqrtiter (improve guess x) x)))`,
scope,
),
).toEqual(0));
it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0));
it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138));
it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145));
// eslint-disable-next-line no-loss-of-precision
it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925));
it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366));
// lexical scoping
it('sicp 39.1', () =>
expect(
evaluate(
`
(def (sqrt x)
(def (goodenough guess)
(lt (abs (- (square guess) x)) 0.001))
(def (improve guess)
(average guess (/ x guess)))
(def (sqrt-iter guess)
(if (goodenough guess) guess (sqrt-iter (improve guess))))
(sqrtiter 1.0))
`,
scope,
),
).toEqual(0));
// recursive fac
it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0));
it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
// iterative fac
it('sicp 41.3', () =>
expect(
evaluate(
`
(def (factorial n) (factiter 1 1 n))
(def (factiter product counter maxcount)
(if (gt counter maxcount)
product
(factiter (* counter product)
(+ counter 1)
maxcount)))
`,
scope,
),
).toEqual(0));
it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
// 46.1
/* (def (+ a b)
(if (= a 0) b (inc (+ (dec a) b))))
(def (+ a b)
(if (= a 0) b (+ (dec a) (inc b)))) */
// Exercise 1.10
// Ackermanns function
it('sicp 47.1', () =>
expect(
evaluate(
`
(def (A x y) (match ((eq y 0) 0)
((eq x 0) (* 2 y))
((eq y 1) 2)
(else (A (- x 1) (A x (- y 1))))))
`,
scope,
),
).toEqual(0));
it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024));
it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536));
it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536));
it('sicp 47.5', () =>
expect(
evaluate(
`
(def (f n) (A 0 n))
(def (g n) (A 1 n)))
(def (h n) (A 2 n))
(def (k n) (* 5 n n))
`,
scope,
),
).toEqual(0));
// Tree Recursion
// recursive process
it('sicp 48.1', () =>
expect(
evaluate(
`
(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1)
(else (+ (fib (- n 1)) (fib (- n 2))))))
(fib 7)
`,
scope,
),
).toEqual(13));
// iterative process
it('sicp 48.2', () =>
expect(
evaluate(
`
(def (fib n) (fibiter 1 0 n))
(def (fibiter a b count) (if (eq count 0)
b
(fibiter (+ a b) a (- count 1))))
(fib 7)
`,
scope,
),
).toEqual(13));
// example: counting change
it('sicp 52.2', () =>
expect(
evaluate(
`
(def (countchange amount) (cc amount 5))
(def (cc amount kindsofcoins)
(match
((eq amount 0) 1)
((or (lt amount 0) (eq kindsofcoins 0)) 0)
(else (+
(cc amount (- kindsofcoins 1))
(cc (- amount (firstdenomination kindsofcoins)) kindsofcoins)))))
(def (firstdenomination kindsofcoins)
(match
((eq kindsofcoins 1) 1)
((eq kindsofcoins 2) 5)
((eq kindsofcoins 3) 10)
((eq kindsofcoins 4) 25)
((eq kindsofcoins 5) 50)))
(countchange 100)
`,
scope,
),
).toEqual(292));
// todo: pascals triangle
it('sicp 57.1', () =>
expect(
evaluate(
`
(def (cube x) (* x x x))
(def (p x) (sub (* 3 x) (* 4 (cube x))))
(def (sine angle)
(if (not (gt (abs angle) 0.1)) angle
(p (sine (/ angle 3.0)))))
(sine 12.15)
`,
scope,
),
).toEqual(-0.39980345741334));
// exponentiation recursive
it('sicp 57.2', () =>
expect(
evaluate(
`
(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1)))))
(expt 2 4)
`,
scope,
),
).toEqual(16));
// exponentiation iterative
it('sicp 58.1b', () =>
expect(
evaluate(
`
(def (expt b n) (exptiter b n 1))
(def (exptiter b counter product) (if (eq counter 0)
product
(exptiter b (- counter 1) (* b product))))
(expt 2 5)
`,
scope,
),
).toEqual(32));
// exponentiation fast
it('sicp 58.2', () =>
expect(
evaluate(
`
(def (fastexpt b n) (match ((eq n 0) 1)
((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1))))))
(def (iseven n)
(eq (mod n 2) 0))
(fastexpt 2 5)
`,
scope,
),
).toEqual(32));
// * = repeated addition
it('sicp 60.1', () =>
expect(
evaluate(
`(def (mult a b) (if (eq b 0)
0
(+ a (* a (- b 1)))))
(mult 3 15)
`,
),
).toEqual(45));
// gcd / euclid
it('sicp 63.1', () =>
expect(
evaluate(
`(def (gcd a b) (if (eq b 0)
a
(gcd b (mod a b))))
(gcd 20 6)
`,
scope,
),
).toEqual(2));
// 65 smallest divisor
// 67 fermat test
// ....
// higher order procedures
it('sicp 77.1', () =>
expect(
evaluate(
`
(def (sum term a next b)
(if (gt a b) 0 (+ (term a)
(sum term (next a) next b))))
`,
scope,
),
).toEqual(0));
it('sicp 78.1', () =>
expect(
evaluate(
`
(def (inc n) (+ n 1))
(def (cube a) (* a a a))
(def (sumcubes a b)
(sum cube a inc b))
(sumcubes 1 10)
`,
scope,
),
).toEqual(3025));
it('sicp 78.2', () =>
expect(
evaluate(
`
(def (identity x) x)
(def (sumintegers a b)
(sum identity a inc b))
(sumintegers 1 10)
`,
scope,
),
).toEqual(55));
// pisum
it('sicp 79.1', () =>
expect(
evaluate(
`
(def (pisum a b)
(def (piterm x)
(/ 1.0 (* x (+ x 2))))
(def (pinext x) (+ x 4))
(sum piterm a pinext b))
(* 8 (pisum 1 1000))
`,
scope,
),
).toEqual(3.139592655589783));
// integral
it('sicp 79.2', () =>
expect(
evaluate(
`
(def (integral f a b dx)
(def (adddx x) (+ x dx))
(* (sum f (+ a (/ dx 2.0)) adddx b) dx))
(integral cube 0 1 0.01)
`,
scope,
),
).toEqual(0.24998750000000042));
// maximum callstack...
//it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001));
//lambdas
it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9));
it('sicp 83.2', () =>
expect(
evaluate(
`
(def (pisum a b)
(sum (fn (x) (/ 1.0 (* x (+ x 2))))
a
(fn (x) (+ x 4))
b))
(* 8 (pisum 1 1000))
`,
scope,
),
).toEqual(3.139592655589783));
it('sicp 83.3', () =>
expect(
evaluate(
`
(def (integral f a b dx)
(* (sum f
(+ a (/ dx 2.0))
(fn (x) (+ x dx))
b)
dx))
(integral cube 0 1 0.01)
`,
scope,
),
).toEqual(0.24998750000000042));
it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12));
// let expressions
it('sicp 87.1', () =>
expect(
evaluate(
`
(+ (let ((x 3))
(+ x (* x 10))) x)
`,
{ x: 5 },
),
).toEqual(38));
it('sicp 87.2', () =>
expect(
evaluate(
`
(let ((x 3)
(y (+ x 2)))
(* x y))
`,
{ x: 2 },
),
).toEqual(12));
it('sicp 88.1', () =>
expect(
evaluate(
`
(def (f g) (g 2))
(f square)
`,
scope,
),
).toEqual(4));
it('sicp 88.2', () =>
expect(
evaluate(
`
(def (f g) (g 2))
(f (fn (z) (* z (+ z 1))))
`,
scope,
),
).toEqual(6));
// Finding roots of equations by the half-interval method
it('sicp 89.1', () =>
expect(
evaluate(
`
(def (search f negpoint pospoint)
(let ((midpoint (average negpoint pospoint)))
(if (closeenough negpoint pospoint)
midpoint
(let ((testvalue (f midpoint)))
(match ((positive testvalue)
(search f negpoint midpoint))
((negative testvalue)
(search f midpoint pospoint))
(else midpoint))))))
(def (closeenough x y) (lt (abs (- x y)) 0.001))
(def (negative x) (lt x 0))
(def (positive x) (gt x 0))
(def (halfintervalmethod f a b) (let ((avalue (f a))
(bvalue (f b)))
(match ((and (negative avalue) (positive bvalue))
(search f a b))
((and (negative bvalue) (positive avalue))
(search f b a)) (else
(error "Values are not of opposite sign" a b)))))
(halfintervalmethod sin 2.0 4.0)
`,
scope,
),
).toEqual(3.14111328125));
it('sicp 89.1', () =>
expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625));
// Finding fixed points of functions
it('sicp 92.1', () =>
expect(
evaluate(
`
(def tolerance 0.00001)
(def (fixedpoint f first-guess)
(def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance))
(def (try guess)
(let ((next (f guess)))
(if (closeenough guess next) next (try next))))
(try first-guess))
(fixedpoint cos 1.0)
`,
scope,
),
).toEqual(0.7390822985224023));
it('sicp 93.1', () =>
expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173));
// Maximum call stack size exceeded (expected)
/* it('sicp 93.2', () =>
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */
it('sicp 93.3', () =>
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
2.6457513110645907,
));
// Procedures as Returned Values
it('sicp 97.1', () =>
expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55));
it('sicp 98.1', () =>
expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
2.6457513110645907,
));
it('sicp 98.2', () =>
expect(
evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope),
).toEqual(1.912934258514886));
it('sicp 99.1', () =>
expect(
evaluate(
`
(def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx)))
(def dx 0.00001)
(def (cube x) (* x x x))
((deriv cube) 5)
`,
scope,
),
).toEqual(75.00014999664018));
// With the aid of deriv, we can express Newtons method as a fixed-point process:
it('sicp 100.1', () =>
expect(
evaluate(
`
(def (newtontransform g)
(fn (x) (- x (/ (g x) ((deriv g) x)))))
(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess))
(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0))
(sqrt 7)
`,
scope,
),
).toEqual(2.6457513110645907));
// whatever this is
it('sicp 101.1', () =>
expect(
evaluate(
`
(def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess))
(def (sqrt x) (fixedpointoftransform
(fn (y) (/ x y)) averagedamp 1.0))
(sqrt 7)
`,
scope,
),
).toEqual(2.6457513110645907));
it('sicp 101.2', () =>
expect(
evaluate(
`
(def (sqrt x) (fixedpointoftransform
(fn (y) (- (square y) x)) newtontransform 1.0))
(sqrt 7)
`,
scope,
),
).toEqual(2.6457513110645907));
// data abstraction
// rational arithmetic
it('sicp 114.1', () =>
expect(
evaluate(
`
(def (addrat x y)
(makerat (+ (*
(numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
(def (subrat x y)
(makerat (- (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
(def (mulrat x y)
(makerat (* (numer x) (numer y))
(* (denom x) (denom y))))
(def (divrat x y)
(makerat (* (numer x) (denom y))
(* (denom x) (numer y))))
(def (equalrat x y)
(eq (* (numer x) (denom y))
(* (numer y) (denom x))))
`,
scope,
),
).toEqual(0));
// markerat number denom
it('sicp 117.1', () =>
expect(
evaluate(
`
(def (makerat n d) (cons n d))
(def (numer x) (car x))
(def (denom x) (cdr x))
(def (printrat x) (concat (numer x) ':' (denom x)))
`,
scope,
),
).toEqual(0));
it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2'));
it('sicp 117.2', () =>
expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6'));
it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6'));
it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9'));
it('sicp 118.1', () =>
expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0));
it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3'));
let lscope = {};
// pairs with lambda
it('sicp 124.1', () =>
expect(
evaluate(
`
(def (cons x y)
(def (dispatch m)
(match
((eq m 0) x)
((eq m 1) y)
(else (error "argument not 0 or 1: CONS" m)))
) dispatch)
(def (car z) (z 0))
(def (cdr z) (z 1))
`,
lscope,
),
).toEqual(0));
it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first'));
it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second'));
// lists
it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4]));
it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1));
it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4]));
it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2));
it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4]));
// listref
it('sicp 138.1', () =>
expect(
evaluate(
`
(def (listref items n) (if (eq n 0) (car items)
(listref (cdr items) (- n 1))))
(def squares (list 1 4 9 16 25))
(listref squares 3)`,
scope,
),
).toEqual(16));
// length recursive
it('sicp 138.2', () =>
expect(
evaluate(
`
(def (length items)
(if (isnull items) 0
(+ 1 (length (cdr items)))))
(def odds (list 1 3 5 7))
(length odds)`,
),
).toEqual(4));
// length iterative
it('sicp 139.1', () =>
expect(
evaluate(
`
(def (length items)
(def (lengthiter a count)
(if (isnull a) count
(lengthiter (cdr a) (+ 1 count))))
(lengthiter items 0))
(def odds (list 1 3 5 7))
(length odds)
`,
scope,
),
).toEqual(4));
// append
it('sicp 139.1', () =>
expect(
evaluate(
`
(def (append list1 list2)
(if (isnull list1)
list2
(cons (car list1) (append (cdr list1) list2))))
(append squares odds)
`,
scope,
),
).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7]));
// (define (f x y . z) ⟨body⟩) <- tbd: variable argument count
// Mapping over lists
it('sicp 143.1', () =>
expect(
evaluate(
`
(def (scalelist items factor) (if (isnull items) nil
(cons (* (car items) factor)
(scalelist (cdr items) factor))))
(scalelist (list 1 2 3 4 5) 10)
`,
scope,
),
).toEqual([10, 20, 30, 40, 50]));
it('sicp 143.1', () =>
expect(
evaluate(
`
(def (map proc items) (if (isnull items) nil
(cons (proc (car items))
(map proc (cdr items)))))
(map abs (list -10 2.5 -11.6 17))
`,
scope,
),
).toEqual([10, 2.5, 11.6, 17]));
it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16]));
it('sicp 143.1', () =>
expect(
evaluate(
`
(def (scalelist items factor) (map (fn (x) (* x factor)) items))
(scalelist (list 1 2 3 4 5) 10)
`,
scope,
),
).toEqual([10, 20, 30, 40, 50]));
});

View file

@ -0,0 +1,19 @@
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, 'mondo.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'mondo.mjs' })[ext],
},
rollupOptions: {
// external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});

View file

@ -0,0 +1,3 @@
# @strudel/mondough
connects mondo to strudel.

View file

@ -0,0 +1,132 @@
import {
strudelScope,
reify,
fast,
slow,
seq,
stepcat,
extend,
expand,
pace,
chooseIn,
degradeBy,
silence,
} from '@strudel/core';
import { registerLanguage } from '@strudel/transpiler';
import { MondoRunner } from 'mondolang';
const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
const arrayRange = (start, stop, step = 1) =>
Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) =>
start < stop ? start + index * step : start - index * step,
);
const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b))));
let nope = (...args) => args[args.length - 1];
let lib = {};
lib['nope'] = nope;
lib['-'] = (a, b) => b.early(a);
lib['+'] = (a, b) => b.late(a);
lib['_'] = silence;
lib['~'] = silence;
lib.curly = stepcat;
lib.square = (...args) => stepcat(...args).setSteps(1);
lib.angle = (...args) => stepcat(...args).pace(1);
lib['*'] = fast;
lib['/'] = slow;
lib['!'] = extend;
lib['@'] = expand;
lib['%'] = pace;
lib['?'] = degradeBy; // todo: default 0.5 not working..
lib[':'] = tail;
lib['..'] = range;
lib['def'] = () => silence;
lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]"
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
function evaluator(node, scope) {
const { type } = node;
// node is list
if (type === 'list') {
const { children } = node;
const [name, ...args] = children;
// some functions wont be reified to make sure they work (e.g. see extend below)
if (typeof name === 'function') {
return name(...args);
}
if (name.value === 'def') {
return silence;
}
// name is expected to be a pattern of functions!
const first = name.firstCycle(true)[0];
const type = typeof first?.value;
if (type !== 'function') {
throw new Error(`[mondough] expected function, got "${first?.value}"`);
}
return name
.fmap((fn) => {
if (typeof fn !== 'function') {
throw new Error(`[mondough] "${fn}" is not a function b`);
}
return fn(...args);
})
.innerJoin();
}
// node is leaf
let { value } = node;
if (type === 'plain' && scope[value]) {
return reify(scope[value]); // -> local scope has no location
}
const variable = lib[value] ?? strudelScope[value];
// problem: collisions when we want a string that happens to also be a variable name
// example: "s sine" -> sine is also a variable
let pat;
if (type === 'plain' && typeof variable !== 'undefined') {
// some function names are not patternable, so we skip reification here
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
return variable;
}
pat = reify(variable);
} else {
pat = reify(value);
}
if (node.loc) {
pat = pat.withLoc(node.loc[0], node.loc[1]);
}
return pat;
}
let runner = new MondoRunner({ evaluator });
export function mondo(code, offset = 0) {
if (Array.isArray(code)) {
code = code.join('');
}
const pat = runner.run(code, undefined, offset);
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
}
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
export const mondi = (str, offset) => {
const code = `[${str}]`;
return mondo(code, offset);
};
// tell transpiler how to get locations for mondo`` calls
registerLanguage('mondo', {
getLocations,
});
// this is like mondo, but with a zero offset
export const mondolang = (code) => mondo(code, 0);
registerLanguage('mondolang', {
getLocations: (code) => getLocations(code, 0),
});
// uncomment the following to use mondo as mini notation language
/* registerLanguage('minilang', {
name: 'mondi',
getLocations,
}); */

View file

@ -0,0 +1,44 @@
{
"name": "@strudel/mondo",
"version": "1.1.0",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
"publishConfig": {
"main": "dist/mondough.mjs"
},
"scripts": {
"test": "vitest run",
"bench": "vitest bench",
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs",
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"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/transpiler": "workspace:*",
"mondolang": "workspace:*"
},
"devDependencies": {
"mondo": "*",
"vite": "^6.0.11",
"vitest": "^3.0.4"
}
}

View file

@ -0,0 +1,19 @@
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, 'mondough.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
},
rollupOptions: {
// external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"titdalcycles",
@ -25,9 +25,9 @@
"author": "Yuta Nakayama <nkymut@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},

View file

@ -1,6 +1,6 @@
/*
mqtt.mjs - for patterning the internet of things from strudel
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/serial/serial.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/serial/serial.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/>.
*/

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"titdalcycles",
@ -25,9 +25,9 @@
"author": "Alex McLean <alex@slab.org>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"paho-mqtt": "^1.1.0"

View file

@ -1,6 +1,6 @@
/*
osc.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/osc/osc.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/osc.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/>.
*/
@ -60,7 +60,7 @@ export function parseControlsFromHap(hap, cps) {
const collator = new ClockCollator({});
export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) {
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
const osc = await connect();
const controls = parseControlsFromHap(hap, cps);
const keyvals = Object.entries(controls).flat();

View file

@ -17,7 +17,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -32,9 +32,9 @@
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"osc-js": "^2.4.1"

View file

@ -1,6 +1,6 @@
/*
server.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/osc/server.js>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
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/>.
*/

View file

@ -1,6 +1,6 @@
/*
tidal-sniffer.js - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/osc/tidal-sniffer.js>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/tidal-sniffer.js>
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/>.
*/

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -28,9 +28,9 @@
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11"
}

View file

@ -17,7 +17,7 @@ You can also pin the version like this:
```
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.
See [releases](https://codeberg.org/uzu/strudel/releases) for the latest versions.
## Use Web Component
@ -91,7 +91,7 @@ or
</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.
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://codeberg.org/uzu/strudel/src/branch/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.

View file

@ -14,7 +14,7 @@
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -29,9 +29,9 @@
],
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",

View file

@ -13,7 +13,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"titdalcycles",
@ -25,9 +25,9 @@
"author": "Alex McLean <alex@slab.org>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},

View file

@ -1,6 +1,6 @@
/*
serial.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/serial/serial.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/serial/serial.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/>.
*/

View file

@ -537,7 +537,7 @@ export default {
],
gm_synth_bass_1: [
// Synth Bass 1: Bass
'0380_Aspirin_sf2_file',
// '0380_Aspirin_sf2_file', // broken in safari https://codeberg.org/uzu/strudel/issues/1384
'0380_Chaos_sf2_file',
'0380_FluidR3_GM_sf2_file',
// 0380_GeneralUserGS_sf2_file // laut

View file

@ -13,7 +13,7 @@
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -25,9 +25,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",

View file

@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
return this.onTrigger((time_deprecate, h, ct, cps, targetTime) => {
return this.onTrigger((h, ct, cps, targetTime) => {
const ctx = getAudioContext();
const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length];

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) {
export function doughTrigger(hap, currentTime, cps, targetTime) {
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
}

View file

@ -1,6 +1,6 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/index.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/index.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/>.
*/

View file

@ -16,7 +16,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -28,9 +28,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*"

View file

@ -1,13 +1,13 @@
/*
superdough.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/superdough.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.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 './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod } from './util.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores';
@ -28,6 +28,13 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@ -107,6 +114,10 @@ export async function aliasBank(...args) {
}
export function getSound(s) {
if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
return soundMap.get().triangle; // is this good?
}
return soundMap.get()[s.toLowerCase()];
}
@ -122,7 +133,7 @@ export const getAudioDevices = async () => {
return devicesMap;
};
const defaultDefaultValues = {
let defaultDefaultValues = {
s: 'triangle',
gain: 0.8,
postgain: 1,
@ -139,13 +150,24 @@ const defaultDefaultValues = {
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
delaytime: 0.25,
delaysync: 3 / 16,
orbit: 1,
i: 1,
velocity: 1,
fft: 8,
};
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
export function setDefault(control, value) {
// const main = getControlName(control); // we cant do this because superdough is independent of strudel/core
defaultDefaultValues[control] = value;
}
export function resetDefaults() {
defaultDefaultValues = { ...defaultDefaultDefaultValues };
}
let defaultControls = new Map(Object.entries(defaultDefaultValues));
export function setDefaultValue(key, value) {
@ -321,9 +343,33 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
return delays[orbit];
}
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
frequency: 1,
depth,
skew: 0.5,
phaseoffset: 0,
time: begin,
begin,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) {
const frequency = cycle / cps;
return getWorklet(audioContext, 'lfo-processor', {
frequency,
depth: 1,
skew: 0,
phaseoffset: 0,
@ -446,9 +492,10 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration, cps) => {
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
let { stretch } = value;
if (stretch != null) {
//account for phase vocoder latency
@ -474,6 +521,12 @@ export const superdough = async (value, t, hapDuration, cps) => {
}
// destructure
let {
tremolo,
tremolosync,
tremolodepth = 1,
tremoloskew,
tremolophase = 0,
tremoloshape,
s = getDefaultValue('s'),
bank,
source,
@ -483,6 +536,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
release = 0,
// low pass
cutoff,
lpenv,
@ -515,6 +569,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
phasercenter,
//
coarse,
crush,
shape,
shapevol = getDefaultValue('shapevol'),
@ -524,7 +579,8 @@ export const superdough = async (value, t, hapDuration, cps) => {
vowel,
delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'),
delaytime = getDefaultValue('delaytime'),
delaysync = getDefaultValue('delaysync'),
delaytime,
orbit = getDefaultValue('orbit'),
room,
roomfade,
@ -543,6 +599,8 @@ export const superdough = async (value, t, hapDuration, cps) => {
compressorRelease,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
@ -554,8 +612,11 @@ export const superdough = async (value, t, hapDuration, cps) => {
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
const end = t + hapDuration;
const endWithRelease = end + release;
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
@ -571,6 +632,9 @@ export const superdough = async (value, t, hapDuration, cps) => {
let audioNodes = [];
if (['-', '~', '_'].includes(s)) {
return;
}
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
@ -627,7 +691,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
lprelease,
lpenv,
t,
t + hapDuration,
end,
fanchor,
ftype,
drive,
@ -651,7 +715,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
hprelease,
hpenv,
t,
t + hapDuration,
end,
fanchor,
);
chain.push(hp());
@ -662,20 +726,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
if (bandf !== undefined) {
let bp = () =>
createFilter(
ac,
'bandpass',
bandf,
bandq,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
t,
t + hapDuration,
fanchor,
);
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
@ -693,6 +744,33 @@ export const superdough = async (value, t, hapDuration, cps) => {
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
if (tremolosync != null) {
tremolo = cps * tremolosync;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, t, endWithRelease, {
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: tremoloshape,
phaseoffset: tremolophase,
min: 0,
max: 1,
curve: 1.5,
});
lfo.connect(amGain.gain);
chain.push(amGain);
}
compressorThreshold !== undefined &&
chain.push(
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
@ -706,7 +784,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
}
// phaser
if (phaser !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep);
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
chain.push(phaserFX);
}
@ -718,8 +796,8 @@ export const superdough = async (value, t, hapDuration, cps) => {
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delyNode, delay);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
// reverb

View file

@ -1,5 +1,5 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, getLfo } from './superdough.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import {
applyFM,
gainNode,
@ -34,6 +34,12 @@ function destroyAudioWorkletNode(node) {
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const waveformAliases = [
['tri', 'triangle'],
['sqr', 'square'],
['saw', 'sawtooth'],
['sin', 'sine'],
];
const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() {
@ -115,7 +121,10 @@ export function registerSynthSounds() {
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);
// const fm = applyFM(o.parameters.get('frequency'), value, begin);
// https://codeberg.org/uzu/strudel/issues/1428
// if you think about re-enabling this, please test with fm > 1 first
// it's like 10x gain, so it's really dangerous
let envGain = gainNode(1);
envGain = o.connect(envGain);
@ -127,7 +136,7 @@ export function registerSynthSounds() {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
// fm?.stop();
vibratoOscillator?.stop();
},
begin,
@ -342,6 +351,7 @@ export function registerSynthSounds() {
{ type: 'synth', prebake: true },
);
});
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
}
export function waveformN(partials, type) {

View file

@ -68,3 +68,11 @@ export const _mod = (n, m) => ((n % m) + m) % m;
export const getSoundIndex = (n, numSounds) => {
return _mod(Math.round(nanFallback(n, 0)), numSounds);
};
export function cycleToSeconds(cycle, cps) {
return cycle / cps;
}
export function secondsToCycle(t, cps) {
return t * cps;
}

View file

@ -31,7 +31,7 @@ function polyBlep(phase, dt) {
return 0;
}
}
// The order is important for dough integration
const waveshapes = {
tri(phase, skew = 0.5) {
const x = 1 - skew;
@ -81,10 +81,12 @@ function getParamValue(block, param) {
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0 },
{ name: 'time', defaultValue: 0 },
{ name: 'end', defaultValue: 0 },
{ name: 'frequency', defaultValue: 0.5 },
@ -92,7 +94,10 @@ class LFOProcessor extends AudioWorkletProcessor {
{ name: 'depth', defaultValue: 1 },
{ name: 'phaseoffset', defaultValue: 0 },
{ name: 'shape', defaultValue: 0 },
{ name: 'curve', defaultValue: 1 },
{ name: 'dcoffset', defaultValue: 0 },
{ name: 'min', defaultValue: 0 },
{ name: 'max', defaultValue: 1 },
];
}
@ -109,10 +114,14 @@ class LFOProcessor extends AudioWorkletProcessor {
}
process(inputs, outputs, parameters) {
const begin = parameters['begin'][0];
// eslint-disable-next-line no-undef
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= begin) {
return true;
}
const output = outputs[0];
const frequency = parameters['frequency'][0];
@ -122,7 +131,11 @@ class LFOProcessor extends AudioWorkletProcessor {
const skew = parameters['skew'][0];
const phaseoffset = parameters['phaseoffset'][0];
const curve = parameters['curve'][0];
const dcoffset = parameters['dcoffset'][0];
const min = parameters['min'][0];
const max = parameters['max'][0];
const shape = waveShapeNames[parameters['shape'][0]];
const blockSize = output[0].length ?? 0;
@ -134,8 +147,9 @@ class LFOProcessor extends AudioWorkletProcessor {
const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < output.length; i++) {
const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
output[i][n] = modval;
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
modval = Math.pow(modval, curve);
output[i][n] = clamp(modval, min, max);
}
this.incrementPhase(dt);
}
@ -882,7 +896,6 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
const funcValue = this.func(local_t);
let signal = (funcValue & 255) / 127.5 - 1;
const out = signal * 0.2;
for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(out, -0.4, 0.4);

View file

@ -6,7 +6,7 @@
"module": "tidal.mjs",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel/tree/main/packages/tidal"
"url": "git+https://codeberg.org/uzu/strudel/src/branch/main/packages/tidal"
},
"keywords": [
"haskell",
@ -15,9 +15,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel/tree/main/packages/hs2js",
"homepage": "https://codeberg.org/uzu/strudel/src/branch/main/packages/hs2js",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",

View file

@ -14,7 +14,7 @@
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -26,9 +26,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@tonaljs/tonal": "^4.10.0",

View file

@ -1,6 +1,6 @@
/*
tonal.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonal.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/tonal/test/tonal.test.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/>.
*/
@ -25,28 +25,35 @@ describe('tonal', () => {
});
it('scale with n values', () => {
expect(
n(0, 1, 2)
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(0, 1, 2)
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale without tonic', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(0, 1, 2)
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('transposes note numbers with interval numbers', () => {
expect(
note(40, 40, 40)
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
@ -54,7 +61,7 @@ describe('tonal', () => {
});
it('transposes note numbers with interval strings', () => {
expect(
note(40, 40, 40)
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
@ -62,7 +69,7 @@ describe('tonal', () => {
});
it('transposes note strings with interval numbers', () => {
expect(
note('c', 'c', 'c')
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
@ -70,7 +77,7 @@ describe('tonal', () => {
});
it('transposes note strings with interval strings', () => {
expect(
note('c', 'c', 'c')
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);

View file

@ -1,6 +1,6 @@
/*
tonleiter.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/test/tonleiter.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/tonal/test/tonleiter.test.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/>.
*/

View file

@ -1,6 +1,6 @@
/*
tonal.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/tonal.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/tonal/tonal.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/>.
*/
@ -14,7 +14,7 @@ function scaleStep(step, scale) {
scale = scale.replaceAll(':', ' ');
step = Math.ceil(step);
let { intervals, tonic, empty } = Scale.get(scale);
if ((empty && isNote(scale)) || (!empty && !tonic)) {
if ((empty && isNote(scale)) || (empty && !tonic)) {
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
} else if (empty) {
throw new Error(`invalid scale "${scale}"`);

View file

@ -178,7 +178,7 @@ export function renderVoicing({ chord, dictionary, offset = 0, n, mode = 'below'
return notes;
}
// https://github.com/tidalcycles/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs
// https://codeberg.org/uzu/strudel/blob/14184993d0ee7d69c47df57ac864a1a0f99a893f/packages/tonal/tonleiter.mjs
const steps = [1, 0, 2, 0, 3, 4, 0, 5, 0, 6, 0, 7];
const notes = ['C', '', 'D', '', 'E', 'F', '', 'G', '', 'A', '', 'B'];
const noteLetters = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];

View file

@ -1,6 +1,6 @@
/*
voicings.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tonal/voicings.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/tonal/voicings.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/>.
*/

View file

@ -14,7 +14,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
@ -26,9 +26,9 @@
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/mini": "workspace:*",

View file

@ -1,6 +1,6 @@
/*
transpiler.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/transpiler/test/transpiler.test.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/transpiler/test/transpiler.test.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/>.
*/

View file

@ -8,6 +8,16 @@ export function registerWidgetType(type) {
widgetMethods.push(type);
}
let languages = new Map();
// config = { getLocations: (code: string, offset?: number) => number[][] }
// see mondough.mjs for example use
// the language will kick in when the code contains a template literal of type
// example: mondo`...` will use language of type "mondo"
// TODO: refactor tidal.mjs to use this
export function registerLanguage(type, config) {
languages.set(type, config);
}
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
@ -19,14 +29,33 @@ export function transpiler(input, options = {}) {
let miniLocations = [];
const collectMiniLocations = (value, node) => {
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
miniLocations = miniLocations.concat(leafLocs);
const minilang = languages.get('minilang');
if (minilang) {
const code = `[${value}]`;
const locs = minilang.getLocations(code, node.start);
miniLocations = miniLocations.concat(locs);
} else {
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
miniLocations = miniLocations.concat(leafLocs);
}
};
let widgets = [];
walk(ast, {
enter(node, parent /* , prop, index */) {
if (isTidalTeplateLiteral(node)) {
if (isLanguageLiteral(node)) {
const { name } = node.tag;
const language = languages.get(name);
const code = node.quasi.quasis[0].value.raw;
const offset = node.quasi.start + 1;
if (emitMiniLocations) {
const locs = language.getLocations(code, offset);
miniLocations = miniLocations.concat(locs);
}
this.skip();
return this.replace(languageWithLocation(name, code, offset));
}
if (isTemplateLiteral(node, 'tidal')) {
const raw = node.quasi.quasis[0].value.raw;
const offset = node.quasi.start + 1;
if (emitMiniLocations) {
@ -84,8 +113,18 @@ export function transpiler(input, options = {}) {
leave(node, parent, prop, index) {},
});
const { body } = ast;
if (!body?.[body.length - 1]?.expression) {
let { body } = ast;
if (!body.length) {
console.warn('empty body -> fallback to silence');
body.push({
type: 'ExpressionStatement',
expression: {
type: 'Identifier',
name: 'silence',
},
});
} else if (!body?.[body.length - 1]?.expression) {
throw new Error('unexpected ast format without body expression');
}
@ -120,11 +159,18 @@ function isBackTickString(node, parent) {
function miniWithLocation(value, node) {
const { start: fromOffset } = node;
const minilang = languages.get('minilang');
let name = 'm';
if (minilang && minilang.name) {
name = minilang.name; // name is expected to be exported from the package of the minilang
}
return {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'm',
name,
},
arguments: [
{ type: 'Literal', value },
@ -194,7 +240,7 @@ function isLabelStatement(node) {
}
// converts label expressions to p calls: "x: y" to "y.p('x')"
// see https://github.com/tidalcycles/strudel/issues/990
// see https://codeberg.org/uzu/strudel/issues/990
function labelToP(node) {
return {
type: 'ExpressionStatement',
@ -219,12 +265,16 @@ function labelToP(node) {
};
}
function isLanguageLiteral(node) {
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
}
// tidal highlighting
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
// but maybe it's the only way
function isTidalTeplateLiteral(node) {
return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal';
function isTemplateLiteral(node, value) {
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
}
function collectHaskellMiniLocations(haskellCode, offset) {
@ -262,3 +312,18 @@ function tidalWithLocation(value, offset) {
optional: false,
};
}
function languageWithLocation(name, value, offset) {
return {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: name,
},
arguments: [
{ type: 'Literal', value },
{ type: 'Literal', value: offset },
],
optional: false,
};
}

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