mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 05:05:26 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12fac86012 | |||
| cea6301ffd | |||
| 0b69ce21c7 | |||
| 4f05674cb3 | |||
| feab3f5c86 |
@@ -1,44 +0,0 @@
|
|||||||
name: Build and Deploy hot PRs
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [labeled]
|
|
||||||
|
|
||||||
# Allow one concurrent deployment
|
|
||||||
concurrency:
|
|
||||||
group: "warm-pages"
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: ${{ github.event.label.name == 'serve-hot' }}
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 9.12.2
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
env:
|
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
run: |
|
|
||||||
eval $(ssh-agent -s)
|
|
||||||
echo "$SSH_PRIVATE_KEY" | ssh-add -
|
|
||||||
apt update && apt install -y rsync
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
|
||||||
rsync -atv --delete --delete-after --progress \
|
|
||||||
./website/dist/ \
|
|
||||||
strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc
|
|
||||||
@@ -29,7 +29,6 @@ const editor = new StrudelMirror({
|
|||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel/edo'),
|
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"@strudel/codemirror": "workspace:*",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/edo": "workspace:*",
|
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/soundfonts": "workspace:*",
|
"@strudel/soundfonts": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export * from './packages/core/index.mjs';
|
|||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/desktopbridge/index.mjs';
|
export * from './packages/desktopbridge/index.mjs';
|
||||||
export * from './packages/draw/index.mjs';
|
export * from './packages/draw/index.mjs';
|
||||||
export * from './packages/edo/index.mjs';
|
|
||||||
export * from './packages/embed/index.mjs';
|
export * from './packages/embed/index.mjs';
|
||||||
export * from './packages/hydra/index.mjs';
|
export * from './packages/hydra/index.mjs';
|
||||||
export * from './packages/midi/index.mjs';
|
export * from './packages/midi/index.mjs';
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
"homepage": "https://strudel.cc",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/edo": "workspace:*",
|
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export class StrudelMirror {
|
|||||||
afterEval: (options) => {
|
afterEval: (options) => {
|
||||||
// remember for when highlighting is toggled on
|
// remember for when highlighting is toggled on
|
||||||
this.miniLocations = options.meta?.miniLocations || [];
|
this.miniLocations = options.meta?.miniLocations || [];
|
||||||
this.widgets = options.meta?.widgets || [];
|
this.widgets = options.meta?.widgets;
|
||||||
|
|
||||||
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
const sliders = this.widgets.filter((w) => w.type === 'slider');
|
||||||
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
const widgets = this.widgets.filter((w) => w.type !== 'slider');
|
||||||
@@ -293,9 +293,6 @@ export class StrudelMirror {
|
|||||||
// Handle global evaluation requests (e.g., from Vim :w)
|
// Handle global evaluation requests (e.g., from Vim :w)
|
||||||
this.onEvaluateRequest = (e) => {
|
this.onEvaluateRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
if (e.detail.view !== this.editor) {
|
|
||||||
return; // ignore events from other editors
|
|
||||||
}
|
|
||||||
// Evaluate current editor on repl-evaluate
|
// Evaluate current editor on repl-evaluate
|
||||||
logger('[repl] evaluate via event');
|
logger('[repl] evaluate via event');
|
||||||
this.evaluate();
|
this.evaluate();
|
||||||
@@ -310,9 +307,6 @@ export class StrudelMirror {
|
|||||||
// Toggle comments requested from Vim (gc)
|
// Toggle comments requested from Vim (gc)
|
||||||
this.onToggleComment = (e) => {
|
this.onToggleComment = (e) => {
|
||||||
try {
|
try {
|
||||||
if (e.detail.view !== this.editor) {
|
|
||||||
return; // ignore events from other editors
|
|
||||||
}
|
|
||||||
// Honor selections; toggleLineComment handles both selections and
|
// Honor selections; toggleLineComment handles both selections and
|
||||||
// single line
|
// single line
|
||||||
toggleLineComment(this.editor);
|
toggleLineComment(this.editor);
|
||||||
@@ -353,9 +347,6 @@ export class StrudelMirror {
|
|||||||
// Listen for global stop requests (e.g., from Vim :q)
|
// Listen for global stop requests (e.g., from Vim :q)
|
||||||
onStopRequest = (e) => {
|
onStopRequest = (e) => {
|
||||||
try {
|
try {
|
||||||
if (e.detail.view !== this.editor) {
|
|
||||||
return; // ignore events from other editors
|
|
||||||
}
|
|
||||||
this.stop();
|
this.stop();
|
||||||
e?.cancelable && e.preventDefault?.();
|
e?.cancelable && e.preventDefault?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -444,15 +435,13 @@ export class StrudelMirror {
|
|||||||
this.setFontSize(value);
|
this.setFontSize(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
replaceCode(code, from, to) {
|
|
||||||
const changes = { from, to, insert: code };
|
|
||||||
this.editor.dispatch({ changes });
|
|
||||||
}
|
|
||||||
insertCode(code, position) {
|
|
||||||
this.replaceCode(code, position, position);
|
|
||||||
}
|
|
||||||
setCode(code) {
|
setCode(code) {
|
||||||
this.replaceCode(code, 0, this.editor.state.doc.length);
|
const changes = {
|
||||||
|
from: 0,
|
||||||
|
to: this.editor.state.doc.length,
|
||||||
|
insert: code,
|
||||||
|
};
|
||||||
|
this.editor.dispatch({ changes });
|
||||||
}
|
}
|
||||||
// used for debugging but could serve other purposes
|
// used for debugging but could serve other purposes
|
||||||
getActiveWidgets() {
|
getActiveWidgets() {
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ export * from './slider.mjs';
|
|||||||
export * from './themes.mjs';
|
export * from './themes.mjs';
|
||||||
export * from './widget.mjs';
|
export * from './widget.mjs';
|
||||||
export { Vim } from './keybindings.mjs';
|
export { Vim } from './keybindings.mjs';
|
||||||
export * from './basicSetup.mjs';
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ try {
|
|||||||
// internal actions and works with current selections/visual mode.
|
// internal actions and works with current selections/visual mode.
|
||||||
try {
|
try {
|
||||||
Vim.defineAction('strudelToggleComment', (cm) => {
|
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||||
const view = cm.cm6;
|
const view = cm?.view || cm;
|
||||||
try {
|
try {
|
||||||
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||||
document.dispatchEvent(ev);
|
document.dispatchEvent(ev);
|
||||||
@@ -112,16 +112,15 @@ try {
|
|||||||
|
|
||||||
// :q to pause/stop
|
// :q to pause/stop
|
||||||
Vim.defineEx('quit', 'q', (cm) => {
|
Vim.defineEx('quit', 'q', (cm) => {
|
||||||
const view = cm.cm6;
|
const view = cm?.view || cm;
|
||||||
logger('[vim] :q — stopping repl');
|
|
||||||
replStop(view);
|
replStop(view);
|
||||||
});
|
});
|
||||||
|
|
||||||
// :w to evaluate
|
// :w to evaluate
|
||||||
Vim.defineEx('write', 'w', (cm) => {
|
Vim.defineEx('write', 'w', (cm) => {
|
||||||
const view = cm.cm6;
|
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
|
||||||
try {
|
try {
|
||||||
view.focus?.();
|
view?.focus?.();
|
||||||
// Let the app know this came from Vim :w
|
// Let the app know this came from Vim :w
|
||||||
try {
|
try {
|
||||||
logger('[vim] :w — evaluating code');
|
logger('[vim] :w — evaluating code');
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "catalog:",
|
"@codemirror/autocomplete": "^6.18.4",
|
||||||
"@codemirror/commands": "catalog:",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@codemirror/lang-javascript": "catalog:",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/language": "catalog:",
|
"@codemirror/language": "^6.10.8",
|
||||||
"@codemirror/search": "catalog:",
|
"@codemirror/search": "^6.5.8",
|
||||||
"@codemirror/state": "catalog:",
|
"@codemirror/state": "^6.5.1",
|
||||||
"@codemirror/view": "catalog:",
|
"@codemirror/view": "^6.36.2",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
|
|||||||
+13
-16
@@ -1893,6 +1893,19 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
|||||||
*/
|
*/
|
||||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the level of the signal that is fed back into the delay.
|
||||||
|
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||||
|
*
|
||||||
|
* @name delayfeedback
|
||||||
|
* @tags orbit, superdough, supradough
|
||||||
|
* @param {number | Pattern} feedback between 0 and 1
|
||||||
|
* @synonyms delayfb, dfb
|
||||||
|
* @example
|
||||||
|
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { delayspeed } = registerControl('delayspeed');
|
||||||
/**
|
/**
|
||||||
* Sets the time of the delay effect.
|
* Sets the time of the delay effect.
|
||||||
*
|
*
|
||||||
@@ -1904,22 +1917,6 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
|
|||||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { delayspeed } = registerControl('delayspeed');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the time of the delay effect in seconds.
|
|
||||||
*
|
|
||||||
* @name delaytime
|
|
||||||
* @tags orbit, superdough, supradough
|
|
||||||
* @param {number | Pattern} delay in seconds
|
|
||||||
* @synonyms delayt, dt
|
|
||||||
* @example
|
|
||||||
* note("d d a# a".fast(2))
|
|
||||||
* .s("sawtooth")
|
|
||||||
* .delay(.8)
|
|
||||||
* .delaytime(1/2)
|
|
||||||
* .delayspeed("<2 .5 -1 -2>")
|
|
||||||
*/
|
|
||||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1056,55 +1056,7 @@ function _composeOp(a, b, func) {
|
|||||||
|
|
||||||
// pattern composers
|
// pattern composers
|
||||||
const COMPOSERS = {
|
const COMPOSERS = {
|
||||||
/**
|
|
||||||
* When called on a pattern `a`, with a input pattern `b` (`a.set(b)`),
|
|
||||||
* combines `a` and `b` such that anything defined in `b`
|
|
||||||
* and anything defined in `a` that is *not* defined in `b`
|
|
||||||
* will be in the resulting pattern.
|
|
||||||
*
|
|
||||||
* The structure is maintained from `a`,
|
|
||||||
* because the default pattern alignment is `in`,
|
|
||||||
* see the section on `Pattern Alignment`
|
|
||||||
* in the technical manual in the docs
|
|
||||||
*
|
|
||||||
* This is the inverse of `keep`
|
|
||||||
*
|
|
||||||
* See examples below
|
|
||||||
* @name set
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @returns {Pattern}
|
|
||||||
* @memberof Pattern
|
|
||||||
* @tags internal, combiners
|
|
||||||
* @example
|
|
||||||
* // because input pattern has `s` set,
|
|
||||||
* // it overrides the "sine" declared earlier
|
|
||||||
* note("c a f e").s("sine").set(s("triangle"))
|
|
||||||
*/
|
|
||||||
set: [(a, b) => b],
|
set: [(a, b) => b],
|
||||||
/**
|
|
||||||
* When called on a pattern `a`, with a input pattern `b` (`a.keep(b)`),
|
|
||||||
* combines `a` and `b` such that anything defined in `a`,
|
|
||||||
* and anything defined in `b` that is *not* defined in `a`
|
|
||||||
* will be in the resulting pattern
|
|
||||||
*
|
|
||||||
* The structure is maintained from `a`,
|
|
||||||
* because the default pattern alignment is `in`,
|
|
||||||
* see the section on `Pattern Alignment`
|
|
||||||
* in the technical manual in the docs
|
|
||||||
*
|
|
||||||
* This is the inverse of `set`
|
|
||||||
*
|
|
||||||
* See examples below
|
|
||||||
* @name keep
|
|
||||||
* @param {Pattern} pat
|
|
||||||
* @memberof Pattern
|
|
||||||
* @returns {Pattern}
|
|
||||||
* @tags internal, combiners
|
|
||||||
* @example
|
|
||||||
* // notes, already defined, will stay "c a f e",
|
|
||||||
* // while "s", not defined, will be set to "piano"
|
|
||||||
* note("c a f e").keep(note("e f a c").s("piano"))
|
|
||||||
*/
|
|
||||||
keep: [(a) => a],
|
keep: [(a) => a],
|
||||||
keepif: [(a, b) => (b ? a : undefined)],
|
keepif: [(a, b) => (b ? a : undefined)],
|
||||||
|
|
||||||
@@ -1158,6 +1110,7 @@ const COMPOSERS = {
|
|||||||
div: [numeralArgs((a, b) => a / b)],
|
div: [numeralArgs((a, b) => a / b)],
|
||||||
mod: [numeralArgs(_mod)],
|
mod: [numeralArgs(_mod)],
|
||||||
pow: [numeralArgs(Math.pow)],
|
pow: [numeralArgs(Math.pow)],
|
||||||
|
log2: [numeralArgs(Math.log2)],
|
||||||
band: [numeralArgs((a, b) => a & b)],
|
band: [numeralArgs((a, b) => a & b)],
|
||||||
bor: [numeralArgs((a, b) => a | b)],
|
bor: [numeralArgs((a, b) => a | b)],
|
||||||
bxor: [numeralArgs((a, b) => a ^ b)],
|
bxor: [numeralArgs((a, b) => a ^ b)],
|
||||||
@@ -1863,8 +1816,6 @@ export const floor = register('floor', function (pat) {
|
|||||||
return pat.asNumber().fmap((v) => Math.floor(v));
|
return pat.asNumber().fmap((v) => Math.floor(v));
|
||||||
});
|
});
|
||||||
|
|
||||||
export const log2 = register('log2', (pat) => pat.asNumber().fmap((v) => Math.log2(v)));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assumes a numerical pattern. Returns a new pattern with all values set to
|
* Assumes a numerical pattern. Returns a new pattern with all values set to
|
||||||
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
|
* their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export const run = (n) => saw.range(0, n).round().segment(n);
|
|||||||
* // "hh".s().struct("1 0 1")
|
* // "hh".s().struct("1 0 1")
|
||||||
*/
|
*/
|
||||||
export const binary = (n) => {
|
export const binary = (n) => {
|
||||||
const nBits = reify(n).log2().floor().add(1);
|
const nBits = reify(n).log2(0).floor().add(1);
|
||||||
return binaryN(n, nBits);
|
return binaryN(n, nBits);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ export const binaryN = (n, nBits = 16) => {
|
|||||||
* .partials(binaryL(irand(4096).add(1)))
|
* .partials(binaryL(irand(4096).add(1)))
|
||||||
*/
|
*/
|
||||||
export const binaryL = (n) => {
|
export const binaryL = (n) => {
|
||||||
const nBits = reify(n).log2().floor().add(1);
|
const nBits = reify(n).log2(0).floor().add(1);
|
||||||
return binaryNL(n, nBits);
|
return binaryNL(n, nBits);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -389,9 +389,7 @@ export const randL = (n) => {
|
|||||||
export const randrun = (n) => {
|
export const randrun = (n) => {
|
||||||
return signal((t, controls) => {
|
return signal((t, controls) => {
|
||||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||||
let rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
||||||
// Support n = 1
|
|
||||||
if (!Array.isArray(rands)) rands = [rands];
|
|
||||||
const nums = rands
|
const nums = rands
|
||||||
.map((n, i) => [n, i])
|
.map((n, i) => [n, i])
|
||||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Fraction from 'fraction.js';
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
||||||
import { fastcat, sequence, State, TimeSpan, Hap, note } from '../index.mjs';
|
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
|
||||||
|
|
||||||
const st = (begin, end) => new State(ts(begin, end));
|
const st = (begin, end) => new State(ts(begin, end));
|
||||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||||
@@ -59,9 +59,3 @@ describe('perx', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shuffle', () => {
|
|
||||||
it('returns original pattern if input is 1', () => {
|
|
||||||
expect(note('c d e f').sound('piano').shuffle(1).firstCycle()).toEqual(note('c d e f').sound('piano').firstCycle());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -54,42 +54,11 @@ export function pitchwheel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (edo) {
|
if (edo) {
|
||||||
edo = haps.length >= 1 && haps[0].value && haps[0].value.edo ? haps[0].value.edo : edo;
|
|
||||||
root = haps.length >= 1 && haps[0].value && haps[0].value.root ? haps[0].value.root : root;
|
|
||||||
const degreeIndexes =
|
|
||||||
haps.length >= 1 && haps[0].value && haps[0].value.degreeIndexes ? haps[0].value.degreeIndexes : null;
|
|
||||||
const intLabels = haps.length >= 1 && haps[0].value && haps[0].value.intLabels ? haps[0].value.intLabels : null;
|
|
||||||
ctx.font = '20px sans-serif';
|
|
||||||
const label = `${edo} EDO`;
|
|
||||||
// Draw EDO label:
|
|
||||||
ctx.fillText(label, centerX + radius - ctx.measureText(label).width + 15, centerY + radius);
|
|
||||||
Array.from({ length: edo }, (_, i) => {
|
Array.from({ length: edo }, (_, i) => {
|
||||||
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
const angle = freq2angle(root * Math.pow(2, i / edo), root);
|
||||||
|
|
||||||
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
const [x, y] = circlePos(centerX, centerY, radius, angle);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
// Draw interval label for degree i when it exists:
|
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
||||||
if (degreeIndexes === null || degreeIndexes.includes(i)) {
|
|
||||||
ctx.globalAlpha = 1;
|
|
||||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
|
||||||
if (intLabels !== null) {
|
|
||||||
const degree = degreeIndexes.indexOf(i);
|
|
||||||
if (intLabels[degree]) {
|
|
||||||
if (angle < 0.32 && angle > 0.125) {
|
|
||||||
ctx.fillText(intLabels[degree], x - 34, y);
|
|
||||||
} else {
|
|
||||||
if (angle < 0.1 && angle > -1.125) {
|
|
||||||
ctx.fillText(intLabels[degree], x - 7, y - 12);
|
|
||||||
} else {
|
|
||||||
ctx.fillText(intLabels[degree], x + 9, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.globalAlpha = 0.15;
|
|
||||||
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
|
|
||||||
}
|
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# @strudel/edo
|
|
||||||
|
|
||||||
This package adds EDO scale functions to strudel Patterns.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i @strudel/edo --save
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { n } from '@strudel/core';
|
|
||||||
import '@strudel/edo';
|
|
||||||
|
|
||||||
// E.g. edoScale for Gorgo-6 scale, 16 EDO, LLsLLLs
|
|
||||||
// base note C3, large step size 3, small step size 1:
|
|
||||||
// C3:LLsLLL:3:1
|
|
||||||
const [baseNote, sequence, largeStep, smallStep] = ['C3', 'LLsLLL', 3, 1]
|
|
||||||
const pattern = n("0 2 4 6 4 2").edoScale([baseNote, sequence, largeStep, smallStep]);
|
|
||||||
|
|
||||||
const events = pattern.firstCycle().map((e) => e.show());
|
|
||||||
console.log(events);
|
|
||||||
```
|
|
||||||
|
|
||||||
yields:
|
|
||||||
```
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 |
|
|
||||||
{
|
|
||||||
\"degree\":1,
|
|
||||||
\"degreeIndexes\":[0,3,6,7,10,13],
|
|
||||||
\"intLabels\":[null,\"S2\",\"d4\",\"N4\",\"s6\",\"s7\",\"P8\"],
|
|
||||||
\"root\":\"130.8128\",
|
|
||||||
\"freq\":130.813,
|
|
||||||
\"edo\":16
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
edo.mjs - Equal division of the octave (EDO) scale functions for strudel
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edo.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 { register, pure, noteToMidi, isNote, tokenizeNote } from '@strudel/core';
|
|
||||||
import { EdoScale } from './edoscale.mjs';
|
|
||||||
import { Intervals } from './intervals.mjs';
|
|
||||||
import { Pitches } from './pitches.mjs';
|
|
||||||
|
|
||||||
const pitchesCache = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns numbers into notes in the given EDO scale (zero indexed).
|
|
||||||
*
|
|
||||||
* An EDO scale definition looks like this:
|
|
||||||
*
|
|
||||||
* e.g. C:LLsLLLs:2:1 <- this is the C major scale, 12 EDO
|
|
||||||
*
|
|
||||||
* e.g. C:LLsLLL:3:1 <- this is the Gorgo 6 note scale, 16 EDO
|
|
||||||
*
|
|
||||||
* An EDO scale, e.g. C:LLsLLLs:2:1, consists of a root note (e.g. C)
|
|
||||||
* followed by semicolon (':')
|
|
||||||
* and then a [Large/small step notation sequence](https://en.xen.wiki/w/MOS_scale)
|
|
||||||
* (e.g. LLsLLLs)
|
|
||||||
* followed by semicolon, then the large step size (e.g. 2)
|
|
||||||
* followed by semicolon, then the small step size (e.g. 1).
|
|
||||||
*
|
|
||||||
* The number of divisions of the octave is calculated as the sum
|
|
||||||
* of the steps in the EDO scale definition.
|
|
||||||
*
|
|
||||||
* e.g. C:LLsLLLs:2:1 is 2+2+1+2+2+2+1 = 12 EDO, 7 note scale
|
|
||||||
*
|
|
||||||
* e.g. C:LLsLLL:3:1 is 3+3+1+3+3+3 = 16 EDO, 6 note scale
|
|
||||||
*
|
|
||||||
* The root note defaults to octave 3, if no octave number is given.
|
|
||||||
*
|
|
||||||
* @name edoScale
|
|
||||||
* @param {string} scale Definition of EDO scale.
|
|
||||||
* @returns Pattern
|
|
||||||
* @example
|
|
||||||
* n("0 2 4 6 4 2").edoScale("C:LLsLLLs:2:1")
|
|
||||||
* @example
|
|
||||||
* n("[0,7] 4 [2,7] 4")
|
|
||||||
* .edoScale("G2:<LLsLLL LLLLsL>:3:1")
|
|
||||||
* .s("piano")._pitchwheel()
|
|
||||||
* @example
|
|
||||||
* n(rand.range(0,5).segment(6))
|
|
||||||
* .edoScale("<G2 C3>:LLsLL:3:1")
|
|
||||||
* .s("piano")._pitchwheel()
|
|
||||||
*/
|
|
||||||
export const edoScale = register(
|
|
||||||
'edoScale',
|
|
||||||
function (scaleDefinition, pat) {
|
|
||||||
// console.log(scaleDefinition);
|
|
||||||
|
|
||||||
// if (Array.isArray(scale)) {
|
|
||||||
const key = scaleDefinition.flat().join(':');
|
|
||||||
// }
|
|
||||||
// console.log(scaleDefinition);
|
|
||||||
let pitches;
|
|
||||||
if (pitchesCache.has(key)) {
|
|
||||||
pitches = pitchesCache.get(key);
|
|
||||||
} else {
|
|
||||||
// console.log({ key });
|
|
||||||
const [base_note, sequence, large, small] = scaleDefinition;
|
|
||||||
const root_octave = tokenizeNote(base_note)[2] || 3;
|
|
||||||
// console.log({ root_octave });
|
|
||||||
const scale = new EdoScale(large, small, sequence);
|
|
||||||
const intervals = new Intervals(scale);
|
|
||||||
// console.log({ intervals });
|
|
||||||
pitches = new Pitches(scale, intervals, 440, noteToMidi(base_note), root_octave);
|
|
||||||
pitchesCache.set(key, pitches);
|
|
||||||
// console.log({ base_note: noteToMidi(base_note) });
|
|
||||||
// console.log({ sequence });
|
|
||||||
// console.log({ edivisions: scale.edivisions });
|
|
||||||
// console.log({ intervals });
|
|
||||||
// console.log({ pat });
|
|
||||||
// console.log({ pitches: pitches });
|
|
||||||
}
|
|
||||||
return pat
|
|
||||||
.fmap((value) => {
|
|
||||||
const isObject = typeof value === 'object';
|
|
||||||
const n = isObject ? value.n : value;
|
|
||||||
if (isObject) {
|
|
||||||
delete value.n; // remove n so it won't cause trouble
|
|
||||||
}
|
|
||||||
if (isNote(n)) {
|
|
||||||
// legacy..
|
|
||||||
return pure(n);
|
|
||||||
}
|
|
||||||
const deg = (typeof n === 'string' ? parseInt(n, 10) : Number.isInteger(n) ? n : Math.round(n)) + 1;
|
|
||||||
|
|
||||||
const [oct, degree] = pitches.octdeg(deg);
|
|
||||||
const freq = pitches.octdegfreq(oct, degree);
|
|
||||||
const note = pitches.octdegmidi(oct, degree);
|
|
||||||
const edo = pitches.scale.edivisions;
|
|
||||||
const root = pitches.base_freq;
|
|
||||||
const degreeIndexes = pitches.scale.divisions;
|
|
||||||
const intLabels = pitches.intervals.intLabels;
|
|
||||||
// const color = 'red';
|
|
||||||
value = pure(isObject ? { ...value, degree, degreeIndexes, intLabels, root, freq, edo } : note);
|
|
||||||
// value = pure(isObject ? { ...value, key } : note);
|
|
||||||
// value = pure(isObject ? { ...value, edo } : note);
|
|
||||||
// console.log({ value });
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.outerJoin()
|
|
||||||
.withHap((hap) => hap.setContext({ ...hap.context, scaleDefinition }));
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
true, // preserve tactus
|
|
||||||
);
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
/*
|
|
||||||
edoscale.mjs - EdoScale defines equal division of the octave (EDO) scale in Ls notation
|
|
||||||
- Port of pitfalls/lib/Scale.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Scale.lua>
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/edoscale.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/>.
|
|
||||||
*/
|
|
||||||
const M = 2;
|
|
||||||
const L = 1;
|
|
||||||
const S = 0;
|
|
||||||
const LABELS = ['s', 'L', 'M'];
|
|
||||||
|
|
||||||
export class EdoScale {
|
|
||||||
constructor(large, small, sequence, medium) {
|
|
||||||
this.stepbackup = [L, L, S, L, L, L, S, L, L, L, S, L, L, L, S, L];
|
|
||||||
this.large = large;
|
|
||||||
this.medium = medium || large;
|
|
||||||
this.small = small;
|
|
||||||
this.divisions = [];
|
|
||||||
this.edivisions = null;
|
|
||||||
this.sequence = null;
|
|
||||||
this.tonic = 1;
|
|
||||||
this.mode = 1;
|
|
||||||
this.max_steps = 12;
|
|
||||||
this.min_steps = 3;
|
|
||||||
this.setSequence(sequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMedium() {
|
|
||||||
return this.step.some((_, i) => this.step[this.offset(i)] === M);
|
|
||||||
}
|
|
||||||
|
|
||||||
stepSize(i) {
|
|
||||||
return LABELS[this.step[this.offset(i)]];
|
|
||||||
}
|
|
||||||
|
|
||||||
sequence() {
|
|
||||||
return this.step.map((_, i) => this.stepSize(i)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
stepValue(i) {
|
|
||||||
const step = this.step[this.offset(i)];
|
|
||||||
return step === L ? this.large : step === M ? this.medium : this.small;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset(i) {
|
|
||||||
if (this.mode === 1) {
|
|
||||||
return i;
|
|
||||||
} else {
|
|
||||||
const offset = (this.mode - 1 + i) % this.length;
|
|
||||||
return offset === 0 ? this.length : offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static setMaxSteps(max) {
|
|
||||||
this.max_steps = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
static setMinSteps(min) {
|
|
||||||
this.min_steps = min;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSequence(sequence) {
|
|
||||||
if (this.sequence !== sequence) {
|
|
||||||
this.sequence = sequence;
|
|
||||||
this.length = sequence.length;
|
|
||||||
this.step = [];
|
|
||||||
for (let i = 0; i < sequence.length; i++) {
|
|
||||||
const char = sequence[i];
|
|
||||||
this.step[i] = char === 'L' ? L : char === 'M' ? M : S;
|
|
||||||
}
|
|
||||||
this.updateEdo();
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLarge(l) {
|
|
||||||
if (this.large !== l) {
|
|
||||||
this.large = l;
|
|
||||||
this.updateEdo();
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMedium(m) {
|
|
||||||
if (this.medium !== m) {
|
|
||||||
this.medium = m;
|
|
||||||
this.updateEdo();
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSmall(s) {
|
|
||||||
if (this.small !== s) {
|
|
||||||
this.small = s;
|
|
||||||
this.updateEdo();
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMode(mode) {
|
|
||||||
this.mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTonic(tonic) {
|
|
||||||
this.tonic = tonic;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeMode(d) {
|
|
||||||
const orig = this.mode;
|
|
||||||
this.mode = Math.max(1, Math.min(this.mode + d, this.length));
|
|
||||||
return orig !== this.mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeTonic(d) {
|
|
||||||
const orig = this.tonic;
|
|
||||||
this.tonic = Math.max(1, Math.min(this.tonic + d, this.edivisions));
|
|
||||||
return orig !== this.tonic;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateEdo() {
|
|
||||||
const orig = this.edivisions;
|
|
||||||
this.edivisions = this.step.reduce((sum, _, i) => {
|
|
||||||
this.divisions[i] = sum;
|
|
||||||
return sum + this.stepValue(i);
|
|
||||||
}, 0);
|
|
||||||
// console.log(this.divisions);
|
|
||||||
const changed = orig !== this.edivisions;
|
|
||||||
if (changed) {
|
|
||||||
this.tonic = Math.max(1, Math.min(this.tonic, this.edivisions));
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeStep(d, i) {
|
|
||||||
const index = this.offset(i);
|
|
||||||
const orig = this.step[index];
|
|
||||||
this.step[index] = Math.max(S, Math.min(this.step[index] + d, M));
|
|
||||||
this.stepbackup[index] = this.step[index];
|
|
||||||
const changed = orig !== this.step[index];
|
|
||||||
if (changed) {
|
|
||||||
this.updateEdo();
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeLarge(d) {
|
|
||||||
const orig = this.large;
|
|
||||||
this.setLarge(Math.max(this.small + 1, Math.min(this.large + d, this.large + 1)));
|
|
||||||
const changed = this.large !== orig;
|
|
||||||
if (changed) {
|
|
||||||
if (this.large <= this.medium) {
|
|
||||||
this.setMedium(Math.max(this.small + 1, Math.min(this.large - 1, this.large)));
|
|
||||||
}
|
|
||||||
this.updateEdo();
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeMedium(d) {
|
|
||||||
const orig = this.medium;
|
|
||||||
this.setMedium(Math.max(this.small + 1, Math.min(this.medium + d, this.large - 1)));
|
|
||||||
const changed = this.medium !== orig;
|
|
||||||
if (changed) {
|
|
||||||
this.updateEdo();
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeSmall(d) {
|
|
||||||
const orig = this.small;
|
|
||||||
const value = this.small + d;
|
|
||||||
|
|
||||||
if (this.hasMedium()) {
|
|
||||||
this.setSmall(Math.max(1, Math.min(value, this.medium - 1)));
|
|
||||||
} else {
|
|
||||||
this.setSmall(Math.max(1, Math.min(value, this.large - 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const changed = this.small !== orig;
|
|
||||||
if (changed) {
|
|
||||||
if (this.small >= this.medium) {
|
|
||||||
this.setMedium(Math.max(this.small + 1, Math.min(this.large)));
|
|
||||||
}
|
|
||||||
this.updateEdo();
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeLength(d) {
|
|
||||||
const orig = this.length;
|
|
||||||
if (d === 1) {
|
|
||||||
this.length = Math.min(this.length + 1, this.max_steps);
|
|
||||||
} else if (d === -1) {
|
|
||||||
this.length = Math.max(this.length - 1, this.min_steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
const changed = this.length !== orig;
|
|
||||||
if (changed) {
|
|
||||||
this.mode = 1;
|
|
||||||
if (d === 1) {
|
|
||||||
this.step[this.length] = this.stepbackup[this.length] || L;
|
|
||||||
} else if (d === -1 && this.length >= this.min_steps) {
|
|
||||||
this.step.pop();
|
|
||||||
}
|
|
||||||
this.updateEdo();
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import './edo.mjs';
|
|
||||||
|
|
||||||
export * from './edo.mjs';
|
|
||||||
|
|
||||||
export const packageName = '@strudel/edo';
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
intervals.mjs - defines Intervals for equal division of the octave (EDO) scale
|
|
||||||
- Port of pitfalls/lib/Intervals.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Intervals.lua>
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/intervals.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 ratiointervals from './ratios.mjs';
|
|
||||||
|
|
||||||
function ratio(division, edivisions) {
|
|
||||||
return division === 0 ? 1 : Math.pow(2, division / edivisions);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Intervals {
|
|
||||||
constructor(scale) {
|
|
||||||
this.scale = scale;
|
|
||||||
this.intLabels = [];
|
|
||||||
this.intNoms = [];
|
|
||||||
this.intRatios = [];
|
|
||||||
this.uniqLabels = [];
|
|
||||||
this.intErrors = [];
|
|
||||||
this.ratios = [];
|
|
||||||
|
|
||||||
const BLANK = '';
|
|
||||||
let division = 0;
|
|
||||||
const labToErr = {};
|
|
||||||
const labToInd = {};
|
|
||||||
|
|
||||||
this.ratios[0] = 1;
|
|
||||||
|
|
||||||
for (let i = 0; i < scale.length; i++) {
|
|
||||||
division += scale.stepValue(i);
|
|
||||||
this.ratios[i + 1] = ratio(division, scale.edivisions);
|
|
||||||
|
|
||||||
if (i < scale.length) {
|
|
||||||
const nearest = ratiointervals.nearestInterval(this.ratios[i + 1]);
|
|
||||||
const closeness = nearest[0];
|
|
||||||
const ratio = nearest[1];
|
|
||||||
const intLabel = ratiointervals.key(ratio);
|
|
||||||
this.intLabels[i + 1] = intLabel;
|
|
||||||
this.intErrors[i + 1] = closeness;
|
|
||||||
this.intNoms[i + 1] = ratio ? ratiointervals.nom(ratio) : 0;
|
|
||||||
this.intRatios[i + 1] = ratio ? `${ratiointervals.nom(ratio)}/${ratiointervals.denom(ratio)}` : '';
|
|
||||||
this.uniqLabels[i + 1] = BLANK;
|
|
||||||
|
|
||||||
if (intLabel && intLabel !== 'P1' && intLabel !== 'P8') {
|
|
||||||
if (!labToErr[intLabel]) {
|
|
||||||
this.uniqLabels[i + 1] = intLabel;
|
|
||||||
labToInd[intLabel] = i + 1;
|
|
||||||
labToErr[intLabel] = closeness;
|
|
||||||
} else if (closeness < labToErr[intLabel]) {
|
|
||||||
this.uniqLabels[labToInd[intLabel]] = BLANK;
|
|
||||||
this.uniqLabels[i + 1] = intLabel;
|
|
||||||
labToInd[intLabel] = i + 1;
|
|
||||||
labToErr[intLabel] = closeness;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ratio(i) {
|
|
||||||
return this.ratios[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalLabel(i) {
|
|
||||||
return this.intLabels[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalNominator(i) {
|
|
||||||
return this.intNoms[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalRatio(i) {
|
|
||||||
return this.intRatios[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqIntervalLabel(i) {
|
|
||||||
return this.uniqLabels[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalError(i) {
|
|
||||||
return this.intErrors[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
nearestDegreeTo(r, threshold) {
|
|
||||||
let min = 1;
|
|
||||||
let degree = null;
|
|
||||||
|
|
||||||
for (const [i, v] of Object.entries(this.ratios)) {
|
|
||||||
const diff = Math.abs((r - v) / r);
|
|
||||||
if (diff < min) {
|
|
||||||
min = diff;
|
|
||||||
degree = parseInt(i, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threshold == null) {
|
|
||||||
return degree;
|
|
||||||
} else {
|
|
||||||
return min < threshold ? degree : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/edo",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Equal division of the octave (EDO) scale functions for strudel",
|
|
||||||
"main": "index.mjs",
|
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"test": "vitest run",
|
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://codeberg.org/uzu/strudel.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tidalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Rob McKinnon <rob@movingflow>",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://codeberg.org/uzu/strudel/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel/core": "workspace:*",
|
|
||||||
"@tonaljs/tonal": "^4.10.0",
|
|
||||||
"chord-voicings": "^0.0.1",
|
|
||||||
"webmidi": "^3.1.12"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^6.0.11",
|
|
||||||
"vitest": "^3.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
/*
|
|
||||||
pitches.mjs - defines Pitches for equal division of the octave (EDO) scale
|
|
||||||
- Port of pitfalls/lib/Pitches.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/Pitches.lua>
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/pitches.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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function ratio(division, edivisions) {
|
|
||||||
return division === 0 ? 1 : Math.pow(2, division / edivisions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function gets frequency based on index, edo, octave, and base frequency
|
|
||||||
function get_freq(base_freq, edo, index, oct, base_octave) {
|
|
||||||
let f = base_freq * ratio(index - 1, edo);
|
|
||||||
if (oct < base_octave) {
|
|
||||||
f /= Math.pow(2, base_octave - oct);
|
|
||||||
} else if (oct > base_octave) {
|
|
||||||
f *= Math.pow(2, oct - base_octave);
|
|
||||||
}
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert MIDI number to Hz using a given tuning multiplier
|
|
||||||
function midi_to_hz(n, tuning) {
|
|
||||||
return tuning * Math.pow(2, (n - 69) / 12.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const denom = Math.log(2);
|
|
||||||
function hz_to_midi(freq, tuning) {
|
|
||||||
return 12.0 * (Math.log(freq / tuning) / denom) + 69;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Pitches {
|
|
||||||
constructor(scale, intervals, tuning, midi_start, root_octave) {
|
|
||||||
this.scale = scale;
|
|
||||||
this.intervals = intervals;
|
|
||||||
this.base_freq = midi_to_hz(midi_start, tuning);
|
|
||||||
this.root_octave = root_octave;
|
|
||||||
this.freqs = {};
|
|
||||||
this.midis = {};
|
|
||||||
this.degrees = {};
|
|
||||||
this.octdegfreqs = {};
|
|
||||||
this.octdegmidis = {};
|
|
||||||
let index = 0;
|
|
||||||
let f = null;
|
|
||||||
|
|
||||||
for (let oct = 0; oct <= 8; oct++) {
|
|
||||||
this.octdegfreqs[oct] = {};
|
|
||||||
this.octdegmidis[oct] = {};
|
|
||||||
f = get_freq(this.base_freq, scale.edivisions, scale.tonic, oct, this.root_octave);
|
|
||||||
for (let deg = 0; deg < scale.length; deg++) {
|
|
||||||
index = index + 1;
|
|
||||||
this.freqs[index] = parseFloat((f * intervals.ratio(deg)).toFixed(3));
|
|
||||||
this.midis[index] = parseFloat(hz_to_midi(f * intervals.ratio(deg), tuning).toFixed(4));
|
|
||||||
this.degrees[index] = deg;
|
|
||||||
this.octdegfreqs[oct][deg + 1] = this.freqs[index];
|
|
||||||
this.octdegmidis[oct][deg + 1] = this.midis[index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.base_freq = parseFloat(this.base_freq).toFixed(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
base_freq() {
|
|
||||||
return this.base_freq;
|
|
||||||
}
|
|
||||||
|
|
||||||
degree(index) {
|
|
||||||
return this.degrees[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
freq(index) {
|
|
||||||
return this.freqs[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
octdeg(deg) {
|
|
||||||
const higherOcatve = deg > this.scale.length;
|
|
||||||
const octave = this.root_octave + (higherOcatve ? Math.floor((deg - 1) / this.scale.length) : 0);
|
|
||||||
const degree = higherOcatve ? (deg % this.scale.length === 0 ? this.scale.length : deg % this.scale.length) : deg;
|
|
||||||
// console.log([octave, degree]);
|
|
||||||
return [octave, degree];
|
|
||||||
}
|
|
||||||
|
|
||||||
octdegfreq(oct, deg) {
|
|
||||||
if (this.octdegfreqs[oct]) {
|
|
||||||
return this.octdegfreqs[oct][deg];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
octdegmidi(oct, deg) {
|
|
||||||
if (this.octdegmidis[oct]) {
|
|
||||||
return this.octdegmidis[oct][deg];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
ratios.mjs - lists whole number ratios for pitch intervals
|
|
||||||
- Port of pitfalls/lib/ratios.lua - see <https://github.com/robmckinnon/pitfalls/blob/main/lib/ratios.lua>
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/ratios.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/>.
|
|
||||||
*/
|
|
||||||
const ratiointervals = {};
|
|
||||||
|
|
||||||
// FJS Calculators - https://misotanni.github.io/fjs/en/calc.html
|
|
||||||
// List of pitch intervals - https://en.wikipedia.org/wiki/List_of_pitch_intervals
|
|
||||||
// Gallery of just intervals - https://en.xen.wiki/w/Gallery_of_just_intervals
|
|
||||||
// Two letter codes changed to make different interval labels more unique.
|
|
||||||
ratiointervals.list = new Map([
|
|
||||||
[1, ['P1', 'P1', 'P1', 1, 1]], // unison P1
|
|
||||||
[16 / 15, ['m2', 'm2', 'm2_5', 16, 15]], // minor second m2
|
|
||||||
[15 / 14, ['A1', 'A1', 'A1^5_7', 15, 14]], // augmented unison
|
|
||||||
[13 / 12, ['t2', 'm2', 'm2^13', 13, 12]], // tridecimal neutral second
|
|
||||||
[12 / 11, ['N2', 'M2', 'M2_11', 12, 11]], // undecimal neutral second
|
|
||||||
[11 / 10, ['n2', 'm2', 'm2^11_5', 11, 10]], // undecimal submajor second
|
|
||||||
[10 / 9, ['T2', 'M2', 'M2^5', 10, 9]], // classic (whole) tone
|
|
||||||
[9 / 8, ['M2', 'M2', 'M2', 9, 8]], // major second M2
|
|
||||||
[8 / 7, ['S2', 'M2', 'M2_7', 8, 7]], // septimal major second
|
|
||||||
[7 / 6, ['s3', 'm3', 'm3^7', 7, 6]], // septimal minor third
|
|
||||||
[19 / 16, ['o3', 'm3', 'm3^19', 19, 16]], // otonal minor third
|
|
||||||
[6 / 5, ['m3', 'm3', 'm3_5', 6, 5]], // minor third m3
|
|
||||||
[17 / 14, ['t3', 'm3', 'm3^17_7', 17, 14]], // septendecimal supraminor third
|
|
||||||
[11 / 9, ['n3', 'm3', 'm3^11', 11, 9]], // undecimal neutral third
|
|
||||||
[5 / 4, ['M3', 'M3', 'M3^5', 5, 4]], // major third M3
|
|
||||||
[9 / 7, ['S3', 'M3', 'M3_7', 9, 7]], // septimal major third SM3
|
|
||||||
[13 / 10, ['d4', 'd4', 'd4^13_5', 13, 10]], // Barbados third
|
|
||||||
[4 / 3, ['P4', 'P4', 'P4', 4, 3]], // perfect fourth P4
|
|
||||||
[19 / 14, ['N4', 'P4', 'P4^19_7', 19, 14]], // undevicesimal wide fourth
|
|
||||||
[11 / 8, ['n4', 'P4', 'P4^11', 11, 8]], // super-fourth
|
|
||||||
[25 / 18, ['a4', 'A4', 'A4^5,5', 25, 18]], // classic augmented fourth
|
|
||||||
[7 / 5, ['sT', 'd5', 'd5^7_5', 7, 5]], // lesser septimal tritone
|
|
||||||
[45 / 32, ['A4', 'A4', 'A4^5', 45, 32]], // just augmented fourth
|
|
||||||
[17 / 12, ['d5', 'd5', 'd5^17', 17, 12]], // larger septendecimal tritone
|
|
||||||
[10 / 7, ['ST', 'A4', 'A4^5_7', 10, 7]], // greater septimal tritone
|
|
||||||
[13 / 9, ['t5', 'd5', 'd5^13', 13, 9]], // tridecimal diminished fifth
|
|
||||||
[3 / 2, ['P5', 'P5', 'P5', 3, 2]], // perfect fifth P5
|
|
||||||
[14 / 9, ['s6', 'M6', 'm6^7', 14, 9]], // subminor sixth or septimal sixth
|
|
||||||
[25 / 16, ['a5', 'A5', 'A5^5,5', 25, 16]], // classic augmented fifth
|
|
||||||
[11 / 7, ['A5', 'P5', 'P5^11_7', 11, 7]], // undecimal minor sixth
|
|
||||||
[8 / 5, ['m6', 'm6', 'm6_5', 8, 5]], // minor sixth m6
|
|
||||||
[13 / 8, ['N6', 'm6', 'm6^13', 13, 8]], // tridecimal neutral sixth
|
|
||||||
[18 / 11, ['n6', 'M6', 'M6_11', 18, 11]], // undecimal neutral sixth
|
|
||||||
[5 / 3, ['M6', 'M6', 'M6^5', 5, 3]], // just major sixth M6
|
|
||||||
[128 / 75, ['d7', 'd7', 'd7_5,5', 128, 75]], // diminished seventh
|
|
||||||
[17 / 10, ['T6', 'd7', 'd7^17_5', 17, 10]], // septendecimal diminished seventh
|
|
||||||
[12 / 7, ['S6', 'M6', 'M6_7', 12, 7]], // septimal major sixth
|
|
||||||
[7 / 4, ['s7', 'm7', 'm7^7', 7, 4]], // septimal minor seventh
|
|
||||||
[16 / 9, ['m7', 'm7', 'm7', 16, 9]], // lesser minor seventh
|
|
||||||
[9 / 5, ['g7', 'm7', 'm7_5', 9, 5]], // greater just minor seventh
|
|
||||||
[11 / 6, ['n7', 'm7', 'm7^11', 11, 6]], // undecimal neutral seventh
|
|
||||||
[13 / 7, ['N7', 'm7', 'm7^13_7', 13, 7]], // tridecimal neutral seventh
|
|
||||||
[15 / 8, ['M7', 'M7', 'M7^5', 15, 8]], // major seventh
|
|
||||||
[17 / 9, ['T7', 'd8', 'd8^17', 17, 9]], // large septendecimal major seventh
|
|
||||||
[19 / 10, ['d8', 'd8', 'd8^19_5', 19, 10]], // large undevicesimal major seventh
|
|
||||||
[2, ['P8', 'P8', 'P8', 2, 1]], // octave P8
|
|
||||||
]);
|
|
||||||
|
|
||||||
ratiointervals.key = function (ratio) {
|
|
||||||
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[0] || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
ratiointervals.label = function (ratio) {
|
|
||||||
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[1] || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
ratiointervals.fjs = function (ratio) {
|
|
||||||
return ratio == null ? '' : ratiointervals.list.get(ratio)?.[2] || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
ratiointervals.nom = function (ratio) {
|
|
||||||
return ratio == null ? null : ratiointervals.list.get(ratio)?.[3] || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
ratiointervals.denom = function (ratio) {
|
|
||||||
return ratio == null ? null : ratiointervals.list.get(ratio)?.[4] || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
ratiointervals.nearestInterval = function (v) {
|
|
||||||
let min = 1;
|
|
||||||
let match = null;
|
|
||||||
for (const [ratio, _labels] of ratiointervals.list) {
|
|
||||||
const diff = Math.abs((ratio - v) / ratio);
|
|
||||||
if (diff < min) {
|
|
||||||
min = diff;
|
|
||||||
match = ratio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return min < 0.01 ? [min, match] : [null, null];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ratiointervals;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
edo.test.mjs - tests of edo.mjs
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/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/>.
|
|
||||||
*/
|
|
||||||
import '../edo.mjs';
|
|
||||||
import { n } from '@strudel/core';
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('edoScale', () => {
|
|
||||||
it('Should run tonal functions ', () => {
|
|
||||||
// base note A3 = 220Hz
|
|
||||||
let baseNote = 'A3';
|
|
||||||
let root = 220;
|
|
||||||
let freq = 220;
|
|
||||||
let pattern = n('0').edoScale([baseNote, 'LLsLLLs', 2, 1]);
|
|
||||||
let cycle = pattern.firstCycleValues[0];
|
|
||||||
expect(cycle.freq).toEqual(freq);
|
|
||||||
expect(cycle.edo).toEqual(12);
|
|
||||||
expect(cycle.degree).toEqual(1);
|
|
||||||
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
|
|
||||||
|
|
||||||
// base note A4 = 440Hz
|
|
||||||
baseNote = 'A4';
|
|
||||||
root = 440;
|
|
||||||
freq = 880;
|
|
||||||
pattern = n('7').edoScale([baseNote, 'LLsLLLs', 2, 1]);
|
|
||||||
cycle = pattern.firstCycleValues[0];
|
|
||||||
expect(cycle.freq).toEqual(freq);
|
|
||||||
expect(cycle.edo).toEqual(12);
|
|
||||||
expect(cycle.degree).toEqual(1);
|
|
||||||
expect(parseFloat(cycle.root).toFixed(0)).toEqual(root.toFixed(0));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/*
|
|
||||||
edoscale.test.mjs - tests of EdoScale from edoscale.mjs
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/edoscale.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 '../edoscale.mjs'; // need to import this to add prototypes
|
|
||||||
import { EdoScale } from '../edoscale.mjs';
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('EdoScale', () => {
|
|
||||||
it('updates edivisions', () => {
|
|
||||||
let scale = new EdoScale(2, 1, ['L', 'L', 's', 'L', 'L', 'L', 's']);
|
|
||||||
expect(scale.stepSize(0)).toEqual('L');
|
|
||||||
expect(scale.stepValue(0)).toEqual(2);
|
|
||||||
|
|
||||||
expect(scale.stepSize(2)).toEqual('s');
|
|
||||||
expect(scale.stepValue(2)).toEqual(1);
|
|
||||||
|
|
||||||
expect(scale.edivisions).toEqual(12);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/*
|
|
||||||
ratios.test.mjs - tests of ratios.mjs
|
|
||||||
Copyright (C) 2025 Rob McKinnon and Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/edo/test/ratios.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 ratiointervals from '../ratios.mjs';
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('ratiointervals', () => {
|
|
||||||
it('updates edivisions', () => {
|
|
||||||
let ratio = 9 / 7;
|
|
||||||
expect(ratiointervals.key(ratio)).toEqual('S3');
|
|
||||||
expect(ratiointervals.label(ratio)).toEqual('M3');
|
|
||||||
expect(ratiointervals.fjs(ratio)).toEqual('M3_7');
|
|
||||||
expect(ratiointervals.nom(ratio)).toEqual(9);
|
|
||||||
expect(ratiointervals.denom(ratio)).toEqual(7);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { dependencies } from './package.json';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
|
||||||
formats: ['es'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: [...Object.keys(dependencies)],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
"@strudel/codemirror": "workspace:*",
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/edo": "workspace:*",
|
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export async function prebake() {
|
|||||||
core,
|
core,
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel/edo'),
|
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
import('@strudel/codemirror'),
|
import('@strudel/codemirror'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
@@ -14,6 +14,13 @@ import { logger } from './logger.mjs';
|
|||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @typedef {Object} SampleMetaData
|
||||||
|
* @property {string} url
|
||||||
|
* @property {midi} number
|
||||||
|
*/
|
||||||
|
|
||||||
export const getCachedBuffer = (url) => bufferCache[url];
|
export const getCachedBuffer = (url) => bufferCache[url];
|
||||||
|
|
||||||
function humanFileSize(bytes, si) {
|
function humanFileSize(bytes, si) {
|
||||||
@@ -30,33 +37,10 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the duration, in seconds, of the given sample.
|
|
||||||
* Has optional param `n` (for instance, the `2` in `s("casio:2")`)
|
|
||||||
*
|
|
||||||
* Note: `must` be called with await, otherwise you'll get a pending Promise object.
|
|
||||||
*
|
|
||||||
* @name getDuration,getDur
|
|
||||||
* @tag samples
|
|
||||||
* @param {string} sampleName
|
|
||||||
* @param {number} (optional) n
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Set a patterns cycle length to exactly the length of the sample
|
|
||||||
* samples('github:tidalcycles/dirt-samples')
|
|
||||||
* let k = await getDuration('sax')
|
|
||||||
* s("sax").cps(1/k)
|
|
||||||
*/
|
|
||||||
export const getDuration = (s, n = 0) => {
|
|
||||||
return getSampleBufferSource({ s, n }, soundMap.get(s)[s].data.samples).then((x) => x.bufferDuration);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDur = getDuration;
|
|
||||||
|
|
||||||
export function getSampleInfo(hapValue, bank) {
|
export function getSampleInfo(hapValue, bank) {
|
||||||
const { speed = 1.0 } = hapValue;
|
const { speed = 1.0 } = hapValue;
|
||||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
return { transpose, url, index, midi, label, playbackRate };
|
return { transpose, url, index, midi, label, playbackRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,15 +163,20 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
|
|||||||
if (baseUrl.startsWith('github:')) {
|
if (baseUrl.startsWith('github:')) {
|
||||||
baseUrl = githubPath(baseUrl, '');
|
baseUrl = githubPath(baseUrl, '');
|
||||||
}
|
}
|
||||||
const fullUrl = (v) => baseUrl + v;
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} v
|
||||||
|
* @returns {SampleMetaData}
|
||||||
|
*/
|
||||||
|
const getMetaData = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
//return [key, value.map(replaceUrl)];
|
//return [key, value.map(replaceUrl)];
|
||||||
value = value.map(fullUrl);
|
value = value.map(getMetaData);
|
||||||
} else {
|
} else {
|
||||||
// must be object
|
// must be object
|
||||||
value = Object.fromEntries(
|
value = Object.fromEntries(
|
||||||
Object.entries(value).map(([note, samples]) => {
|
Object.entries(value).map(([note, samples]) => {
|
||||||
return [note, (typeof samples === 'string' ? [samples] : samples).map(fullUrl)];
|
return [note, (typeof samples === 'string' ? [samples] : samples).map(getMetaData)];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -390,3 +379,16 @@ export function registerSampleSource(key, bank, params) {
|
|||||||
registerSample(key, bank, params);
|
registerSample(key, bank, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractMidiNoteFromString(str) {
|
||||||
|
const regex = /_([a-gA-G])([#b])?([1-9])?\b/;
|
||||||
|
const match = str.match(regex);
|
||||||
|
if (match == null) {
|
||||||
|
return BASE_MIDI_NOTE;
|
||||||
|
}
|
||||||
|
const base = match[1].toUpperCase();
|
||||||
|
const accidental = match[2] ?? '';
|
||||||
|
const octave_str = match[3] ?? '';
|
||||||
|
const parsedVal = base + accidental + octave_str;
|
||||||
|
return noteToMidi(parsedVal);
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
||||||
import { getSampleBufferSource, loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
import { resetSeenKeys } from './wavetable.mjs';
|
import { resetSeenKeys } from './wavetable.mjs';
|
||||||
|
|||||||
@@ -485,10 +485,8 @@ export function waveformN(partials, phases, type) {
|
|||||||
if (phase !== 0) {
|
if (phase !== 0) {
|
||||||
const c = Math.cos(PI2 * phase);
|
const c = Math.cos(PI2 * phase);
|
||||||
const s = Math.sin(PI2 * phase);
|
const s = Math.sin(PI2 * phase);
|
||||||
const R0 = R;
|
R = c * R - s * I;
|
||||||
const I0 = I;
|
I = s * R + c * I;
|
||||||
R = c * R0 - s * I0;
|
|
||||||
I = s * R0 + c * I0;
|
|
||||||
}
|
}
|
||||||
real[n + 1] = R;
|
real[n + 1] = R;
|
||||||
imag[n + 1] = I;
|
imag[n + 1] = I;
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
|||||||
export const getAccidentalsOffset = (accidentals) => {
|
export const getAccidentalsOffset = (accidentals) => {
|
||||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} note
|
||||||
|
* @param {number} defaultOctave
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
@@ -29,6 +34,11 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||||||
const offset = getAccidentalsOffset(acc);
|
const offset = getAccidentalsOffset(acc);
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} n
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
export const midiToFreq = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
@@ -38,7 +48,17 @@ export const freqToMidi = (freq) => {
|
|||||||
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const valueToMidi = (value, fallbackValue) => {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} note
|
||||||
|
* @param {number} defaultOctave
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const noteToFreq = (note, defaultOctave = 3) => {
|
||||||
|
return midiToFreq(noteToMidi(note, defaultOctave));
|
||||||
|
};
|
||||||
|
function __valueToMidi(value) {
|
||||||
if (typeof value !== 'object') {
|
if (typeof value !== 'object') {
|
||||||
throw new Error('valueToMidi: expected object value');
|
throw new Error('valueToMidi: expected object value');
|
||||||
}
|
}
|
||||||
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
if (!fallbackValue) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
|
const parsedValue = __valueToMidi(value) ?? fallbackValue;
|
||||||
|
if (parsedValue == null) {
|
||||||
throw new Error('valueToMidi: expected freq or note to be set');
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
}
|
}
|
||||||
return fallbackValue;
|
return parsedValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function nanFallback(value, fallback = 0, silent) {
|
export function nanFallback(value, fallback = 0, silent) {
|
||||||
@@ -84,15 +109,18 @@ export function secondsToCycle(t, cps) {
|
|||||||
// deduces relevant info for sample loading from hap.value and sample definition
|
// deduces relevant info for sample loading from hap.value and sample definition
|
||||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||||
|
export const BASE_MIDI_NOTE = 36;
|
||||||
|
|
||||||
export function getCommonSampleInfo(hapValue, bank) {
|
export function getCommonSampleInfo(hapValue, bank) {
|
||||||
const { s, n = 0 } = hapValue;
|
const { s, n = 0 } = hapValue;
|
||||||
let midi = valueToMidi(hapValue, 36);
|
const maybeMidiNote = __valueToMidi(hapValue);
|
||||||
let transpose = midi - 36; // C3 is middle C;
|
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
|
||||||
let url;
|
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
let samplemeta;
|
||||||
if (Array.isArray(bank)) {
|
if (Array.isArray(bank)) {
|
||||||
index = getSoundIndex(n, bank.length);
|
index = getSoundIndex(n, bank.length);
|
||||||
url = bank[index];
|
samplemeta = bank[index];
|
||||||
} else {
|
} else {
|
||||||
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
|
||||||
// object format will expect keys as notes
|
// object format will expect keys as notes
|
||||||
@@ -104,10 +132,14 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
);
|
);
|
||||||
transpose = -midiDiff(closest); // semitones to repitch
|
transpose = -midiDiff(closest); // semitones to repitch
|
||||||
index = getSoundIndex(n, bank[closest].length);
|
index = getSoundIndex(n, bank[closest].length);
|
||||||
url = bank[closest][index];
|
samplemeta = bank[closest][index];
|
||||||
}
|
}
|
||||||
const label = `${s}:${index}`;
|
const label = `${s}:${index}`;
|
||||||
return { transpose, url, index, midi, label };
|
if (maybeMidiNote != null) {
|
||||||
|
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { transpose, index, midi, label, url: samplemeta.url };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Selects entries from `source` and renames them via `map`
|
/** Selects entries from `source` and renames them via `map`
|
||||||
|
|||||||
@@ -1,31 +1,10 @@
|
|||||||
# @strudel/transpiler
|
# @strudel/transpiler
|
||||||
|
|
||||||
This package contains a JS code transpiler with a plugin system that can be used to transform the code.
|
This package contains a JS code transpiler with the following features:
|
||||||
|
|
||||||
A plugin can be registered via a `registerTranspilerPlugin` call :
|
|
||||||
|
|
||||||
```js
|
|
||||||
registerTranspilerPlugin({
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function(node, parent, prop, index) {},
|
|
||||||
leave: function(node, parent, prop, index) {}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
where the enter/leave functions (both optional) follow the `estree-walker` walk API.
|
|
||||||
|
|
||||||
4 plugins are currently hosted inside `@strudel/transpiler`
|
|
||||||
|
|
||||||
- mini: add locations of mini notation strings (double quoted or backticked) for highlighting
|
|
||||||
- widgets: add handling of sliders & draw widgets
|
|
||||||
- sample: make it possible to call a sample without await
|
|
||||||
- kabelsalat: transform the code to handle the `K(..)` kabelsalat notation
|
|
||||||
|
|
||||||
it also
|
|
||||||
|
|
||||||
|
- add locations of mini notation strings (double quoted or backticked) for highlighting
|
||||||
|
- converts pseudo note variables to note strings
|
||||||
- adds return statement to the last expression
|
- adds return statement to the last expression
|
||||||
- handles label capturing for block-based eval
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
/*
|
|
||||||
index.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import { evaluate as _evaluate } from '@strudel/core';
|
import { evaluate as _evaluate } from '@strudel/core';
|
||||||
import { transpiler } from './transpiler.mjs';
|
import { transpiler } from './transpiler.mjs';
|
||||||
|
|
||||||
export * from './transpiler.mjs';
|
export * from './transpiler.mjs';
|
||||||
|
|
||||||
import './plugin-kabelsalat.mjs';
|
export const evaluate = (code) => _evaluate(code, transpiler);
|
||||||
import './plugin-mini.mjs';
|
|
||||||
import './plugin-sample.mjs';
|
|
||||||
import './plugin-widgets.mjs';
|
|
||||||
|
|
||||||
export { registerWidgetType, getWidgetID } from './plugin-widgets.mjs';
|
|
||||||
export const evaluate = (code, transpilerOptions) => _evaluate(code, transpiler, transpilerOptions);
|
|
||||||
|
|||||||
@@ -1,235 +0,0 @@
|
|||||||
/*
|
|
||||||
plugin-kabelsalat.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { walk } from 'estree-walker';
|
|
||||||
import escodegen from 'escodegen';
|
|
||||||
import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs';
|
|
||||||
|
|
||||||
export function genExprSource(expr) {
|
|
||||||
return escodegen.generate(expr, { format: { semicolons: false } });
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStrudelPatternWrap(node) {
|
|
||||||
if (node.type !== 'CallExpression') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const callee = node.callee;
|
|
||||||
if (callee.type === 'Identifier') {
|
|
||||||
return callee.name === 'S';
|
|
||||||
}
|
|
||||||
if (callee.type === 'MemberExpression' && !callee.computed) {
|
|
||||||
return callee.property?.name === 'S';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to identify transpiled `m(...)` calls for proper conversion
|
|
||||||
// to, say, kabelsalat placeholders
|
|
||||||
function isMiniCall(node) {
|
|
||||||
if (node.type !== 'CallExpression') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const callee = node.callee;
|
|
||||||
if (callee.type !== 'Identifier') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (callee.name !== getMinilangName()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const firstArg = node.arguments?.[0];
|
|
||||||
return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMinilangName() {
|
|
||||||
const minilang = getLanguages().get('minilang');
|
|
||||||
return minilang?.name || 'm';
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceNode(node, replacement, parents, currentRoot) {
|
|
||||||
const info = parents.get(node);
|
|
||||||
if (!info || !info.parent) {
|
|
||||||
return replacement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { parent, prop, index } = info;
|
|
||||||
if (Array.isArray(parent[prop])) {
|
|
||||||
parent[prop][index] = replacement;
|
|
||||||
} else {
|
|
||||||
parent[prop] = replacement;
|
|
||||||
}
|
|
||||||
parents.set(replacement, { parent, prop, index });
|
|
||||||
return currentRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If `start` is available, we use it. If it's already been transpiled
|
|
||||||
// to `m(...)`, use the provided offset
|
|
||||||
function getPatternNodeOrder(node) {
|
|
||||||
if (typeof node.start === 'number') {
|
|
||||||
return node.start;
|
|
||||||
}
|
|
||||||
if (isMiniCall(node)) {
|
|
||||||
const offsetArg = node.arguments?.[1];
|
|
||||||
if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
|
|
||||||
return offsetArg.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function placeholderAst(index) {
|
|
||||||
return {
|
|
||||||
type: 'MemberExpression',
|
|
||||||
object: { type: 'Identifier', name: 'pat' },
|
|
||||||
property: { type: 'Literal', value: index },
|
|
||||||
computed: true,
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStrudelPatternExpr(node) {
|
|
||||||
if (isStrudelPatternWrap(node)) {
|
|
||||||
const arg = node.arguments?.[0];
|
|
||||||
if (!arg) {
|
|
||||||
throw new Error('S(...) requires an argument');
|
|
||||||
}
|
|
||||||
return arg;
|
|
||||||
}
|
|
||||||
if (isMiniCall(node)) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneNode(node) {
|
|
||||||
return JSON.parse(JSON.stringify(node));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractPatternPlaceholders(expr) {
|
|
||||||
const templateExpr = cloneNode(expr);
|
|
||||||
const parents = new Map();
|
|
||||||
const targets = [];
|
|
||||||
|
|
||||||
walk(templateExpr, {
|
|
||||||
enter(node, parent, prop, index) {
|
|
||||||
parents.set(node, { parent, prop, index });
|
|
||||||
const patternExpr = getStrudelPatternExpr(node);
|
|
||||||
if (patternExpr) {
|
|
||||||
targets.push({ node, patternExpr });
|
|
||||||
this.skip();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!targets.length) {
|
|
||||||
return { template: genExprSource(templateExpr), patternExprs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
|
|
||||||
|
|
||||||
const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
|
|
||||||
|
|
||||||
let currentExpr = templateExpr;
|
|
||||||
targets.forEach(({ node }, index) => {
|
|
||||||
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
|
|
||||||
});
|
|
||||||
|
|
||||||
const template = genExprSource(currentExpr);
|
|
||||||
return { template, patternExprs };
|
|
||||||
}
|
|
||||||
|
|
||||||
const transpilerPlugin = {
|
|
||||||
walk: (context) => ({
|
|
||||||
leave: function (node, parent, prop, index) {
|
|
||||||
if (!isKabelCall(node)) return;
|
|
||||||
let [expr, ...rest] = node.arguments;
|
|
||||||
if (!expr) throw new Error('K(...) requires an expression');
|
|
||||||
if (shouldCallKabelExpression(expr)) {
|
|
||||||
expr = {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: expr,
|
|
||||||
arguments: [],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const language = 'kabelsalat';
|
|
||||||
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
|
||||||
if (patternExprs.length) {
|
|
||||||
const workletArgs = [
|
|
||||||
/*{ type: 'Literal', value: language },*/
|
|
||||||
{ type: 'Literal', value: template },
|
|
||||||
...patternExprs,
|
|
||||||
...rest,
|
|
||||||
];
|
|
||||||
let callee = node.callee;
|
|
||||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
|
||||||
if (callee.type === 'MemberExpression') {
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: workletMemberAst(callee.object),
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: { type: 'Identifier', name: 'worklet' },
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const kabelSrc = genExprSource(expr);
|
|
||||||
const workletArgs = [/*{ type: 'Literal', value: language },*/ { type: 'Literal', value: kabelSrc }, ...rest];
|
|
||||||
|
|
||||||
let callee = node.callee;
|
|
||||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
|
||||||
if (callee.type === 'MemberExpression') {
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: workletMemberAst(callee.object),
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.replace({
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: { type: 'Identifier', name: 'worklet' },
|
|
||||||
arguments: workletArgs,
|
|
||||||
optional: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
registerTranspilerPlugin(transpilerPlugin);
|
|
||||||
|
|
||||||
function isKabelCall(node) {
|
|
||||||
if (node.type !== 'CallExpression') return false;
|
|
||||||
let callee = node.callee;
|
|
||||||
if (callee.type === 'ChainExpression') callee = callee.expression;
|
|
||||||
if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
|
|
||||||
return callee.type === 'Identifier' && callee.name === 'K';
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldCallKabelExpression(expr) {
|
|
||||||
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (expr.params.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return expr.body?.type === 'BlockStatement';
|
|
||||||
}
|
|
||||||
|
|
||||||
function workletMemberAst(objectExpr) {
|
|
||||||
return {
|
|
||||||
type: 'MemberExpression',
|
|
||||||
object: objectExpr,
|
|
||||||
property: { type: 'Identifier', name: 'worklet' },
|
|
||||||
computed: false,
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
/*
|
|
||||||
plugin-mini.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import { getLeafLocations } from '@strudel/mini';
|
|
||||||
import { getLanguages, registerTranspilerPlugin } from './transpiler.mjs';
|
|
||||||
|
|
||||||
const languageLiteral = {
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function (node, parent, prop, index) {
|
|
||||||
if (!isLanguageLiteral(node)) return;
|
|
||||||
context.miniLocations ??= [];
|
|
||||||
const { options, miniLocations } = context;
|
|
||||||
const { emitMiniLocations } = options;
|
|
||||||
const { name } = node.tag;
|
|
||||||
const language = getLanguages().get(name);
|
|
||||||
const code = node.quasi.quasis[0].value.raw;
|
|
||||||
const offset = node.quasi.start + 1;
|
|
||||||
if (emitMiniLocations) {
|
|
||||||
const locs = language.getLocations(code, offset);
|
|
||||||
miniLocations.push(...locs);
|
|
||||||
}
|
|
||||||
this.skip();
|
|
||||||
return this.replace(languageWithLocation(name, code, offset));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tidal = {
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function (node, parent, prop, index) {
|
|
||||||
if (!isTemplateLiteral(node, 'tidal')) return;
|
|
||||||
context.miniLocations ??= [];
|
|
||||||
const { options, miniLocations } = context;
|
|
||||||
const { emitMiniLocations } = options;
|
|
||||||
const raw = node.quasi.quasis[0].value.raw;
|
|
||||||
const offset = node.quasi.start + 1;
|
|
||||||
if (emitMiniLocations) {
|
|
||||||
const stringLocs = collectHaskellMiniLocations(raw, offset);
|
|
||||||
miniLocations.push(...stringLocs);
|
|
||||||
}
|
|
||||||
this.skip();
|
|
||||||
return this.replace(tidalWithLocation(raw, offset));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const backtick = {
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function (node, parent, prop, index) {
|
|
||||||
if (!isBackTickString(node, parent)) return;
|
|
||||||
context.miniLocations ??= [];
|
|
||||||
const { options, input, miniDisableRanges, miniLocations } = context;
|
|
||||||
const { emitMiniLocations } = options;
|
|
||||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { quasis } = node;
|
|
||||||
const { raw } = quasis[0].value;
|
|
||||||
this.skip();
|
|
||||||
emitMiniLocations && collectMiniLocations(raw, node, miniLocations, input);
|
|
||||||
return this.replace(miniWithLocation(raw, node));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const doublequotes = {
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function (node, parent, prop, index) {
|
|
||||||
if (!isStringWithDoubleQuotes(node)) return;
|
|
||||||
context.miniLocations ??= [];
|
|
||||||
const { options, input, miniDisableRanges, miniLocations } = context;
|
|
||||||
const { emitMiniLocations } = options;
|
|
||||||
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { value } = node;
|
|
||||||
this.skip();
|
|
||||||
emitMiniLocations && collectMiniLocations(value, node, miniLocations, input);
|
|
||||||
return this.replace(miniWithLocation(value, node));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
function isLanguageLiteral(node) {
|
|
||||||
return node.type === 'TaggedTemplateExpression' && getLanguages().has(node.tag.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function languageWithLocation(name, value, offset) {
|
|
||||||
return {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: {
|
|
||||||
type: 'Identifier',
|
|
||||||
name: name,
|
|
||||||
},
|
|
||||||
arguments: [
|
|
||||||
{ type: 'Literal', value },
|
|
||||||
{ type: 'Literal', value: offset },
|
|
||||||
],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectHaskellMiniLocations(haskellCode, offset) {
|
|
||||||
return haskellCode
|
|
||||||
.split('')
|
|
||||||
.reduce((acc, char, i) => {
|
|
||||||
if (char !== '"') {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
if (!acc.length || acc[acc.length - 1].length > 1) {
|
|
||||||
acc.push([i + 1]);
|
|
||||||
} else {
|
|
||||||
acc[acc.length - 1].push(i);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, [])
|
|
||||||
.map(([start, end]) => {
|
|
||||||
const miniString = haskellCode.slice(start, end);
|
|
||||||
return getLeafLocations(`"${miniString}"`, offset + start - 1);
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTemplateLiteral(node, value) {
|
|
||||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tidalWithLocation(value, offset) {
|
|
||||||
return {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: {
|
|
||||||
type: 'Identifier',
|
|
||||||
name: 'tidal',
|
|
||||||
},
|
|
||||||
arguments: [
|
|
||||||
{ type: 'Literal', value },
|
|
||||||
{ type: 'Literal', value: offset },
|
|
||||||
],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBackTickString(node, parent) {
|
|
||||||
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMiniDisabled(offset, miniDisableRanges) {
|
|
||||||
for (const [start, end] of miniDisableRanges) {
|
|
||||||
if (offset >= start && offset < end) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function miniWithLocation(value, node) {
|
|
||||||
const { start: fromOffset } = node;
|
|
||||||
|
|
||||||
const minilang = getLanguages().get('minilang');
|
|
||||||
let name = 'm';
|
|
||||||
if (minilang && minilang.name) {
|
|
||||||
name = minilang.name; // name is expected to be exported from the package of the minilang
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'CallExpression',
|
|
||||||
callee: {
|
|
||||||
type: 'Identifier',
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
arguments: [
|
|
||||||
{ type: 'Literal', value },
|
|
||||||
{ type: 'Literal', value: fromOffset },
|
|
||||||
],
|
|
||||||
optional: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectMiniLocations = (value, node, miniLocations, input) => {
|
|
||||||
const minilang = getLanguages().get('minilang');
|
|
||||||
if (minilang) {
|
|
||||||
const code = `[${value}]`;
|
|
||||||
const locs = minilang.getLocations(code, node.start);
|
|
||||||
miniLocations.push(...locs);
|
|
||||||
} else {
|
|
||||||
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
|
||||||
miniLocations.push(...leafLocs);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function isStringWithDoubleQuotes(node, locations, code) {
|
|
||||||
if (node.type !== 'Literal') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return node.raw[0] === '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
registerTranspilerPlugin([languageLiteral, tidal, backtick, doublequotes]);
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
plugin-sample.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { registerTranspilerPlugin } from './transpiler.mjs';
|
|
||||||
|
|
||||||
export function withAwait(node) {
|
|
||||||
return {
|
|
||||||
type: 'AwaitExpression',
|
|
||||||
argument: node,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const bareSample = {
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function (node, parent, prop, index) {
|
|
||||||
if (!isBareSamplesCall(node, parent)) return;
|
|
||||||
this.replace(withAwait(node));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
function isBareSamplesCall(node, parent) {
|
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
|
||||||
}
|
|
||||||
|
|
||||||
registerTranspilerPlugin(bareSample);
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
plugin-widgets.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { registerTranspilerPlugin } from './transpiler.mjs';
|
|
||||||
|
|
||||||
let widgetMethods = [];
|
|
||||||
export function registerWidgetType(type) {
|
|
||||||
widgetMethods.push(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const widgetTranspilerPlugin = {
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function (node, parent, prop, index) {
|
|
||||||
if (!isWidgetMethod(node)) return;
|
|
||||||
context.widgets ??= [];
|
|
||||||
const { widgets, options } = context;
|
|
||||||
const { emitWidgets } = options;
|
|
||||||
const type = node.callee.property.name;
|
|
||||||
const idx = widgets.filter((w) => w.type === type).length;
|
|
||||||
const widgetConfig = {
|
|
||||||
from: node.start,
|
|
||||||
to: node.end,
|
|
||||||
index: idx,
|
|
||||||
type,
|
|
||||||
id: options.id,
|
|
||||||
};
|
|
||||||
emitWidgets && widgets.push(widgetConfig);
|
|
||||||
return this.replace(widgetWithLocation(node, widgetConfig));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const sliderTranspilerPlugin = {
|
|
||||||
walk: (context) => ({
|
|
||||||
enter: function (node, parent, prop, index) {
|
|
||||||
if (!isSliderFunction(node)) return;
|
|
||||||
context.widgets ??= [];
|
|
||||||
context.sliders ??= [];
|
|
||||||
const { options, widgets, sliders, nodeOffset } = context;
|
|
||||||
const { emitWidgets } = options;
|
|
||||||
const from = node.arguments[0].start + nodeOffset;
|
|
||||||
const to = node.arguments[0].end + nodeOffset;
|
|
||||||
const id = `${from}:${to}`; // Range-based ID for stability
|
|
||||||
const sliderConfig = {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
id,
|
|
||||||
value: node.arguments[0].raw, // don't use value!
|
|
||||||
min: node.arguments[1]?.value ?? 0,
|
|
||||||
max: node.arguments[2]?.value ?? 1,
|
|
||||||
step: node.arguments[3]?.value,
|
|
||||||
type: 'slider',
|
|
||||||
};
|
|
||||||
emitWidgets && widgets.push(sliderConfig);
|
|
||||||
sliders.push(sliderConfig);
|
|
||||||
return this.replace(sliderWithLocation(node, nodeOffset));
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const widgetTranspilerPlugins = [sliderTranspilerPlugin, widgetTranspilerPlugin];
|
|
||||||
|
|
||||||
// these functions are connected to @strudel/codemirror -> slider.mjs
|
|
||||||
// maybe someday there will be pluggable transpiler functions, then move this there
|
|
||||||
function isSliderFunction(node) {
|
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'slider';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWidgetMethod(node) {
|
|
||||||
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sliderWithLocation(node, nodeOffset = 0) {
|
|
||||||
// Apply nodeOffset for block-based evaluation to generate correct range
|
|
||||||
const from = node.arguments[0].start + nodeOffset;
|
|
||||||
const to = node.arguments[0].end + nodeOffset;
|
|
||||||
|
|
||||||
// Use range-based ID for stability during block evaluation
|
|
||||||
const id = `${from}:${to}`;
|
|
||||||
|
|
||||||
// add loc as identifier to first argument
|
|
||||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
|
||||||
node.arguments.unshift({
|
|
||||||
type: 'Literal',
|
|
||||||
value: id,
|
|
||||||
raw: id,
|
|
||||||
});
|
|
||||||
node.callee.name = 'sliderWithID';
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWidgetID(widgetConfig) {
|
|
||||||
// the widget id is used as id for the dom element + as key for eventual resources
|
|
||||||
// for example, for each scope widget, a new analyser + buffer (large) is created
|
|
||||||
// Update: use range-based ID generation for better stability during block evaluation
|
|
||||||
// When we have both from and to, use them together for stability
|
|
||||||
// Otherwise fall back to position-based ID for backward compatibility
|
|
||||||
let uniqueIdentifier;
|
|
||||||
if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) {
|
|
||||||
// Use range for more stable identification
|
|
||||||
uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`;
|
|
||||||
} else {
|
|
||||||
// Fallback to single position (for backward compatibility)
|
|
||||||
uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0;
|
|
||||||
}
|
|
||||||
const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`;
|
|
||||||
return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function widgetWithLocation(node, widgetConfig) {
|
|
||||||
const id = getWidgetID(widgetConfig);
|
|
||||||
// Store the unique ID back into the config so it's available for widget management
|
|
||||||
// This is critical for block-based evaluation to match existing widgets with new ones
|
|
||||||
widgetConfig.id = id;
|
|
||||||
// add loc as identifier to first argument
|
|
||||||
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
|
||||||
node.arguments.unshift({
|
|
||||||
type: 'Literal',
|
|
||||||
value: id,
|
|
||||||
raw: id,
|
|
||||||
});
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerTranspilerPlugin(widgetTranspilerPlugins);
|
|
||||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { transpiler } from '../index.mjs';
|
import { transpiler } from '../transpiler.mjs';
|
||||||
|
|
||||||
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
/*
|
import { getLeafLocations } from '@strudel/mini';
|
||||||
transpiler.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdough.mjs>
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import { parse } from 'acorn';
|
import { parse } from 'acorn';
|
||||||
import escodegen from 'escodegen';
|
import escodegen from 'escodegen';
|
||||||
import { walk } from 'estree-walker';
|
import { walk } from 'estree-walker';
|
||||||
|
|
||||||
|
let widgetMethods = [];
|
||||||
|
export function registerWidgetType(type) {
|
||||||
|
widgetMethods.push(type);
|
||||||
|
}
|
||||||
|
|
||||||
let languages = new Map();
|
let languages = new Map();
|
||||||
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
||||||
// see mondough.mjs for example use
|
// see mondough.mjs for example use
|
||||||
@@ -16,31 +17,16 @@ let languages = new Map();
|
|||||||
export function registerLanguage(type, config) {
|
export function registerLanguage(type, config) {
|
||||||
languages.set(type, config);
|
languages.set(type, config);
|
||||||
}
|
}
|
||||||
export function getLanguages() {
|
|
||||||
return languages;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugins = [];
|
|
||||||
|
|
||||||
export function registerTranspilerPlugin(plugin) {
|
|
||||||
plugins.push(plugin);
|
|
||||||
}
|
|
||||||
export function getPlugins() {
|
|
||||||
return plugins.flat(Infinity);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transpiler(input, options = {}) {
|
export function transpiler(input, options = {}) {
|
||||||
options = {
|
const {
|
||||||
wrapAsync: false,
|
wrapAsync = false,
|
||||||
addReturn: true,
|
addReturn = true,
|
||||||
emitMiniLocations: true,
|
emitMiniLocations = true,
|
||||||
emitWidgets: true,
|
emitWidgets = true,
|
||||||
blockBased: false,
|
blockBased = false,
|
||||||
range: [],
|
range = [],
|
||||||
...options,
|
} = options;
|
||||||
};
|
|
||||||
|
|
||||||
const { wrapAsync, addReturn, emitMiniLocations, emitWidgets, blockBased, range } = options;
|
|
||||||
|
|
||||||
const comments = [];
|
const comments = [];
|
||||||
let ast = parse(input, {
|
let ast = parse(input, {
|
||||||
@@ -50,7 +36,8 @@ export function transpiler(input, options = {}) {
|
|||||||
onComment: comments,
|
onComment: comments,
|
||||||
});
|
});
|
||||||
|
|
||||||
let miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||||
|
let miniLocations = [];
|
||||||
|
|
||||||
// Position offset for block-based evaluation
|
// Position offset for block-based evaluation
|
||||||
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
let nodeOffset = range && range.length > 0 ? range[0] : 0;
|
||||||
@@ -58,13 +45,23 @@ export function transpiler(input, options = {}) {
|
|||||||
// Track declarations to add to strudelScope for block-based eval
|
// Track declarations to add to strudelScope for block-based eval
|
||||||
let scopeDeclarations = [];
|
let scopeDeclarations = [];
|
||||||
|
|
||||||
|
const collectMiniLocations = (value, node) => {
|
||||||
|
const minilang = languages.get('minilang');
|
||||||
|
if (minilang) {
|
||||||
|
const code = `[${value}]`;
|
||||||
|
const locs = minilang.getLocations(code, node.start);
|
||||||
|
miniLocations = miniLocations.concat(locs);
|
||||||
|
} else {
|
||||||
|
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
||||||
|
miniLocations = miniLocations.concat(leafLocs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let widgets = [];
|
||||||
|
let sliders = [];
|
||||||
let labels = [];
|
let labels = [];
|
||||||
|
|
||||||
const context = { options, input, nodeOffset, miniDisableRanges, labels };
|
|
||||||
const plugins = getPlugins().map((plugin) => plugin.walk?.(context));
|
|
||||||
|
|
||||||
walk(ast, {
|
walk(ast, {
|
||||||
enter(node, parent, prop, index) {
|
enter(node, parent /* , prop, index */) {
|
||||||
// Apply position offset for block-based evaluation
|
// Apply position offset for block-based evaluation
|
||||||
if (blockBased && node.start !== undefined) {
|
if (blockBased && node.start !== undefined) {
|
||||||
node.start = node.start + nodeOffset;
|
node.start = node.start + nodeOffset;
|
||||||
@@ -82,12 +79,82 @@ export function transpiler(input, options = {}) {
|
|||||||
scopeDeclarations.push(node.id.name);
|
scopeDeclarations.push(node.id.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isLanguageLiteral(node)) {
|
||||||
for (const plugin of plugins) {
|
const { name } = node.tag;
|
||||||
if (!plugin?.enter?.call(this, node, parent, prop, index)) continue;
|
const language = languages.get(name);
|
||||||
return;
|
const code = node.quasi.quasis[0].value.raw;
|
||||||
|
const offset = node.quasi.start + 1;
|
||||||
|
if (emitMiniLocations) {
|
||||||
|
const locs = language.getLocations(code, offset);
|
||||||
|
miniLocations = miniLocations.concat(locs);
|
||||||
|
}
|
||||||
|
this.skip();
|
||||||
|
return this.replace(languageWithLocation(name, code, offset));
|
||||||
}
|
}
|
||||||
|
if (isTemplateLiteral(node, 'tidal')) {
|
||||||
|
const raw = node.quasi.quasis[0].value.raw;
|
||||||
|
const offset = node.quasi.start + 1;
|
||||||
|
if (emitMiniLocations) {
|
||||||
|
const stringLocs = collectHaskellMiniLocations(raw, offset);
|
||||||
|
miniLocations = miniLocations.concat(stringLocs);
|
||||||
|
}
|
||||||
|
this.skip();
|
||||||
|
return this.replace(tidalWithLocation(raw, offset));
|
||||||
|
}
|
||||||
|
if (isBackTickString(node, parent)) {
|
||||||
|
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { quasis } = node;
|
||||||
|
const { raw } = quasis[0].value;
|
||||||
|
this.skip();
|
||||||
|
emitMiniLocations && collectMiniLocations(raw, node);
|
||||||
|
return this.replace(miniWithLocation(raw, node));
|
||||||
|
}
|
||||||
|
if (isStringWithDoubleQuotes(node)) {
|
||||||
|
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { value } = node;
|
||||||
|
this.skip();
|
||||||
|
emitMiniLocations && collectMiniLocations(value, node);
|
||||||
|
return this.replace(miniWithLocation(value, node));
|
||||||
|
}
|
||||||
|
if (isSliderFunction(node)) {
|
||||||
|
const from = node.arguments[0].start + nodeOffset;
|
||||||
|
const to = node.arguments[0].end + nodeOffset;
|
||||||
|
const id = `${from}:${to}`; // Range-based ID for stability
|
||||||
|
|
||||||
|
const sliderConfig = {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
id,
|
||||||
|
value: node.arguments[0].raw, // don't use value!
|
||||||
|
min: node.arguments[1]?.value ?? 0,
|
||||||
|
max: node.arguments[2]?.value ?? 1,
|
||||||
|
step: node.arguments[3]?.value,
|
||||||
|
type: 'slider',
|
||||||
|
};
|
||||||
|
emitWidgets && widgets.push(sliderConfig);
|
||||||
|
sliders.push(sliderConfig);
|
||||||
|
return this.replace(sliderWithLocation(node, nodeOffset));
|
||||||
|
}
|
||||||
|
if (isWidgetMethod(node)) {
|
||||||
|
const type = node.callee.property.name;
|
||||||
|
const index = widgets.filter((w) => w.type === type).length;
|
||||||
|
const widgetConfig = {
|
||||||
|
from: node.start,
|
||||||
|
to: node.end,
|
||||||
|
index,
|
||||||
|
type,
|
||||||
|
id: options.id,
|
||||||
|
};
|
||||||
|
emitWidgets && widgets.push(widgetConfig);
|
||||||
|
return this.replace(widgetWithLocation(node, widgetConfig));
|
||||||
|
}
|
||||||
|
if (isBareSamplesCall(node, parent)) {
|
||||||
|
return this.replace(withAwait(node));
|
||||||
|
}
|
||||||
if (isLabelStatement(node)) {
|
if (isLabelStatement(node)) {
|
||||||
// Collect label info for block-based evaluation
|
// Collect label info for block-based evaluation
|
||||||
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
// Store positions WITHOUT offset so repl can slice the transpiler output correctly
|
||||||
@@ -116,10 +183,61 @@ export function transpiler(input, options = {}) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
leave(node, parent, prop, index) {
|
leave(node, parent, prop, index) {
|
||||||
for (const plugin of plugins) {
|
if (!isKabelCall(node)) return;
|
||||||
if (!plugin?.leave?.call(this, node, parent, prop, index)) continue;
|
|
||||||
return;
|
let [expr, ...rest] = node.arguments;
|
||||||
|
if (!expr) throw new Error('K(...) requires an expression');
|
||||||
|
|
||||||
|
if (shouldCallKabelExpression(expr)) {
|
||||||
|
expr = {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: expr,
|
||||||
|
arguments: [],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { template, patternExprs } = extractPatternPlaceholders(expr);
|
||||||
|
if (patternExprs.length) {
|
||||||
|
const workletArgs = [{ type: 'Literal', value: template }, ...patternExprs, ...rest];
|
||||||
|
|
||||||
|
let callee = node.callee;
|
||||||
|
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||||
|
if (callee.type === 'MemberExpression') {
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: workletMemberAst(callee.object),
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: { type: 'Identifier', name: 'worklet' },
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const kabelSrc = genExprSource(expr);
|
||||||
|
const workletArgs = [{ type: 'Literal', value: kabelSrc }, ...rest];
|
||||||
|
|
||||||
|
let callee = node.callee;
|
||||||
|
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||||
|
if (callee.type === 'MemberExpression') {
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: workletMemberAst(callee.object),
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.replace({
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: { type: 'Identifier', name: 'worklet' },
|
||||||
|
arguments: workletArgs,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,7 +256,12 @@ export function transpiler(input, options = {}) {
|
|||||||
body.push(silenceExpression);
|
body.push(silenceExpression);
|
||||||
} else if (!body?.[body.length - 1]?.expression) {
|
} else if (!body?.[body.length - 1]?.expression) {
|
||||||
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
|
||||||
body.push(silenceExpression);
|
if (blockBased) {
|
||||||
|
// For block-based eval, add silence as the return value when block ends with declaration
|
||||||
|
body.push(silenceExpression);
|
||||||
|
} else {
|
||||||
|
throw new Error('unexpected ast format without body expression');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For block-based eval, add scope assignments before the return statement
|
// For block-based eval, add scope assignments before the return statement
|
||||||
@@ -164,17 +287,280 @@ export function transpiler(input, options = {}) {
|
|||||||
if (!emitMiniLocations) {
|
if (!emitMiniLocations) {
|
||||||
return { output };
|
return { output };
|
||||||
}
|
}
|
||||||
|
return { output, miniLocations, widgets, sliders, labels };
|
||||||
|
}
|
||||||
|
|
||||||
let pluginContext;
|
function isKabelCall(node) {
|
||||||
({ options, input, miniDisableRanges, nodeOffset, ...pluginContext } = context);
|
if (node.type !== 'CallExpression') return false;
|
||||||
|
let callee = node.callee;
|
||||||
|
if (callee.type === 'ChainExpression') callee = callee.expression;
|
||||||
|
if (callee.type === 'MemberExpression') return !callee.computed && callee.property?.name === 'K';
|
||||||
|
return callee.type === 'Identifier' && callee.name === 'K';
|
||||||
|
}
|
||||||
|
|
||||||
return { output, ...pluginContext };
|
function shouldCallKabelExpression(expr) {
|
||||||
|
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (expr.params.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return expr.body?.type === 'BlockStatement';
|
||||||
|
}
|
||||||
|
|
||||||
|
function genExprSource(expr) {
|
||||||
|
return escodegen.generate(expr, { format: { semicolons: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPatternPlaceholders(expr) {
|
||||||
|
const templateExpr = cloneNode(expr);
|
||||||
|
const parents = new Map();
|
||||||
|
const targets = [];
|
||||||
|
|
||||||
|
walk(templateExpr, {
|
||||||
|
enter(node, parent, prop, index) {
|
||||||
|
parents.set(node, { parent, prop, index });
|
||||||
|
const patternExpr = getStrudelPatternExpr(node);
|
||||||
|
if (patternExpr) {
|
||||||
|
targets.push({ node, patternExpr });
|
||||||
|
this.skip();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return { template: genExprSource(templateExpr), patternExprs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.sort((a, b) => getPatternNodeOrder(a.node) - getPatternNodeOrder(b.node));
|
||||||
|
|
||||||
|
const patternExprs = targets.map(({ patternExpr }) => cloneNode(patternExpr));
|
||||||
|
|
||||||
|
let currentExpr = templateExpr;
|
||||||
|
targets.forEach(({ node }, index) => {
|
||||||
|
currentExpr = replaceNode(node, placeholderAst(index), parents, currentExpr);
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = genExprSource(currentExpr);
|
||||||
|
return { template, patternExprs };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStrudelPatternExpr(node) {
|
||||||
|
if (isStrudelPatternWrap(node)) {
|
||||||
|
const arg = node.arguments?.[0];
|
||||||
|
if (!arg) {
|
||||||
|
throw new Error('S(...) requires an argument');
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
if (isMiniCall(node)) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStrudelPatternWrap(node) {
|
||||||
|
if (node.type !== 'CallExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const callee = node.callee;
|
||||||
|
if (callee.type === 'Identifier') {
|
||||||
|
return callee.name === 'S';
|
||||||
|
}
|
||||||
|
if (callee.type === 'MemberExpression' && !callee.computed) {
|
||||||
|
return callee.property?.name === 'S';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMinilangName() {
|
||||||
|
const minilang = languages.get('minilang');
|
||||||
|
return minilang?.name || 'm';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to identify transpiled `m(...)` calls for proper conversion
|
||||||
|
// to, say, kabelsalat placeholders
|
||||||
|
function isMiniCall(node) {
|
||||||
|
if (node.type !== 'CallExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const callee = node.callee;
|
||||||
|
if (callee.type !== 'Identifier') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (callee.name !== getMinilangName()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstArg = node.arguments?.[0];
|
||||||
|
return firstArg?.type === 'Literal' && typeof firstArg.value === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If `start` is available, we use it. If it's already been transpiled
|
||||||
|
// to `m(...)`, use the provided offset
|
||||||
|
function getPatternNodeOrder(node) {
|
||||||
|
if (typeof node.start === 'number') {
|
||||||
|
return node.start;
|
||||||
|
}
|
||||||
|
if (isMiniCall(node)) {
|
||||||
|
const offsetArg = node.arguments?.[1];
|
||||||
|
if (offsetArg?.type === 'Literal' && typeof offsetArg.value === 'number') {
|
||||||
|
return offsetArg.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeholderAst(index) {
|
||||||
|
return {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: { type: 'Identifier', name: 'pat' },
|
||||||
|
property: { type: 'Literal', value: index },
|
||||||
|
computed: true,
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceNode(node, replacement, parents, currentRoot) {
|
||||||
|
const info = parents.get(node);
|
||||||
|
if (!info || !info.parent) {
|
||||||
|
return replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { parent, prop, index } = info;
|
||||||
|
if (Array.isArray(parent[prop])) {
|
||||||
|
parent[prop][index] = replacement;
|
||||||
|
} else {
|
||||||
|
parent[prop] = replacement;
|
||||||
|
}
|
||||||
|
parents.set(replacement, { parent, prop, index });
|
||||||
|
return currentRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneNode(node) {
|
||||||
|
return JSON.parse(JSON.stringify(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
function workletMemberAst(objectExpr) {
|
||||||
|
return {
|
||||||
|
type: 'MemberExpression',
|
||||||
|
object: objectExpr,
|
||||||
|
property: { type: 'Identifier', name: 'worklet' },
|
||||||
|
computed: false,
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringWithDoubleQuotes(node, locations, code) {
|
||||||
|
if (node.type !== 'Literal') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return node.raw[0] === '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBackTickString(node, parent) {
|
||||||
|
return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression';
|
||||||
|
}
|
||||||
|
|
||||||
|
function miniWithLocation(value, node) {
|
||||||
|
const { start: fromOffset } = node;
|
||||||
|
|
||||||
|
const minilang = languages.get('minilang');
|
||||||
|
let name = 'm';
|
||||||
|
if (minilang && minilang.name) {
|
||||||
|
name = minilang.name; // name is expected to be exported from the package of the minilang
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: fromOffset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// these functions are connected to @strudel/codemirror -> slider.mjs
|
||||||
|
// maybe someday there will be pluggable transpiler functions, then move this there
|
||||||
|
function isSliderFunction(node) {
|
||||||
|
return node.type === 'CallExpression' && node.callee.name === 'slider';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWidgetMethod(node) {
|
||||||
|
return node.type === 'CallExpression' && widgetMethods.includes(node.callee.property?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliderWithLocation(node, nodeOffset = 0) {
|
||||||
|
// Apply nodeOffset for block-based evaluation to generate correct range
|
||||||
|
const from = node.arguments[0].start + nodeOffset;
|
||||||
|
const to = node.arguments[0].end + nodeOffset;
|
||||||
|
|
||||||
|
// Use range-based ID for stability during block evaluation
|
||||||
|
const id = `${from}:${to}`;
|
||||||
|
|
||||||
|
// add loc as identifier to first argument
|
||||||
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
|
node.arguments.unshift({
|
||||||
|
type: 'Literal',
|
||||||
|
value: id,
|
||||||
|
raw: id,
|
||||||
|
});
|
||||||
|
node.callee.name = 'sliderWithID';
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWidgetID(widgetConfig) {
|
||||||
|
// the widget id is used as id for the dom element + as key for eventual resources
|
||||||
|
// for example, for each scope widget, a new analyser + buffer (large) is created
|
||||||
|
// Update: use range-based ID generation for better stability during block evaluation
|
||||||
|
// When we have both from and to, use them together for stability
|
||||||
|
// Otherwise fall back to position-based ID for backward compatibility
|
||||||
|
let uniqueIdentifier;
|
||||||
|
if (widgetConfig.from !== undefined && widgetConfig.to !== undefined) {
|
||||||
|
// Use range for more stable identification
|
||||||
|
uniqueIdentifier = `${widgetConfig.from}-${widgetConfig.to}`;
|
||||||
|
} else {
|
||||||
|
// Fallback to single position (for backward compatibility)
|
||||||
|
uniqueIdentifier = widgetConfig.to || widgetConfig.from || 0;
|
||||||
|
}
|
||||||
|
const baseId = `${widgetConfig.id || ''}_widget_${widgetConfig.type}`;
|
||||||
|
return `${baseId}_${widgetConfig.index}_${uniqueIdentifier}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetWithLocation(node, widgetConfig) {
|
||||||
|
const id = getWidgetID(widgetConfig);
|
||||||
|
// Store the unique ID back into the config so it's available for widget management
|
||||||
|
// This is critical for block-based evaluation to match existing widgets with new ones
|
||||||
|
widgetConfig.id = id;
|
||||||
|
// add loc as identifier to first argument
|
||||||
|
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
|
||||||
|
node.arguments.unshift({
|
||||||
|
type: 'Literal',
|
||||||
|
value: id,
|
||||||
|
raw: id,
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBareSamplesCall(node, parent) {
|
||||||
|
return node.type === 'CallExpression' && node.callee.name === 'samples' && parent.type !== 'AwaitExpression';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAllCall(node) {
|
function isAllCall(node) {
|
||||||
return node.type === 'CallExpression' && node.callee.name === 'all';
|
return node.type === 'CallExpression' && node.callee.name === 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withAwait(node) {
|
||||||
|
return {
|
||||||
|
type: 'AwaitExpression',
|
||||||
|
argument: node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isLabelStatement(node) {
|
function isLabelStatement(node) {
|
||||||
return node.type === 'LabeledStatement';
|
return node.type === 'LabeledStatement';
|
||||||
}
|
}
|
||||||
@@ -205,6 +591,69 @@ function labelToP(node) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLanguageLiteral(node) {
|
||||||
|
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tidal highlighting
|
||||||
|
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
|
||||||
|
// but maybe it's the only way
|
||||||
|
|
||||||
|
function isTemplateLiteral(node, value) {
|
||||||
|
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectHaskellMiniLocations(haskellCode, offset) {
|
||||||
|
return haskellCode
|
||||||
|
.split('')
|
||||||
|
.reduce((acc, char, i) => {
|
||||||
|
if (char !== '"') {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
if (!acc.length || acc[acc.length - 1].length > 1) {
|
||||||
|
acc.push([i + 1]);
|
||||||
|
} else {
|
||||||
|
acc[acc.length - 1].push(i);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
.map(([start, end]) => {
|
||||||
|
const miniString = haskellCode.slice(start, end);
|
||||||
|
return getLeafLocations(`"${miniString}"`, offset + start - 1);
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tidalWithLocation(value, offset) {
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: 'tidal',
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: offset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageWithLocation(name, value, offset) {
|
||||||
|
return {
|
||||||
|
type: 'CallExpression',
|
||||||
|
callee: {
|
||||||
|
type: 'Identifier',
|
||||||
|
name: name,
|
||||||
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value },
|
||||||
|
{ type: 'Literal', value: offset },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// List of non-inline widgets that need cleanup
|
// List of non-inline widgets that need cleanup
|
||||||
// These are Pattern.prototype methods that create persistent visualizations
|
// These are Pattern.prototype methods that create persistent visualizations
|
||||||
// (should be repalced by a function call producing an actual list of registered widgets)
|
// (should be repalced by a function call producing an actual list of registered widgets)
|
||||||
@@ -346,3 +795,12 @@ function findMiniDisableRanges(comments, codeEnd) {
|
|||||||
}
|
}
|
||||||
return ranges;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMiniDisabled(offset, miniDisableRanges) {
|
||||||
|
for (const [start, end] of miniDisableRanges) {
|
||||||
|
if (offset >= start && offset < end) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/edo": "workspace:*",
|
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export * from '@strudel/webaudio';
|
|||||||
//export * from '@strudel/soundfonts';
|
//export * from '@strudel/soundfonts';
|
||||||
export * from '@strudel/transpiler';
|
export * from '@strudel/transpiler';
|
||||||
export * from '@strudel/mini';
|
export * from '@strudel/mini';
|
||||||
export * from '@strudel/edo';
|
|
||||||
export * from '@strudel/tonal';
|
export * from '@strudel/tonal';
|
||||||
export * from '@strudel/webaudio';
|
export * from '@strudel/webaudio';
|
||||||
import { Pattern, evalScope, setTime } from '@strudel/core';
|
import { Pattern, evalScope, setTime } from '@strudel/core';
|
||||||
@@ -18,7 +17,6 @@ export async function defaultPrebake() {
|
|||||||
evalScope,
|
evalScope,
|
||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel/edo'),
|
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
{ hush, evaluate },
|
{ hush, evaluate },
|
||||||
|
|||||||
Generated
+7
-83
@@ -4,30 +4,6 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
catalogs:
|
|
||||||
default:
|
|
||||||
'@codemirror/autocomplete':
|
|
||||||
specifier: ^6.18.4
|
|
||||||
version: 6.18.4
|
|
||||||
'@codemirror/commands':
|
|
||||||
specifier: ^6.8.0
|
|
||||||
version: 6.8.0
|
|
||||||
'@codemirror/lang-javascript':
|
|
||||||
specifier: ^6.2.2
|
|
||||||
version: 6.2.2
|
|
||||||
'@codemirror/language':
|
|
||||||
specifier: ^6.10.8
|
|
||||||
version: 6.10.8
|
|
||||||
'@codemirror/search':
|
|
||||||
specifier: ^6.5.8
|
|
||||||
version: 6.5.8
|
|
||||||
'@codemirror/state':
|
|
||||||
specifier: ^6.5.1
|
|
||||||
version: 6.5.1
|
|
||||||
'@codemirror/view':
|
|
||||||
specifier: ^6.36.2
|
|
||||||
version: 6.36.2
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -35,9 +11,6 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/core
|
version: link:packages/core
|
||||||
'@strudel/edo':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:packages/edo
|
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/mini
|
version: link:packages/mini
|
||||||
@@ -120,9 +93,6 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/draw
|
version: link:../../packages/draw
|
||||||
'@strudel/edo':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../../packages/edo
|
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/mini
|
version: link:../../packages/mini
|
||||||
@@ -201,25 +171,25 @@ importers:
|
|||||||
packages/codemirror:
|
packages/codemirror:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/autocomplete':
|
'@codemirror/autocomplete':
|
||||||
specifier: 'catalog:'
|
specifier: ^6.18.4
|
||||||
version: 6.18.4
|
version: 6.18.4
|
||||||
'@codemirror/commands':
|
'@codemirror/commands':
|
||||||
specifier: 'catalog:'
|
specifier: ^6.8.0
|
||||||
version: 6.8.0
|
version: 6.8.0
|
||||||
'@codemirror/lang-javascript':
|
'@codemirror/lang-javascript':
|
||||||
specifier: 'catalog:'
|
specifier: ^6.2.2
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
'@codemirror/language':
|
'@codemirror/language':
|
||||||
specifier: 'catalog:'
|
specifier: ^6.10.8
|
||||||
version: 6.10.8
|
version: 6.10.8
|
||||||
'@codemirror/search':
|
'@codemirror/search':
|
||||||
specifier: 'catalog:'
|
specifier: ^6.5.8
|
||||||
version: 6.5.8
|
version: 6.5.8
|
||||||
'@codemirror/state':
|
'@codemirror/state':
|
||||||
specifier: 'catalog:'
|
specifier: ^6.5.1
|
||||||
version: 6.5.1
|
version: 6.5.1
|
||||||
'@codemirror/view':
|
'@codemirror/view':
|
||||||
specifier: 'catalog:'
|
specifier: ^6.36.2
|
||||||
version: 6.36.2
|
version: 6.36.2
|
||||||
'@lezer/highlight':
|
'@lezer/highlight':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
@@ -313,28 +283,6 @@ importers:
|
|||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||||
|
|
||||||
packages/edo:
|
|
||||||
dependencies:
|
|
||||||
'@strudel/core':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../core
|
|
||||||
'@tonaljs/tonal':
|
|
||||||
specifier: ^4.10.0
|
|
||||||
version: 4.10.0
|
|
||||||
chord-voicings:
|
|
||||||
specifier: ^0.0.1
|
|
||||||
version: 0.0.1
|
|
||||||
webmidi:
|
|
||||||
specifier: ^3.1.12
|
|
||||||
version: 3.1.12
|
|
||||||
devDependencies:
|
|
||||||
vite:
|
|
||||||
specifier: ^6.0.11
|
|
||||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
|
||||||
vitest:
|
|
||||||
specifier: ^3.0.4
|
|
||||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
|
||||||
|
|
||||||
packages/embed: {}
|
packages/embed: {}
|
||||||
|
|
||||||
packages/gamepad:
|
packages/gamepad:
|
||||||
@@ -501,9 +449,6 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../draw
|
version: link:../draw
|
||||||
'@strudel/edo':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../edo
|
|
||||||
'@strudel/hydra':
|
'@strudel/hydra':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../hydra
|
version: link:../hydra
|
||||||
@@ -679,9 +624,6 @@ importers:
|
|||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
'@strudel/edo':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../edo
|
|
||||||
'@strudel/mini':
|
'@strudel/mini':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../mini
|
version: link:../mini
|
||||||
@@ -763,21 +705,6 @@ importers:
|
|||||||
'@astrojs/tailwind':
|
'@astrojs/tailwind':
|
||||||
specifier: ^5.1.5
|
specifier: ^5.1.5
|
||||||
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
|
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
|
||||||
'@codemirror/commands':
|
|
||||||
specifier: 'catalog:'
|
|
||||||
version: 6.8.0
|
|
||||||
'@codemirror/lang-javascript':
|
|
||||||
specifier: 'catalog:'
|
|
||||||
version: 6.2.2
|
|
||||||
'@codemirror/language':
|
|
||||||
specifier: 'catalog:'
|
|
||||||
version: 6.10.8
|
|
||||||
'@codemirror/state':
|
|
||||||
specifier: 'catalog:'
|
|
||||||
version: 6.5.1
|
|
||||||
'@codemirror/view':
|
|
||||||
specifier: 'catalog:'
|
|
||||||
version: 6.36.2
|
|
||||||
'@docsearch/css':
|
'@docsearch/css':
|
||||||
specifier: ^3.8.3
|
specifier: ^3.8.3
|
||||||
version: 3.8.3
|
version: 3.8.3
|
||||||
@@ -811,9 +738,6 @@ importers:
|
|||||||
'@strudel/draw':
|
'@strudel/draw':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/draw
|
version: link:../packages/draw
|
||||||
'@strudel/edo':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../packages/edo
|
|
||||||
'@strudel/gamepad':
|
'@strudel/gamepad':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/gamepad
|
version: link:../packages/gamepad
|
||||||
|
|||||||
@@ -10,12 +10,3 @@ onlyBuiltDependencies:
|
|||||||
- sharp
|
- sharp
|
||||||
- tree-sitter
|
- tree-sitter
|
||||||
- tree-sitter-haskell
|
- tree-sitter-haskell
|
||||||
|
|
||||||
catalog:
|
|
||||||
"@codemirror/commands": "^6.8.0"
|
|
||||||
"@codemirror/language": "^6.10.8"
|
|
||||||
"@codemirror/lang-javascript": "^6.2.2"
|
|
||||||
"@codemirror/state": "^6.5.1"
|
|
||||||
"@codemirror/view": "^6.36.2"
|
|
||||||
"@codemirror/autocomplete": "^6.18.4"
|
|
||||||
"@codemirror/search": "^6.5.8"
|
|
||||||
|
|||||||
@@ -3012,6 +3012,15 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
|
||||||
|
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
|
||||||
|
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
|
||||||
|
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "delayspeed" example index 0 1`] = `
|
exports[`runs examples > example "delayspeed" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
@@ -3062,43 +3071,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "delaytime" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
|
||||||
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
|
||||||
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
|
||||||
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "density" example index 0 1`] = `
|
exports[`runs examples > example "density" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
||||||
@@ -3987,93 +3959,6 @@ exports[`runs examples > example "echoWith" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "edoScale" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
|
||||||
"[ 1/6 → 1/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
"[ 1/3 → 1/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 1/2 → 2/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
|
||||||
"[ 2/3 → 5/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 5/6 → 1/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
"[ 1/1 → 7/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
|
||||||
"[ 7/6 → 4/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
"[ 4/3 → 3/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 3/2 → 5/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
|
||||||
"[ 5/3 → 11/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 11/6 → 2/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
"[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
|
||||||
"[ 13/6 → 7/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
"[ 7/3 → 5/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 5/2 → 8/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
|
||||||
"[ 8/3 → 17/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 17/6 → 3/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
"[ 3/1 → 19/6 | degree:1 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:130.813 edo:12 ]",
|
|
||||||
"[ 19/6 → 10/3 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
"[ 10/3 → 7/2 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 7/2 → 11/3 | degree:7 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:246.942 edo:12 ]",
|
|
||||||
"[ 11/3 → 23/6 | degree:5 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:195.998 edo:12 ]",
|
|
||||||
"[ 23/6 → 4/1 | degree:3 degreeIndexes:[0 2 4 5 7 9 11] intLabels:[null M2 M3 P4 P5 M6 T7 P8] root:130.8128 freq:164.814 edo:12 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "edoScale" example index 1 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
|
||||||
"[ 0/1 → 1/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 1/4 → 1/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
|
||||||
"[ 1/2 → 3/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
|
||||||
"[ 1/2 → 3/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 3/4 → 1/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
|
||||||
"[ 1/1 → 5/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
|
||||||
"[ 1/1 → 5/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 5/4 → 3/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
|
||||||
"[ 3/2 → 7/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
|
||||||
"[ 3/2 → 7/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 7/4 → 2/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
|
||||||
"[ 2/1 → 9/4 | degree:1 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
|
||||||
"[ 2/1 → 9/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 9/4 → 5/2 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
|
||||||
"[ 5/2 → 11/4 | degree:3 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
|
||||||
"[ 5/2 → 11/4 | degree:2 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 11/4 → 3/1 | degree:5 degreeIndexes:[0 3 6 7 10 13] intLabels:[null S2 d4 N4 s6 s7 P8] root:97.9989 freq:151.135 edo:16 s:piano ]",
|
|
||||||
"[ 3/1 → 13/4 | degree:1 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:97.999 edo:16 s:piano ]",
|
|
||||||
"[ 3/1 → 13/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 13/4 → 7/2 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
|
||||||
"[ 7/2 → 15/4 | degree:3 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:127.089 edo:16 s:piano ]",
|
|
||||||
"[ 7/2 → 15/4 | degree:2 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:223.2 edo:16 s:piano ]",
|
|
||||||
"[ 15/4 → 4/1 | degree:5 degreeIndexes:[0 3 6 9 12 13] intLabels:[null S2 d4 M6 s7 P8] root:97.9989 freq:164.814 edo:16 s:piano ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "edoScale" example index 2 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]",
|
|
||||||
"[ 1/6 → 1/3 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:167.025 edo:13 s:piano ]",
|
|
||||||
"[ 1/3 → 1/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
|
||||||
"[ 1/2 → 2/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]",
|
|
||||||
"[ 2/3 → 5/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
|
||||||
"[ 5/6 → 1/1 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:97.999 edo:13 s:piano ]",
|
|
||||||
"[ 1/1 → 7/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
|
||||||
"[ 7/6 → 4/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
|
||||||
"[ 4/3 → 3/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
|
||||||
"[ 3/2 → 5/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
|
||||||
"[ 5/3 → 11/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:130.813 edo:13 s:piano ]",
|
|
||||||
"[ 11/6 → 2/1 | degree:5 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:222.952 edo:13 s:piano ]",
|
|
||||||
"[ 2/1 → 13/6 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:195.998 edo:13 s:piano ]",
|
|
||||||
"[ 13/6 → 7/3 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]",
|
|
||||||
"[ 7/3 → 5/2 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
|
||||||
"[ 5/2 → 8/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:134.945 edo:13 s:piano ]",
|
|
||||||
"[ 8/3 → 17/6 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:142.336 edo:13 s:piano ]",
|
|
||||||
"[ 17/6 → 3/1 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:97.9989 freq:114.998 edo:13 s:piano ]",
|
|
||||||
"[ 3/1 → 19/6 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
|
||||||
"[ 19/6 → 10/3 | degree:2 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:153.504 edo:13 s:piano ]",
|
|
||||||
"[ 10/3 → 7/2 | degree:1 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:261.626 edo:13 s:piano ]",
|
|
||||||
"[ 7/2 → 11/3 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
|
||||||
"[ 11/3 → 23/6 | degree:3 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:180.13 edo:13 s:piano ]",
|
|
||||||
"[ 23/6 → 4/1 | degree:4 degreeIndexes:[0 3 6 7 10] intLabels:[null s3 n4 t5 d7 P8] root:130.8128 freq:189.995 edo:13 s:piano ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "end" example index 0 1`] = `
|
exports[`runs examples > example "end" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:oh end:0.1 ]",
|
"[ 0/1 → 1/8 | s:oh end:0.1 ]",
|
||||||
@@ -5306,15 +5191,6 @@ exports[`runs examples > example "gain" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
|
exports[`runs examples > example "gap" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "getDur" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/1 | s:sax cps:null ]",
|
|
||||||
"[ 1/1 → 2/1 | s:sax cps:null ]",
|
|
||||||
"[ 2/1 → 3/1 | s:sax cps:null ]",
|
|
||||||
"[ 3/1 → 4/1 | s:sax cps:null ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "grow" example index 0 1`] = `
|
exports[`runs examples > example "grow" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/10 | s:tha bank:mridangam ]",
|
"[ 0/1 → 1/10 | s:tha bank:mridangam ]",
|
||||||
@@ -6527,27 +6403,6 @@ exports[`runs examples > example "juxBy" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "keep" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/4 | note:c s:piano ]",
|
|
||||||
"[ 1/4 → 1/2 | note:a s:piano ]",
|
|
||||||
"[ 1/2 → 3/4 | note:f s:piano ]",
|
|
||||||
"[ 3/4 → 1/1 | note:e s:piano ]",
|
|
||||||
"[ 1/1 → 5/4 | note:c s:piano ]",
|
|
||||||
"[ 5/4 → 3/2 | note:a s:piano ]",
|
|
||||||
"[ 3/2 → 7/4 | note:f s:piano ]",
|
|
||||||
"[ 7/4 → 2/1 | note:e s:piano ]",
|
|
||||||
"[ 2/1 → 9/4 | note:c s:piano ]",
|
|
||||||
"[ 9/4 → 5/2 | note:a s:piano ]",
|
|
||||||
"[ 5/2 → 11/4 | note:f s:piano ]",
|
|
||||||
"[ 11/4 → 3/1 | note:e s:piano ]",
|
|
||||||
"[ 3/1 → 13/4 | note:c s:piano ]",
|
|
||||||
"[ 13/4 → 7/2 | note:a s:piano ]",
|
|
||||||
"[ 7/2 → 15/4 | note:f s:piano ]",
|
|
||||||
"[ 15/4 → 4/1 | note:e s:piano ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "keyDown" example index 0 1`] = `[]`;
|
exports[`runs examples > example "keyDown" example index 0 1`] = `[]`;
|
||||||
|
|
||||||
exports[`runs examples > example "lastOf" example index 0 1`] = `
|
exports[`runs examples > example "lastOf" example index 0 1`] = `
|
||||||
@@ -11255,27 +11110,6 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "set" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/4 | note:c s:triangle ]",
|
|
||||||
"[ 1/4 → 1/2 | note:a s:triangle ]",
|
|
||||||
"[ 1/2 → 3/4 | note:f s:triangle ]",
|
|
||||||
"[ 3/4 → 1/1 | note:e s:triangle ]",
|
|
||||||
"[ 1/1 → 5/4 | note:c s:triangle ]",
|
|
||||||
"[ 5/4 → 3/2 | note:a s:triangle ]",
|
|
||||||
"[ 3/2 → 7/4 | note:f s:triangle ]",
|
|
||||||
"[ 7/4 → 2/1 | note:e s:triangle ]",
|
|
||||||
"[ 2/1 → 9/4 | note:c s:triangle ]",
|
|
||||||
"[ 9/4 → 5/2 | note:a s:triangle ]",
|
|
||||||
"[ 5/2 → 11/4 | note:f s:triangle ]",
|
|
||||||
"[ 11/4 → 3/1 | note:e s:triangle ]",
|
|
||||||
"[ 3/1 → 13/4 | note:c s:triangle ]",
|
|
||||||
"[ 13/4 → 7/2 | note:a s:triangle ]",
|
|
||||||
"[ 7/2 → 15/4 | note:f s:triangle ]",
|
|
||||||
"[ 15/4 → 4/1 | note:e s:triangle ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "setDefaultJoin" example index 0 1`] = `
|
exports[`runs examples > example "setDefaultJoin" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
|
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { mini, m } from '@strudel/mini/mini.mjs';
|
|||||||
// import euclid from '@strudel/core/euclid.mjs';
|
// import euclid from '@strudel/core/euclid.mjs';
|
||||||
//import '@strudel/midi/midi.mjs';
|
//import '@strudel/midi/midi.mjs';
|
||||||
import * as tonalHelpers from '@strudel/tonal';
|
import * as tonalHelpers from '@strudel/tonal';
|
||||||
import * as edoHelpers from '@strudel/edo';
|
|
||||||
import '@strudel/xen/xen.mjs';
|
import '@strudel/xen/xen.mjs';
|
||||||
// import '@strudel/xen/tune.mjs';
|
// import '@strudel/xen/tune.mjs';
|
||||||
// import '@strudel/core/euclid.mjs';
|
// import '@strudel/core/euclid.mjs';
|
||||||
@@ -127,7 +126,6 @@ const loadSoundfont = () => {};
|
|||||||
const loadCsound = () => {};
|
const loadCsound = () => {};
|
||||||
const loadCSound = () => {};
|
const loadCSound = () => {};
|
||||||
const loadcsound = () => {};
|
const loadcsound = () => {};
|
||||||
const getDuration = () => {};
|
|
||||||
|
|
||||||
const midin = () => {
|
const midin = () => {
|
||||||
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
return (ccNum) => strudel.ref(() => 0); // returns ref with default value 0
|
||||||
@@ -146,7 +144,6 @@ evalScope(
|
|||||||
toneHelpersMocked,
|
toneHelpersMocked,
|
||||||
uiHelpersMocked,
|
uiHelpersMocked,
|
||||||
webaudio,
|
webaudio,
|
||||||
edoHelpers,
|
|
||||||
tonalHelpers,
|
tonalHelpers,
|
||||||
gamepadHelpers,
|
gamepadHelpers,
|
||||||
/*
|
/*
|
||||||
@@ -171,7 +168,6 @@ evalScope(
|
|||||||
loadCSound,
|
loadCSound,
|
||||||
loadCsound,
|
loadCsound,
|
||||||
loadcsound,
|
loadcsound,
|
||||||
getDuration,
|
|
||||||
setcps: id,
|
setcps: id,
|
||||||
setcpm: id,
|
setcpm: id,
|
||||||
Clock: {}, // whatever
|
Clock: {}, // whatever
|
||||||
|
|||||||
@@ -17,11 +17,6 @@
|
|||||||
"@astrojs/react": "^4.1.6",
|
"@astrojs/react": "^4.1.6",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/rss": "^4.0.11",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@astrojs/tailwind": "^5.1.5",
|
||||||
"@codemirror/commands": "catalog:",
|
|
||||||
"@codemirror/language": "catalog:",
|
|
||||||
"@codemirror/lang-javascript": "catalog:",
|
|
||||||
"@codemirror/state": "catalog:",
|
|
||||||
"@codemirror/view": "catalog:",
|
|
||||||
"@docsearch/css": "^3.8.3",
|
"@docsearch/css": "^3.8.3",
|
||||||
"@docsearch/react": "^3.8.3",
|
"@docsearch/react": "^3.8.3",
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
@@ -33,7 +28,6 @@
|
|||||||
"@strudel/csound": "workspace:*",
|
"@strudel/csound": "workspace:*",
|
||||||
"@strudel/desktopbridge": "workspace:*",
|
"@strudel/desktopbridge": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/edo": "workspace:*",
|
|
||||||
"@strudel/gamepad": "workspace:*",
|
"@strudel/gamepad": "workspace:*",
|
||||||
"@strudel/hydra": "workspace:*",
|
"@strudel/hydra": "workspace:*",
|
||||||
"@strudel/midi": "workspace:*",
|
"@strudel/midi": "workspace:*",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Pattern Alignment
|
title: Pattern Aligment
|
||||||
layout: ../../layouts/MainLayout.astro
|
layout: ../../layouts/MainLayout.astro
|
||||||
---
|
---
|
||||||
|
|
||||||
import { MiniRepl } from '../../docs/MiniRepl';
|
import { MiniRepl } from '../../docs/MiniRepl';
|
||||||
|
|
||||||
# Pattern Alignment & Combination
|
# Pattern Aligment & Combination
|
||||||
|
|
||||||
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of _how_ this is done, only _what_ is to be done.
|
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of _how_ this is done, only _what_ is to be done.
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -10,50 +10,50 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-scroller {
|
#code .cm-scroller {
|
||||||
padding-top: 10px !important;
|
padding-top: 10px !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
.code-container .cm-content {
|
#code .cm-content {
|
||||||
padding-bottom: 50vh;
|
padding-bottom: 50vh;
|
||||||
}
|
}
|
||||||
.code-container .cm-line > * {
|
#code .cm-line > * {
|
||||||
background: var(--lineBackground);
|
background: var(--lineBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-editor {
|
#code .cm-editor {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-theme {
|
#code .cm-theme {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-theme-light {
|
#code .cm-theme-light {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-cursorLayer {
|
#code .cm-cursorLayer {
|
||||||
animation-name: inherit !important;
|
animation-name: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-cursor {
|
#code .cm-cursor {
|
||||||
border-left: 2px solid currentcolor !important;
|
border-left: 2px solid currentcolor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-foldGutter {
|
#code .cm-foldGutter {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-focused {
|
#code .cm-focused {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-container .cm-matchingBracket {
|
#code .cm-matchingBracket {
|
||||||
text-decoration: underline 0.18rem;
|
text-decoration: underline 0.18rem;
|
||||||
text-underline-offset: 0.22rem;
|
text-underline-offset: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export function Code(Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={'code-container text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
|
||||||
|
id="code"
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
containerRef.current = el;
|
containerRef.current = el;
|
||||||
if (!editorRef.current) {
|
if (!editorRef.current) {
|
||||||
|
|||||||
@@ -8,3 +8,31 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SpecialActionButton(props) {
|
||||||
|
const { className, ...buttonProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActionInput({ label, className, ...props }) {
|
||||||
|
return (
|
||||||
|
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
|
||||||
|
<input {...props} className="sr-only peer" />
|
||||||
|
|
||||||
|
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpecialActionInput({ className, ...props }) {
|
||||||
|
return (
|
||||||
|
<ActionInput
|
||||||
|
{...props}
|
||||||
|
className={className}
|
||||||
|
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,50 +1,42 @@
|
|||||||
import { errorLogger } from '@strudel/core';
|
import { errorLogger } from '@strudel/core';
|
||||||
import { storePrebakeScript } from '../../../settings.mjs';
|
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
||||||
import { confirmDialog } from '@src/repl/util.mjs';
|
import { SpecialActionInput } from '../button/action-button';
|
||||||
|
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
|
||||||
|
|
||||||
async function importScript(script, updateEditor) {
|
async function importScript(script) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsText(script);
|
reader.readAsText(script);
|
||||||
|
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const text = reader.result;
|
const text = reader.result;
|
||||||
storePrebakeScript(text);
|
storePrebakeScript(text);
|
||||||
updateEditor && updateEditor(text);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => {
|
reader.onerror = () => {
|
||||||
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export function ImportPrebakeScriptButton() {
|
||||||
|
const settings = useSettings();
|
||||||
|
|
||||||
export async function exportScript(script) {
|
|
||||||
const blob = new Blob([script], { type: 'application/javascript' });
|
|
||||||
const downloadLink = document.createElement('a');
|
|
||||||
downloadLink.href = window.URL.createObjectURL(blob);
|
|
||||||
const date = new Date().toISOString().split('T')[0];
|
|
||||||
downloadLink.download = `prebake_${date}.strudel`;
|
|
||||||
document.body.appendChild(downloadLink);
|
|
||||||
downloadLink.click();
|
|
||||||
document.body.removeChild(downloadLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImportPrebakeScriptButton({ updateEditor }) {
|
|
||||||
const handleChange = async (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
const confirmed = await confirmDialog('Warning: This will overwrite the current prebake.\nContinue?');
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await importScript(file, updateEditor);
|
|
||||||
} catch (e) {
|
|
||||||
errorLogger(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<label className="space-x-1 inline-flex items-center cursor-pointer">
|
<SpecialActionInput
|
||||||
<input type="file" accept=".strudel,.js" className="sr-only peer" onChange={handleChange} />
|
type="file"
|
||||||
<span className="inline-flex items-center peer-hover:opacity-50 text-xs max-w-[300px]">import</span>
|
label="import prebake script"
|
||||||
</label>
|
accept=".strudel"
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await importScript(file);
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
errorLogger(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { defaultSettings, settingsMap, useSettings, storePrebakeScript, setSettingsTab } from '../../../settings.mjs';
|
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
||||||
import { themes } from '@strudel/codemirror';
|
import { themes } from '@strudel/codemirror';
|
||||||
import { PrebakeCodeMirror } from '../../../repl/prebakeCodeMirror.mjs';
|
|
||||||
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||||
import { confirmDialog } from '../../util.mjs';
|
import { confirmDialog } from '../../util.mjs';
|
||||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||||
import { ActionButton } from '../button/action-button.jsx';
|
import { SpecialActionButton } from '../button/action-button.jsx';
|
||||||
import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import cx from '@src/cx.mjs';
|
function cx(...classes) {
|
||||||
|
// : Array<string | undefined>
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
const inputClass =
|
const inputClass =
|
||||||
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
|
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
|
||||||
@@ -121,7 +123,7 @@ const fontFamilyOptions = {
|
|||||||
galactico: 'galactico',
|
galactico: 'galactico',
|
||||||
};
|
};
|
||||||
|
|
||||||
function MainSettingsContent({ started }) {
|
export function SettingsTab({ started }) {
|
||||||
const {
|
const {
|
||||||
theme,
|
theme,
|
||||||
keybindings,
|
keybindings,
|
||||||
@@ -147,12 +149,13 @@ function MainSettingsContent({ started }) {
|
|||||||
isTabIndentationEnabled,
|
isTabIndentationEnabled,
|
||||||
isMultiCursorEnabled,
|
isMultiCursorEnabled,
|
||||||
patternAutoStart,
|
patternAutoStart,
|
||||||
|
includePrebakeScriptInShare,
|
||||||
isBlockBasedEvalEnabled,
|
isBlockBasedEvalEnabled,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-foreground space-y-4 w-full overflow-auto" style={{ fontFamily }}>
|
<div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
|
||||||
{canChangeAudioDevice && (
|
{canChangeAudioDevice && (
|
||||||
<FormItem label="Audio Output Device">
|
<FormItem label="Audio Output Device">
|
||||||
<AudioDeviceSelector
|
<AudioDeviceSelector
|
||||||
@@ -230,6 +233,14 @@ function MainSettingsContent({ started }) {
|
|||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
|
<FormItem label="Prebake">
|
||||||
|
<ImportPrebakeScriptButton />
|
||||||
|
<Checkbox
|
||||||
|
label="Include prebake script in share"
|
||||||
|
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
||||||
|
value={includePrebakeScriptInShare}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="Keybindings">
|
<FormItem label="Keybindings">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
@@ -335,7 +346,7 @@ function MainSettingsContent({ started }) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||||
<FormItem label="Reset Settings">
|
<FormItem label="Reset Settings">
|
||||||
<ActionButton
|
<SpecialActionButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirmDialog('Sure?').then((r) => {
|
confirmDialog('Sure?').then((r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
@@ -344,70 +355,10 @@ function MainSettingsContent({ started }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="bg-background p-2 max-w-[300px] hover:opacity-50"
|
|
||||||
>
|
>
|
||||||
restore default settings
|
restore default settings
|
||||||
</ActionButton>
|
</SpecialActionButton>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrebakeSettingsContent() {
|
|
||||||
const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings();
|
|
||||||
const editorRef = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
editorRef.current?.cleanup();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full text-foreground w-full overflow-auto" style={{ fontFamily }}>
|
|
||||||
<div className="flex flex-col grow overflow-hidden h-full bg-background">
|
|
||||||
<section
|
|
||||||
className="pb-0 overflow-auto grow z-10 code-container"
|
|
||||||
ref={(el) => {
|
|
||||||
if (editorRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editorRef.current = new PrebakeCodeMirror(prebakeScript, (code) => storePrebakeScript(code), el);
|
|
||||||
}}
|
|
||||||
></section>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center border-t border-muted px-4 whitespace-nowrap">
|
|
||||||
<Checkbox
|
|
||||||
label="share with patterns"
|
|
||||||
className="whitespace-nowrap max-w-[200px]"
|
|
||||||
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
|
||||||
value={includePrebakeScriptInShare}
|
|
||||||
/>
|
|
||||||
<div className="py-2 flex flex-row items-center space-x-3 ">
|
|
||||||
<ImportPrebakeScriptButton updateEditor={(code) => editorRef?.current.setCode(code)} />
|
|
||||||
<ActionButton onClick={() => exportScript(prebakeScript)}>export</ActionButton>
|
|
||||||
<ActionButton onClick={() => editorRef.current?.savePrebake()}>save</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export function SettingsTab({ started }) {
|
|
||||||
const { settingsTab } = useSettings();
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
|
|
||||||
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-b border-muted">
|
|
||||||
<ButtonGroup
|
|
||||||
wrap
|
|
||||||
value={settingsTab}
|
|
||||||
onChange={(value) => setSettingsTab(value)}
|
|
||||||
items={{
|
|
||||||
settings: 'settings',
|
|
||||||
prebake: 'prebake',
|
|
||||||
}}
|
|
||||||
></ButtonGroup>
|
|
||||||
</div>
|
|
||||||
{settingsTab === 'settings' && <MainSettingsContent started={started} />}
|
|
||||||
{settingsTab === 'prebake' && <PrebakeSettingsContent />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { registerSampleSource } from '@strudel/webaudio';
|
import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
|
||||||
import { isAudioFile } from './files.mjs';
|
import { isAudioFile } from './files.mjs';
|
||||||
import { logger } from '@strudel/core';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
Promise.all(
|
Promise.all(
|
||||||
[...soundFiles]
|
[...soundFiles]
|
||||||
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
.map((soundFile, i) => {
|
.map((soundFile) => {
|
||||||
const title = soundFile.title;
|
const title = soundFile.title;
|
||||||
if (!isAudioFile(title)) {
|
if (!isAudioFile(title)) {
|
||||||
return;
|
return;
|
||||||
@@ -58,25 +58,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
|
|||||||
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
|
||||||
const blob = soundFile.blob;
|
const blob = soundFile.blob;
|
||||||
|
|
||||||
return blobToDataUrl(blob).then((soundPath) => {
|
return blobToDataUrl(blob).then((path) => {
|
||||||
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
|
const sampleInfoMap = sounds.get(parentDirectory) ?? new Map();
|
||||||
|
const midi = extractMidiNoteFromString(title);
|
||||||
|
/** @type {import('@strudel/webaudio').SampleMetaData} */
|
||||||
|
const samplemetadata = { url: path, midi };
|
||||||
|
sampleInfoMap.set(title, samplemetadata);
|
||||||
|
|
||||||
titlePathMap.set(title, soundPath);
|
sounds.set(parentDirectory, sampleInfoMap);
|
||||||
|
|
||||||
sounds.set(parentDirectory, titlePathMap);
|
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
sounds.forEach((titlePathMap, key) => {
|
sounds.forEach((sampleInfoMap, key) => {
|
||||||
const value = Array.from(titlePathMap.keys())
|
const bank = Array.from(sampleInfoMap.keys())
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
})
|
})
|
||||||
.map((title) => titlePathMap.get(title));
|
.map((title) => sampleInfoMap.get(title));
|
||||||
|
registerSampleSource(key, bank, { prebake: false });
|
||||||
registerSampleSource(key, value, { prebake: false });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger('imported sounds registered!', 'success');
|
logger('imported sounds registered!', 'success');
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
import { toggleLineComment } from '@codemirror/commands';
|
|
||||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
|
||||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
||||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
|
||||||
import { drawSelection, EditorView, keymap } from '@codemirror/view';
|
|
||||||
import { logger } from '@strudel/core';
|
|
||||||
import { basicSetup, flash, initTheme, extensions, parseBooleans, codemirrorSettings } from '@strudel/codemirror';
|
|
||||||
import { evaluate } from '@strudel/transpiler';
|
|
||||||
|
|
||||||
export class PrebakeCodeMirror {
|
|
||||||
constructor(initialCode, storePrebake, container) {
|
|
||||||
const settings = codemirrorSettings.get();
|
|
||||||
this.storePrebake = storePrebake;
|
|
||||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
|
||||||
const initialSettings = Object.keys(compartments).map((key) =>
|
|
||||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
|
||||||
);
|
|
||||||
initTheme(settings.theme);
|
|
||||||
let state = EditorState.create({
|
|
||||||
doc: initialCode,
|
|
||||||
extensions: [
|
|
||||||
...initialSettings,
|
|
||||||
basicSetup,
|
|
||||||
javascript(),
|
|
||||||
javascriptLanguage.data.of({
|
|
||||||
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
|
||||||
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
|
||||||
}),
|
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
|
||||||
EditorView.updateListener.of((v) => {
|
|
||||||
if (v.docChanged) {
|
|
||||||
this.code = v.state.doc.toString();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
drawSelection({ cursorBlinkRate: 0 }),
|
|
||||||
Prec.highest(
|
|
||||||
keymap.of([
|
|
||||||
{
|
|
||||||
mac: 'Meta-Enter',
|
|
||||||
run: () => {
|
|
||||||
this.savePrebake();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Ctrl-Enter',
|
|
||||||
run: () => {
|
|
||||||
this.savePrebake();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Alt-Enter',
|
|
||||||
run: () => {
|
|
||||||
this.savePrebake();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.code = initialCode;
|
|
||||||
this.view = new EditorView({
|
|
||||||
state,
|
|
||||||
parent: container,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSaveEvent = async (e) => {
|
|
||||||
if (e.detail.view !== this.view) {
|
|
||||||
return; // ignore events from other editors
|
|
||||||
}
|
|
||||||
await this.savePrebake();
|
|
||||||
e?.cancelable && e.preventDefault?.();
|
|
||||||
};
|
|
||||||
const handleToggleComment = (e) => {
|
|
||||||
if (e.detail.view !== this.view) {
|
|
||||||
return; // ignore events from other editors
|
|
||||||
}
|
|
||||||
this.toggleComment();
|
|
||||||
e?.cancelable && e.preventDefault?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('repl-evaluate', handleSaveEvent);
|
|
||||||
document.addEventListener('repl-toggle-comment', handleToggleComment);
|
|
||||||
this.cleanup = () => {
|
|
||||||
document.removeEventListener('prebake-evaluate', handleSaveEvent);
|
|
||||||
document.removeEventListener('prebake-toggle-comment', handleToggleComment);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async savePrebake() {
|
|
||||||
flash(this.view);
|
|
||||||
this.storePrebake(this.code);
|
|
||||||
evaluate(this.code, { addReturn: false }); // run prebake
|
|
||||||
logger('[prebake] prebake saved');
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleComment() {
|
|
||||||
try {
|
|
||||||
// Honor selections; toggleLineComment handles both selections and
|
|
||||||
// single line
|
|
||||||
toggleLineComment(this.view);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error handling repl-toggle-comment event', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCode(code) {
|
|
||||||
const changes = {
|
|
||||||
from: 0,
|
|
||||||
to: this.view.state.doc.length,
|
|
||||||
insert: code,
|
|
||||||
};
|
|
||||||
this.view.dispatch({ changes });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||||
import { getDrawContext } from '@strudel/draw';
|
import { getDrawContext } from '@strudel/draw';
|
||||||
import { transpiler, evaluate } from '@strudel/transpiler';
|
import { evaluate, transpiler } from '@strudel/transpiler';
|
||||||
import {
|
import {
|
||||||
getAudioContextCurrentTime,
|
getAudioContextCurrentTime,
|
||||||
renderPatternAudio,
|
renderPatternAudio,
|
||||||
@@ -87,12 +87,12 @@ export function useReplContext() {
|
|||||||
pattern: silence,
|
pattern: silence,
|
||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
prebake: async () => {
|
prebake: async () =>
|
||||||
await Promise.all([modulesLoading, presets]);
|
Promise.all([modulesLoading, presets]).then(() => {
|
||||||
if (prebakeScript) {
|
if (prebakeScript?.length) {
|
||||||
return evaluate(prebakeScript, { addReturn: false });
|
return evaluate(prebakeScript ?? '');
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
onUpdateState: (state) => {
|
onUpdateState: (state) => {
|
||||||
setReplState({ ...state });
|
setReplState({ ...state });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ export function loadModules() {
|
|||||||
let modules = [
|
let modules = [
|
||||||
import('@strudel/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel/draw'),
|
import('@strudel/draw'),
|
||||||
import('@strudel/edo'),
|
|
||||||
import('@strudel/tonal'),
|
import('@strudel/tonal'),
|
||||||
import('@strudel/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel/xen'),
|
import('@strudel/xen'),
|
||||||
@@ -109,7 +108,6 @@ export function confirmDialog(msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||||
|
|
||||||
export function confirmAndReloadPage(onSuccess) {
|
export function confirmAndReloadPage(onSuccess) {
|
||||||
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
||||||
if (r == true) {
|
if (r == true) {
|
||||||
|
|||||||
@@ -18,20 +18,6 @@ export const soundFilterType = {
|
|||||||
ALL: 'all',
|
ALL: 'all',
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialPrebakeScript = `// Prebake script
|
|
||||||
//
|
|
||||||
// This is code that is loaded before your pattern is run.
|
|
||||||
// You can use it to define custom functions to use in any pattern.
|
|
||||||
//
|
|
||||||
// This is an initial example script. You can edit it to add
|
|
||||||
// your own funtions.
|
|
||||||
//
|
|
||||||
// To use a script shared by some other user you can use
|
|
||||||
// the import-button or paste the script in this editor.
|
|
||||||
|
|
||||||
const ratchet = register('ratchet', (pat) => pat.sometimes(ply(2)))
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
activeFooter: 'intro',
|
activeFooter: 'intro',
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
@@ -61,14 +47,13 @@ export const defaultSettings = {
|
|||||||
isPanelPinned: false,
|
isPanelPinned: false,
|
||||||
isPanelOpen: true,
|
isPanelOpen: true,
|
||||||
userPatterns: '{}',
|
userPatterns: '{}',
|
||||||
prebakeScript: initialPrebakeScript,
|
prebakeScript: '',
|
||||||
audioEngineTarget: audioEngineTargets.webaudio,
|
audioEngineTarget: audioEngineTargets.webaudio,
|
||||||
isButtonRowHidden: false,
|
isButtonRowHidden: false,
|
||||||
isCSSAnimationDisabled: false,
|
isCSSAnimationDisabled: false,
|
||||||
maxPolyphony: 128,
|
maxPolyphony: 128,
|
||||||
multiChannelOrbits: false,
|
multiChannelOrbits: false,
|
||||||
includePrebakeScriptInShare: true,
|
includePrebakeScriptInShare: true,
|
||||||
settingsTab: 'settings',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let search = null;
|
let search = null;
|
||||||
@@ -130,7 +115,6 @@ export function useSettings() {
|
|||||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
||||||
export const setSettingsTab = (tab) => settingsMap.setKey('settingsTab', tab);
|
|
||||||
|
|
||||||
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user