Merge branch 'main' into daslyfe/jade/delaycycle

This commit is contained in:
Jade (Rose) Rowland 2025-06-27 15:53:48 -04:00
commit 3acc364a03
214 changed files with 5779 additions and 2322 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

@ -62,7 +62,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 +75,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 +209,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

@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.0",
"version": "1.2.2",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@ -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,

View file

@ -1,5 +1,5 @@
/**
* @name Cutie Pi
/*
* Cutie Pi
* by Switch Angel
*/
import { tags as t } from '@lezer/highlight';

View file

@ -1,5 +1,5 @@
/**
* @name androidstudio
/*
* androidstudio
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';

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

@ -1,5 +1,5 @@
/**
* @name Atom One
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax

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] },
],
});

View file

@ -1,6 +1,5 @@
/**
* @name darcula
* @author darcula
/*
* darcula
* Name: IntelliJ IDEA darcula theme
* From IntelliJ IDEA by JetBrains
*/

View file

@ -1,6 +1,5 @@
/**
/*
* @name dracula
* @author dracula
* Michael Kaminsky (http://github.com/mkaminsky11)
* Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
*/

View file

@ -1,6 +1,6 @@
/**
* @name duotone
* @author Bram de Haan
/*
* duotone
* author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';

View file

@ -1,6 +1,6 @@
/**
* @name duotone
* @author Bram de Haan
/*
* duotone
* author Bram de Haan
* by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
*/
import { tags as t } from '@lezer/highlight';

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

@ -1,6 +1,3 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';

View file

@ -1,6 +1,3 @@
/**
* @name github
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';

View file

@ -1,5 +1,5 @@
/**
* @name Atom One
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax

View file

@ -1,7 +1,6 @@
/**
* @name gruvbox-dark
* @author morhetz
* Name: Gruvbox
/*
* gruvbox-dark
* author morhetz
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css
*/
import { tags as t } from '@lezer/highlight';

View file

@ -1,7 +1,6 @@
/**
* @name gruvbox-light
* @author morhetz
* Name: Gruvbox
/*
* gruvbox-light
* author morhetz
* From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css
*/
import { tags as t } from '@lezer/highlight';

View file

@ -1,5 +1,5 @@
/**
* @name Atom One
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax

View file

@ -1,5 +1,5 @@
/**
* @name Atom One
/*
* Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax

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');
/**
@ -445,6 +455,32 @@ export const { coarse } = registerControl('coarse');
*/
export const { drive } = registerControl('drive');
/**
* Create byte beats with custom expressions
*
* @name byteBeatExpression
* @synonyms bbexpr
*
* @param {number | Pattern} byteBeatExpression bitwise expression for creating bytebeat
* @example
* s("bytebeat").bbexpr('t*(t>>15^t>>66)')
*
*/
export const { byteBeatExpression, bbexpr } = registerControl('byteBeatExpression', 'bbexpr');
/**
* Create byte beats with custom expressions
*
* @name byteBeatStartTime
* @synonyms bbst
*
* @param {number | Pattern} byteBeatStartTime in samples (t)
* @example
* note("c3!8".add("{0 0 12 0 7 5 3}%8")).s("bytebeat:5").bbst("<3 1>".mul(10000))._scope()
*
*/
export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', 'bbst');
/**
* Allows you to set the output channels on the interface
*
@ -458,6 +494,41 @@ export const { drive } = registerControl('drive');
*/
export const { channels, ch } = registerControl('channels', 'ch');
/**
* controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
* @example
* note("{f a c e}%16").s("pulse").pw(".8:1:.2")
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0 .75 .5 1")
*/
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
/**
* controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
* @param {number | Pattern} rate
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
export const { pwrate } = registerControl('pwrate');
/**
* controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
* @param {number | Pattern} sweep
* @example
* n(run(8)).scale("D:pentatonic").s("pulse").pw("0.5").pwrate("<5 .1 25>").pwsweep("<0.3 .8>")
*
*/
export const { pwsweep } = registerControl('pwsweep');
/**
* Phaser audio effect that approximates popular guitar pedals.
*
@ -794,7 +865,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.
@ -861,7 +932,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.
@ -1227,7 +1298,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
@ -1461,7 +1532,7 @@ export const { vowel } = registerControl('vowel');
* @name waveloss
*/
export const { waveloss } = registerControl('waveloss');
/*
/**
* Noise crackle density
*
* @name density

View file

@ -1,6 +1,6 @@
/*
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/>.
*/
@ -65,7 +65,7 @@ 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;
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {

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

@ -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/>.
*/

View file

@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.0",
"version": "1.2.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
@ -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/>.
*/
@ -904,12 +904,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 +918,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 +1984,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
*/
@ -2441,9 +2445,14 @@ const _chunk = function (n, func, pat, back = false, fast = false) {
return pat.when(binary_pat, func);
};
export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, false);
});
export const { chunk, slowchunk, slowChunk } = register(
['chunk', 'slowchunk', 'slowChunk'],
function (n, func, pat) {
return _chunk(n, func, pat, false, false);
},
true,
true,
);
/**
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
@ -2455,9 +2464,14 @@ export const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', '
* "0 1 2 3".chunkBack(4, x=>x.add(7))
* .scale("A:minor").note()
*/
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
return _chunk(n, func, pat, true);
});
export const { chunkBack, chunkback } = register(
['chunkBack', 'chunkback'],
function (n, func, pat) {
return _chunk(n, func, pat, true);
},
true,
true,
);
/**
* Like `chunk`, but the cycles of the source pattern aren't repeated
@ -2471,9 +2485,14 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
* .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
*/
export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, true);
});
export const { fastchunk, fastChunk } = register(
['fastchunk', 'fastChunk'],
function (n, func, pat) {
return _chunk(n, func, pat, false, true);
},
true,
true,
);
// TODO - redefine elsewhere in terms of mask
export const bypass = register(
@ -2704,7 +2723,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);
@ -3177,6 +3196,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((t_deprecate, hap, currentTime, cps = 1, 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/>.
*/
@ -156,8 +156,7 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze

View file

@ -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,7 +245,7 @@ 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
// 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);

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")
@ -279,26 +277,26 @@ const _rearrangeWith = (ipat, n, pat) => {
};
/**
* @name shuffle
* Slices a pattern into the given number of parts, then plays those parts in random order.
* Each part will be played exactly once per cycle.
* @name shuffle
* @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);
});
/**
* @name scramble
* Slices a pattern into the given number of parts, then plays those parts at random. Similar to `shuffle`,
* but parts might be played more than once, or not at all, per cycle.
* @name scramble
* @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/>.
*/

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', () => {

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

@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.1",
"version": "1.2.3",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
@ -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

@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.0",
"version": "1.2.2",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.0",
"version": "1.2.2",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.0",
"version": "1.2.2",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
@ -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/>.
*/

View file

@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.1",
"version": "1.2.3",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.0",
"version": "1.2.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
@ -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,131 @@
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['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

@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.0",
"version": "1.2.2",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.0",
"version": "1.2.2",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
@ -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/>.
*/

View file

@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.2.0",
"version": "1.2.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.1",
"version": "1.2.3",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.0",
"version": "1.2.2",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
@ -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

@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.1",
"version": "1.2.3",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
@ -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

@ -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

@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.1",
"version": "1.2.3",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
@ -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,6 +1,6 @@
/*
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/>.
*/
@ -22,6 +22,12 @@ let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
}
let multiChannelOrbits = false;
export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@ -101,6 +107,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()];
}
@ -131,8 +141,9 @@ const defaultDefaultValues = {
shapevol: 1,
distortvol: 1,
delay: 0,
byteBeatExpression: '0',
delayfeedback: 0.5,
delaytime: 3 / 16,
delaysync: 3 / 16,
orbit: 1,
i: 1,
velocity: 1,
@ -195,8 +206,15 @@ function loadWorklets() {
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
const {
disableWorklets = false,
maxPolyphony,
audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME,
multiChannelOrbits = false,
} = options;
setMaxPolyphony(maxPolyphony);
setMultiChannelOrbits(multiChannelOrbits);
if (typeof window === 'undefined') {
return;
}
@ -277,7 +295,7 @@ export const connectToDestination = (input, channels = [0, 1]) => {
});
stereoMix.connect(splitter);
channels.forEach((ch, i) => {
splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1));
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
});
};
@ -290,7 +308,7 @@ export const panic = () => {
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
@ -299,7 +317,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToDestination(dly, [0, 1]);
connectToDestination(dly, channels);
delays[orbit] = dly;
}
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
@ -307,16 +325,9 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
return delays[orbit];
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
//gain
const ac = getAudioContext();
const lfoGain = ac.createGain();
lfoGain.gain.value = sweep * 2;
// centerFrequency = centerFrequency * 2;
// sweep = sweep * 1.5;
const lfo = getWorklet(ac, 'lfo-processor', {
frequency,
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
frequency: 1,
depth: 1,
skew: 0,
phaseoffset: 0,
@ -324,8 +335,13 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
end,
shape: 1,
dcoffset: -0.5,
...properties,
});
lfo.connect(lfoGain);
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters
const numStages = 2; //num of filters in series
@ -356,12 +372,12 @@ function getFilterType(ftype) {
let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir) {
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
// If no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, [0, 1]);
connectToDestination(reverb, channels);
reverbs[orbit] = reverb;
}
if (
@ -428,8 +444,13 @@ export function resetGlobalEffects() {
}
let activeSoundSources = new Map();
//music programs/audio gear usually increments inputs/outputs from 1, we need to subtract 1 from the input because the webaudio API channels start at 0
export const superdough = async (value, t, hapDuration, cps = 1) => {
function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration, cps = 0.5) => {
const ac = getAudioContext();
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
let { stretch } = value;
@ -490,7 +511,7 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
bpsustain,
bprelease,
bandq = getDefaultValue('bandq'),
channels = getDefaultValue('channels'),
//phaser
phaserrate: phaser,
phaserdepth = getDefaultValue('phaserdepth'),
@ -507,7 +528,8 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
vowel,
delay = getDefaultValue('delay'),
delayfeedback = getDefaultValue('delayfeedback'),
delaytime = getDefaultValue('delaytime'),
delaysync = getDefaultValue('delaysync'),
delaytime,
orbit = getDefaultValue('orbit'),
room,
roomfade,
@ -526,6 +548,13 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
compressorRelease,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync)
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain);
shapevol = applyGainCurve(shapevol);
@ -547,11 +576,11 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
activeSoundSources.delete(chainID);
}
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
let audioNodes = [];
if (['-', '~', '_'].includes(s)) {
return;
}
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
@ -560,7 +589,7 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, value, hapDuration);
sourceNode = source(t, value, hapDuration, cps);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const onEnded = () => {
@ -699,8 +728,8 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, cycleToSeconds(delaytime, cps), delayfeedback, t);
delaySend = effectSend(post, delyNode, delay);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
// reverb
@ -717,7 +746,7 @@ export const superdough = async (value, t, hapDuration, cps = 1) => {
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}

View file

@ -1,5 +1,5 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
import {
applyFM,
gainNode,
@ -26,11 +26,20 @@ const getFrequencyFromValue = (value) => {
return Number(freq);
};
function destroyAudioWorkletNode(node) {
if (node == null) {
return;
}
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
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() {
@ -141,11 +150,100 @@ export function registerSynthSounds() {
{ prebake: true, type: 'synth' },
);
registerSound(
'bytebeat',
(begin, value, onended) => {
const defaultBeats = [
'(t%255 >= t/255%255)*255',
'(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400',
't',
't*(t >> 10^t)',
't&128',
't&t>>8',
'((t%255+t%128+t%64+t%32+t%16+t%127.8+t%64.8+t%32.8+t%16.8)/3)',
'((t%64+t%63.8+t%64.15+t%64.35+t%63.5)/1.25)',
'(t&(t>>7)-t)',
'(sin(t*PI/128)*127+127)',
'((t^t/2+t+64*(sin((t*PI/64)+(t*PI/32768))+64))%128*2)',
'((t^t/2+t+64*(cos >> 0))%127.85*2)',
'((t^t/2+t+64)%128*2)',
'(((t * .25)^(t * .25)/100+(t * .25))%128)*2',
'((t^t/2+t+64)%7 * 24)',
];
const { n = 0 } = value;
const frequency = getFrequencyFromValue(value);
const { byteBeatExpression = defaultBeats[n % defaultBeats.length], byteBeatStartTime } = value;
const ac = getAudioContext();
let { duration } = value;
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
ac,
'byte-beat-processor',
{
frequency,
begin,
end,
},
{
outputChannelCount: [2],
},
);
o.port.postMessage({ codeText: byteBeatExpression, byteBeatStartTime, frequency });
let envGain = gainNode(1);
envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
},
begin,
end,
);
return {
node: envGain,
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
);
registerSound(
'pulse',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n: pulsewidth = 0.5 } = value;
let { pwrate, pwsweep } = value;
if (pwsweep == null) {
if (pwrate != null) {
pwsweep = 0.3;
} else {
pwsweep = 0;
}
}
if (pwrate == null && pwsweep != null) {
pwrate = 1;
}
let { duration, pw: pulsewidth = 0.5 } = value;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
@ -153,10 +251,8 @@ export function registerSynthSounds() {
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
ac,
'pulse-oscillator',
@ -178,11 +274,16 @@ export function registerSynthSounds() {
envGain = o.connect(envGain);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let lfo;
if (pwsweep != 0) {
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
lfo.connect(o.parameters.get('pulsewidth'));
}
let timeoutNode = webAudioTimeout(
ac,
() => {
destroyAudioWorkletNode(o);
destroyAudioWorkletNode(lfo);
envGain.disconnect();
onended();
fm?.stop();
@ -247,6 +348,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

@ -295,7 +295,7 @@ class LadderProcessor extends AudioWorkletProcessor {
cutoff = (cutoff * 2 * _PI) / sampleRate;
cutoff = cutoff > 1 ? 1 : cutoff;
const k = Math.min(8, resonance * 0.4);
const k = Math.min(8, resonance * 0.13);
// drive makeup * resonance volume loss makeup
let makeupgain = (1 / drive) * Math.min(1.75, 1 + k);
@ -761,3 +761,136 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
}
registerProcessor('pulse-oscillator', PulseOscillatorProcessor);
/** BYTE BEATS */
const chyx = {
/*bit*/ bitC: function (x, y, z) {
return x & y ? z : 0;
},
/*bit reverse*/ br: function (x, size = 8) {
if (size > 32) {
throw new Error('br() Size cannot be greater than 32');
} else {
let result = 0;
for (let idx = 0; idx < size - 0; idx++) {
result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1)));
}
return result;
}
},
/*sin that loops every 128 "steps", instead of every pi steps*/ sinf: function (x) {
return Math.sin(x / (128 / Math.PI));
},
/*cos that loops every 128 "steps", instead of every pi steps*/ cosf: function (x) {
return Math.cos(x / (128 / Math.PI));
},
/*tan that loops every 128 "steps", instead of every pi steps*/ tanf: function (x) {
return Math.tan(x / (128 / Math.PI));
},
/*converts t into a string composed of it's bits, regex's that*/ regG: function (t, X) {
return X.test(t.toString(2));
},
};
// Create shortened Math functions
let mathParams, byteBeatHelperFuncs;
function getByteBeatFunc(codetext) {
if ((mathParams || byteBeatHelperFuncs) == null) {
mathParams = Object.getOwnPropertyNames(Math);
byteBeatHelperFuncs = mathParams.map((k) => Math[k]);
const chyxNames = Object.getOwnPropertyNames(chyx);
const chyxFuncs = chyxNames.map((k) => chyx[k]);
mathParams.push('int', 'window', ...chyxNames);
byteBeatHelperFuncs.push(Math.floor, globalThis, ...chyxFuncs);
}
return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
}
class ByteBeatProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.port.onmessage = (event) => {
let { codeText } = event.data;
const { byteBeatStartTime } = event.data;
if (byteBeatStartTime != null) {
this.t = 0;
this.initialOffset = Math.floor(byteBeatStartTime);
}
//Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
//Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
codeText = codeText
.trim()
.replace(
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
);
this.func = getByteBeatFunc(codeText);
};
this.initialOffset = null;
this.t = null;
this.func = null;
}
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'detune',
defaultValue: 0,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
];
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) {
return false;
}
if (this.t == null) {
this.t = params.begin[0] * sampleRate;
}
const output = outputs[0];
for (let i = 0; i < output[0].length; i++) {
const detune = getParamValue(i, params.detune);
const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100);
let local_t = (this.t / (sampleRate / 256)) * freq + this.initialOffset;
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);
}
this.t = this.t + 1;
}
return true; // keep the audio processing going
}
}
registerProcessor('byte-beat-processor', ByteBeatProcessor);

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

@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.0",
"version": "1.2.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
@ -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}"`);

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