Compare commits

..

2 Commits

Author SHA1 Message Date
Felix Roos 4603a34f7c fix: tests 2025-07-21 00:07:13 +02:00
Felix Roos 30977019cc move many things from core to cyclist package 2025-07-20 23:25:52 +02:00
127 changed files with 759 additions and 5150 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
name: Strudel tests
on: [push, pull_request]
on: [push]
jobs:
build:
@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
cache: 'pnpm'
- run: pnpm install
- run: pnpm run format-check
- run: pnpm run lint
-21
View File
@@ -1,21 +0,0 @@
FROM node:24
WORKDIR /app
RUN npm install pnpm --global
COPY pnpm-workspace.yaml ./
COPY package.json pnpm-lock.yaml ./
COPY packages/ ./packages/
COPY examples/ ./examples/
RUN mkdir -p website/public
COPY website/package.json ./website/
RUN pnpm install
COPY . .
EXPOSE 4321
CMD ["pnpm", "dev"]
+2 -2
View File
@@ -3,6 +3,8 @@
Live coding patterns on the web
https://strudel.cc/
Development is moving to https://codeberg.org/uzu/strudel
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
@@ -45,5 +47,3 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
-11
View File
@@ -42,7 +42,6 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
@@ -84,14 +83,4 @@ export default [
],
},
},
{
// Properties provided by AudioWorkletGlobalScope
files: ['packages/superdough/worklets.mjs'],
languageOptions: {
globals: {
currentTime: 'readonly',
sampleRate: 'readonly',
},
},
},
];
+1 -1
View File
@@ -1,5 +1,5 @@
/*
jsdoc-synonyms.js - Add support for @synonyms tag
jsdoc-synonyms.js - Add support for @synonym tag
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+2 -1
View File
@@ -48,6 +48,7 @@
"homepage": "https://strudel.cc",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/cyclist": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/tonal": "workspace:*",
"@strudel/transpiler": "workspace:*",
@@ -74,4 +75,4 @@
"vitest": "^3.0.4",
"vite-plugin-bundle-audioworklet": "workspace:*"
}
}
}
+59 -107
View File
@@ -1,122 +1,68 @@
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html';
const escapeHtml = (str) => {
function plaintext(str) {
const div = document.createElement('div');
div.innerText = str;
return div.innerHTML;
};
}
const stripHtml = (html) => {
const div = document.createElement('div');
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
const getDocLabel = (doc) => doc.name || doc.longname;
export function Autocomplete({ doc, label }) {
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
${doc.description}
<ul>
${doc.params?.map(
({ name, type, description }) =>
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
)}
</ul>
<div>
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
</div>
</div>`[0];
/*
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
*/
}
const buildParamsList = (params) =>
params?.length
? `
<div class="autocomplete-info-params-section">
<h4 class="autocomplete-info-section-title">Parameters</h4>
<ul class="autocomplete-info-params-list">
${params
.map(
({ name, type, description }) => `
<li class="autocomplete-info-param-item">
<span class="autocomplete-info-param-name">${name}</span>
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
</li>
`,
)
.join('')}
</ul>
</div>
`
: '';
const buildExamples = (examples) =>
examples?.length
? `
<div class="autocomplete-info-examples-section">
<h4 class="autocomplete-info-section-title">Examples</h4>
${examples
.map(
(example) => `
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
`,
)
.join('')}
</div>
`
: '';
export const Autocomplete = (doc) =>
h`
<div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)}
${buildExamples(doc.examples)}
</div>
</div>
`[0];
const isValidDoc = (doc) => {
const label = getDocLabel(doc);
return label && !label.startsWith('_') && !['package'].includes(doc.kind);
};
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
export const getSynonymDoc = (doc, synonym) => {
const synonyms = doc.synonyms || [];
const docLabel = getDocLabel(doc);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
return {
...doc,
name: synonym,
longname: synonym,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
};
};
const jsdocCompletions = (() => {
const seen = new Set(); // avoid repetition
const completions = [];
for (const doc of jsdoc.docs) {
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
const docLabel = getDocLabel(doc);
// Remove duplicates
const synonyms = doc.synonyms || [];
let labels = [docLabel, ...synonyms];
for (const label of labels) {
// https://codemirror.net/docs/ref/#autocomplete.Completion
if (label && !seen.has(label)) {
seen.add(label);
completions.push({
label,
info: () => Autocomplete(getSynonymDoc(doc, label)),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
});
}
}
}
return completions;
})();
export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/);
if (word.from === word.to && !context.explicit) return null;
const jsdocCompletions = jsdoc.docs
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => Autocomplete({ doc }),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
@@ -128,5 +74,11 @@ export const strudelAutocomplete = (context) => {
};
};
export const isAutoCompletionEnabled = (enabled) =>
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
export function isAutoCompletionEnabled(on) {
return on
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}
-63
View File
@@ -1,63 +0,0 @@
import {
keymap,
highlightSpecialChars,
drawSelection,
highlightActiveLine,
dropCursor,
rectangularSelection,
crosshairCursor,
lineNumbers,
highlightActiveLineGutter,
} from '@codemirror/view';
import {
defaultHighlightStyle,
syntaxHighlighting,
bracketMatching,
foldGutter,
foldKeymap,
} from '@codemirror/language';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
export const basicSetup = (() => [
// lineNumbers(),
// highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
// foldGutter(),
// drawSelection(),
dropCursor(),
// EditorState.allowMultipleSelections.of(true),
// indentOnInput(),
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
// autocompletion(),
rectangularSelection(),
crosshairCursor(),
// highlightActiveLine(),
// highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
// ...searchKeymap,
...historyKeymap,
// ...foldKeymap,
// ...completionKeymap,
]),
])();
/// A minimal set of extensions to create a functional editor. Only
/// includes [the default keymap](#commands.defaultKeymap), [undo
/// history](#commands.history), [special character
/// highlighting](#view.highlightSpecialChars), [custom selection
/// drawing](#view.drawSelection), and [default highlight
/// style](#language.defaultHighlightStyle).
export const minimalSetup = (() => [
highlightSpecialChars(),
history(),
drawSelection(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
keymap.of([...defaultKeymap, ...historyKeymap]),
])();
+5 -9
View File
@@ -1,8 +1,8 @@
import { closeBrackets } from '@codemirror/autocomplete';
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { indentWithTab } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { history, indentWithTab } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import {
@@ -13,7 +13,8 @@ import {
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { repl, registerControl } from '@strudel/core';
import { registerControl } from '@strudel/core';
import { repl } from '@strudel/cyclist';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
@@ -24,7 +25,6 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { basicSetup } from './basicSetup.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
@@ -86,17 +86,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
/* search(),
highlightSelectionMatches(), */
...initialSettings,
basicSetup,
mondo ? [] : javascript(),
javascriptLanguage.data.of({
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
}),
sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
syntaxHighlighting(defaultHighlightStyle),
history(),
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
Prec.highest(
+2 -3
View File
@@ -1,7 +1,6 @@
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
export let html = (string) => {
const template = document.createElement('template');
template.innerHTML = string.trim();
return template.content.childNodes;
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
};
let parseChunk = (chunk) => {
if (Array.isArray(chunk)) return chunk.flat().join('');
+2 -3
View File
@@ -3,9 +3,8 @@ import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
// import { vim } from './vim_test.mjs';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { defaultKeymap } from '@codemirror/commands';
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
@@ -28,5 +27,5 @@ const keymaps = {
export function keybindings(name) {
const active = keymaps[name];
return [active ? Prec.high(active()) : []];
return [active ? active() : [], keymap.of(historyKeymap)];
}
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/codemirror",
"version": "1.2.5",
"version": "1.2.2",
"description": "Codemirror Extensions for Strudel",
"main": "index.mjs",
"publishConfig": {
@@ -42,9 +42,10 @@
"@lezer/highlight": "^1.2.1",
"@nanostores/persistent": "^0.10.2",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.3.0",
"@replit/codemirror-vim": "^6.2.1",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*",
"@strudel/cyclist": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/transpiler": "workspace:*",
"nanostores": "^0.11.3"
+4 -5
View File
@@ -1,6 +1,6 @@
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json';
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
import { Autocomplete } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname;
@@ -52,11 +52,10 @@ export const strudelTooltip = hoverTooltip(
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
// Try for synonyms
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!doc) {
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!entry) {
return null;
}
entry = getSynonymDoc(doc, word);
}
return {
@@ -67,7 +66,7 @@ export const strudelTooltip = hoverTooltip(
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
const ac = Autocomplete(entry);
const ac = Autocomplete({ doc: entry, label: word });
dom.appendChild(ac);
return { dom };
},
+6 -6
View File
@@ -1,11 +1,11 @@
import { describe, bench } from 'vitest';
import { calculateSteps, sequence, stack } from '../index.mjs';
import { calculateTactus, sequence, stack } from '../index.mjs';
const pat64 = sequence(...Array(64).keys());
describe('steps', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -14,7 +14,7 @@ describe('steps', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -25,7 +25,7 @@ describe('steps', () => {
});
describe('stack', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -34,7 +34,7 @@ describe('stack', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -43,4 +43,4 @@ describe('stack', () => {
{ time: 1000 },
);
});
calculateSteps(true);
calculateTactus(true);
+28 -266
View File
@@ -91,7 +91,6 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
* Define a custom webaudio node to use as a sound source.
*
* @name source
* @synonyms src
* @param {function} getSource
* @synonyms src
*
@@ -114,7 +113,7 @@ export const { n } = registerControl('n');
*
* - a letter (a-g or A-G)
* - optional accidentals (b or #)
* - optional (possibly negative) octave number (0-9). Defaults to 3
* - optional octave number (0-9). Defaults to 3
*
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
*
@@ -127,8 +126,6 @@ export const { n } = registerControl('n');
* note("c4 a4 f4 e4")
* @example
* note("60 69 65 64")
* @example
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
*/
export const { note } = registerControl(['note', 'n']);
@@ -144,8 +141,8 @@ export const { note } = registerControl(['note', 'n']);
*/
export const { accelerate } = registerControl('accelerate');
/**
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* Sets the velocity from 0 to 1. Is multiplied together with gain.
* @name velocity
* @example
* s("hh*8")
@@ -255,20 +252,6 @@ export const { fmenv } = registerControl('fmenv');
*
*/
export const { fmattack } = registerControl('fmattack');
/**
* Waveform of the fm modulator
*
* @name fmwave
* @param {number | Pattern} wave waveform
* @example
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("<sine square sawtooth crackle>").fm(4).fmh(2.01)
* @example
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
*
*/
export const { fmwave } = registerControl('fmwave');
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
@@ -312,17 +295,6 @@ export const { fmvelocity } = registerControl('fmvelocity');
*/
export const { bank } = registerControl('bank');
/**
* mix control for the chorus effect
*
* @name chorus
* @param {string | Pattern} chorus mix amount between 0 and 1
* @example
* note("d d a# a").s("sawtooth").chorus(.5)
*
*/
export const { chorus } = registerControl('chorus');
// analyser node send amount 0 - 1 (used by scope)
export const { analyze } = registerControl('analyze');
// fftSize of analyser
@@ -334,7 +306,6 @@ export const { fft } = registerControl('fft');
*
* @name decay
* @param {number | Pattern} time decay time in seconds
* @synonyms dec
* @example
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
*
@@ -391,7 +362,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
// ['bpq'],
export const { bandq, bpq } = registerControl('bandq', 'bpq');
/**
* A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
* @memberof Pattern
* @name begin
@@ -413,32 +384,6 @@ export const { begin } = registerControl('begin');
*
*/
export const { end } = registerControl('end');
/**
* the offset of the sample buffer in seconds
*
* @memberof Pattern
* @name beginSeconds
* @param {number | Pattern} seconds
* @example
* samples('github:switchangel/pad')
* s("swpad!4").clip(1).beginSeconds("2 5 .2 3").fast(2)
* @example
* samples('github:switchangel/pad')
* $: s("swpad!16").beginseconds(time).dec(.1)
*
*/
export const { beginSeconds, beginseconds } = registerControl('beginSeconds', 'beginseconds');
/**
* The end of the sample buffer in seconds
*
* @memberof Pattern
* @name endSeconds
* @param {number | Pattern} seconds
* @example
* s("bd*2,oh*4").endSeconds("<.1 .05 .2 1>").fast(2)
*
*/
export const { endSeconds, endseconds } = registerControl('endSeconds', 'endseconds');
/**
* Loops the sample.
* Note that the tempo of the loop is not synced with the cycle tempo.
@@ -478,7 +423,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
*/
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
/**
* Bit crusher effect.
* bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
@@ -489,7 +434,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
// ['clhatdecay'],
export const { crush } = registerControl('crush');
/**
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
@@ -500,80 +445,7 @@ export const { crush } = registerControl('crush');
export const { coarse } = registerControl('coarse');
/**
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolo
* @synonyms trem
* @param {number | Pattern} speed modulation speed in HZ
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
*
*/
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
/**
* Modulate the amplitude of a sound with a continuous waveform
*
* @name tremolosync
* @synonyms tremsync
* @param {number | Pattern} cycles modulation speed in cycles
* @example
* note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>")
*
*/
export const { tremolosync } = registerControl(
['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'],
'tremsync',
);
/**
* Depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
* @param {number | Pattern} depth
* @example
* note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>")
*
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* Alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>")
*
*/
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* Alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
* @param {number | Pattern} offset the offset in cycles of the modulation
* @example
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>")
*
*/
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* Shape of amplitude modulation
*
* @name tremoloshape
* @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
*
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* Filter overdrive for supported filter types
* filter overdrive for supported filter types
*
* @name drive
* @param {number | Pattern} amount
@@ -583,92 +455,6 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
*/
export const { drive } = registerControl('drive');
/**
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
*
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
*
* @name duckorbit
* @synonyms duck
* @param {number | Pattern} orbit target orbit
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
*
*/
export const { duck } = registerControl('duckorbit', 'duck');
/**
* The amount of ducking applied to target orbit
*
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckdepth
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
* @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
* $: s("hh*16").orbit(3)
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
*
*/
export const { duckdepth } = registerControl('duckdepth');
/**
* The time required for the ducked signal(s) to reach their lowest volume.
* Can be used to prevent clicking or for creative rhythmic effects.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckonset
* @synonyms duckons
*
* @param {number | Pattern} time The onset time in seconds
* @example
* // Clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
* @example
* // No clicks
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
* @example
* // Rhythmic
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
* hhat: s("hh*16").orbit(7)
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
*
*/
export const { duckonset } = registerControl('duckonset', 'duckons');
/**
* The time required for the ducked signal(s) to return to their normal volume.
*
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
*
* @name duckattack
* @synonyms duckatt
*
* @param {number | Pattern} time The attack time in seconds
* @example
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
* @example
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
* lessduck: s("hh*16").orbit(5)
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
*
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
/**
* Create byte beats with custom expressions
*
@@ -709,7 +495,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
export const { channels, ch } = registerControl('channels', 'ch');
/**
* Controls the pulsewidth of the pulse oscillator
* controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
@@ -721,7 +507,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
/**
* Controls the lfo rate for the pulsewidth of the pulse oscillator
* controls the lfo rate for the pulsewidth of the pulse oscillator
*
* @name pwrate
* @param {number | Pattern} rate
@@ -733,7 +519,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
export const { pwrate } = registerControl('pwrate');
/**
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
* controls the lfo sweep for the pulsewidth of the pulse oscillator
*
* @name pwsweep
* @param {number | Pattern} sweep
@@ -774,7 +560,7 @@ export const { phaserrate, ph, phaser } = registerControl(
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
/**
* The center frequency of the phaser in HZ. Defaults to 1000
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
* @synonyms phc
@@ -791,7 +577,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
* @synonyms phd, phasdp
* @synonyms phd
* @param {number | Pattern} depth number between 0 and 1
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
@@ -802,7 +588,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
/**
* Choose the channel the pattern is sent to in superdirt
* choose the channel the pattern is sent to in superdirt
*
* @name channel
* @param {number | Pattern} channel channel number
@@ -1188,27 +974,14 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
*
*/
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
* @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.
*
* @name delayspeed
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
* @name delaytime
* @param {number | Pattern} seconds between 0 and Infinity
* @synonyms delayt, dt
* @example
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
*
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
@@ -1286,7 +1059,6 @@ export const { dry } = registerControl('dry');
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
*
* @name fadeTime
* @synonyms fadeOutTime
* @param {number | Pattern} time between 0 and 1
* @example
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
@@ -1621,29 +1393,6 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
*
*/
export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
/**
* Sets speed of the sample for the impulse response.
* @name irspeed
* @param {string | Pattern} speed
* @example
* samples('github:switchangel/pad')
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5)
*
*/
export const { irspeed } = registerControl('irspeed');
/**
* Sets the beginning of the IR response sample
* @name irbegin
* @param {string | Pattern} begin between 0 and 1
* @synonyms ir
* @example
* samples('github:switchangel/pad')
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6)
*
*/
export const { irbegin } = registerControl('irbegin');
/**
* Sets the room size of the reverb, see `room`.
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
@@ -1812,6 +1561,18 @@ export const { density } = registerControl('density');
// ['modwheel'],
export const { expression } = registerControl('expression');
export const { sustainpedal } = registerControl('sustainpedal');
/* // TODO: doesn't seem to do anything
*
* Tremolo Audio DSP effect
*
* @name tremolodepth
* @param {number | Pattern} depth between 0 and 1
* @example
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
*
*/
export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp');
export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr');
export const { fshift } = registerControl('fshift');
export const { fshiftnote } = registerControl('fshiftnote');
@@ -1888,6 +1649,7 @@ export const { zmod } = registerControl('zmod');
// like crush but scaled differently
export const { zcrush } = registerControl('zcrush');
export const { zdelay } = registerControl('zdelay');
export const { tremolo } = registerControl('tremolo');
export const { zzfx } = registerControl('zzfx');
/**
+1 -24
View File
@@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
import { timeCat, register, silence } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction, { lcm } from './fraction.mjs';
@@ -196,26 +196,3 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat);
});
/**
* A 'euclid' variant with an additional parameter that morphs the resulting
* rhythm from 0 (no morphing) to 1 (completely 'even'). For example
* `sound("bd").euclidish(3,8,0)` would be the same as
* `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the
* same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a
* groove somewhere between.
* Inspired by the work of Malcom Braff.
* @name euclidish
* @synonyms eish
* @memberof Pattern
* @param {number} pulses the number of onsets
* @param {number} steps the number of steps to fill
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
* @example
* sound("hh").euclidish(7,12,sine.slow(8))
* .pan(sine.slow(8))
*/
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});
+7 -2
View File
@@ -4,7 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Fraction from './fraction.mjs';
import { stringifyValues } from './util.mjs';
export class Hap {
/*
@@ -149,7 +148,13 @@ export class Hap {
}
showWhole(compact = false) {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
typeof this.value === 'object'
? compact
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(this.value)
: this.value
}`;
}
combineContext(b) {
+1 -8
View File
@@ -7,9 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th
import * as controls from './controls.mjs'; // legacy
export * from './euclid.mjs';
import Fraction from './fraction.mjs';
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export { Fraction, controls, createClock };
export { Fraction, controls };
export * from './controls.mjs';
export * from './hap.mjs';
export * from './pattern.mjs';
@@ -18,13 +17,7 @@ export * from './pick.mjs';
export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
-6
View File
@@ -4,12 +4,6 @@ let debounce = 1000,
lastMessage,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
export function logger(message, type, data = {}) {
let t = performance.now();
if (lastMessage === message && t - lastTime < debounce) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.4",
"version": "1.2.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
+14 -150
View File
@@ -21,8 +21,6 @@ import {
numeralArgs,
parseNumeral,
pairs,
zipWith,
stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs';
@@ -854,29 +852,14 @@ export class Pattern {
);
}
/**
* Writes the content of the current event to the console (visible in the side menu).
* @name log
* @memberof Pattern
* @example
* s("bd sd").log()
*/
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args));
}, false);
}
/**
* A simplified version of `log` which writes all "values" (various configurable parameters)
* within the event to the console (visible in the side menu).
* @name logValues
* @memberof Pattern
* @example
* s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
*/
logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
return this.log((hap) => func(hap.value));
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
}
//////////////////////////////////////////////////////////////////////
@@ -1246,8 +1229,7 @@ export const silence = gap(1);
/* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0);
/**
* A discrete value that repeats once per cycle.
/** A discrete value that repeats once per cycle.
*
* @returns {Pattern}
* @example
@@ -1300,14 +1282,13 @@ export function sequenceP(pats) {
return result;
}
/**
* The given items are played at the same time at the same length.
/** The given items are played at the same time at the same length.
*
* @return {Pattern}
* @synonyms polyrhythm, pr
* @example
* stack("g3", "b3", ["e4", "d4"]).note()
* // "g3,b3,[e4 d4]".note()
* // "g3,b3,[e4,d4]".note()
*
* @example
* // As a chained function:
@@ -1384,11 +1365,11 @@ export function stackBy(by, ...pats) {
.setSteps(steps);
}
/**
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
*
* synonyms: `cat`
*
* @return {Pattern}
* @synonyms cat
* @example
* slowcat("e5", "b4", ["d5", "c5"])
*
@@ -1588,7 +1569,7 @@ export const func = curry((a, b) => reify(b).func(a));
/**
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
*
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
* @param {string} name name of the function
* @param {function} func function with 1 or more params, where last is the current pattern
* @noAutocomplete
*
@@ -2399,57 +2380,6 @@ export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
export const applyN = register('applyN', function (n, func, p) {
let result = p;
for (let i = 0; i < n; i++) {
result = func(result);
}
return result;
});
/**
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
* @name plyWith
* @synonyms plywith
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern
* @example
* "<0 [2 4]>"
* .plyWith(4, (p) => p.add(2))
* .scale("C:minor").note()
*/
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
* @name plyForEach
* @synonyms plyforeach
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>"
* .plyForEach(4, (p,n) => p.add(n*2))
* .scale("C:minor").note()
*/
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
* @name iter
@@ -2592,7 +2522,7 @@ export const { fastchunk, fastChunk } = register(
/**
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
* @name chunkInto
* @synonyms chunkinto
* @synonym chunkinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
@@ -2605,7 +2535,7 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], fun
/**
* Like `chunkInto`, but moves backwards through the chunks.
* @name chunkBackInto
* @synonyms chunkbackinto
* @synonym chunkbackinto
* @memberof Pattern
* @example
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
@@ -2635,7 +2565,7 @@ export const bypass = register(
* Loops the pattern inside an `offset` for `cycles`.
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
* @name ribbon
* @synonyms rib
* @synonym rib
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
@@ -3330,7 +3260,7 @@ export const slice = register(
* @memberof Pattern
* @returns Pattern
* @example
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
*/
Pattern.prototype.onTriggerTime = function (func) {
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
@@ -3470,69 +3400,3 @@ export const { beat } = register(
['beat'],
__beat((x) => x.innerJoin()),
);
export const _morph = (from, to, by) => {
by = Fraction(by);
const dur = Fraction(1).div(from.length);
const positions = (list) => {
const result = [];
for (const [pos, value] of list.entries()) {
if (value) {
result.push([Fraction(pos).div(list.length), value]);
}
}
return result;
};
const arcs = zipWith(
([posa, valuea], [posb, valueb]) => {
const b = by.mul(posb - posa).add(posa);
const e = b.add(dur);
return new TimeSpan(b, e);
},
positions(from),
positions(to),
);
function query(state) {
const cycle = state.span.begin.sam();
const cycleArc = state.span.cycleArc();
const result = [];
for (const whole of arcs) {
const part = whole.intersection(cycleArc);
if (part !== undefined) {
result.push(
new Hap(
whole.withTime((x) => x.add(cycle)),
part.withTime((x) => x.add(cycle)),
true,
),
);
}
}
return result;
}
return new Pattern(query).splitQueries();
};
/**
* Takes two binary rhythms represented as lists of 1s and 0s, and a number
* between 0 and 1 that morphs between them. The two lists should contain the same
* number of true values.
* @example
* sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
* [1,1,0,1,0,1,0], // wonky rhythm
* 0.25 // creates a slightly wonky rhythm
* )
* )
* @example
* sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
* "1:1:0:1:0:1:0", // wonky rhythm
* sine.slow(8) // slowly morph between the rhythms
* )
* )
*/
export const morph = (frompat, topat, bypat) => {
frompat = reify(frompat);
topat = reify(topat);
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
+1 -3
View File
@@ -152,9 +152,7 @@ export const itri2 = fastcat(isaw2, saw2);
*
* @return {Pattern}
*/
export const time = signal((x) => {
return x.valueOf();
});
export const time = signal(id);
/**
* The mouse's x position value ranges from 0 to 1.
+2 -40
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import {
TimeSpan,
@@ -55,8 +55,6 @@ import {
expand,
} from '../index.mjs';
import { log, logValues } from '../pattern.mjs';
import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs';
@@ -877,7 +875,7 @@ describe('Pattern', () => {
.squeezeJoin()
.queryArc(3, 4)
.map((x) => x.value),
).toStrictEqual([3]);
).toStrictEqual([Fraction(3)]);
});
});
describe('ply', () => {
@@ -1308,40 +1306,4 @@ describe('Pattern', () => {
);
});
});
describe('log', () => {
it('logs to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').log();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] 0/1 → 1/1: a',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
describe('logValues', () => {
it('logs values to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').note('c#').logValues();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] value:a note:c#',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
});
+1 -1
View File
@@ -72,7 +72,7 @@ export class TimeSpan {
}
intersection(other) {
// Intersection of two timespans, returns undefined if they don't intersect.
// Intersection of two timespans, returns None if they don't intersect.
const intersect_begin = this.begin.max(other.begin);
const intersect_end = this.end.min(other.end);
+2 -12
View File
@@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
// returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -487,13 +487,3 @@ export function getCurrentKeyboardState() {
// }
// return lcm((x * y) / gcd(x, y), ...z);
// };
// Takes values -- typically derived from events, i.e. `hap`s -- and renders them
// into a readable format
export function stringifyValues(value, compact = false) {
return typeof value === 'object'
? compact
? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(value)
: value;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/csound",
"version": "1.2.5",
"version": "1.2.3",
"description": "csound bindings for strudel",
"main": "index.mjs",
"type": "module",
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import createClock from './zyklus.mjs';
import { errorLogger, logger } from './logger.mjs';
import { logger } from '@strudel/core';
export class Cyclist {
constructor({
@@ -76,7 +76,7 @@ export class Cyclist {
}
});
} catch (e) {
errorLogger(e);
logger(`[cyclist] error: ${e.message}`);
onError?.(e);
}
},
+8
View File
@@ -0,0 +1,8 @@
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './time.mjs';
export * from './ui.mjs';
import createClock from './zyklus.mjs';
export { createClock };
+28
View File
@@ -0,0 +1,28 @@
export const logKey = 'strudel.log';
let debounce = 1000,
lastMessage,
lastTime;
export function logger(message, type, data = {}) {
let t = performance.now();
if (lastMessage === message && t - lastTime < debounce) {
return;
}
lastMessage = message;
lastTime = t;
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent(
new CustomEvent(logKey, {
detail: {
message,
type,
data,
},
}),
);
}
}
logger.key = logKey;
@@ -5,12 +5,13 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { logger } from './logger.mjs';
import { ClockCollator, cycleToSeconds } from './util.mjs';
import { ClockCollator, cycleToSeconds } from '@strudel/core';
export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
// the clock of the worker and the audio context clock can drift apart over time
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@strudel/cyclist",
"version": "1.2.2",
"description": "Event Scheduler for Strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"test": "vitest run",
"bench": "vitest bench",
"build": "vite build",
"prepublishOnly": "pnpm build"
},
"repository": {
"type": "git",
"url": "git+https://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://strudel.cc",
"dependencies": {
"@strudel/core": "*"
},
"devDependencies": {
"vite": "^6.0.11",
"vitest": "^3.0.4"
}
}
@@ -1,10 +1,10 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
import { errorLogger, logger } from './logger.mjs';
import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { register, Pattern, isPattern, silence, stack } from '@strudel/core';
export function repl({
defaultOutput,
@@ -256,6 +256,6 @@ export const getTrigger =
await hap.context.onTrigger(hap, getTime(), cps, t);
}
} catch (err) {
errorLogger(err, 'getTrigger');
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
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 } from './index.mjs';
import { register } from '@strudel/core';
let synth;
try {
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+2 -1
View File
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Pattern, getTime, State, TimeSpan } from '@strudel/core';
import { Pattern, State, TimeSpan } from '@strudel/core';
import { getTime } from '@strudel/cyclist';
export const getDrawContext = (id = 'test-canvas', options) => {
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/draw",
"version": "1.2.4",
"version": "1.2.2",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
@@ -29,7 +29,8 @@
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
"@strudel/core": "workspace:*",
"@strudel/cyclist": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/embed",
"version": "1.1.1",
"version": "1.1.0",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/gamepad",
"version": "1.2.4",
"version": "1.2.2",
"description": "Gamepad Inputs for strudel",
"main": "index.mjs",
"type": "module",
+2 -1
View File
@@ -1,5 +1,6 @@
import { getDrawContext } from '@strudel/draw';
import { controls, getTime, reify } from '@strudel/core';
import { controls, reify } from '@strudel/core';
import { getTime } from '@strudel/cyclist';
let latestOptions;
let hydra;
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/hydra",
"version": "1.2.4",
"version": "1.2.2",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"type": "module",
@@ -34,6 +34,7 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/cyclist": "workspace:*",
"@strudel/draw": "workspace:*",
"hydra-synth": "^1.3.29"
},
-3
View File
@@ -493,9 +493,6 @@ export async function midin(input) {
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
}
// ensure refs for this input are initialized
if (!refs[input]) {
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/midi",
"version": "1.2.5",
"version": "1.2.3",
"description": "Midi API for strudel",
"main": "index.mjs",
"type": "module",
+4 -4
View File
@@ -1,10 +1,10 @@
import { describe, bench } from 'vitest';
import { calculateSteps } from '../../core/index.mjs';
import { calculateTactus } from '../../core/index.mjs';
import { mini } from '../index.mjs';
describe('mini', () => {
calculateSteps(true);
calculateTactus(true);
bench(
'+tactus',
() => {
@@ -13,7 +13,7 @@ describe('mini', () => {
{ time: 1000 },
);
calculateSteps(false);
calculateTactus(false);
bench(
'-tactus',
() => {
@@ -21,5 +21,5 @@ describe('mini', () => {
},
{ time: 1000 },
);
calculateSteps(true);
calculateTactus(true);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mini",
"version": "1.2.4",
"version": "1.2.2",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mondolang",
"version": "1.1.1",
"version": "1.1.0",
"description": "a language for functional composition that translates to js",
"main": "mondo.mjs",
"type": "module",
+3 -15
View File
@@ -1,17 +1,5 @@
import {
strudelScope,
reify,
fast,
slow,
seq,
stepcat,
extend,
expand,
pace,
chooseIn,
degradeBy,
silence,
} from '@strudel/core';
import { reify, fast, slow, seq, stepcat, extend, expand, pace, chooseIn, degradeBy, silence } from '@strudel/core';
import { strudelScope } from '@strudel/cyclist';
import { registerLanguage } from '@strudel/transpiler';
import { MondoRunner } from 'mondolang';
@@ -108,7 +96,7 @@ export function mondo(code, offset = 0) {
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
}
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
export const mondi = (str, offset) => {
const code = `[${str}]`;
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mondo",
"version": "1.1.4",
"version": "1.1.0",
"description": "mondo notation for strudel",
"main": "mondough.mjs",
"type": "module",
@@ -33,6 +33,7 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/cyclist": "workspace:*",
"@strudel/transpiler": "workspace:*",
"mondolang": "workspace:*"
},
+2 -2
View File
@@ -1,5 +1,5 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
//import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
@@ -12,7 +12,7 @@ export default defineConfig({
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
// external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/motion",
"version": "1.2.4",
"version": "1.2.2",
"description": "DeviceMotion API for strudel",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/mqtt",
"version": "1.2.4",
"version": "1.2.2",
"description": "MQTT API for strudel",
"main": "mqtt.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/osc",
"version": "1.2.4",
"version": "1.2.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"type": "module",
+1 -1
View File
@@ -17,7 +17,7 @@ const config = {
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
port: 7771, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
+4 -4
View File
@@ -1,10 +1,10 @@
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
import { isTauri } from '../desktopbridge/utils.mjs'; */
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
import { isTauri } from '../desktopbridge/utils.mjs';
import { oscTrigger } from './osc.mjs';
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
const currentTime = performance.now() / 1000;
return trigger(hap, currentTime, cps, targetTime);
return trigger(null, hap, currentTime, cps, targetTime);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/reference",
"version": "1.2.1",
"version": "1.2.0",
"description": "Headless reference of all strudel functions",
"main": "index.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/repl",
"version": "1.2.6",
"version": "1.2.3",
"description": "Strudel REPL as a Web Component",
"module": "index.mjs",
"publishConfig": {
+1 -1
View File
@@ -36,7 +36,7 @@ export async function prebake() {
samples(`${ds}/tidal-drum-machines.json`),
samples(`${ds}/piano.json`),
samples(`${ds}/Dirt-Samples.json`),
samples(`${ds}/uzu-drumkit.json`),
samples(`${ds}/EmuSP12.json`),
samples(`${ds}/vcsl.json`),
samples(`${ds}/mridangam.json`),
]);
-10
View File
@@ -20,13 +20,3 @@ samples('http://localhost:5432')
LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port
```
## static json
when running with `--json`, you will simply get the json logged back:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
this is useful if you want to create a sample pack from the current folder.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/sampler",
"version": "0.2.3",
"version": "0.2.0",
"description": "",
"keywords": [
"tidalcycles",
+23 -69
View File
@@ -1,20 +1,22 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream, existsSync, writeFileSync } from 'fs';
import { createReadStream, existsSync } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join, resolve, sep } from 'path';
import readline from 'readline';
import { join, sep } from 'path';
import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
const isAudioFile = (f) => {
const ext = f.split('.').slice(-1)[0].toLowerCase();
return VALID_AUDIO_EXTENSIONS.includes(ext);
};
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
async function getFilesInDirectory(directory) {
let files = [];
@@ -27,90 +29,42 @@ async function getFilesInDirectory(directory) {
continue;
}
try {
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
);
files = files.concat(subFiles);
LOG && console.log(`${dirent.name} (${subFiles.length})`);
} catch (err) {
LOG && console.warn(`skipped due to error: ${fullPath}`);
}
} else {
isAudioFile(fullPath) && files.push(fullPath);
files.push(fullPath);
}
}
return files;
}
async function getBanks(directory, flat = false) {
async function getBanks(directory) {
let files = await getFilesInDirectory(directory);
let banks = {};
directory = directory.split(sep).join('/');
files = files.map((path) => {
path = path.split(sep).join('/');
const subDir = path.replace(directory, '');
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
const [bank] = path.split('/').slice(-2);
banks[bank] = banks[bank] || [];
banks[bank].push(subDir);
return subDir;
const relativeUrl = path.replace(directory, '');
banks[bank].push(relativeUrl);
return relativeUrl;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
const args = process.argv.slice(2);
function getArgValue(flag) {
const i = args.indexOf(flag);
if (i !== -1) {
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
if (nextIsFlag) return true;
return args[i + 1];
}
}
function getInput(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(query, (response) => {
rl.close();
resolve(response);
}),
);
}
let directory = getArgValue('--dir') || process.cwd();
directory = resolve(directory);
if (args.includes('--json')) {
const { banks } = await getBanks(directory, getArgValue('--flat'));
const json = JSON.stringify(banks);
const outFile = resolve(directory, 'strudel.json');
if (existsSync(outFile)) {
const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
if (answer.toLowerCase() !== 'y') {
console.log('Aborted.');
process.exit(0);
}
}
writeFileSync(outFile, json, 'utf8');
console.log(`Wrote json to ${outFile}`);
}
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
// eslint-disable-next-line
const directory = process.cwd();
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
const { banks, files } = await getBanks(directory);
if (req.url === '/') {
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify(banks));
@@ -118,7 +72,7 @@ const server = http.createServer(async (req, res) => {
let subpath = decodeURIComponent(req.url);
const filePath = join(directory, subpath.split('/').join(sep));
// console.log('GET:', filePath);
//console.log('GET:', filePath);
const isFound = existsSync(filePath);
if (!isFound) {
res.statusCode = 404;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/serial",
"version": "1.2.4",
"version": "1.2.2",
"description": "Webserial API for strudel",
"main": "serial.mjs",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/soundfonts",
"version": "1.2.5",
"version": "1.2.3",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"publishConfig": {
+9 -39
View File
@@ -1,8 +1,5 @@
import { getAudioContext } from './superdough.mjs';
import { clamp, nanFallback } from './util.mjs';
import { getNoiseBuffer } from './noise.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
export function gainNode(value) {
const node = getAudioContext().createGain();
@@ -174,7 +171,7 @@ let curves = ['linear', 'exponential'];
export function getPitchEnvelope(param, value, t, holdEnd) {
// envelope is active when any of these values is set
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
if (hasEnvelope === undefined) {
if (!hasEnvelope) {
return;
}
const penv = nanFallback(value.penv, 1, true);
@@ -209,46 +206,19 @@ export function getVibratoOscillator(param, value, t) {
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = new ConstantSourceNode(audioContext);
// Certain browsers requires audio nodes to be connected in order for their onended events
// to fire, so we _mute it_ and then connect it to the destination
const zeroGain = gainNode(0);
zeroGain.connect(audioContext.destination);
constantNode.connect(zeroGain);
// Schedule the `onComplete` callback to occur at `stopTime`
constantNode.onended = () => {
// Ensure garbage collection
try {
zeroGain.disconnect();
} catch {
// pass
}
try {
constantNode.disconnect();
} catch {
// pass
}
onComplete();
};
const constantNode = audioContext.createConstantSource();
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
onComplete();
};
return constantNode;
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
let osc;
if (noises.includes(type)) {
osc = ctx.createBufferSource();
osc.buffer = getNoiseBuffer(type, 2);
osc.loop = true;
} else {
osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
}
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
@@ -283,7 +253,7 @@ export function applyFM(param, value, begin) {
modulator = fmmod.node;
stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
-6
View File
@@ -1,11 +1,5 @@
let log = (msg) => console.log(msg);
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
export const logger = (...args) => log(...args);
export const setLogger = (fn) => {
+1 -1
View File
@@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
let noiseCache = {};
// lazy generates noise buffers and keeps them forever
export function getNoiseBuffer(type, density) {
function getNoiseBuffer(type, density) {
const ac = getAudioContext();
if (noiseCache[type]) {
return noiseCache[type];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "1.2.5",
"version": "1.2.3",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+6 -16
View File
@@ -1,9 +1,7 @@
import reverbGen from './reverbGen.mjs';
import { clamp } from './util.mjs';
if (typeof AudioContext !== 'undefined') {
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
AudioContext.prototype.adjustLength = function (duration, buffer) {
const newLength = buffer.sampleRate * duration;
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
@@ -11,30 +9,22 @@ if (typeof AudioContext !== 'undefined') {
let newData = newBuffer.getChannelData(channel);
for (let i = 0; i < newLength; i++) {
// loop the buffer around to prevent
let position = (sampleOffset + i * Math.abs(speed)) % oldData.length;
if (speed < 1) {
position = position * -1;
}
newData[i] = oldData.at(position) || 0;
newData[i] = oldData[i] || 0;
}
}
return newBuffer;
};
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
const convolver = this.createConvolver();
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
convolver.duration = d;
convolver.fade = fade;
convolver.lp = lp;
convolver.dim = dim;
convolver.ir = ir;
convolver.irspeed = irspeed;
convolver.irbegin = irbegin;
if (ir) {
convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin);
convolver.buffer = this.adjustLength(d, ir);
} else {
reverbGen.generateReverb(
{
@@ -51,7 +41,7 @@ if (typeof AudioContext !== 'undefined') {
);
}
};
convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
convolver.generate(duration, fade, lp, dim, ir);
return convolver;
};
}
+48 -64
View File
@@ -78,23 +78,13 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer;
bufferSource.playbackRate.value = playbackRate;
const { s, loopBegin = 0, loopEnd = 1, begin, end, beginSeconds, endSeconds } = hapValue;
let offset = 0;
let endTime = bufferSource.buffer.duration;
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
if (begin != null) {
offset = begin * bufferSource.buffer.duration;
} else if (beginSeconds != null) {
offset = beginSeconds;
}
if (end != null) {
endTime = end * bufferSource.buffer.duration;
} else if (endSeconds != null) {
endTime = endSeconds;
}
const offset = begin * bufferSource.buffer.duration;
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
if (loop) {
@@ -103,7 +93,7 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
const sliceDuration = (endTime - offset) / bufferSource.playbackRate.value;
const sliceDuration = (end - begin) * bufferDuration;
return { bufferSource, offset, bufferDuration, sliceDuration };
};
@@ -206,52 +196,6 @@ function getSamplesPrefixHandler(url) {
return;
}
export async function fetchSampleMap(url) {
// check if custom prefix handler
const handler = getSamplesPrefixHandler(url);
if (handler) {
return handler(url);
}
url = resolveSpecialPaths(url);
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
/**
* Loads a collection of samples to use with `s`
* @example
@@ -273,8 +217,49 @@ export async function fetchSampleMap(url) {
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
if (typeof sampleMap === 'string') {
const [json, base] = await fetchSampleMap(sampleMap);
return samples(json, baseUrl || base, options);
// check if custom prefix handler
const handler = getSamplesPrefixHandler(sampleMap);
if (handler) {
return handler(sampleMap);
}
sampleMap = resolveSpecialPaths(sampleMap);
if (sampleMap.startsWith('github:')) {
sampleMap = githubPath(sampleMap, 'strudel.json');
}
if (sampleMap.startsWith('local:')) {
sampleMap = `http://localhost:5432`;
}
if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:');
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (sampleMap.startsWith('shabda/speech')) {
let [_, path] = sampleMap.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = sampleMap.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
return fetch(sampleMap)
.then((res) => res.json())
.then((json) => samples(json, baseUrl || json._base || base, options))
.catch((error) => {
console.error(error);
throw new Error(`error loading "${sampleMap}"`);
});
}
const { prebake, tag } = options;
processSampleMap(
@@ -357,7 +342,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
out.disconnect();
onended();
};
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (endTime) => {
+60 -203
View File
@@ -7,11 +7,11 @@ This program is free software: you can redistribute it and/or modify it under th
import './feedbackdelay.mjs';
import './reverb.mjs';
import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
import { map } from 'nanostores';
import { logger, errorLogger } from './logger.mjs';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
@@ -28,13 +28,6 @@ export function setMultiChannelOrbits(bool) {
multiChannelOrbits = bool == true;
}
function getModulationShapeInput(val) {
if (typeof val === 'number') {
return val % 5;
}
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
@@ -113,19 +106,6 @@ export async function aliasBank(...args) {
}
}
/**
* Register an alias for a sound.
* @param {string} original - The original sound name
* @param {string} alias - The alias to use for the sound
*/
export function soundAlias(original, alias) {
if (getSound(original) == null) {
logger('soundAlias: original sound not found');
return;
}
soundMap.setKey(alias, getSound(original));
}
export function getSound(s) {
if (typeof s !== 'string') {
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
@@ -209,7 +189,7 @@ export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
export const setDefaultAudioContext = () => {
audioContext = new AudioContext({ latencyHint: 'playback' });
audioContext = new AudioContext();
return audioContext;
};
@@ -225,17 +205,11 @@ export function getAudioContextCurrentTime() {
return getAudioContext().currentTime;
}
let externalWorklets = [];
export function registerWorklet(url) {
externalWorklets.push(url);
}
let workletsLoading;
function loadWorklets() {
if (!workletsLoading) {
const audioCtx = getAudioContext();
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
}
return workletsLoading;
@@ -301,6 +275,7 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
let delays = {};
const maxfeedback = 0.98;
let channelMerger, destinationGain;
@@ -344,44 +319,35 @@ export const panic = () => {
channelMerger == null;
};
function getDelay(orbit, delaytime, delayfeedback, t) {
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
}
delayfeedback = clamp(delayfeedback, 0, 0.98);
let delayNode = orbits[orbit].delayNode;
if (delayNode === undefined) {
if (!delays[orbit]) {
const ac = getAudioContext();
delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
connectToOrbit(delayNode, orbit);
orbits[orbit].delayNode = delayNode;
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToDestination(dly, channels);
delays[orbit] = dly;
}
delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t);
delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t);
return delayNode;
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
return delays[orbit];
}
export function getLfo(audioContext, begin, end, properties = {}) {
const { shape = 0, ...props } = properties;
const { dcoffset = -0.5, depth = 1 } = properties;
const lfoprops = {
export function getLfo(audioContext, time, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', {
frequency: 1,
depth,
skew: 0.5,
depth: 1,
skew: 0,
phaseoffset: 0,
time: begin,
begin,
time,
end,
shape: getModulationShapeInput(shape),
dcoffset,
min: dcoffset * depth,
max: dcoffset * depth + depth,
curve: 1,
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
shape: 1,
dcoffset: -0.5,
...properties,
});
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
@@ -415,95 +381,31 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
}
// type orbit {
// output: GainNode,
// reverbNode: ConvolverNode
// delayNode: FeedbackDelayNode
// }
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].output);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
// Setup output node through which all audio filters prior to hitting
// the destination (and thus allows for global volume automation)
output: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
};
connectToDestination(orbits[orbit].output, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
const onsetArr = [onsettime].flat();
const attackArr = [attacktime].flat();
const depthArr = [duckdepth].flat();
targetArr.forEach((target, idx) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
const onset = onsetArr[idx] ?? onsetArr[0];
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
const depth = depthArr[idx] ?? depthArr[0];
const gainParam = orbits[target].output.gain;
webAudioTimeout(
audioContext,
() => {
const now = audioContext.currentTime;
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
// on browsers which lack that method
const currVal = gainParam.value;
gainParam.cancelScheduledValues(now);
gainParam.setValueAtTime(currVal, now);
const t0 = Math.max(t, now); // guard against now > t
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
},
0,
t - 0.01,
);
});
}
let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
// If no reverb has been created for a given orbit, create one
let reverbNode = orbits[orbit].reverbNode;
if (reverbNode === undefined) {
if (!reverbs[orbit]) {
const ac = getAudioContext();
reverbNode = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
connectToOrbit(reverbNode, orbit);
orbits[orbit].reverbNode = reverbNode;
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, channels);
reverbs[orbit] = reverb;
}
if (
hasChanged(duration, reverbNode.duration) ||
hasChanged(fade, reverbNode.fade) ||
hasChanged(lp, reverbNode.lp) ||
hasChanged(dim, reverbNode.dim) ||
hasChanged(irspeed, reverbNode.irspeed) ||
hasChanged(irbegin, reverbNode.irbegin) ||
reverbNode.ir !== ir
hasChanged(duration, reverbs[orbit].duration) ||
hasChanged(fade, reverbs[orbit].fade) ||
hasChanged(lp, reverbs[orbit].lp) ||
hasChanged(dim, reverbs[orbit].dim) ||
reverbs[orbit].ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
reverbs[orbit].generate(duration, fade, lp, dim, ir);
}
return reverbNode;
return reverbs[orbit];
}
export let analysers = {},
@@ -546,7 +448,8 @@ function effectSend(input, effect, wet) {
}
export function resetGlobalEffects() {
orbits = {};
delays = {};
reverbs = {};
analysers = {};
analysersData = {};
}
@@ -558,10 +461,9 @@ function mapChannelNumbers(channels) {
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
}
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
export const superdough = async (value, t, hapDuration, cps = 0.5) => {
// new: t is always expected to be the absolute target onset time
const ac = getAudioContext();
let { stretch } = value;
if (stretch != null) {
//account for phase vocoder latency
@@ -587,26 +489,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// destructure
let {
tremolo,
tremolosync,
tremolodepth = 1,
tremoloskew,
tremolophase = 0,
tremoloshape,
s = getDefaultValue('s'),
bank,
source,
gain = getDefaultValue('gain'),
postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'),
duckorbit,
duckonset,
duckattack,
duckdepth,
// filters
fanchor = getDefaultValue('fanchor'),
drive = 0.69,
release = 0,
// low pass
cutoff,
lpenv,
@@ -639,9 +530,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
phasercenter,
//
coarse,
crush,
dry,
shape,
shapevol = getDefaultValue('shapevol'),
distort,
@@ -659,8 +548,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
roomdim,
roomsize,
ir,
irspeed,
irbegin,
i = getDefaultValue('i'),
velocity = getDefaultValue('velocity'),
analyze, // analyser wet
@@ -677,13 +564,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
);
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckonset, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain);
@@ -691,11 +572,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
tremolodepth = applyGainCurve(tremolodepth);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
const end = t + hapDuration;
const endWithRelease = end + release;
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
@@ -770,7 +648,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
lprelease,
lpenv,
t,
end,
t + hapDuration,
fanchor,
ftype,
drive,
@@ -794,7 +672,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
hprelease,
hpenv,
t,
end,
t + hapDuration,
fanchor,
);
chain.push(hp());
@@ -805,7 +683,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
if (bandf !== undefined) {
let bp = () =>
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
createFilter(
ac,
'bandpass',
bandf,
bandq,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
t,
t + hapDuration,
fanchor,
);
chain.push(bp());
if (ftype === '24db') {
chain.push(bp());
@@ -823,33 +714,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
if (tremolosync != null) {
tremolo = cps * tremolosync;
}
if (tremolo !== undefined) {
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
const gain = Math.max(1 - tremolodepth, 0);
const amGain = new GainNode(ac, { gain });
const time = cycle / cps;
const lfo = getLfo(ac, t, endWithRelease, {
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
frequency: tremolo,
depth: tremolodepth,
time,
dcoffset: 0,
shape: tremoloshape,
phaseoffset: tremolophase,
min: 0,
max: 1,
curve: 1.5,
});
lfo.connect(amGain.gain);
chain.push(amGain);
}
compressorThreshold !== undefined &&
chain.push(
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
@@ -863,18 +727,19 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// phaser
if (phaser !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep);
chain.push(phaserFX);
}
// last gain
const post = new GainNode(ac, { gain: postgain });
chain.push(post);
connectToDestination(post, channels);
// delay
let delaySend;
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
delaySend = effectSend(post, delayNode, delay);
audioNodes.push(delaySend);
}
@@ -892,7 +757,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}
@@ -904,14 +769,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
} else {
connectToOrbit(post, orbit);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
+8 -85
View File
@@ -9,13 +9,12 @@ import {
getVibratoOscillator,
webAudioTimeout,
getWorklet,
noises,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value, defaultNote = 36) => {
const getFrequencyFromValue = (value) => {
let { note, freq } = value;
note = note || defaultNote;
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
@@ -41,17 +40,7 @@ const waveformAliases = [
['saw', 'sawtooth'],
['sin', 'sine'],
];
function makeSaturationCurve(amount, n_samples) {
const k = typeof amount === 'number' ? amount : 50;
const curve = new Float32Array(n_samples);
for (let i = 0; i < n_samples; i++) {
const x = (i * 2) / n_samples - 1;
curve[i] = Math.tanh(x * k);
}
return curve;
}
const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() {
[...waveforms].forEach((s) => {
@@ -95,75 +84,6 @@ export function registerSynthSounds() {
{ type: 'synth', prebake: true },
);
});
registerSound(
'sbd',
(t, value, onended) => {
const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value;
const ctx = getAudioContext();
const attackhold = 0.02;
const noiselvl = 1.2;
const noisedecay = 0.025;
const mixGain = 1;
const o = ctx.createOscillator();
o.type = 'triangle';
o.frequency.value = getFrequencyFromValue(value, 29);
o.detune.setValueAtTime(penv * 100, 0);
o.detune.setValueAtTime(penv * 100, t);
o.detune.exponentialRampToValueAtTime(0.001, t + pdecay);
const g = gainNode(1);
g.gain.setValueAtTime(1, t + attackhold);
g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay);
o.start(t);
const noise = getNoiseOscillator('brown', t, 2);
const noiseGain = gainNode(1);
noiseGain.gain.setValueAtTime(noiselvl, t);
noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay);
const sat = new WaveShaperNode(ctx);
// tri to sine diode shaper emulation
sat.curve = makeSaturationCurve(2, ctx.sampleRate);
const mix = gainNode(mixGain);
o.onended = () => {
o.disconnect();
g.disconnect();
sat.disconnect();
noise.node.disconnect();
noiseGain.disconnect();
mix.disconnect();
onended();
};
const node = o.connect(sat).connect(g).connect(mix);
noise.node.connect(noiseGain).connect(mix);
const holdEnd = t + decay;
let end = holdEnd + 0.01;
if (clip != null) {
end = Math.min(t + clip * duration, end);
}
// prevent clicking
mix.gain.setValueAtTime(mixGain, end - 0.01);
mix.gain.linearRampToValueAtTime(0, end);
o.stop(end);
noise.stop(end);
return {
node,
stop: (endTime) => {
o.stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
);
registerSound(
'supersaw',
(begin, value, onended) => {
@@ -201,7 +121,10 @@ export function registerSynthSounds() {
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
// const fm = applyFM(o.parameters.get('frequency'), value, begin);
// https://codeberg.org/uzu/strudel/issues/1428
// if you think about re-enabling this, please test with fm > 1 first
// it's like 10x gain, so it's really dangerous
let envGain = gainNode(1);
envGain = o.connect(envGain);
@@ -213,7 +136,7 @@ export function registerSynthSounds() {
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
// fm?.stop();
vibratoOscillator?.stop();
},
begin,
+1 -5
View File
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
if (!pc) {
return [];
}
@@ -72,7 +72,3 @@ export const getSoundIndex = (n, numSounds) => {
export function cycleToSeconds(cycle, cps) {
return cycle / cps;
}
export function secondsToCycle(t, cps) {
return t * cps;
}
+26 -39
View File
@@ -8,28 +8,18 @@ import FFT from './fft.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const _mod = (n, m) => ((n % m) + m) % m;
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
if (phase >= maxPhase) {
phase -= maxPhase;
} else if (phase < 0) {
phase += maxPhase;
}
return phase;
}
const blockSize = 128;
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
function polyBlep(phase, dt) {
dt = Math.min(dt, 1 - dt);
// Start of cycle
// 0 <= phase < 1
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
// End of cycle
// -1 < phase < 0
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
@@ -41,7 +31,7 @@ function polyBlep(phase, dt) {
return 0;
}
}
// The order is important for dough integration
const waveshapes = {
tri(phase, skew = 0.5) {
const x = 1 - skew;
@@ -91,12 +81,10 @@ function getParamValue(block, param) {
}
return param[0];
}
const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0 },
{ name: 'time', defaultValue: 0 },
{ name: 'end', defaultValue: 0 },
{ name: 'frequency', defaultValue: 0.5 },
@@ -104,10 +92,7 @@ class LFOProcessor extends AudioWorkletProcessor {
{ name: 'depth', defaultValue: 1 },
{ name: 'phaseoffset', defaultValue: 0 },
{ name: 'shape', defaultValue: 0 },
{ name: 'curve', defaultValue: 1 },
{ name: 'dcoffset', defaultValue: 0 },
{ name: 'min', defaultValue: 0 },
{ name: 'max', defaultValue: 1 },
];
}
@@ -124,13 +109,10 @@ class LFOProcessor extends AudioWorkletProcessor {
}
process(inputs, outputs, parameters) {
const begin = parameters['begin'][0];
// eslint-disable-next-line no-undef
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= begin) {
return true;
}
const output = outputs[0];
const frequency = parameters['frequency'][0];
@@ -140,11 +122,7 @@ class LFOProcessor extends AudioWorkletProcessor {
const skew = parameters['skew'][0];
const phaseoffset = parameters['phaseoffset'][0];
const curve = parameters['curve'][0];
const dcoffset = parameters['dcoffset'][0];
const min = parameters['min'][0];
const max = parameters['max'][0];
const shape = waveShapeNames[parameters['shape'][0]];
const blockSize = output[0].length ?? 0;
@@ -152,12 +130,12 @@ class LFOProcessor extends AudioWorkletProcessor {
if (this.phase == null) {
this.phase = _mod(time * frequency + phaseoffset, 1);
}
// eslint-disable-next-line no-undef
const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < output.length; i++) {
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
modval = Math.pow(modval, curve);
output[i][n] = clamp(modval, min, max);
const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
output[i][n] = modval;
}
this.incrementPhase(dt);
}
@@ -313,6 +291,7 @@ class LadderProcessor extends AudioWorkletProcessor {
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
let cutoff = parameters.frequency[0];
// eslint-disable-next-line no-undef
cutoff = (cutoff * 2 * _PI) / sampleRate;
cutoff = cutoff > 1 ? 1 : cutoff;
@@ -445,13 +424,18 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
];
}
process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) {
return true;
}
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0];
const voices = params.voices[0];
@@ -462,6 +446,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
@@ -469,21 +456,21 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2;
gainR = gain1;
}
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) {
// Main detuning
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
// Individual voice detuning
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = _mod(freq / sampleRate, 1);
this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR;
this.phase[n] = wrapPhase(this.phase[n] + dt);
this.phase[n] += dt;
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
}
}
}
return true;
@@ -492,7 +479,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048;
function genHannWindow(length) {
-1
View File
@@ -1 +0,0 @@
pattern.wav
-3
View File
@@ -1,3 +0,0 @@
# supradough
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
-123
View File
@@ -1,123 +0,0 @@
// this is a poc of how a pattern can be rendered as a wav file using node
// run via: node dough-export.mjs
import fs from 'node:fs';
import WavEncoder from 'wav-encoder';
import { evalScope } from '@strudel/core';
import { miniAllStrings } from '@strudel/mini';
import { Dough } from './dough.mjs';
await evalScope(
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/tonal'),
// import('@strudel/tonal'),
);
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
let sampleRate = 48000,
cps = 0.4;
/* await doughsamples('github:eddyflux/crate');
await doughsamples('github:eddyflux/wax'); */
let pat = note('c,eb,g,<bb c4 d4 eb4>')
.s('sine')
.press()
.add(note(24))
.fmi(3)
.fmh(5.01)
.dec(0.4)
.delay('.6:<.12 .22>:.8')
.jux(press)
.rarely(add(note('12')))
.lpf(400)
.lpq(0.2)
.lpd(0.4)
.lpenv(3)
.fmdecay(0.4)
.fmenv(1)
.postgain(0.6)
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
.stack(
s('[- white@3]*2')
.dec(0.4)
.hpf('<2000!3 <4000 8000>>*4')
.hpq(0.6)
.ply('<1 2>*4')
.postgain(0.5)
.delay(0.5)
.jux(rev)
.lpf(5000),
)
.stack(
note('<c2 - [- f1] ->*2')
.s('square')
.lpf(sine.range(100, 300).slow(4))
.lpe(1)
.segment(8)
.lpd(0.3)
.lpq(0.2)
.dec(0.2)
.speed('<1 2>')
.ply('<1 2>')
.postgain(1),
)
.stack(
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
.voicing()
.s('<sine>')
.clip(1)
.rel(0.4)
.vib('4:.2')
.gain(0.7)
.hpf(1200)
.fm(0.5)
.att(1)
.lpa(0.5)
.lpf(200)
.lpenv(4)
.chorus(0.8),
)
.slow(1 / cps);
let cycles = 30;
let seconds = cycles + 1; // 1s release tail
const haps = pat.queryArc(0, cycles);
const dough = new Dough(sampleRate);
console.log('spawn voices...');
haps.forEach((hap) => {
hap.value._begin = Number(hap.whole.begin);
hap.value._duration = hap.duration /* / cps */;
dough.scheduleSpawn(hap.value);
});
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
let t = performance.now();
while (dough.t <= buffers[0].length) {
dough.update();
buffers[0][dough.t] = dough.out[0];
buffers[1][dough.t] = dough.out[1];
if (dough.t % sampleRate === 0) {
process.stdout.write('.');
}
}
const took = (performance.now() - t) / 1000;
const load = (took / seconds) * 100;
const speed = (seconds / took).toFixed(2);
console.log('');
console.log(`done!
rendered ${seconds}s in ${took.toFixed(2)}s
speed: ${speed}x
load: ${load.toFixed(2)}%`);
const patternAudio = {
sampleRate,
channelData: buffers,
};
WavEncoder.encode(patternAudio).then((buffer) => {
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
});
-39
View File
@@ -1,39 +0,0 @@
import { Dough } from './dough.mjs';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
class DoughProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.dough = new Dough(sampleRate, currentTime);
this.port.onmessage = (event) => {
if (event.data.spawn) {
this.dough.scheduleSpawn(event.data.spawn);
} else if (event.data.sample) {
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
} else if (event.data.samples) {
event.data.samples.forEach(([name, channels, sampleRate]) => {
this.dough.loadSample(name, channels, sampleRate);
});
} else {
console.log('unrecognized event type', event.data);
}
};
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
const output = outputs[0];
for (let i = 0; i < output[0].length; i++) {
this.dough.update();
for (let c = 0; c < output.length; c++) {
//prevent speaker blowout via clipping if threshold exceeds
output[c][i] = clamp(this.dough.out[c], -1, 1);
}
}
return true; // keep the audio processing going
}
}
registerProcessor('dough-processor', DoughProcessor);
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
export * from './dough.mjs';
export const workletUrl = _workletUrl;
-37
View File
@@ -1,37 +0,0 @@
{
"name": "supradough",
"version": "1.2.3",
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"dependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/tonal",
"version": "1.2.4",
"version": "1.2.2",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"publishConfig": {
+69 -119
View File
@@ -7,130 +7,80 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
import { pure, n, seq, note } from '@strudel/core';
import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs';
describe('tonal', () => {
describe('scaleTranspose', () => {
it('transposes notes by scale degrees', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
});
describe('scale', () => {
it('converts plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (no tonic)', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts n values (explicit mini notation)', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('converts decorated n values', () => {
expect(
n(seq('0b', '1#', '-2', '3##', '4bb'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
});
it('produces silence for mixed sharps and flats', () => {
expect(
n(seq('0b#', '1#b', '2#b#'))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual([]);
});
it('snaps notes (upwards) to scale', () => {
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
expect(
note(seq(inputNotes))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('snaps notes to the correct octave', () => {
const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
expect(
note(seq(inputNotes))
.scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('handles scale names provided with colons', () => {
const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
expect(
note(seq(inputNotes))
.scale('F#:pentatonic') // F#, G#, A#, C#, and D#
.firstCycleValues.map((h) => h.note),
).toEqual(expectedNotes);
});
it('scale with plain values', () => {
expect(
seq(0, 1, 2)
.scale('C major')
.note()
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
describe('transpose', () => {
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
it('scale with n values', () => {
expect(
n(seq(0, 1, 2))
.scale('C major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with colon', () => {
expect(
n(seq(0, 1, 2))
.scale('C:major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale without tonic', () => {
expect(
n(seq(0, 1, 2))
.scale('major')
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('scale with mininotation colon', () => {
expect(
n(seq(0, 1, 2))
.scale(mini('C:major'))
.firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']);
});
it('transposes note numbers with interval numbers', () => {
expect(
note(seq(40, 40, 40))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(seq(40, 40, 40))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note(seq('c', 'c', 'c'))
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
});
+69 -122
View File
@@ -6,28 +6,19 @@ This program is free software: you can redistribute it and/or modify it under th
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
import { noteToMidi } from '../core/util.mjs';
import { stepInNamedScale } from './tonleiter.mjs';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
function getScale(scaleName) {
scaleName = scaleName.replaceAll(':', ' ');
const scale = Scale.get(scaleName);
const { tonic, empty } = scale;
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
throw new Error(
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
);
} else if (empty) {
throw new Error(`Invalid scale name "${scaleName}"`);
}
return scale;
}
function scaleStep(step, scale) {
scale = scale.replaceAll(':', ' ');
step = Math.ceil(step);
let { intervals, tonic } = getScale(scale);
let { intervals, tonic, empty } = Scale.get(scale);
if ((empty && isNote(scale)) || (empty && !tonic)) {
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
} else if (empty) {
throw new Error(`invalid scale "${scale}"`);
}
tonic = tonic || 'C';
const { pc, oct = 3 } = Note.get(tonic);
const octaveOffset = Math.floor(step / intervals.length);
@@ -39,7 +30,8 @@ function scaleStep(step, scale) {
// transpose note inside scale by offset steps
// function scaleOffset(scale: string, offset: number, note: string) {
function scaleOffset(scale, offset, note) {
let { notes } = getScale(scale);
let [tonic, scaleName] = Scale.tokenize(scale);
let { notes } = Scale.get(`${tonic} ${scaleName}`);
notes = notes.map((note) => Note.get(note).pc); // use only pc!
offset = Number(offset);
if (isNaN(offset)) {
@@ -96,14 +88,13 @@ function scaleOffset(scale, offset, note) {
* @returns Pattern
* @memberof Pattern
* @name transpose
* @synonyms trans
* @example
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
* @example
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
*/
export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) {
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
return pat.withHap((hap) => {
const note = hap.value.note ?? hap.value;
if (typeof note === 'number') {
@@ -128,7 +119,10 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones)
: String(intervalOrSemitones);
const targetNote = Note.transpose(note, interval);
// TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
// TODO: check if this is still relevant..
const targetNote = Note.simplify(Note.transpose(note, interval));
if (typeof hap.value === 'object') {
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
}
@@ -148,7 +142,6 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
* @name scaleTranspose
* @param {offset} offset number of steps inside the scale
* @returns Pattern
* @synonyms scaleTrans, strans
* @example
* "-8 [2,4,6]"
* .scale('C4 bebop major')
@@ -156,79 +149,25 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
* .note()
*/
export const { scaleTranspose, scaleTrans, strans } = register(
['scaleTranspose', 'scaleTrans', 'strans'],
function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({
...hap.value,
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
}));
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
},
);
// Converts a step value, which is a number optionally decorated with sharps and flats,
// to a number and an `offset` number of semitones
function _convertStepToNumberAndOffset(step) {
let asNumber = Number(step);
let offset = 0;
if (isNaN(asNumber)) {
step = String(step);
// Check to see if the step matches the expected format:
// - A number (possibly negative)
// - Some number of sharps or flats (but not both)
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
if (!match) {
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
return pat.withHap((hap) => {
if (!hap.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
asNumber = Number(match[1]);
// These decorations will determine the semitone offset based on the number of
// sharps or flats
const decorations = match[2] || '';
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
}
return [asNumber, offset];
}
let scaleToMidisAndNotes = {};
// Finds the nearest scale note to `note`
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
if (scaleToMidisAndNotes[scaleName] === undefined) {
const { intervals, tonic } = getScale(scaleName);
const { pc } = Note.get(tonic);
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
const sMidi = sNotes.map(noteToMidi);
// Cache
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
}
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
const rootMidi = scaleMidis[0];
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
const noteMatch = scaleNotes[noteIdx];
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
}
if (typeof hap.value === 'object')
return hap.withValue(() => ({
...hap.value,
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
}));
if (typeof hap.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
});
});
/**
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
*
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
*
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
*
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
*
@@ -247,12 +186,6 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
* n(rand.range(0,12).segment(8))
* .scale("C:ritusen")
* .s("piano")
* @example
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
* .scale("C:<major minor>/2")
* .s("piano")
* @example
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
*/
export const scale = register(
@@ -266,35 +199,49 @@ export const scale = register(
pat
.fmap((value) => {
const isObject = typeof value === 'object';
// The case where the note has been defined via `n` or `pure`
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
const step = isObject ? (value.n ?? value.value) : value;
let step = isObject ? value.n : value;
if (isObject) {
delete value.n; // remove n so it won't cause trouble
if (isNote(step)) {
// legacy..
return pure(step);
}
try {
const [number, offset] = _convertStepToNumberAndOffset(step);
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(number, scale, value.anchor);
} else {
note = scaleStep(number, scale);
}
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
}
if (isNote(step)) {
// legacy..
return pure(step);
}
let asNumber = Number(step);
let semitones = 0;
if (isNaN(asNumber)) {
step = String(step);
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
logger(
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
'error',
);
return silence;
}
return value;
const isharp = step.indexOf('#');
if (isharp >= 0) {
asNumber = Number(step.substring(0, isharp));
semitones = step.length - isharp;
} else {
const iflat = step.indexOf('b');
asNumber = Number(step.substring(0, iflat));
semitones = iflat - step.length;
}
}
// The case where the note has been defined via `note`
else {
const note = _getNearestScaleNote(scale, value.note);
return pure(isObject ? { ...value, note } : note);
try {
let note;
if (isObject && value.anchor) {
note = stepInNamedScale(asNumber, scale, value.anchor);
} else {
note = scaleStep(asNumber, scale);
}
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
value = pure(isObject ? { ...value, note } : note);
} catch (err) {
logger(`[tonal] ${err.message}`, 'error');
value = silence;
}
return value;
})
.outerJoin()
// legacy:
+4 -3
View File
@@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
let scaleSteps = {}; // [scaleName]: semitones[]
export function stepInNamedScale(step, scale, anchor, preferHigher) {
const [root, scaleName] = Scale.tokenize(scale);
let [root, scaleName] = Scale.tokenize(scale);
const rootMidi = x2midi(root);
const rootChroma = midi2chroma(rootMidi);
if (!scaleSteps[scaleName]) {
const { intervals } = Scale.get(`C ${scaleName}`);
let { intervals } = Scale.get(`C ${scaleName}`);
// cache result
scaleSteps[scaleName] = intervals.map(step2semitones);
}
@@ -222,7 +222,6 @@ export const Note = {
};
// TODO: support octave numbers
// Example: Note("Bb3").transpose("c3")
export function transpose(note, step) {
// example: E, 3
const stepNumber = Step.tokenize(step)[1]; // 3
@@ -236,3 +235,5 @@ export function transpose(note, step) {
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
return [targetNote, offsetAccidentals].join('');
}
//Note("Bb3").transpose("c3")
+1 -1
View File
@@ -1,4 +1,4 @@
import { evaluate as _evaluate } from '@strudel/core';
import { evaluate as _evaluate } from '@strudel/cyclist';
import { transpiler } from './transpiler.mjs';
export * from './transpiler.mjs';
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/transpiler",
"version": "1.2.4",
"version": "1.2.2",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs",
"type": "module",
@@ -31,6 +31,7 @@
"homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/cyclist": "workspace:*",
"@strudel/mini": "workspace:*",
"acorn": "^8.14.0",
"escodegen": "^2.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/web",
"version": "1.2.5",
"version": "1.2.3",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs",
"publishConfig": {
-1
View File
@@ -7,5 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from './spectrum.mjs';
export * from './supradough.mjs';
export * from 'superdough';
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/webaudio",
"version": "1.2.5",
"version": "1.2.3",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -35,8 +35,7 @@
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*",
"supradough": "workspace:*"
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
-130
View File
@@ -1,130 +0,0 @@
import { Pattern } from '@strudel/core';
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
let doughWorklet;
function initDoughWorklet() {
const ac = getAudioContext();
doughWorklet = getWorklet(
ac,
'dough-processor',
{},
{
outputChannelCount: [2],
},
);
connectToDestination(doughWorklet); // channels?
}
const soundMap = new Map();
const loadedSounds = new Map();
Pattern.prototype.supradough = function () {
return this.onTrigger((hap, __, cps, begin) => {
hap.value._begin = begin;
hap.value._duration = hap.duration / cps;
!doughWorklet && initDoughWorklet();
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
const n = hap.value.n ?? 0;
const soundKey = `${s}:${n}`;
if (soundMap.has(s)) {
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
}
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
const urls = soundMap.get(s);
const url = urls[n % urls.length];
console.log(`load ${soundKey} from ${url}`);
const loadSample = fetchSample(url);
loadedSounds.set(soundKey, loadSample);
loadSample.then(({ channels, sampleRate }) =>
doughWorklet.port.postMessage({
sample: soundKey,
channels,
sampleRate,
}),
);
}
doughWorklet.port.postMessage({ spawn: hap.value });
}, 1);
};
function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export async function fetchSampleMap(url) {
if (url.startsWith('github:')) {
url = githubPath(url, 'strudel.json');
}
if (url.startsWith('local:')) {
url = `http://localhost:5432`;
}
if (url.startsWith('shabda:')) {
let [_, path] = url.split('shabda:');
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
}
if (url.startsWith('shabda/speech')) {
let [_, path] = url.split('shabda/speech');
path = path.startsWith('/') ? path.substring(1) : path;
let [params, words] = path.split(':');
let gender = 'f';
let language = 'en-GB';
if (params) {
[language, gender] = params.split('/');
}
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
}
if (typeof fetch !== 'function') {
// not a browser
return;
}
const base = url.split('/').slice(0, -1).join('/');
if (typeof fetch === 'undefined') {
// skip fetch when in node / testing
return;
}
const json = await fetch(url)
.then((res) => res.json())
.catch((error) => {
console.error(error);
throw new Error(`error loading "${url}"`);
});
return [json, json._base || base];
}
// for some reason, only piano and flute work.. is it because mp3??
async function fetchSample(url) {
const buffer = await fetch(url)
.then((res) => res.arrayBuffer())
.then((buf) => getAudioContext().decodeAudioData(buf));
let channels = [];
for (let i = 0; i < buffer.numberOfChannels; i++) {
channels.push(buffer.getChannelData(i));
}
return { channels, sampleRate: buffer.sampleRate };
}
export async function doughsamples(sampleMap, baseUrl) {
if (typeof sampleMap === 'string') {
const [json, base] = await fetchSampleMap(sampleMap);
// console.log('json', json, 'base', base);
return doughsamples(json, base);
}
Object.entries(sampleMap).map(async ([key, urls]) => {
if (key !== '_base') {
urls = urls.map((url) => baseUrl + url);
// console.log('set', key, urls);
soundMap.set(key, urls);
}
});
}
+3 -8
View File
@@ -5,12 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel/core';
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
import './supradough.mjs';
import { workletUrl } from 'supradough';
registerWorklet(workletUrl);
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
const { Pattern, logger, repl } = strudel;
setLogger(logger);
@@ -22,8 +17,8 @@ const hap2value = (hap) => {
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
// TODO: refactor output callbacks to eliminate deadline
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => {
return superdough(hap2value(hap), t, hapDuration, cps);
};
export function webaudioRepl(options = {}) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel/xen",
"version": "1.2.4",
"version": "1.2.2",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"type": "module",
+4 -9
View File
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
/* Load a new scale */
Tune.prototype.loadScale = function(scale){
Tune.prototype.loadScale = function(name){
/* load the scale */
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
var freqs = TuningList[name].frequencies
this.scale = []
for (var i=0;i<freqs.length-1;i++) {
this.scale.push(freqs[i]/freqs[0])
@@ -207,13 +207,8 @@ Tune.prototype.search = function(letters) {
return possible
}
function isArrayOfNumbers(arg) {
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
}
/* allow an array of values too */
Tune.prototype.isValidScale = function(scale) {
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
Tune.prototype.isValidScale = function(name) {
return !!TuningList[name];
}
/* Return a collection of notes as an array */
+51 -32
View File
@@ -11,6 +11,9 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:packages/core
'@strudel/cyclist':
specifier: workspace:*
version: link:packages/cyclist
'@strudel/mini':
specifier: workspace:*
version: link:packages/mini
@@ -201,14 +204,17 @@ importers:
specifier: ^6.1.0
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vim':
specifier: ^6.3.0
version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
specifier: ^6.2.1
version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@replit/codemirror-vscode-keymap':
specifier: ^6.0.2
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
'@strudel/core':
specifier: workspace:*
version: link:../core
'@strudel/cyclist':
specifier: workspace:*
version: link:../cyclist
'@strudel/draw':
specifier: workspace:*
version: link:../draw
@@ -252,6 +258,19 @@ importers:
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)
packages/cyclist:
dependencies:
'@strudel/core':
specifier: '*'
version: 1.2.2
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/desktopbridge:
dependencies:
'@strudel/core':
@@ -266,6 +285,9 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:../core
'@strudel/cyclist':
specifier: workspace:*
version: link:../cyclist
devDependencies:
vite:
specifier: ^6.0.11
@@ -301,6 +323,9 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:../core
'@strudel/cyclist':
specifier: workspace:*
version: link:../cyclist
'@strudel/draw':
specifier: workspace:*
version: link:../draw
@@ -361,6 +386,9 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:../core
'@strudel/cyclist':
specifier: workspace:*
version: link:../cyclist
'@strudel/transpiler':
specifier: workspace:*
version: link:../transpiler
@@ -517,18 +545,6 @@ importers:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
packages/supradough:
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)
vite-plugin-bundle-audioworklet:
specifier: workspace:*
version: link:../vite-plugin-bundle-audioworklet
wav-encoder:
specifier: ^1.3.0
version: 1.3.0
packages/tidal:
dependencies:
'@strudel/core':
@@ -572,6 +588,9 @@ importers:
'@strudel/core':
specifier: workspace:*
version: link:../core
'@strudel/cyclist':
specifier: workspace:*
version: link:../cyclist
'@strudel/mini':
specifier: workspace:*
version: link:../mini
@@ -637,9 +656,6 @@ importers:
superdough:
specifier: workspace:*
version: link:../superdough
supradough:
specifier: workspace:*
version: link:../supradough
devDependencies:
vite:
specifier: ^6.0.11
@@ -711,6 +727,9 @@ importers:
'@strudel/csound':
specifier: workspace:*
version: link:../packages/csound
'@strudel/cyclist':
specifier: workspace:*
version: link:../packages/cyclist
'@strudel/desktopbridge':
specifier: workspace:*
version: link:../packages/desktopbridge
@@ -2247,14 +2266,14 @@ packages:
'@codemirror/state': ^6.0.1
'@codemirror/view': ^6.3.0
'@replit/codemirror-vim@6.3.0':
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
'@replit/codemirror-vim@6.2.1':
resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==}
peerDependencies:
'@codemirror/commands': 6.x.x
'@codemirror/language': 6.x.x
'@codemirror/search': 6.x.x
'@codemirror/state': 6.x.x
'@codemirror/view': 6.x.x
'@codemirror/commands': ^6.0.0
'@codemirror/language': ^6.1.0
'@codemirror/search': ^6.2.0
'@codemirror/state': ^6.0.1
'@codemirror/view': ^6.0.3
'@replit/codemirror-vscode-keymap@6.0.2':
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
@@ -2468,6 +2487,9 @@ packages:
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@strudel/core@1.2.2':
resolution: {integrity: sha512-rPFAwV7Emz85HyKwfVVn+2cNOHCGBbWw6XImv0elnzRiXxKWMdmZfuSL3xgpheEg9WUgacgmzJL9kbvfCitGtA==}
'@supabase/auth-js@2.67.3':
resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==}
@@ -5729,7 +5751,6 @@ packages:
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch-native@1.6.6:
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
@@ -6819,7 +6840,6 @@ packages:
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
@@ -7552,9 +7572,6 @@ packages:
walk-up-path@3.0.1:
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
wav-encoder@1.3.0:
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
wav@1.0.2:
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
@@ -9616,7 +9633,7 @@ snapshots:
'@codemirror/state': 6.5.1
'@codemirror/view': 6.36.2
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
'@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
dependencies:
'@codemirror/commands': 6.8.0
'@codemirror/language': 6.10.8
@@ -9829,6 +9846,10 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
'@strudel/core@1.2.2':
dependencies:
fraction.js: 5.2.1
'@supabase/auth-js@2.67.3':
dependencies:
'@supabase/node-fetch': 2.6.15
@@ -15976,8 +15997,6 @@ snapshots:
walk-up-path@3.0.1: {}
wav-encoder@1.3.0: {}
wav@1.0.2:
dependencies:
buffer-alloc: 1.2.0
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@
// import * as tunes from './tunes.mjs';
import { evaluate } from '@strudel/transpiler';
import { evalScope } from '@strudel/core';
import { evalScope } from '@strudel/cyclist';
import * as strudel from '@strudel/core';
import * as webaudio from '@strudel/webaudio';
// import gist from '@strudel/core/gist.js';
+3 -1
View File
@@ -8,7 +8,8 @@
"start": "astro dev",
"build": "astro build",
"preview": "astro preview --port 3009 --host 0.0.0.0",
"astro": "astro"
"astro": "astro",
"postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public"
},
"dependencies": {
"@algolia/client-search": "^5.20.0",
@@ -25,6 +26,7 @@
"@nanostores/react": "^0.8.4",
"@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*",
"@strudel/cyclist": "workspace:*",
"@strudel/csound": "workspace:*",
"@strudel/desktopbridge": "workspace:*",
"@strudel/draw": "workspace:*",

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