Compare commits

..

11 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 42d7781d44 Merge branch 'main' into ziglets 2024-02-25 21:49:18 -05:00
Felix Roos a746bf5461 add assemblyscript version 2024-01-10 22:08:41 +01:00
Felix Roos 700f0be0ba optimize c output 2024-01-10 21:49:04 +01:00
Felix Roos cb59d632a8 add c info to readme 2024-01-10 21:45:21 +01:00
Felix Roos 9fd86458e6 add csaw 2024-01-10 21:44:16 +01:00
Felix Roos 3cd49065c8 prettierignore superdough-wasm 2024-01-10 21:32:20 +01:00
Felix Roos a7726e2334 add rust + shrink zig wasm file 2024-01-10 21:31:22 +01:00
Felix Roos 26793aec91 frequency control (bad) 2024-01-10 18:27:40 +01:00
Felix Roos 7c4ed7e05f cleanup 2024-01-10 18:14:49 +01:00
Felix Roos 93cfe9802c zig dsp poc 2024-01-10 18:11:05 +01:00
Felix Roos 0106a129c9 add minimal zig wasm testing ground 2024-01-10 17:04:10 +01:00
253 changed files with 13177 additions and 15953 deletions
-3
View File
@@ -127,6 +127,3 @@ fabric.properties
.idea/caches/build_file_checksums.ser .idea/caches/build_file_checksums.ser
# END JetBrains -> BEGIN JetBrains # END JetBrains -> BEGIN JetBrains
samples/*
!samples/README.md
+1
View File
@@ -10,4 +10,5 @@ paper
pnpm-lock.yaml pnpm-lock.yaml
pnpm-workspace.yaml pnpm-workspace.yaml
**/dev-dist **/dev-dist
superdough-wasm
website/.astro website/.astro
+14 -4
View File
@@ -15,15 +15,25 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally: After cloning the project, you can run the REPL locally:
```bash ```bash
pnpm i pnpm run setup
pnpm dev pnpm run repl
``` ```
## Using Strudel In Your Project ## Using Strudel In Your Project
This project is organized into many [packages](./packages), which are also available on [npm](https://www.npmjs.com/search?q=%40strudel). There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start). - [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`transpiler`](./packages/transpiler): user code transpiler
- [`webaudio`](./packages/webaudio): webaudio output
- [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions
- ... [and there are more](./packages/)
Click on the package names to find out more about each one.
## Contributing ## Contributing
+4 -3
View File
@@ -1,9 +1,10 @@
<!doctype html> <!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="play">play</button> <button id="play">play</button>
<button id="stop">stop</button> <button id="stop">stop</button>
<script> <script type="module">
strudel.initStrudel(); import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel();
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)')); document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
document.getElementById('play').addEventListener('stop', () => hush()); document.getElementById('play').addEventListener('stop', () => hush());
</script> </script>
@@ -1,10 +1,10 @@
<!doctype html> <!doctype html>
<script src="https://unpkg.com/@strudel/web@1.0.3"></script>
<button id="a">A</button> <button id="a">A</button>
<button id="b">B</button> <button id="b">B</button>
<button id="c">C</button> <button id="c">C</button>
<button id="stop">stop</button> <button id="stop">stop</button>
<script> <script type="module">
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
initStrudel({ initStrudel({
prebake: () => samples('github:tidalcycles/dirt-samples'), prebake: () => samples('github:tidalcycles/dirt-samples'),
}); });
+1 -2
View File
@@ -16,7 +16,6 @@
</div> </div>
<div id="output"></div> <div id="output"></div>
<script type="module"> <script type="module">
// TODO: refactor to use newer version without controls import
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0'; import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0'; import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0'; import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
@@ -54,7 +53,7 @@
function getTune() { function getTune() {
return `samples('github:tidalcycles/dirt-samples') return `samples('github:tidalcycles/dirt-samples')
setcps(1);
stack( stack(
// amen // amen
n("0 1 2 3 4 5 6 7") n("0 1 2 3 4 5 6 7")
+2 -1
View File
@@ -1,6 +1,6 @@
import { StrudelMirror } from '@strudel/codemirror'; import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes'; import { funk42 } from './tunes';
import { drawPianoroll, evalScope } from '@strudel/core'; import { drawPianoroll, evalScope, controls } from '@strudel/core';
import './style.css'; import './style.css';
import { initAudioOnFirstClick } from '@strudel/webaudio'; import { initAudioOnFirstClick } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel/transpiler';
@@ -25,6 +25,7 @@ const editor = new StrudelMirror({
prebake: async () => { prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..) initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope( const loadModules = evalScope(
controls,
import('@strudel/core'), import('@strudel/core'),
import('@strudel/mini'), import('@strudel/mini'),
import('@strudel/tonal'), import('@strudel/tonal'),
+9 -4
View File
@@ -1,5 +1,5 @@
import { repl, evalScope } from '@strudel/core'; import { controls, repl, evalScope } from '@strudel/core';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick, registerSynthSounds } from '@strudel/webaudio'; import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel/transpiler';
import tune from './tune.mjs'; import tune from './tune.mjs';
@@ -7,9 +7,14 @@ const ctx = getAudioContext();
const input = document.getElementById('text'); const input = document.getElementById('text');
input.innerHTML = tune; input.innerHTML = tune;
initAudioOnFirstClick(); initAudioOnFirstClick();
registerSynthSounds();
evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal')); evalScope(
controls,
import('@strudel/core'),
import('@strudel/mini'),
import('@strudel/webaudio'),
import('@strudel/tonal'),
);
const { evaluate } = repl({ const { evaluate } = repl({
defaultOutput: webaudioOutput, defaultOutput: webaudioOutput,
+1 -1
View File
@@ -1,5 +1,5 @@
export default `samples('github:tidalcycles/dirt-samples') export default `samples('github:tidalcycles/dirt-samples')
setcps(1)
stack( stack(
// amen // amen
n("0 1 2 3 4 5 6 7") n("0 1 2 3 4 5 6 7")
+2 -7
View File
@@ -1,20 +1,15 @@
// this barrel export is currently only used to find undocumented exports // this barrel export is currently only used to find undocumented exports
export * from './packages/codemirror/index.mjs';
export * from './packages/core/index.mjs'; export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs'; export * from './packages/csound/index.mjs';
export * from './packages/desktopbridge/index.mjs';
export * from './packages/draw/index.mjs';
export * from './packages/embed/index.mjs'; export * from './packages/embed/index.mjs';
export * from './packages/hydra/index.mjs'; export * from './packages/desktopbridge/index.mjs';
export * from './packages/midi/index.mjs'; export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs'; export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs'; export * from './packages/osc/index.mjs';
export * from './packages/repl/index.mjs'; export * from './packages/react/index.mjs';
export * from './packages/serial/index.mjs'; export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs'; export * from './packages/soundfonts/index.mjs';
export * from './packages/superdough/index.mjs';
export * from './packages/tonal/index.mjs'; export * from './packages/tonal/index.mjs';
export * from './packages/transpiler/index.mjs'; export * from './packages/transpiler/index.mjs';
export * from './packages/web/index.mjs';
export * from './packages/webaudio/index.mjs'; export * from './packages/webaudio/index.mjs';
export * from './packages/xen/index.mjs'; export * from './packages/xen/index.mjs';
+2 -3
View File
@@ -25,7 +25,6 @@
"format-check": "prettier --check .", "format-check": "prettier --check .",
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json", "report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
"check": "npm run format-check && npm run lint && npm run test", "check": "npm run format-check && npm run lint && npm run test",
"sampler": "cd samples && node ../packages/sampler/sample-server.mjs",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf" "iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
}, },
"repository": { "repository": {
@@ -54,10 +53,10 @@
"@strudel/xen": "workspace:*" "@strudel/xen": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"dependency-tree": "^10.0.9",
"@tauri-apps/cli": "^1.5.9", "@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^1.1.0", "@vitest/ui": "^1.1.0",
"acorn": "^8.11.3", "canvas": "^2.11.2",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"events": "^3.3.0", "events": "^3.3.0",
+16 -31
View File
@@ -12,22 +12,19 @@ import {
lineNumbers, lineNumbers,
drawSelection, drawSelection,
} from '@codemirror/view'; } from '@codemirror/view';
import { Pattern, repl } from '@strudel/core'; import { Pattern, Drawer, repl, cleanupDraw } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs'; import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs'; import { isTooltipEnabled } from './tooltip.mjs';
import { flash, isFlashEnabled } from './flash.mjs'; import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs'; import { keybindings } from './keybindings.mjs';
import { initTheme, activateTheme, theme } from './themes.mjs'; import { initTheme, activateTheme, theme } from './themes.mjs';
import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { updateWidgets, sliderPlugin } from './slider.mjs';
import { widgetPlugin, updateWidgets } from './widget.mjs';
import { persistentAtom } from '@nanostores/persistent'; import { persistentAtom } from '@nanostores/persistent';
const extensions = { const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []), isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []), isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme, theme,
isAutoCompletionEnabled, isAutoCompletionEnabled,
@@ -42,7 +39,6 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke
export const defaultSettings = { export const defaultSettings = {
keybindings: 'codemirror', keybindings: 'codemirror',
isBracketMatchingEnabled: false, isBracketMatchingEnabled: false,
isBracketClosingEnabled: true,
isLineNumbersDisplayed: true, isLineNumbersDisplayed: true,
isActiveLineHighlighted: false, isActiveLineHighlighted: false,
isAutoCompletionEnabled: false, isAutoCompletionEnabled: false,
@@ -75,9 +71,9 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
...initialSettings, ...initialSettings,
javascript(), javascript(),
sliderPlugin, sliderPlugin,
widgetPlugin,
// indentOnInput(), // works without. already brought with javascript extension? // indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything // bracketMatching(), // does not do anything
closeBrackets(),
syntaxHighlighting(defaultHighlightStyle), syntaxHighlighting(defaultHighlightStyle),
history(), history(),
EditorView.updateListener.of((v) => onChange(v)), EditorView.updateListener.of((v) => onChange(v)),
@@ -129,7 +125,6 @@ export class StrudelMirror {
id, id,
initialCode = '', initialCode = '',
onDraw, onDraw,
drawContext,
drawTime = [0, 0], drawTime = [0, 0],
autodraw, autodraw,
prebake, prebake,
@@ -142,16 +137,23 @@ export class StrudelMirror {
this.widgets = []; this.widgets = [];
this.painters = []; this.painters = [];
this.drawTime = drawTime; this.drawTime = drawTime;
this.drawContext = drawContext; this.onDraw = onDraw;
this.onDraw = onDraw || this.draw; const self = this;
this.id = id || s4(); this.id = id || s4();
this.drawer = new Drawer((haps, time) => { this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => hap.isActive(time)); const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time); this.highlight(currentFrame, time);
this.onDraw(haps, time, this.painters); this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime); }, drawTime);
// this approach does not work with multiple repls on screen
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake(); this.prebaked = prebake();
autodraw && this.drawFirstFrame(); autodraw && this.drawFirstFrame();
@@ -177,14 +179,6 @@ export class StrudelMirror {
beforeEval: async () => { beforeEval: async () => {
cleanupDraw(); cleanupDraw();
this.painters = []; this.painters = [];
const self = this;
// this is similar to repl.mjs > injectPatternMethods
// maybe there is a solution without prototype hacking, but hey, it works
// we need to do this befor every eval to make sure it works with multiple StrudelMirror's side by side
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
await this.prebaked; await this.prebaked;
await replOptions?.beforeEval?.(); await replOptions?.beforeEval?.();
}, },
@@ -192,10 +186,7 @@ export class StrudelMirror {
// remember for when highlighting is toggled on // remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations; this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets; this.widgets = options.meta?.widgets;
const sliders = this.widgets.filter((w) => w.type === 'slider'); updateWidgets(this.editor, this.widgets);
updateSliderWidgets(this.editor, sliders);
const widgets = this.widgets.filter((w) => w.type !== 'slider');
updateWidgets(this.editor, widgets);
updateMiniLocations(this.editor, this.miniLocations); updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options); replOptions?.afterEval?.(options);
this.adjustDrawTime(); this.adjustDrawTime();
@@ -239,9 +230,6 @@ export class StrudelMirror {
// when no painters are set, [0,0] is enough (just highlighting) // when no painters are set, [0,0] is enough (just highlighting)
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]); this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
} }
draw(haps, time) {
this.painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
}
async drawFirstFrame() { async drawFirstFrame() {
if (!this.onDraw) { if (!this.onDraw) {
return; return;
@@ -252,7 +240,7 @@ export class StrudelMirror {
await this.repl.evaluate(this.code, false); await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler, -0.001); this.drawer.invalidate(this.repl.scheduler, -0.001);
// draw at -0.001 to avoid haps at 0 to be visualized as active // draw at -0.001 to avoid haps at 0 to be visualized as active
this.onDraw?.(this.drawer.visibleHaps, -0.001, this.painters); this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters);
} catch (err) { } catch (err) {
console.warn('first frame could not be painted'); console.warn('first frame could not be painted');
} }
@@ -310,9 +298,6 @@ export class StrudelMirror {
setLineNumbersDisplayed(enabled) { setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled); this.reconfigureExtension('isLineNumbersDisplayed', enabled);
} }
setBracketClosingEnabled(enabled) {
this.reconfigureExtension('isBracketClosingEnabled', enabled);
}
setTheme(theme) { setTheme(theme) {
this.reconfigureExtension('theme', theme); this.reconfigureExtension('theme', theme);
} }
+1 -1
View File
@@ -92,7 +92,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) { if (haps.has(id)) {
const hap = haps.get(id); const hap = haps.get(id);
const color = hap.value?.color ?? 'var(--foreground)'; const color = hap.context.color ?? 'var(--foreground)';
// Get explicit channels for color values // Get explicit channels for color values
/* /*
const swatch = document.createElement('div'); const swatch = document.createElement('div');
-1
View File
@@ -3,4 +3,3 @@ export * from './highlight.mjs';
export * from './flash.mjs'; export * from './flash.mjs';
export * from './slider.mjs'; export * from './slider.mjs';
export * from './themes.mjs'; export * from './themes.mjs';
export * from './widget.mjs';
-2
View File
@@ -45,8 +45,6 @@
"@replit/codemirror-vim": "^6.1.0", "@replit/codemirror-vim": "^6.1.0",
"@replit/codemirror-vscode-keymap": "^6.0.2", "@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel/core": "workspace:*", "@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/transpiler": "workspace:*",
"@uiw/codemirror-themes": "^4.21.21", "@uiw/codemirror-themes": "^4.21.21",
"@uiw/codemirror-themes-all": "^4.21.21", "@uiw/codemirror-themes-all": "^4.21.21",
"nanostores": "^0.9.5" "nanostores": "^0.9.5"
+8 -10
View File
@@ -1,6 +1,6 @@
import { ref, pure } from '@strudel/core'; import { ref, pure } from '@strudel/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view'; import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect } from '@codemirror/state'; import { StateEffect, StateField } from '@codemirror/state';
export let sliderValues = {}; export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`; const getSliderID = (from) => `slider_${from}`;
@@ -60,16 +60,14 @@ export class SliderWidget extends WidgetType {
} }
} }
export const setSliderWidgets = StateEffect.define(); export const setWidgets = StateEffect.define();
export const updateSliderWidgets = (view, widgets) => { export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setSliderWidgets.of(widgets) }); view.dispatch({ effects: setWidgets.of(widgets) });
}; };
function getSliders(widgetConfigs, view) { function getWidgets(widgetConfigs, view) {
return widgetConfigs return widgetConfigs.map(({ from, to, value, min, max, step }) => {
.filter((w) => w.type === 'slider')
.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({ return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view), widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0, side: 0,
@@ -101,8 +99,8 @@ export const sliderPlugin = ViewPlugin.fromClass(
} }
} }
for (let e of tr.effects) { for (let e of tr.effects) {
if (e.is(setSliderWidgets)) { if (e.is(setWidgets)) {
this.decorations = Decoration.set(getSliders(e.value, update.view)); this.decorations = Decoration.set(getWidgets(e.value, update.view));
} }
} }
}); });
-2
View File
@@ -37,7 +37,6 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
import teletext, { settings as teletextSettings } from './themes/teletext'; import teletext, { settings as teletextSettings } from './themes/teletext';
import algoboy, { settings as algoboySettings } from './themes/algoboy'; import algoboy, { settings as algoboySettings } from './themes/algoboy';
import terminal, { settings as terminalSettings } from './themes/terminal'; import terminal, { settings as terminalSettings } from './themes/terminal';
import { setTheme } from '@strudel/draw';
export const themes = { export const themes = {
strudelTheme, strudelTheme,
@@ -514,7 +513,6 @@ export function activateTheme(name) {
.map(([key, value]) => `--${key}: ${value} !important;`) .map(([key, value]) => `--${key}: ${value} !important;`)
.join('\n')} .join('\n')}
}`; }`;
setTheme(themeSettings);
// tailwind dark mode // tailwind dark mode
if (themeSettings.light) { if (themeSettings.light) {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
-1
View File
@@ -18,7 +18,6 @@ export default createTheme({
theme: 'light', theme: 'light',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: '#0f380f' },
{ tag: t.keyword, color: '#0f380f' }, { tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' }, { tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' }, { tag: t.special(t.variableName), color: '#0f380f' },
-1
View File
@@ -15,7 +15,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' }, { tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' }, { tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' }, { tag: t.special(t.variableName), color: 'white' },
-1
View File
@@ -18,7 +18,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' }, { tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' }, { tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' }, { tag: t.special(t.variableName), color: 'white' },
-1
View File
@@ -15,7 +15,6 @@ export default createTheme({
gutterForeground: '#8a919966', gutterForeground: '#8a919966',
}, },
styles: [ styles: [
{ tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' }, { tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' }, { tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' }, { tag: t.special(t.variableName), color: '#eeffff' },
-1
View File
@@ -27,7 +27,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: colorB },
{ tag: t.keyword, color: colorA }, { tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini }, { tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA }, { tag: t.special(t.variableName), color: colorA },
-1
View File
@@ -14,7 +14,6 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: '#41FF00' },
{ tag: t.keyword, color: '#41FF00' }, { tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' }, { tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' }, { tag: t.special(t.variableName), color: '#41FF00' },
-1
View File
@@ -16,7 +16,6 @@ export default createTheme({
theme: 'light', theme: 'light',
settings, settings,
styles: [ styles: [
{ tag: t.labelName, color: 'black' },
{ tag: t.keyword, color: 'black' }, { tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' }, { tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' }, { tag: t.special(t.variableName), color: 'black' },
-135
View File
@@ -1,135 +0,0 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
import { Pattern } from '@strudel/core';
export const addWidget = StateEffect.define({
map: ({ from, to }, change) => {
return { from: change.mapPos(from), to: change.mapPos(to) };
},
});
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: addWidget.of(widgets) });
};
function getWidgets(widgetConfigs) {
return (
widgetConfigs
// codemirror throws an error if we don't sort
.sort((a, b) => a.to - b.to)
.map((widgetConfig) => {
return Decoration.widget({
widget: new BlockWidget(widgetConfig),
side: 0,
block: true,
}).range(widgetConfig.to);
})
);
}
const widgetField = StateField.define(
/* <DecorationSet> */ {
create() {
return Decoration.none;
},
update(widgets, tr) {
widgets = widgets.map(tr.changes);
for (let e of tr.effects) {
if (e.is(addWidget)) {
try {
widgets = widgets.update({
filter: () => false,
add: getWidgets(e.value),
});
} catch (error) {
console.log('err', error);
}
}
}
return widgets;
},
provide: (f) => EditorView.decorations.from(f),
},
);
const widgetElements = {};
export function setWidget(id, el) {
widgetElements[id] = el;
el.id = id;
}
export class BlockWidget extends WidgetType {
constructor(widgetConfig) {
super();
this.widgetConfig = widgetConfig;
}
eq() {
return true;
}
toDOM() {
const id = getWidgetID(this.widgetConfig);
const el = widgetElements[id];
return el;
}
ignoreEvent(e) {
return true;
}
}
export const widgetPlugin = [widgetField];
// widget implementer API to create a new widget type
export function registerWidget(type, fn) {
registerWidgetType(type);
if (fn) {
Pattern.prototype[type] = function (id, options = { fold: 1 }) {
// fn is expected to create a dom element and call setWidget(id, el);
// fn should also return the pattern
return fn(id, options, this);
};
}
}
// wire up @strudel/draw functions
function getCanvasWidget(id, options = {}) {
const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
let canvas = document.getElementById(id) || document.createElement('canvas');
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
setWidget(id, canvas);
return canvas;
}
registerWidget('_pianoroll', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
});
registerWidget('_punchcard', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
});
registerWidget('_spiral', (id, options = {}, pat) => {
let _size = options.size || 275;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).spiral({ ...options, ctx, id });
});
registerWidget('_scope', (id, options = {}, pat) => {
options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.tag(id).scope({ ...options, ctx, id });
});
registerWidget('_pitchwheel', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pitchwheel({ ...options, ctx, id });
});
@@ -1,14 +1,13 @@
import { Pattern, silence, register, pure, createParams } from '@strudel/core'; import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import { getDrawContext } from './draw.mjs'; import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls;
let clearColor = '#22222210'; let clearColor = '#22222210';
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) { Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
window.frame && cancelAnimationFrame(window.frame); window.frame && cancelAnimationFrame(window.frame);
const ctx = getDrawContext(); const ctx = getDrawContext();
let { clientWidth: ww, clientHeight: wh } = ctx.canvas; const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
ww *= window.devicePixelRatio;
wh *= window.devicePixelRatio;
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0); let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart; smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
clearColor = `#200010${smearPart}`; clearColor = `#200010${smearPart}`;
-168
View File
@@ -1,168 +0,0 @@
// eslint-disable-next-line no-undef
// TODO: swap below line with above one when firefox supports esm imports in service workers
// see https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker?retiredLocale=de#browser_compatibility
// import createClock from './zyklus.mjs';
function getTime() {
const precision = 10 ** 4;
const seconds = performance.now() / 1000;
return Math.round(seconds * precision) / precision;
}
let num_cycles_at_cps_change = 0;
let num_ticks_since_cps_change = 0;
let num_seconds_at_cps_change = 0;
let cps = 0.5;
// {id: {started: boolean}}
const clients = new Map();
const duration = 0.1;
const channel = new BroadcastChannel('strudeltick');
const sendMessage = (type, payload) => {
channel.postMessage({ type, payload });
};
const sendTick = (phase, duration, tick, time) => {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
const tickdeadline = phase - time;
const lastTick = time + tickdeadline;
const num_cycles_since_cps_change = num_seconds_since_cps_change * cps;
const begin = num_cycles_at_cps_change + num_cycles_since_cps_change;
const secondsSinceLastTick = time - lastTick - duration;
const eventLength = duration * cps;
const end = begin + eventLength;
const cycle = begin + secondsSinceLastTick * cps;
sendMessage('tick', {
begin,
end,
cps,
tickdeadline,
num_cycles_at_cps_change,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
cycle,
});
num_ticks_since_cps_change++;
};
//create clock method from zyklus
const clock = createClock(getTime, sendTick, duration);
let started = false;
const startClock = (id) => {
clients.set(id, { started: true });
if (started) {
return;
}
clock.start();
started = true;
};
const stopClock = async (id) => {
clients.set(id, { started: false });
const otherClientStarted = Array.from(clients.values()).some((c) => c.started);
//dont stop the clock if other instances are running...
if (!started || otherClientStarted) {
return;
}
clock.stop();
setCycle(0);
started = false;
};
const setCycle = (cycle) => {
num_ticks_since_cps_change = 0;
num_cycles_at_cps_change = cycle;
};
const processMessage = (message) => {
const { type, payload } = message;
switch (type) {
case 'cpschange': {
if (payload.cps !== cps) {
const num_seconds_since_cps_change = num_ticks_since_cps_change * duration;
num_cycles_at_cps_change = num_cycles_at_cps_change + num_seconds_since_cps_change * cps;
num_seconds_at_cps_change = num_seconds_at_cps_change + num_seconds_since_cps_change;
cps = payload.cps;
num_ticks_since_cps_change = 0;
}
break;
}
case 'setcycle': {
setCycle(payload.cycle);
break;
}
case 'toggle': {
if (payload.started) {
startClock(message.id);
} else {
stopClock(message.id);
}
break;
}
}
};
self.onconnect = function (e) {
// the incoming port
const port = e.ports[0];
port.addEventListener('message', function (e) {
processMessage(e.data);
});
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
};
// used to consistently schedule events, for use in a service worker - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/clockworker.mjs>
function createClock(
getTime,
callback, // called slightly before each cycle
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
let precision = 10 ** 4; // used to round phase
let minLatency = 0.01;
const setDuration = (setter) => (duration = setter(duration));
overlap = overlap || interval / 2;
const onTick = () => {
const t = getTime();
const lookahead = t + interval + overlap; // the time window for this tick
if (phase === 0) {
phase = t + minLatency;
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick, t);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration
tick++;
}
};
let intervalID;
const start = () => {
clear(); // just in case start was called more than once
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => intervalID !== undefined && clearInterval(intervalID);
const pause = () => clear();
const stop = () => {
tick = 0;
phase = 0;
clear();
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
}
+279 -364
View File
File diff suppressed because it is too large Load Diff
+20 -33
View File
@@ -1,5 +1,5 @@
/* /*
cyclist.mjs - event scheduler for a single strudel instance. for multi-instance scheduler, see - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs> cyclist.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@@ -8,7 +8,7 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
export class Cyclist { export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1, setInterval, clearInterval }) { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false; this.started = false;
this.cps = 0.5; this.cps = 0.5;
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
@@ -16,47 +16,41 @@ export class Cyclist {
this.lastBegin = 0; // query begin of last tick this.lastBegin = 0; // query begin of last tick
this.lastEnd = 0; // query end of last tick this.lastEnd = 0; // query end of last tick
this.getTime = getTime; // get absolute time this.getTime = getTime; // get absolute time
this.num_cycles_at_cps_change = 0; this.num_cycles_since_last_cps_change = 0;
this.seconds_at_cps_change; // clock phase when cps was changed
this.onToggle = onToggle; this.onToggle = onToggle;
this.latency = latency; // fixed trigger time offset this.latency = latency; // fixed trigger time offset
this.clock = createClock( this.clock = createClock(
getTime, getTime,
// called slightly before each cycle // called slightly before each cycle
(phase, duration, _, t) => { (phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
if (this.num_ticks_since_cps_change === 0) { if (this.num_ticks_since_cps_change === 0) {
this.num_cycles_at_cps_change = this.lastEnd; this.num_cycles_since_last_cps_change = this.lastEnd;
this.seconds_at_cps_change = phase;
} }
this.num_ticks_since_cps_change++; this.num_ticks_since_cps_change++;
const seconds_since_cps_change = this.num_ticks_since_cps_change * duration;
const num_cycles_since_cps_change = seconds_since_cps_change * this.cps;
try { try {
const time = getTime();
const begin = this.lastEnd; const begin = this.lastEnd;
this.lastBegin = begin; this.lastBegin = begin;
const end = this.num_cycles_at_cps_change + num_cycles_since_cps_change;
this.lastEnd = end;
this.lastTick = phase;
if (phase < t) { //convert ticks to cycles, so you can query the pattern for events
// avoid querying haps that are in the past anyway const eventLength = duration * this.cps;
console.log(`skip query: too late`); const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
return; this.lastEnd = end;
}
// query the pattern for events // query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const tickdeadline = phase - time; // time left until the phase is a whole number
this.lastTick = time + tickdeadline;
haps.forEach((hap) => { haps.forEach((hap) => {
if (hap.hasOnset()) { if (hap.part.begin.equals(hap.whole.begin)) {
const targetTime = const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
const duration = hap.duration / this.cps; const duration = hap.duration / this.cps;
// the following line is dumb and only here for backwards compatibility onTrigger?.(hap, deadline, duration, this.cps);
// see https://github.com/tidalcycles/strudel/pull/1004
const deadline = targetTime - phase;
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
} }
}); });
} catch (e) { } catch (e) {
@@ -65,16 +59,9 @@ export class Cyclist {
} }
}, },
interval, // duration of each cycle interval, // duration of each cycle
0.1,
0.1,
setInterval,
clearInterval,
); );
} }
now() { now() {
if (!this.started) {
return 0;
}
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
} }
@@ -84,7 +71,7 @@ export class Cyclist {
} }
start() { start() {
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
this.num_cycles_at_cps_change = 0; this.num_cycles_since_last_cps_change = 0;
if (!this.pattern) { if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.'); throw new Error('Scheduler: no pattern set! call .setPattern first.');
} }
@@ -1,88 +1,80 @@
/* /*
draw.mjs - <short description TODO> draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/draw.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, getTime, State, TimeSpan } from '@strudel/core'; import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas', options) => { export const getDrawContext = (id = 'test-canvas') => {
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
let canvas = document.querySelector('#' + id); let canvas = document.querySelector('#' + id);
if (!canvas) { if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
canvas.id = id; canvas.id = id;
canvas.width = window.innerWidth * pixelRatio; canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * pixelRatio; canvas.height = window.innerHeight * scale;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0'; canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
pixelated && (canvas.style.imageRendering = 'pixelated');
document.body.prepend(canvas); document.body.prepend(canvas);
let timeout; let timeout;
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
timeout && clearTimeout(timeout); timeout && clearTimeout(timeout);
timeout = setTimeout(() => { timeout = setTimeout(() => {
canvas.width = window.innerWidth * pixelRatio; canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * pixelRatio; canvas.height = window.innerHeight * scale;
}, 200); }, 200);
}); });
} }
return canvas.getContext(contextType); return canvas.getContext('2d');
}; };
let animationFrames = {}; Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
function stopAnimationFrame(id) {
if (animationFrames[id] !== undefined) {
cancelAnimationFrame(animationFrames[id]);
delete animationFrames[id];
}
}
function stopAllAnimations() {
Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id));
}
let memory = {};
Pattern.prototype.draw = function (fn, options) {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return this; return this;
} }
let { id = 1, lookbehind = 0, lookahead = 0 } = options; if (window.strudelAnimation) {
let __t = Math.max(getTime(), 0); cancelAnimationFrame(window.strudelAnimation);
stopAnimationFrame(id); }
lookbehind = Math.abs(lookbehind); const ctx = getDrawContext();
// init memory, clear future haps of old pattern let cycle,
memory[id] = (memory[id] || []).filter((h) => !h.isInFuture(__t)); events = [];
let newFuture = this.queryArc(__t, __t + lookahead).filter((h) => h.hasOnset()); const animate = (time) => {
memory[id] = memory[id].concat(newFuture); const t = getTime();
if (from !== undefined && to !== undefined) {
let last; const currentCycle = Math.floor(t);
const animate = () => { if (cycle !== currentCycle) {
const _t = getTime(); cycle = currentCycle;
const t = _t + lookahead; const begin = currentCycle + from;
// filter out haps that are too far in the past const end = currentCycle + to;
memory[id] = memory[id].filter((h) => h.isInNearPast(lookbehind, _t)); setTimeout(() => {
// begin where we left off in last frame, but max -0.1s (inactive tab throttles to 1fps) events = this.query(new State(new TimeSpan(begin, end)))
let begin = Math.max(last || t, t - 1 / 10); .filter(Boolean)
const haps = this.queryArc(begin, t).filter((h) => h.hasOnset()); .filter((event) => event.part.begin.equals(event.whole.begin));
memory[id] = memory[id].concat(haps); onQuery?.(events);
last = t; // makes sure no haps are missed }, 0);
fn(memory[id], _t, t, this); }
animationFrames[id] = requestAnimationFrame(animate); }
callback(ctx, events, t, time);
window.strudelAnimation = requestAnimationFrame(animate);
}; };
animationFrames[id] = requestAnimationFrame(animate); requestAnimationFrame(animate);
return this; return this;
}; };
export const cleanupDraw = (clearScreen = true) => { export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext(); const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
stopAllAnimations(); if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
if (window.strudelScheduler) { if (window.strudelScheduler) {
clearInterval(window.strudelScheduler); clearInterval(window.strudelScheduler);
} }
}; };
Pattern.prototype.onPaint = function () { Pattern.prototype.onPaint = function (onPaint) {
console.warn('[draw] onPaint was not overloaded. Some drawings might not work'); // this is evil! TODO: add pattern.context
this.context = { onPaint };
return this; return this;
}; };
@@ -142,7 +134,7 @@ export class Drawer {
this.lastFrame = phase; this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || []) this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll) // filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.endClipped >= phase - lookbehind - lookahead) .filter((h) => h.whole?.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in) // add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset())); .concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead; const time = phase - lookahead;
@@ -183,18 +175,3 @@ export class Drawer {
} }
} }
} }
export function getComputedPropertyValue(name) {
if (typeof window === 'undefined') {
return '#fff';
}
return getComputedStyle(document.documentElement).getPropertyValue(name);
}
let theme = {};
export function getTheme() {
return theme;
}
export function setTheme(_theme) {
theme = _theme;
}
+1 -7
View File
@@ -41,17 +41,11 @@ const _bjork = function (n, x) {
}; };
export const bjork = function (ons, steps) { export const bjork = function (ons, steps) {
const inverted = ons < 0;
ons = Math.abs(ons);
const offs = steps - ons; const offs = steps - ons;
const x = Array(ons).fill([1]); const x = Array(ons).fill([1]);
const y = Array(offs).fill([0]); const y = Array(offs).fill([0]);
const result = _bjork([ons, offs], [x, y]); const result = _bjork([ons, offs], [x, y]);
const p = flatten(result[1][0]).concat(flatten(result[1][1])); return flatten(result[1][0]).concat(flatten(result[1][1]));
if (inverted) {
return p.map((x) => (x === 0 ? 1 : 0));
}
return p;
}; };
/** /**
-1
View File
@@ -22,7 +22,6 @@ export const evalScope = async (...args) => {
globalThis[name] = value; globalThis[name] = value;
}); });
}); });
return modules;
}; };
function safeEval(str, options = {}) { function safeEval(str, options = {}) {
-9
View File
@@ -51,11 +51,6 @@ Fraction.prototype.max = function (other) {
return this.gt(other) ? this : other; return this.gt(other) ? this : other;
}; };
Fraction.prototype.maximum = function (...others) {
others = others.map((x) => new Fraction(x));
return others.reduce((max, other) => other.max(max), this);
};
Fraction.prototype.min = function (other) { Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other; return this.lt(other) ? this : other;
}; };
@@ -88,10 +83,6 @@ export const gcd = (...fractions) => {
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1)); return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
}; };
export const lcm = (...fractions) => {
return fractions.reduce((lcm, fraction) => lcm.lcm(fraction), fraction(1));
};
fraction._original = Fraction; fraction._original = Fraction;
export default fraction; export default fraction;
+1 -36
View File
@@ -3,7 +3,6 @@ hap.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/hap.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. 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';
export class Hap { export class Hap {
/* /*
@@ -33,43 +32,13 @@ export class Hap {
} }
get duration() { get duration() {
let duration; return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
if (typeof this.value?.duration === 'number') {
duration = Fraction(this.value.duration);
} else {
duration = this.whole.end.sub(this.whole.begin);
}
if (typeof this.value?.clip === 'number') {
return duration.mul(this.value.clip);
}
return duration;
} }
get endClipped() { get endClipped() {
return this.whole.begin.add(this.duration); return this.whole.begin.add(this.duration);
} }
isActive(currentTime) {
return this.whole.begin <= currentTime && this.endClipped >= currentTime;
}
isInPast(currentTime) {
return currentTime > this.endClipped;
}
isInNearPast(margin, currentTime) {
return currentTime - margin <= this.endClipped;
}
isInFuture(currentTime) {
return currentTime < this.whole.begin;
}
isInNearFuture(margin, currentTime) {
return currentTime < this.whole.begin && currentTime > this.whole.begin - margin;
}
isWithinTime(min, max) {
return this.whole.begin <= max && this.endClipped >= min;
}
wholeOrPart() { wholeOrPart() {
return this.whole ? this.whole : this.part; return this.whole ? this.whole : this.part;
} }
@@ -91,10 +60,6 @@ export class Hap {
return this.whole != undefined && this.whole.begin.equals(this.part.begin); return this.whole != undefined && this.whole.begin.equals(this.part.begin);
} }
hasTag(tag) {
return this.context.tags?.includes(tag);
}
resolveState(state) { resolveState(state) {
if (this.stateful && this.hasOnset()) { if (this.stateful && this.hasOnset()) {
console.log('stateful'); console.log('stateful');
+6 -4
View File
@@ -4,13 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. 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 * as controls from './controls.mjs'; // legacy import controls from './controls.mjs';
export * from './euclid.mjs'; export * from './euclid.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import createClock from './zyklus.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
export { Fraction, controls, createClock }; export { Fraction, controls };
export * from './controls.mjs';
export * from './hap.mjs'; export * from './hap.mjs';
export * from './pattern.mjs'; export * from './pattern.mjs';
export * from './signal.mjs'; export * from './signal.mjs';
@@ -23,6 +21,10 @@ export * from './repl.mjs';
export * from './cyclist.mjs'; export * from './cyclist.mjs';
export * from './logger.mjs'; export * from './logger.mjs';
export * from './time.mjs'; export * from './time.mjs';
export * from './draw.mjs';
export * from './animate.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs'; export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs'; export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails) // below won't work with runtime.mjs (json import fails)
-10
View File
@@ -1,16 +1,6 @@
export const logKey = 'strudel.log'; export const logKey = 'strudel.log';
let debounce = 1000,
lastMessage,
lastTime;
export function logger(message, type, data = {}) { 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'); console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') { if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
document.dispatchEvent( document.dispatchEvent(
-147
View File
@@ -1,147 +0,0 @@
/*
neocyclist.mjs - event scheduler like cyclist, except recieves clock pulses from clockworker in order to sync across multiple instances.
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/neocyclist.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { logger } from './logger.mjs';
export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) {
this.started = false;
this.cps = 0.5;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.getTime = getTime; // get absolute time
this.time_at_last_tick_message = 0;
this.num_cycles_at_cps_change = 0;
this.onToggle = onToggle;
this.latency = 0.1; // fixed trigger time offset
this.cycle = 0;
this.id = Math.round(Date.now() * Math.random());
this.worker_time_dif;
this.worker = new SharedWorker(new URL('./clockworker.js', import.meta.url));
this.worker.port.start();
this.channel = new BroadcastChannel('strudeltick');
let weight = 0; // the amount of weight that is applied to the current average when averaging a new time dif
const maxWeight = 20;
const precision = 10 ** 3; //round off time diff to prevent accumulating outliers
// the clock of the worker and the audio context clock can drift apart over time
// aditionally, the message time of the worker pinging the callback to process haps can be inconsistent.
// we need to keep a rolling weighted average of the time difference between the worker clock and audio context clock
// in order to schedule events consistently.
const setTimeReference = (num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline) => {
const time_dif = getTime() - (num_seconds_at_cps_change + num_seconds_since_cps_change) + tickdeadline;
if (this.worker_time_dif == null) {
this.worker_time_dif = time_dif;
} else {
const w = 1; //weight of new time diff;
const new_dif =
Math.round(((this.worker_time_dif * weight + time_dif * w) / (weight + w)) * precision) / precision;
if (new_dif != this.worker_time_dif) {
// reset the weight so the clock recovers faster from an audio context freeze/dropout if it happens
weight = 4;
}
this.worker_time_dif = new_dif;
}
weight = Math.min(weight + 1, maxWeight);
};
const tickCallback = (payload) => {
const {
num_cycles_at_cps_change,
cps,
num_seconds_at_cps_change,
num_seconds_since_cps_change,
begin,
end,
tickdeadline,
cycle,
} = payload;
this.cps = cps;
this.cycle = cycle;
setTimeReference(num_seconds_at_cps_change, num_seconds_since_cps_change, tickdeadline);
processHaps(begin, end, num_cycles_at_cps_change, num_seconds_at_cps_change);
this.time_at_last_tick_message = this.getTime();
};
const processHaps = (begin, end, num_cycles_at_cps_change, seconds_at_cps_change) => {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const targetTime =
(hap.whole.begin - num_cycles_at_cps_change) / this.cps +
seconds_at_cps_change +
this.latency +
this.worker_time_dif;
const duration = hap.duration / this.cps;
onTrigger?.(hap, 0, duration, this.cps, targetTime);
}
});
};
// receive messages from worker clock and process them
this.channel.onmessage = (message) => {
if (!this.started) {
return;
}
const { payload, type } = message.data;
switch (type) {
case 'tick': {
tickCallback(payload);
}
}
};
}
sendMessage(type, payload) {
this.worker.port.postMessage({ type, payload, id: this.id });
}
now() {
const gap = (this.getTime() - this.time_at_last_tick_message) * this.cps;
return this.cycle + gap;
}
setCps(cps = 1) {
this.sendMessage('cpschange', { cps });
}
setCycle(cycle) {
this.sendMessage('setcycle', { cycle });
}
setStarted(started) {
this.sendMessage('toggle', { started });
this.started = started;
this.onToggle?.(started);
}
start() {
logger('[cyclist] start');
this.setStarted(true);
}
stop() {
this.worker_time_dif = null;
logger('[cyclist] stop');
this.setStarted(false);
}
setPattern(pat, autostart = false) {
this.pattern = pat;
if (autostart && !this.started) {
this.start();
}
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
}
}
+179 -522
View File
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,10 @@
/* /*
pianoroll.mjs - <short description TODO> pianoroll.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/canvas/pianoroll.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, noteToMidi, freqToMidi } from '@strudel/core'; import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
import { getTheme, getDrawContext } from './draw.mjs';
const scale = (normalized, min, max) => normalized * (max - min) + min; const scale = (normalized, min, max) => normalized * (max - min) + min;
const getValue = (e) => { const getValue = (e) => {
@@ -19,13 +18,7 @@ const getValue = (e) => {
} }
note = note ?? n; note = note ?? n;
if (typeof note === 'string') { if (typeof note === 'string') {
try {
// TODO: n(run(32)).scale("D:minor") fails when trying to query negative time..
return noteToMidi(note); return noteToMidi(note);
} catch (err) {
// console.warn(`error converting note to midi: ${err}`); // this spams to crazy
return 0;
}
} }
if (typeof note === 'number') { if (typeof note === 'number') {
return note; return note;
@@ -37,24 +30,25 @@ const getValue = (e) => {
}; };
Pattern.prototype.pianoroll = function (options = {}) { Pattern.prototype.pianoroll = function (options = {}) {
let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options; let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
let from = -cycles * playhead; let from = -cycles * playhead;
let to = cycles * (1 - playhead); let to = cycles * (1 - playhead);
const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to);
this.draw( this.draw(
(haps, time) => { (ctx, haps, t) => {
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
pianoroll({ pianoroll({
...options, ...options,
time, time: t,
ctx, ctx,
haps: haps.filter((hap) => inFrame(hap, time)), haps: haps.filter(inFrame),
}); });
}, },
{ {
lookbehind: from - overscan, from: from - overscan,
lookahead: to + overscan, to: to + overscan,
id,
}, },
); );
return this; return this;
@@ -104,8 +98,11 @@ export function pianoroll({
flipTime = 0, flipTime = 0,
flipValues = 0, flipValues = 0,
hideNegative = false, hideNegative = false,
inactive = getTheme().foreground, // inactive = '#C9E597',
active = getTheme().foreground, // inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent', background = 'transparent',
smear = 0, smear = 0,
playheadColor = 'white', playheadColor = 'white',
@@ -124,17 +121,12 @@ export function pianoroll({
colorizeInactive = 1, colorizeInactive = 1,
fontFamily, fontFamily,
ctx, ctx,
id,
} = {}) { } = {}) {
const w = ctx.canvas.width; const w = ctx.canvas.width;
const h = ctx.canvas.height; const h = ctx.canvas.height;
let from = -cycles * playhead; let from = -cycles * playhead;
let to = cycles * (1 - playhead); let to = cycles * (1 - playhead);
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
if (timeframeProp) { if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead'); console.warn('timeframe is deprecated! use from/to instead');
from = 0; from = 0;
@@ -189,14 +181,13 @@ export function pianoroll({
if (hideInactive && !isActive) { if (hideInactive && !isActive) {
return; return;
} }
let color = event.value?.color; let color = event.value?.color || event.context?.color;
active = color || active; active = color || active;
inactive = colorizeInactive ? color || inactive : inactive; inactive = colorizeInactive ? color || inactive : inactive;
color = isActive ? active : inactive; color = isActive ? active : inactive;
ctx.fillStyle = fillCurrent ? color : 'transparent'; ctx.fillStyle = fillCurrent ? color : 'transparent';
ctx.strokeStyle = color; ctx.strokeStyle = color;
const { velocity = 1, gain = 1 } = event.value || {}; ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
ctx.globalAlpha = velocity * gain;
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent; const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
const timePx = scale(timeProgress, ...timeRange); const timePx = scale(timeProgress, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis); let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
@@ -272,8 +263,8 @@ export function getDrawOptions(drawTime, options = {}) {
export const getPunchcardPainter = export const getPunchcardPainter =
(options = {}) => (options = {}) =>
(ctx, time, haps, drawTime) => (ctx, time, haps, drawTime, paintOptions = {}) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }); pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) });
Pattern.prototype.punchcard = function (options) { Pattern.prototype.punchcard = function (options) {
return this.onPaint(getPunchcardPainter(options)); return this.onPaint(getPunchcardPainter(options));
+11 -19
View File
@@ -1,4 +1,3 @@
import { NeoCyclist } from './neocyclist.mjs';
import { Cyclist } from './cyclist.mjs'; import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs'; import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@@ -7,7 +6,9 @@ import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
export function repl({ export function repl({
interval,
defaultOutput, defaultOutput,
onSchedulerError,
onEvalError, onEvalError,
beforeEval, beforeEval,
afterEval, afterEval,
@@ -16,9 +17,6 @@ export function repl({
onToggle, onToggle,
editPattern, editPattern,
onUpdateState, onUpdateState,
sync = false,
setInterval,
clearInterval,
}) { }) {
const state = { const state = {
schedulerError: undefined, schedulerError: undefined,
@@ -39,20 +37,16 @@ export function repl({
onUpdateState?.(state); onUpdateState?.(state);
}; };
const schedulerOptions = { const scheduler = new Cyclist({
interval,
onTrigger: getTrigger({ defaultOutput, getTime }), onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime, getTime,
onToggle: (started) => { onToggle: (started) => {
updateState({ started }); updateState({ started });
onToggle?.(started); onToggle?.(started);
}, },
setInterval, });
clearInterval,
};
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
const scheduler =
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
let pPatterns = {}; let pPatterns = {};
let allTransform; let allTransform;
@@ -110,7 +104,7 @@ export function repl({
const cpm = register('cpm', function (cpm, pat) { const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps); return pat._fast(cpm / 60 / scheduler.cps);
}); });
return evalScope({ evalScope({
all, all,
hush, hush,
cpm, cpm,
@@ -127,7 +121,7 @@ export function repl({
} }
try { try {
updateState({ code, pending: true }); updateState({ code, pending: true });
await injectPatternMethods(); injectPatternMethods();
await beforeEval?.({ code }); await beforeEval?.({ code });
shouldHush && hush(); shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler); let { pattern, meta } = await _evaluate(code, transpiler);
@@ -156,7 +150,6 @@ export function repl({
return pattern; return pattern;
} catch (err) { } catch (err) {
logger(`[eval] error: ${err.message}`, 'error'); logger(`[eval] error: ${err.message}`, 'error');
console.error(err);
updateState({ evalError: err, pending: false }); updateState({ evalError: err, pending: false });
onEvalError?.(err); onEvalError?.(err);
} }
@@ -167,15 +160,14 @@ export function repl({
export const getTrigger = export const getTrigger =
({ getTime, defaultOutput }) => ({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps, t) => { async (hap, deadline, duration, cps) => {
// TODO: get rid of deadline after https://github.com/tidalcycles/strudel/pull/1004
try { try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) { if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps, t); await defaultOutput(hap, deadline, duration, cps);
} }
if (hap.context.onTrigger) { if (hap.context.onTrigger) {
// call signature of output / onTrigger is different... // call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t); await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
} }
} catch (err) { } catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error'); logger(`[cyclist] error: ${err.message}`, 'error');
+4 -121
View File
@@ -143,24 +143,7 @@ export const rand = signal(timeToRand);
export const rand2 = rand.toBipolar(); export const rand2 = rand.toBipolar();
export const _brandBy = (p) => rand.fmap((x) => x < p); export const _brandBy = (p) => rand.fmap((x) => x < p);
/**
* A continuous pattern of 0 or 1 (binary random), with a probability for the value being 1
*
* @name brandBy
* @param {number} probability - a number between 0 and 1
* @example
* s("hh*10").pan(brandBy(0.2))
*/
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin(); export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
/**
* A continuous pattern of 0 or 1 (binary random)
*
* @name brand
* @example
* s("hh*10").pan(brand)
*/
export const brand = _brandBy(0.5); export const brand = _brandBy(0.5);
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i)); export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
@@ -261,68 +244,9 @@ export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs)); return pat.apply(pickmod(lookup, funcs));
}); });
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/** /**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). /** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat * @param {Pattern} pat
* @param {*} xs * @param {*} xs
* @returns {Pattern} * @returns {Pattern}
@@ -333,23 +257,21 @@ export const pickmodReset = register('pickmodReset', function (lookup, pat) {
* @example * @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4) * s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/ */
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) { export const inhabit = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin(); return _pick(lookup, pat, true).squeezeJoin();
}); });
/** * The same as `inhabit`, but if you pick a number greater than the size of the list, /** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value. * it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the * For example, if you pick the fifth pattern of a list of three, you'll get the
* second one. * second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat * @param {Pattern} pat
* @param {*} xs * @param {*} xs
* @returns {Pattern} * @returns {Pattern}
*/ */
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) { export const inhabitmod = register('inhabit', function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin(); return _pick(lookup, pat, false).squeezeJoin();
}); });
/** /**
@@ -414,8 +336,6 @@ export const chooseInWith = (pat, xs) => {
* Chooses randomly from the given list of elements. * Chooses randomly from the given list of elements.
* @param {...any} xs values / patterns to choose from. * @param {...any} xs values / patterns to choose from.
* @returns {Pattern} - a continuous pattern. * @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(choose("sine", "triangle", "bd:6"))
*/ */
export const choose = (...xs) => chooseWith(rand, xs); export const choose = (...xs) => chooseWith(rand, xs);
@@ -442,7 +362,6 @@ Pattern.prototype.choose2 = function (...xs) {
/** /**
* Picks one of the elements at random each cycle. * Picks one of the elements at random each cycle.
* @synonyms randcat
* @returns {Pattern} * @returns {Pattern}
* @example * @example
* chooseCycles("bd", "hh", "sd").s().fast(8) * chooseCycles("bd", "hh", "sd").s().fast(8)
@@ -471,26 +390,10 @@ const _wchooseWith = function (pat, ...pairs) {
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin(); const wchooseWith = (...args) => _wchooseWith(...args).outerJoin();
/**
* Chooses randomly from the given list of elements by giving a probability to each element
* @param {...any} pairs arrays of value and weight
* @returns {Pattern} - a continuous pattern.
* @example
* note("c2 g2!2 d2 f1").s(wchoose(["sine",10], ["triangle",1], ["bd:6",1]))
*/
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs); export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
/**
* Picks one of the elements at random each cycle by giving a probability to each element
* @synonyms wrandcat
* @returns {Pattern}
* @example
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
*/
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin(); export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
export const wrandcat = wchooseCycles;
// this function expects pat to be a pattern of floats... // this function expects pat to be a pattern of floats...
export const perlinWith = (pat) => { export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor); const pata = pat.fmap(Math.floor);
@@ -559,11 +462,6 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
* @returns Pattern * @returns Pattern
* @example * @example
* s("hh*8").undegradeBy(0.2) * s("hh*8").undegradeBy(0.2)
* @example
* s("hh*10").layer(
* x => x.degradeBy(0.2).pan(0),
* x => x.undegradeBy(0.8).pan(1)
* )
*/ */
export const undegradeBy = register('undegradeBy', function (x, pat) { export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith( return pat._degradeByWith(
@@ -572,21 +470,6 @@ export const undegradeBy = register('undegradeBy', function (x, pat) {
); );
}); });
/**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
* Events that would be removed by degrade are let through by undegrade and vice versa (see second example).
*
* @name undegrade
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").undegrade()
* @example
* s("hh*10").layer(
* x => x.degrade().pan(0),
* x => x.undegrade().pan(1)
* )
*/
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5)); export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/** /**
@@ -1,5 +1,4 @@
import { Pattern } from '@strudel/core'; import { Pattern } from './index.mjs';
import { getTheme } from './draw.mjs';
// polar coords -> xy // polar coords -> xy
function fromPolar(angle, radius, cx, cy) { function fromPolar(angle, radius, cx, cy) {
@@ -20,7 +19,7 @@ function spiralSegment(options) {
cy = 100, cy = 100,
rotate = 0, rotate = 0,
thickness = margin / 2, thickness = margin / 2,
color = getTheme().foreground, color = '#0000ff30',
cap = 'round', cap = 'round',
stretch = 1, stretch = 1,
fromOpacity = 1, fromOpacity = 1,
@@ -50,34 +49,25 @@ function spiralSegment(options) {
ctx.stroke(); ctx.stroke();
} }
function drawSpiral(options) { Pattern.prototype.spiral = function (options = {}) {
let { const {
stretch = 1, stretch = 1,
size = 80, size = 80,
thickness = size / 2, thickness = size / 2,
cap = 'butt', // round butt squar, cap = 'butt', // round butt squar,
inset = 3, // start angl, inset = 3, // start angl,
playheadColor = '#ffffff', playheadColor = '#ffffff90',
playheadLength = 0.02, playheadLength = 0.02,
playheadThickness = thickness, playheadThickness = thickness,
padding = 0, padding = 0,
steady = 1, steady = 1,
activeColor = getTheme().foreground, inactiveColor = '#ffffff20',
inactiveColor = getTheme().gutterForeground,
colorizeInactive = 0, colorizeInactive = 0,
fade = true, fade = true,
// logSpiral = true, // logSpiral = true,
ctx,
time,
haps,
drawTime,
id,
} = options; } = options;
if (id) { function spiral({ ctx, time, haps, drawTime }) {
haps = haps.filter((hap) => hap.hasTag(id));
}
const [w, h] = [ctx.canvas.width, ctx.canvas.height]; const [w, h] = [ctx.canvas.width, ctx.canvas.height];
ctx.clearRect(0, 0, w * 2, h * 2); ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2]; const [cx, cy] = [w / 2, h / 2];
@@ -104,8 +94,7 @@ function drawSpiral(options) {
const isActive = hap.whole.begin <= time && hap.endClipped > time; const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset; const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding; const to = hap.endClipped - time + inset - padding;
const hapColor = hap.value?.color || activeColor; const { color } = hap.context;
const color = colorizeInactive || isActive ? hapColor : inactiveColor;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1; const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
spiralSegment({ spiralSegment({
ctx, ctx,
@@ -113,7 +102,7 @@ function drawSpiral(options) {
from, from,
to, to,
rotate, rotate,
color, color: colorizeInactive || isActive ? color : inactiveColor,
fromOpacity: opacity, fromOpacity: opacity,
toOpacity: opacity, toOpacity: opacity,
}); });
@@ -125,6 +114,5 @@ function drawSpiral(options) {
}); });
} }
Pattern.prototype.spiral = function (options = {}) { return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
return this.onPaint((ctx, time, haps, drawTime) => drawSpiral({ ctx, time, haps, drawTime, ...options }));
}; };
+5 -19
View File
@@ -4,39 +4,25 @@ Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { s, pan } from '../controls.mjs'; import controls from '../controls.mjs';
import { mini } from '../../mini/mini.mjs'; import { mini } from '../../mini/mini.mjs';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import Fraction from '../fraction.mjs';
describe('controls', () => { describe('controls', () => {
it('should support controls', () => { it('should support controls', () => {
expect(s('bd').firstCycleValues).toEqual([{ s: 'bd' }]); expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
}); });
it('should support compound controls', () => { it('should support compound controls', () => {
expect(s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]); expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
expect(s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([ expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
{ s: 'bd', n: 3 }, { s: 'bd', n: 3 },
{ s: 'sd', n: 4, gain: 1.4 }, { s: 'sd', n: 4, gain: 1.4 },
]); ]);
}); });
it('should support ignore extra elements in compound controls', () => { it('should support ignore extra elements in compound controls', () => {
expect(s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([ expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
{ s: 'bd', n: 3, gain: 0.4 }, { s: 'bd', n: 3, gain: 0.4 },
{ s: 'sd', n: 4, gain: 0.5 }, { s: 'sd', n: 4, gain: 0.5 },
]); ]);
}); });
it('should support nested controls', () => {
expect(s(mini('bd').pan(1)).firstCycleValues).toEqual([{ s: 'bd', pan: 1 }]);
expect(s(mini('bd:1').pan(1)).firstCycleValues).toEqual([{ s: 'bd', n: 1, pan: 1 }]);
});
it('preserves tactus of the left pattern', () => {
expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).tactus).toEqual(Fraction(3));
});
it('preserves tactus of the right pattern for .out', () => {
expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(4));
});
it('combines tactus of the pattern for .mix as lcm', () => {
expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).tactus).toEqual(Fraction(12));
});
}); });
+15 -60
View File
@@ -51,8 +51,9 @@ import {
import { steady } from '../signal.mjs'; import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs'; import controls from '../controls.mjs';
const { n, s } = controls;
const st = (begin, end) => new State(ts(begin, end)); const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end)); const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context); const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
@@ -181,18 +182,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]); ]);
}); });
it('can Reset() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.reset(20, 30) .add.trig(20, 30)
.early(2), .early(2),
sequence(26, 27, 36, 37), sequence(26, 27, 36, 37),
); );
}); });
it('can Restart() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.add.restart(20, 30) .add.trigzero(20, 30)
.early(2), .early(2),
sequence(21, 22, 31, 32), sequence(21, 22, 31, 32),
); );
@@ -233,18 +234,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]); ]);
}); });
it('can Reset() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.reset(20, 30) .keep.trig(20, 30)
.early(2), .early(2),
sequence(6, 7, 6, 7), sequence(6, 7, 6, 7),
); );
}); });
it('can Restart() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keep.restart(20, 30) .keep.trigzero(20, 30)
.early(2), .early(2),
sequence(1, 2, 1, 2), sequence(1, 2, 1, 2),
); );
@@ -279,18 +280,18 @@ describe('Pattern', () => {
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2), new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]); ]);
}); });
it('can Reset() structure', () => { it('can Trig() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.reset(false, true) .keepif.trig(false, true)
.early(2), .early(2),
sequence(silence, silence, 6, 7), sequence(silence, silence, 6, 7),
); );
}); });
it('can Restart() structure', () => { it('can Trigzero() structure', () => {
sameFirst( sameFirst(
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10) slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
.keepif.restart(false, true) .keepif.trigzero(false, true)
.early(2), .early(2),
sequence(silence, silence, 1, 2), sequence(silence, silence, 1, 2),
); );
@@ -604,7 +605,7 @@ describe('Pattern', () => {
}); });
}); });
describe('polymeter()', () => { describe('polymeter()', () => {
it('Can layer up cycles, stepwise, with lists', () => { it('Can layer up cycles, stepwise', () => {
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual( expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(), fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
); );
@@ -613,9 +614,6 @@ describe('Pattern', () => {
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(), stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
); );
}); });
it('Can layer up cycles, stepwise, with weighted patterns', () => {
sameFirst(polymeterSteps(3, sequence('a', 'b')).fast(2), sequence('a', 'b', 'a', 'b', 'a', 'b'));
});
}); });
describe('firstOf()', () => { describe('firstOf()', () => {
@@ -1119,47 +1117,4 @@ describe('Pattern', () => {
); );
}); });
}); });
describe('tactus', () => {
it('Is correctly preserved/calculated through transformations', () => {
expect(sequence(0, 1, 2, 3).linger(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).iter(4).tactus).toStrictEqual(Fraction(4));
expect(sequence(0, 1, 2, 3).fast(4).tactus).toStrictEqual(Fraction(16));
expect(sequence(0, 1, 2, 3).hurry(4).tactus).toStrictEqual(Fraction(16));
expect(sequence(0, 1, 2, 3).rev().tactus).toStrictEqual(Fraction(4));
expect(sequence(1).segment(10).tactus).toStrictEqual(Fraction(10));
expect(sequence(1, 0, 1).invert().tactus).toStrictEqual(Fraction(3));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).tactus).toStrictEqual(Fraction(8));
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual(
Fraction(4),
);
});
});
describe('steptaper', () => {
it('can taper', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).steptaper(1, 5), sequence(0, 1, 2, 3, 4, 0, 1, 2, 3, 0, 1, 2, 0, 1, 0)));
});
it('can taper backwards', () => {
expect(
sameFirst(sequence(0, 1, 2, 3, 4).steptaper(-1, 5), sequence(0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4)),
);
});
});
describe('wax and wane, left', () => {
it('can wax from the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(2), sequence(0, 1)));
});
it('can wane to the left', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(2), sequence(0, 1, 2)));
});
it('can wax from the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwax(-2), sequence(3, 4)));
});
it('can wane to the right', () => {
expect(sameFirst(sequence(0, 1, 2, 3, 4).stepwane(-2), sequence(2, 3, 4)));
});
});
}); });
+6 -1
View File
@@ -6,7 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { map, valued, mul } from '../value.mjs'; import { map, valued, mul } from '../value.mjs';
import { n } from '../controls.mjs'; import controls from '../controls.mjs';
const { n } = controls;
describe('Value', () => { describe('Value', () => {
it('unionWith', () => { it('unionWith', () => {
@@ -22,4 +23,8 @@ describe('Value', () => {
expect(valued(mul).ap(3).ap(3).value).toEqual(9); expect(valued(mul).ap(3).ap(3).value).toEqual(9);
expect(valued(3).mul(3).value).toEqual(9); expect(valued(3).mul(3).value).toEqual(9);
}); });
it('union bare numbers for numeral props', () => {
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
});
}); });
+18
View File
@@ -4,6 +4,19 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { getTime } from './time.mjs';
function frame(callback) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
const animate = (animationTime) => {
callback(animationTime, getTime());
window.strudelAnimation = requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
export const backgroundImage = function (src, animateOptions = {}) { export const backgroundImage = function (src, animateOptions = {}) {
const container = document.getElementById('code'); const container = document.getElementById('code');
const bg = 'background-image:url(' + src + ');background-size:contain;'; const bg = 'background-image:url(' + src + ');background-size:contain;';
@@ -22,6 +35,11 @@ export const backgroundImage = function (src, animateOptions = {}) {
if (funcOptions.length === 0) { if (funcOptions.length === 0) {
return; return;
} }
frame((_, t) =>
funcOptions.forEach(([option, value]) => {
handleOption(option, value(t));
}),
);
}; };
export const cleanupUi = () => { export const cleanupUi = () => {
-20
View File
@@ -323,23 +323,3 @@ export function objectMap(obj, fn) {
} }
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)])); return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
} }
// Floating point versions, see Fraction for rational versions
// // greatest common divisor
// export const gcd = function (x, y, ...z) {
// if (!y && z.length > 0) {
// return gcd(x, ...z);
// }
// if (!y) {
// return x;
// }
// return gcd(y, x % y, ...z);
// };
// // lowest common multiple
// export const lcm = function (x, y, ...z) {
// if (z.length == 0) {
// return (x * y) / gcd(x, y);
// }
// return lcm((x * y) / gcd(x, y), ...z);
// };
+6 -5
View File
@@ -5,13 +5,14 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { curry } from './util.mjs'; import { curry } from './util.mjs';
import { logger } from './logger.mjs';
export function unionWithObj(a, b, func) { export function unionWithObj(a, b, func) {
if (b?.value !== undefined && Object.keys(b).length === 1) { if (typeof b?.value === 'number') {
// https://github.com/tidalcycles/strudel/issues/1026 // https://github.com/tidalcycles/strudel/issues/262
logger(`[warn]: Can't do arithmetic on control pattern.`); const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
return a; const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
b = Object.assign(b, numerals);
delete b.value;
} }
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k)); const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])]))); return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
+4 -9
View File
@@ -7,9 +7,6 @@ function createClock(
duration = 0.05, // duration of each cycle duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks overlap = 0.1, // overlap between callbacks
setInterval = globalThis.setInterval,
clearInterval = globalThis.clearInterval,
round = true,
) { ) {
let tick = 0; // counts callbacks let tick = 0; // counts callbacks
let phase = 0; // next callback time let phase = 0; // next callback time
@@ -25,8 +22,9 @@ function createClock(
} }
// callback as long as we're inside the lookahead // callback as long as we're inside the lookahead
while (phase < lookahead) { while (phase < lookahead) {
phase = round ? Math.round(phase * precision) / precision : phase; phase = Math.round(phase * precision) / precision;
callback(phase, duration, tick, t); // callback has to skip / handle phase < t! phase >= t && callback(phase, duration, tick);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration phase += duration; // increment phase by duration
tick++; tick++;
} }
@@ -37,10 +35,7 @@ function createClock(
onTick(); onTick();
intervalID = setInterval(onTick, interval * 1000); intervalID = setInterval(onTick, interval * 1000);
}; };
const clear = () => { const clear = () => intervalID !== undefined && clearInterval(intervalID);
intervalID !== undefined && clearInterval(intervalID);
intervalID = undefined;
};
const pause = () => clear(); const pause = () => clear();
const stop = () => { const stop = () => {
tick = 0; tick = 0;
+1 -3
View File
@@ -152,14 +152,12 @@ export const csoundm = register('csoundm', (instrument, pat) => {
const p2 = tidal_time - getAudioContext().currentTime; const p2 = tidal_time - getAudioContext().currentTime;
const p3 = hap.duration.valueOf() + 0; const p3 = hap.duration.valueOf() + 0;
const frequency = getFrequency(hap); const frequency = getFrequency(hap);
let { gain = 1, velocity = 0.9 } = hap.value;
velocity = gain * velocity;
// Translate frequency to MIDI key number _without_ rounding. // Translate frequency to MIDI key number _without_ rounding.
const C4 = 261.62558; const C4 = 261.62558;
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0; let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
const p4 = octave * 12.0 - 36.0; const p4 = octave * 12.0 - 36.0;
// We prefer floating point precision, but over the MIDI range [0, 127]. // We prefer floating point precision, but over the MIDI range [0, 127].
const p5 = 127 * velocity; const p5 = 127 * (hap.context?.velocity ?? 0.9);
// The Strudel controls as a string. // The Strudel controls as a string.
const p6 = Object.entries({ ...hap.value, frequency }) const p6 = Object.entries({ ...hap.value, frequency })
.flat() .flat()
+4 -6
View File
@@ -6,12 +6,10 @@ const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0; const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) { Pattern.prototype.midi = function (output) {
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { return this.onTrigger((time, hap, currentTime, cps) => {
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value; const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow const offset = (time - currentTime) * 1000;
const latency = 0.034; const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
const offset = (targetTime - currentTime + latency) * 1000;
velocity = Math.floor(gain * velocity * 100);
const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10); const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
const roundedOffset = Math.round(offset); const roundedOffset = Math.round(offset);
const midichan = (hap.value.midichan ?? 1) - 1; const midichan = (hap.value.midichan ?? 1) - 1;
-9
View File
@@ -1,9 +0,0 @@
# @strudel/canvas
Helpers for drawing with the Canvas API and Strudel
## Install
```sh
npm i @strudel/canvas --save
```
-6
View File
@@ -1,6 +0,0 @@
export * from './animate.mjs';
export * from './color.mjs';
export * from './draw.mjs';
export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './pitchwheel.mjs';
-37
View File
@@ -1,37 +0,0 @@
{
"name": "@strudel/draw",
"version": "1.0.1",
"description": "Helpers for drawing with Strudel",
"main": "index.mjs",
"type": "module",
"publishConfig": {
"main": "dist/index.mjs"
},
"scripts": {
"build": "vite build",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*"
},
"devDependencies": {
"vite": "^5.0.10"
}
}
-127
View File
@@ -1,127 +0,0 @@
import { Pattern, midiToFreq, getFrequency } from '@strudel/core';
import { getTheme, getDrawContext } from './draw.mjs';
const c = midiToFreq(36);
const circlePos = (cx, cy, radius, angle) => {
angle = angle * Math.PI * 2;
const x = Math.sin(angle) * radius + cx;
const y = Math.cos(angle) * radius + cy;
return [x, y];
};
const freq2angle = (freq, root) => {
return 0.5 - (Math.log2(freq / root) % 1);
};
export function pitchwheel({
haps,
ctx,
id,
hapcircles = 1,
circle = 0,
edo = 12,
root = c,
thickness = 3,
hapRadius = 6,
mode = 'flake',
margin = 10,
} = {}) {
const connectdots = mode === 'polygon';
const centerlines = mode === 'flake';
const w = ctx.canvas.width;
const h = ctx.canvas.height;
ctx.clearRect(0, 0, w, h);
const color = getTheme().foreground;
const size = Math.min(w, h);
const radius = size / 2 - thickness / 2 - hapRadius - margin;
const centerX = w / 2;
const centerY = h / 2;
if (id) {
haps = haps.filter((hap) => hap.hasTag(id));
}
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.globalAlpha = 1;
ctx.lineWidth = thickness;
if (circle) {
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.stroke();
}
if (edo) {
Array.from({ length: edo }, (_, i) => {
const angle = freq2angle(root * Math.pow(2, i / edo), root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
ctx.beginPath();
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
});
ctx.stroke();
}
let shape = [];
ctx.lineWidth = hapRadius;
haps.forEach((hap) => {
let freq;
try {
freq = getFrequency(hap);
} catch (err) {
return;
}
const angle = freq2angle(freq, root);
const [x, y] = circlePos(centerX, centerY, radius, angle);
const hapColor = hap.value.color || color;
ctx.strokeStyle = hapColor;
ctx.fillStyle = hapColor;
const { velocity = 1, gain = 1 } = hap.value || {};
const alpha = velocity * gain;
ctx.globalAlpha = alpha;
shape.push([x, y, angle, hapColor, alpha]);
ctx.beginPath();
if (hapcircles) {
ctx.moveTo(x + hapRadius, y);
ctx.arc(x, y, hapRadius, 0, 2 * Math.PI);
ctx.fill();
}
if (centerlines) {
ctx.moveTo(centerX, centerY);
ctx.lineTo(x, y);
}
ctx.stroke();
});
ctx.strokeStyle = color;
ctx.globalAlpha = 1;
if (connectdots && shape.length) {
shape = shape.sort((a, b) => a[2] - b[2]);
ctx.beginPath();
ctx.moveTo(shape[0][0], shape[0][1]);
shape.forEach(([x, y, _, color, alpha]) => {
ctx.strokeStyle = color;
ctx.globalAlpha = alpha;
ctx.lineTo(x, y);
});
ctx.lineTo(shape[0][0], shape[0][1]);
ctx.stroke();
}
return;
}
Pattern.prototype.pitchwheel = function (options = {}) {
let { ctx = getDrawContext(), id = 1 } = options;
return this.tag(id).onPaint((_, time, haps) =>
pitchwheel({
...options,
time,
ctx,
haps: haps.filter((hap) => hap.isActive(time)),
id,
}),
);
};
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+19 -50
View File
@@ -2,63 +2,32 @@
This package contains a embeddable web component for the Strudel REPL. This package contains a embeddable web component for the Strudel REPL.
## Usage via Script Tag ## Usage
Use this code in any HTML file: Either install with `npm i @strudel/embed` or just use a cdn to import the script:
```html ```html
<script src="https://unpkg.com/@strudel/embed@latest"></script> <script src="https://unpkg.com/@strudel/embed@latest"></script>
<strudel-repl> <strudel-repl>
<!-- <!--
setcps(1) note(`[[e5 [b4 c5] d5 [c5 b4]]
n("<0 1 2 3 4>*8").scale('G4 minor') [a4 [a4 c5] e5 [d5 c5]]
.s("gm_lead_6_voice") [b4 [~ c5] d5 e5]
.clip(sine.range(.2,.8).slow(8)) [c5 a4 a4 ~]
.jux(rev) [[~ d5] [~ f5] a5 [g5 f5]]
.room(2) [e5 [~ c5] e5 [d5 c5]]
.sometimes(add(note("12"))) [b4 [b4 c5] d5 e5]
.lpf(perlin.range(200,20000).slow(4)) [c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]`).slow(16)
--> -->
</strudel-repl> </strudel-repl>
``` ```
This will load the strudel website in an iframe, using the code provided within the HTML comments `<!-- -->`. Note that the Code is placed inside HTML comments to prevent the browser from treating it as HTML.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<div id="strudel"></div>
<script>
let editor = document.createElement('strudel-repl');
editor.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(editor);
</script>
```
When you're using JSX, you could also use the `code` attribute in your markup:
```html
<script src="https://unpkg.com/@strudel/embed@1.0.2"></script>
<strudel-repl code={`
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
`}></strudel-repl>
```
+1 -1
View File
@@ -4,7 +4,7 @@ class Strudel extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
setTimeout(() => { setTimeout(() => {
const code = this.getAttribute('code') || (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim(); const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`; const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`; // const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
+15 -24
View File
@@ -1,49 +1,40 @@
import { getDrawContext } from '@strudel/draw'; import { getDrawContext } from '@strudel/core';
import { controls } from '@strudel/core';
let latestOptions; let latestOptions;
let hydra;
function appendCanvas(c) {
const { canvas: testCanvas } = getDrawContext();
c.canvas.id = 'hydra-canvas';
c.canvas.style.position = 'fixed';
c.canvas.style.top = '0px';
testCanvas.after(c.canvas);
return testCanvas;
}
export async function initHydra(options = {}) { export async function initHydra(options = {}) {
// reset if options have changed since last init // reset if options have changed since last init
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) { if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
document.getElementById('hydra-canvas')?.remove(); document.getElementById('hydra-canvas').remove();
} }
latestOptions = options; latestOptions = options;
//load and init hydra //load and init hydra
if (!document.getElementById('hydra-canvas')) { if (!document.getElementById('hydra-canvas')) {
console.log('reinit..');
const { const {
src = 'https://unpkg.com/hydra-synth', src = 'https://unpkg.com/hydra-synth',
feedStrudel = false, feedStrudel = false,
contextType = 'webgl',
pixelRatio = 1,
pixelated = true,
...hydraConfig ...hydraConfig
} = { } = { detectAudio: false, ...options };
detectAudio: false,
...options,
};
const { canvas } = getDrawContext('hydra-canvas', { contextType, pixelRatio, pixelated });
hydraConfig.canvas = canvas;
await import(/* @vite-ignore */ src); await import(/* @vite-ignore */ src);
hydra = new Hydra(hydraConfig); const hydra = new Hydra(hydraConfig);
if (feedStrudel) { if (feedStrudel) {
const { canvas } = getDrawContext(); const { canvas } = getDrawContext();
canvas.style.display = 'none'; canvas.style.display = 'none';
hydra.synth.s0.init({ src: canvas }); hydra.synth.s0.init({ src: canvas });
} }
appendCanvas(hydra);
} }
} }
export function clearHydra() {
if (hydra) {
hydra.hush();
}
globalThis.s0?.clear();
document.getElementById('hydra-canvas')?.remove();
globalThis.speed = controls.speed;
globalThis.shape = controls.shape;
}
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value; export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
-1
View File
@@ -34,7 +34,6 @@
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"hydra-synth": "^1.3.29" "hydra-synth": "^1.3.29"
}, },
"devDependencies": { "devDependencies": {
+7 -8
View File
@@ -112,25 +112,24 @@ Pattern.prototype.midi = function (output) {
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
}); });
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) { if (!WebMidi.enabled) {
console.log('not enabled'); console.log('not enabled');
return; return;
} }
const device = getDevice(output, WebMidi.outputs); const device = getDevice(output, WebMidi.outputs);
hap.ensureObjectValue(); hap.ensureObjectValue();
//magic number to get audio engine to line up, can probably be calculated somehow
const latency = 0.034; const offset = (time - currentTime) * 1000;
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output // passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
const timeOffsetString = `+${(targetTime - currentTime + latency) * 1000}`; const timeOffsetString = `+${offset}`;
// destructure value // destructure value
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value; const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
velocity = gain * velocity;
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length // note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
const duration = (hap.duration.valueOf() / cps) * 1000 - 10; const duration = Math.floor((hap.duration.valueOf() / cps) * 1000 - 10);
if (note != null) { if (note != null) {
const midiNumber = typeof note === 'number' ? note : noteToMidi(note); const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
const midiNote = new Note(midiNumber, { attack: velocity, duration }); const midiNote = new Note(midiNumber, { attack: velocity, duration });
+14 -26
View File
@@ -308,7 +308,7 @@ function peg$parse(input, options) {
} }
return result; return result;
}; };
var peg$f17 = function(tactus, s) { return new PatternStub(s, 'fastcat', undefined, !!tactus); }; var peg$f17 = function(s) { return new PatternStub(s, 'fastcat'); };
var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; }; var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; };
var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; }; var peg$f19 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; }; var peg$f20 = function(tail) { return { alignment: 'feet', list: tail, seed: seed++ }; };
@@ -1477,36 +1477,24 @@ function peg$parse(input, options) {
} }
function peg$parsesequence() { function peg$parsesequence() {
var s0, s1, s2, s3; var s0, s1, s2;
s0 = peg$currPos; s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 94) { s1 = [];
s1 = peg$c9; s2 = peg$parseslice_with_ops();
peg$currPos++; if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = peg$parseslice_with_ops();
}
} else { } else {
s1 = peg$FAILED; s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
} }
if (s1 === peg$FAILED) { if (s1 !== peg$FAILED) {
s1 = null;
}
s2 = [];
s3 = peg$parseslice_with_ops();
if (s3 !== peg$FAILED) {
while (s3 !== peg$FAILED) {
s2.push(s3);
s3 = peg$parseslice_with_ops();
}
} else {
s2 = peg$FAILED;
}
if (s2 !== peg$FAILED) {
peg$savedPos = s0; peg$savedPos = s0;
s0 = peg$f17(s1, s2); s1 = peg$f17(s1);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
} }
s0 = s1;
return s0; return s0;
} }
@@ -2488,10 +2476,10 @@ function peg$parse(input, options) {
this.location_ = location(); this.location_ = location();
} }
var PatternStub = function(source, alignment, seed, tactus) var PatternStub = function(source, alignment, seed)
{ {
this.type_ = "pattern"; this.type_ = "pattern";
this.arguments_ = { alignment: alignment, tactus: tactus }; this.arguments_ = { alignment: alignment };
if (seed !== undefined) { if (seed !== undefined) {
this.arguments_.seed = seed; this.arguments_.seed = seed;
} }
+4 -4
View File
@@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th
this.location_ = location(); this.location_ = location();
} }
var PatternStub = function(source, alignment, seed, tactus) var PatternStub = function(source, alignment, seed)
{ {
this.type_ = "pattern"; this.type_ = "pattern";
this.arguments_ = { alignment: alignment, tactus: tactus }; this.arguments_ = { alignment: alignment };
if (seed !== undefined) { if (seed !== undefined) {
this.arguments_.seed = seed; this.arguments_.seed = seed;
} }
@@ -165,8 +165,8 @@ slice_with_ops = s:slice ops:slice_op*
} }
// a sequence is a combination of one or more successive slices (as an array) // a sequence is a combination of one or more successive slices (as an array)
sequence = tactus:'^'? s:(slice_with_ops)+ sequence = s:(slice_with_ops)+
{ return new PatternStub(s, 'fastcat', undefined, !!tactus); } { return new PatternStub(s, 'fastcat'); }
// a stack is a series of vertically aligned sequence, separated by a comma // a stack is a series of vertically aligned sequence, separated by a comma
stack_tail = tail:(comma @sequence)+ stack_tail = tail:(comma @sequence)+
+20 -56
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import * as krill from './krill-parser.js'; import * as krill from './krill-parser.js';
import * as strudel from '@strudel/core'; import * as strudel from '@strudel/core';
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
const randOffset = 0.0003; const randOffset = 0.0003;
@@ -89,79 +88,44 @@ export function patternifyAST(ast, code, onEnter, offset = 0) {
resolveReplications(ast); resolveReplications(ast);
const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter)); const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
const alignment = ast.arguments_.alignment; const alignment = ast.arguments_.alignment;
const with_tactus = children.filter((child) => child.__tactus_source); if (alignment === 'stack') {
let pat; return strudel.stack(...children);
switch (alignment) {
case 'stack': {
pat = strudel.stack(...children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
} }
break; if (alignment === 'polymeter_slowcat') {
const aligned = children.map((child) => child._slow(strudel.Fraction(child.__weight ?? 1)));
return strudel.stack(...aligned);
} }
case 'polymeter_slowcat': { if (alignment === 'polymeter') {
pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
}
break;
}
case 'polymeter': {
// polymeter // polymeter
const stepsPerCycle = ast.arguments_.stepsPerCycle const stepsPerCycle = ast.arguments_.stepsPerCycle
? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1)); : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight)))); const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
pat = strudel.stack(...aligned); return strudel.stack(...aligned);
break;
} }
case 'rand': { if (alignment === 'rand') {
pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
if (with_tactus.length) {
pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus)));
} }
break; if (alignment === 'feet') {
return strudel.fastcat(...children);
} }
case 'feet': {
pat = strudel.fastcat(...children);
break;
}
default: {
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (weightedChildren) { if (weightedChildren) {
const weightSum = ast.source_.reduce( const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
(sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)), const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
strudel.Fraction(0), pat.__weight = weightSum;
); return pat;
pat = strudel.timeCat(
...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
);
pat.__weight = weightSum; // for polymeter
pat.tactus = weightSum;
if (with_tactus.length) {
pat.tactus = pat.tactus.mul(lcm(...with_tactus.map((x) => Fraction(x.tactus))));
}
} else {
pat = strudel.sequence(...children);
pat.tactus = children.length;
}
if (ast.arguments_.tactus) {
pat.__tactus_source = true;
}
}
}
if (with_tactus.length) {
pat.__tactus_source = true;
} }
const pat = strudel.sequence(...children);
pat.__weight = children.length;
return pat; return pat;
} }
case 'element': { case 'element': {
1;
return enter(ast.source_); return enter(ast.source_);
} }
case 'atom': { case 'atom': {
if (ast.source_ === '~' || ast.source_ === '-') { if (ast.source_ === '~') {
return strudel.silence; return strudel.silence;
} }
if (!ast.location_) { if (!ast.location_) {
@@ -197,7 +161,7 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => {
}; };
// takes quoted mini string, returns ast // takes quoted mini string, returns ast
export const mini2ast = (code, start = 0, userCode = code) => { export const mini2ast = (code, start, userCode) => {
try { try {
return krill.parse(code); return krill.parse(code);
} catch (error) { } catch (error) {
-13
View File
@@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs'; import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs';
import '@strudel/core/euclid.mjs'; import '@strudel/core/euclid.mjs';
import { Fraction } from '@strudel/core/index.mjs';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
describe('mini', () => { describe('mini', () => {
@@ -118,9 +117,6 @@ describe('mini', () => {
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~'); checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~'); checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
}); });
it('supports the - alias for ~', () => {
expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]'));
});
it('supports the ? operator', () => { it('supports the ? operator', () => {
expect( expect(
mini('a?') mini('a?')
@@ -208,15 +204,6 @@ describe('mini', () => {
it('_ and @ are almost interchangeable', () => { it('_ and @ are almost interchangeable', () => {
expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3')); expect(minS('a @ b @ @')).toEqual(minS('a _2 b _3'));
}); });
it('supports ^ tactus marking', () => {
expect(mini('a [^b c]').tactus).toEqual(Fraction(4));
expect(mini('[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('^[a b c] [d [e f]]').tactus).toEqual(Fraction(2));
expect(mini('[a b c] [d [^e f]]').tactus).toEqual(Fraction(8));
expect(mini('[a b c] [^d [e f]]').tactus).toEqual(Fraction(4));
expect(mini('[^a b c] [^d [e f]]').tactus).toEqual(Fraction(12));
expect(mini('[^a b c] [d [^e f]]').tactus).toEqual(Fraction(24));
});
}); });
describe('getLeafLocation', () => { describe('getLeafLocation', () => {
+1 -92
View File
@@ -2,95 +2,4 @@
The Strudel REPL as a web component. The Strudel REPL as a web component.
## Add Script Tag [Usage example](https://github.com/tidalcycles/strudel/blob/main/examples/buildless/web-component-no-iframe.html)
First place this script tag once in your HTML:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
```
You can also pin the version like this:
```html
<script src="https://unpkg.com/@strudel/repl@1.0.2"></script>
```
This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase.
See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions.
## Use Web Component
When you've added the script tag, you can use the `strudel-editor` web component:
```html
<strudel-editor>
<!--
setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))
-->
</strudel-editor>
```
This will load the Strudel REPL using the code provided within the HTML comments `<!-- -->`.
The HTML comments are needed to make sure the browser won't interpret it as HTML.
Alternatively you can create a REPL from JavaScript like this:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute(
'code',
`setcps(1)
n("<0 1 2 3 4>*8").scale('G4 minor')
.s("gm_lead_6_voice")
.clip(sine.range(.2,.8).slow(8))
.jux(rev)
.room(2)
.sometimes(add(note("12")))
.lpf(perlin.range(200,20000).slow(4))`,
);
document.getElementById('strudel').append(repl);
</script>
```
## Interacting with the REPL
If you get a hold of the `strudel-editor` element, you can interact with the strudel REPL from Javascript:
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<strudel-editor id="repl">
<!-- ... -->
</strudel-editor>
<script>
const repl = document.getElementById('repl');
console.log(repl.editor);
</script>
```
or
```html
<script src="https://unpkg.com/@strudel/repl@latest"></script>
<div id="strudel"></div>
<script>
const repl = document.createElement('strudel-editor');
repl.setAttribute('code', `...`);
document.getElementById('strudel').append(repl);
console.log(repl.editor);
</script>
```
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL.
For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code.
-1
View File
@@ -35,7 +35,6 @@
"dependencies": { "dependencies": {
"@strudel/codemirror": "workspace:*", "@strudel/codemirror": "workspace:*",
"@strudel/core": "workspace:*", "@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/hydra": "workspace:*", "@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*", "@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*", "@strudel/mini": "workspace:*",
+2 -2
View File
@@ -1,4 +1,4 @@
import { noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel/core'; import { controls, noteToMidi, valueToMidi, Pattern, evalScope } from '@strudel/core';
import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio'; import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel/webaudio';
import * as core from '@strudel/core'; import * as core from '@strudel/core';
@@ -6,7 +6,6 @@ export async function prebake() {
const modulesLoading = evalScope( const modulesLoading = evalScope(
// import('@strudel/core'), // import('@strudel/core'),
core, core,
import('@strudel/draw'),
import('@strudel/mini'), import('@strudel/mini'),
import('@strudel/tonal'), import('@strudel/tonal'),
import('@strudel/webaudio'), import('@strudel/webaudio'),
@@ -18,6 +17,7 @@ export async function prebake() {
// import('@strudel/serial'), // import('@strudel/serial'),
// import('@strudel/csound'), // import('@strudel/csound'),
// import('@strudel/osc'), // import('@strudel/osc'),
controls, // sadly, this cannot be exported from core directly (yet)
); );
// load samples // load samples
const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
+11 -3
View File
@@ -1,5 +1,4 @@
import { silence } from '@strudel/core'; import { getDrawContext, silence } from '@strudel/core';
import { getDrawContext } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput } from '@strudel/webaudio'; import { getAudioContext, webaudioOutput } from '@strudel/webaudio';
import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror'; import { StrudelMirror, codemirrorSettings } from '@strudel/codemirror';
@@ -41,8 +40,17 @@ if (typeof HTMLElement !== 'undefined') {
initialCode: '// LOADING', initialCode: '// LOADING',
pattern: silence, pattern: silence,
drawTime, drawTime,
drawContext, onDraw: (haps, time, frame, painters) => {
painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
painters?.forEach((painter) => {
// ctx time haps drawTime paintOptions
painter(drawContext, time, haps, drawTime, { clear: false });
});
},
prebake, prebake,
afterEval: ({ code }) => {
// window.location.hash = '#' + code2hash(code);
},
onUpdateState: (state) => { onUpdateState: (state) => {
const event = new CustomEvent('update', { const event = new CustomEvent('update', {
detail: state, detail: state,
-22
View File
@@ -1,22 +0,0 @@
# @strudel/sampler
This package allows you to serve your samples on disk to the strudel REPL.
```sh
cd ~/your/samples/
npx @strudel/sampler
```
This will run a server on `http://localhost:5432`.
You can now load the samples via:
```js
samples('http://localhost:5432')
```
## Options
```sh
LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port
```
-19
View File
@@ -1,19 +0,0 @@
{
"name": "@strudel/sampler",
"version": "0.0.8",
"description": "",
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bin": "./sample-server.mjs",
"type": "module",
"dependencies": {
"cowsay": "^1.6.0"
}
}
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env node
import cowsay from 'cowsay';
import { createReadStream } from 'fs';
import { readdir } from 'fs/promises';
import http from 'http';
import { join } from 'path';
import os from 'os';
// eslint-disable-next-line
const LOG = !!process.env.LOG || false;
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
async function getFilesInDirectory(directory) {
let files = [];
const dirents = await readdir(directory, { withFileTypes: true });
for (const dirent of dirents) {
const fullPath = join(directory, dirent.name);
if (dirent.isDirectory()) {
if (dirent.name.startsWith('.')) {
LOG && console.warn(`ignore hidden folder: ${fullPath}`);
continue;
}
try {
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
);
files = files.concat(subFiles);
LOG && console.log(`${dirent.name} (${subFiles.length})`);
} catch (err) {
LOG && console.warn(`skipped due to error: ${fullPath}`);
}
} else {
files.push(fullPath);
}
}
return files;
}
async function getBanks(directory) {
// const directory = resolve(__dirname, '.');
let files = await getFilesInDirectory(directory);
let banks = {};
files = files.map((url) => {
const [bank] = url.split('/').slice(-2);
banks[bank] = banks[bank] || [];
url = url.replace(directory, '');
banks[bank].push(url);
return url;
});
banks._base = `http://localhost:5432`;
return { banks, files };
}
// eslint-disable-next-line
const directory = process.cwd();
const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory);
if (req.url === '/') {
res.setHeader('Content-Type', 'application/json');
return res.end(JSON.stringify(banks));
}
let subpath = decodeURIComponent(req.url);
if (!files.includes(subpath)) {
res.statusCode = 404;
res.end('File not found');
return;
}
const filePath = join(directory, subpath);
const readStream = createReadStream(filePath);
readStream.on('error', (err) => {
res.statusCode = 500;
res.end('Internal server error');
console.error(err);
});
readStream.pipe(res);
});
// eslint-disable-next-line
const PORT = process.env.PORT || 5432;
const IP_ADDRESS = '0.0.0.0';
let IP;
const networkInterfaces = os.networkInterfaces();
Object.keys(networkInterfaces).forEach((key) => {
networkInterfaces[key].forEach((networkInterface) => {
if (networkInterface.family === 'IPv4' && !networkInterface.internal) {
IP = networkInterface.address;
}
});
});
if (!IP) {
console.error("Unable to determine server's IP address.");
// eslint-disable-next-line
process.exit(1);
}
server.listen(PORT, IP_ADDRESS, () => {
console.log(`@strudel/sampler is now serving audio files from:
${directory}
To use them in the Strudel REPL, run:
samples('http://localhost:${PORT}')
Or on a machine in the same network:
samples('http://${IP}:${PORT}')
`);
});
+1
View File
@@ -0,0 +1 @@
rustsaw/target
+57
View File
@@ -0,0 +1,57 @@
# superdough-wasm
This is just a very early experiment to find out how to run wasm in an AudioWorklet.
WASM can be compiled from several languages, which are tested here..
## zig
<https://dev.to/sleibrock/webassembly-with-zig-part-1-4onm>
```sh
# (re)compile dsp.zig
brew install zig # prequisite
cd zigsaw
zig build-lib zigsaw.zig -target wasm32-freestanding -dynamic -rdynamic -O ReleaseSmall # build
npx http-server .. -o # run
```
wasm file size: 690B
## rust
<https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_Wasm>
```sh
# https://www.rust-lang.org/tools/install
cargo install wasm-pack # prequisite
cd rustsaw
wasm-pack build --target bundler # build
npx http-server .. -o # run
```
wasm file size: 653B
## c
<https://emscripten.org/docs/getting_started/Tutorial.html>
```sh
# brew install emscripten
cd csaw
emcc -O2 csaw.c -o csaw # build
npx http-server .. -o
```
wasm file size: 680B
## assemblyscript
<https://www.assemblyscript.org/getting-started.html#setting-up-a-new-project>
```sh
cd ascsaw
# npm i
npm run asbuild # build
```
wasm file size: 122B !
@@ -0,0 +1,22 @@
{
"targets": {
"debug": {
"outFile": "build/debug.wasm",
"textFile": "build/debug.wat",
"sourceMap": true,
"debug": true
},
"release": {
"outFile": "build/release.wasm",
"textFile": "build/release.wat",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 2,
"converge": false,
"noAssert": false
}
},
"options": {
"bindings": "esm"
}
}
@@ -0,0 +1,3 @@
export function saw(t: f64, f: f64): f64 {
return (((f * t * 1.0) % 1.0) - 0.5) * 2.0;
}
@@ -0,0 +1,6 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}
@@ -0,0 +1,2 @@
*
!.gitignore
+54
View File
@@ -0,0 +1,54 @@
{
"name": "ascsaw",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ascsaw",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"assemblyscript": "^0.27.22"
}
},
"node_modules/assemblyscript": {
"version": "0.27.22",
"resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.27.22.tgz",
"integrity": "sha512-6ClobsR4Hxn6K0daYp/+n9qWTqVbpdVeSGSVDqRvUEz66vvFb8atS6nLm+fnQ54JXuXmzLQy0uWYYgB8G59btQ==",
"dev": true,
"dependencies": {
"binaryen": "116.0.0-nightly.20231102",
"long": "^5.2.1"
},
"bin": {
"asc": "bin/asc.js",
"asinit": "bin/asinit.js"
},
"engines": {
"node": ">=16",
"npm": ">=7"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/assemblyscript"
}
},
"node_modules/binaryen": {
"version": "116.0.0-nightly.20231102",
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-116.0.0-nightly.20231102.tgz",
"integrity": "sha512-aPU9tlKdw/gcXx6u4PxtDgOtGjg/ZKnYdk23ctYb70GxZgPhWnGWmnBt01aV5dt5yFFo2V4rbB7SzpSFhViFQA==",
"dev": true,
"bin": {
"wasm-opt": "bin/wasm-opt",
"wasm2js": "bin/wasm2js"
}
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
"dev": true
}
}
}
@@ -0,0 +1,25 @@
{
"name": "ascsaw",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "node tests",
"asbuild:debug": "asc assembly/index.ts --target debug",
"asbuild:release": "asc assembly/index.ts --target release",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"start": "npx serve ."
},
"author": "",
"license": "ISC",
"devDependencies": {
"assemblyscript": "^0.27.22"
},
"type": "module",
"exports": {
".": {
"import": "./build/release.js",
"types": "./build/release.d.ts"
}
}
}
+1
View File
@@ -0,0 +1 @@
csaw
+7
View File
@@ -0,0 +1,7 @@
#include <math.h>
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
double saw(double t, double f) {
return fmod((f * t * 1.0), 1.0) - 0.5 * 2.0;
}
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<title>WASM AudioWorklet Demo</title>
</head>
<body>
<button id="play">play</button>
<input type="range" min="55" max="880" id="freq" />
</body>
<script src="./main.js"></script>
</html>
+24
View File
@@ -0,0 +1,24 @@
let ac;
document.getElementById('play').addEventListener('click', async () => {
ac = ac || new AudioContext();
await ac.resume();
await ac.audioWorklet.addModule('./worklet.js');
const node = new AudioWorkletNode(ac, 'saw-processor');
//let res = await fetch('./zigsaw/zigsaw.wasm');
// let res = await fetch('./csaw/csaw.wasm');
let res = await fetch('./ascsaw/build/release.wasm');
//let res = await fetch('./rustsaw/pkg/rustsaw_bg.wasm');
const buffer = await res.arrayBuffer();
node.port.onmessage = (e) => {
if (e.data === 'OK') {
console.log('worklet ready');
}
};
node.port.postMessage({ webassembly: buffer });
node.connect(ac.destination);
document.getElementById('freq').addEventListener('input', async (e) => {
node.port.postMessage({ frequency: e.target.value });
});
});
+123
View File
@@ -0,0 +1,123 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustsaw"
version = "0.1.0"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasm-bindgen"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
@@ -0,0 +1,13 @@
[package]
name = "rustsaw"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
@@ -0,0 +1,6 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn saw(t: f64, f: f64) -> f64 {
return (((f * t * 1.0) % 1.0) - 0.5) * 2.0;
}
+38
View File
@@ -0,0 +1,38 @@
class SawProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.t = 0; // samples passed
this.f = 110;
this.port.onmessage = (e) => {
const key = Object.keys(e.data)[0];
const value = e.data[key];
switch (key) {
case 'webassembly':
WebAssembly.instantiate(value, this.importObject).then((result) => {
this.api = result.instance.exports;
this.port.postMessage('OK');
});
break;
case 'frequency':
this.f = value;
}
};
}
process(inputs, outputs, parameters) {
if (this.api) {
const output = outputs[0];
for (let i = 0; i < output[0].length; i++) {
let t = this.t;
let out = 0;
out = this.api.saw(t / 44100, this.f);
output.forEach((channel) => {
channel[i] = out;
});
this.t++;
}
}
return true;
}
}
registerProcessor('saw-processor', SawProcessor);
Binary file not shown.
@@ -0,0 +1,5 @@
const std = @import("std");
export fn saw(t: f64, f: f64) f64 {
return ((@mod(f * t, 1.0)) - 0.5) * 2.0;
}
+1 -1
View File
@@ -65,7 +65,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `bandf`: band pass filter cutoff - `bandf`: band pass filter cutoff
- `bandq`: band pass filter resonance - `bandq`: band pass filter resonance
- `crush`: amplitude bit crusher using given number of bits - `crush`: amplitude bit crusher using given number of bits
- `distort`: distortion effect. might get loud! - `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
- `pan`: stereo panning from 0 (left) to 1 (right) - `pan`: stereo panning from 0 (left) to 1 (right)
- `phaser`: sets the speed of the modulation - `phaser`: sets the speed of the modulation
- `phaserdepth`: the amount the signal is affected by the phaser effect. - `phaserdepth`: the amount the signal is affected by the phaser effect.
-73
View File
@@ -186,76 +186,3 @@ export function getVibratoOscillator(param, value, t) {
return vibratoOscillator; return vibratoOscillator;
} }
} }
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = audioContext.createConstantSource();
constantNode.start(startTime);
constantNode.stop(stopTime);
constantNode.onended = () => {
onComplete();
};
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
osc.type = type;
osc.frequency.value = freq;
osc.start();
const g = new GainNode(ctx, { gain: range });
osc.connect(g); // -range, range
return { node: g, stop: (t) => osc.stop(t) };
};
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = frequencyparam.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
export function applyFM(param, value, begin) {
const {
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let modulator;
let stop = () => {};
if (fmModulationIndex) {
const ac = getAudioContext();
const envGain = ac.createGain();
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform);
modulator = fmmod.node;
stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(param);
} else {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = begin + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
begin,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(param);
}
}
return { stop };
}
+1 -4
View File
@@ -194,9 +194,6 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
if (sampleMap.startsWith('github:')) { if (sampleMap.startsWith('github:')) {
sampleMap = githubPath(sampleMap, 'strudel.json'); sampleMap = githubPath(sampleMap, 'strudel.json');
} }
if (sampleMap.startsWith('local:')) {
sampleMap = `http://localhost:5432`;
}
if (sampleMap.startsWith('shabda:')) { if (sampleMap.startsWith('shabda:')) {
let [_, path] = sampleMap.split('shabda:'); let [_, path] = sampleMap.split('shabda:');
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`; sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
@@ -254,7 +251,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
nudge = 0, // TODO: is this in seconds? nudge = 0, // TODO: is this in seconds?
cut, cut,
loop, loop,
clip = undefined, // if set, samples will be cut off when the hap ends clip = undefined, // if 1, samples will be cut off when the hap ends
n = 0, n = 0,
note, note,
speed = 1, // sample playback speed speed = 1, // sample playback speed
+23 -35
View File
@@ -50,8 +50,8 @@ function loadWorklets() {
return workletsLoading; return workletsLoading;
} }
export function getWorklet(ac, processor, params, config) { function getWorklet(ac, processor, params) {
const node = new AudioWorkletNode(ac, processor, config); const node = new AudioWorkletNode(ac, processor);
Object.entries(params).forEach(([key, value]) => { Object.entries(params).forEach(([key, value]) => {
node.parameters.get(key).value = value; node.parameters.get(key).value = value;
}); });
@@ -215,35 +215,35 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
return reverbs[orbit]; return reverbs[orbit];
} }
export let analysers = {}, export let analyser, analyserData /* s = {} */;
analysersData = {};
export function getAnalyserById(id, fftSize = 1024) { export function getAnalyser(/* orbit, */ fftSize = 2048) {
if (!analysers[id]) { if (!analyser /*s [orbit] */) {
// make sure this doesn't happen too often as it piles up garbage
const analyserNode = getAudioContext().createAnalyser(); const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize; analyserNode.fftSize = fftSize;
// getDestination().connect(analyserNode); // getDestination().connect(analyserNode);
analysers[id] = analyserNode; analyser /* s[orbit] */ = analyserNode;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); //analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
} }
if (analysers[id].fftSize !== fftSize) { if (analyser /* s[orbit] */.fftSize !== fftSize) {
analysers[id].fftSize = fftSize; analyser /* s[orbit] */.fftSize = fftSize;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount); //analyserData = new Uint8Array(analyser.frequencyBinCount);
analyserData = new Float32Array(analyser.frequencyBinCount);
} }
return analysers[id]; return analyser /* s[orbit] */;
} }
export function getAnalyzerData(type = 'time', id = 1) { export function getAnalyzerData(type = 'time') {
const getter = { const getter = {
time: () => analysers[id]?.getFloatTimeDomainData(analysersData[id]), time: () => analyser?.getFloatTimeDomainData(analyserData),
frequency: () => analysers[id]?.getFloatFrequencyData(analysersData[id]), frequency: () => analyser?.getFloatFrequencyData(analyserData),
}[type]; }[type];
if (!getter) { if (!getter) {
throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`); throw new Error(`getAnalyzerData: ${type} not supported. use one of ${Object.keys(getter).join(', ')}`);
} }
getter(); getter();
return analysersData[id]; return analyserData;
} }
function effectSend(input, effect, wet) { function effectSend(input, effect, wet) {
@@ -256,11 +256,9 @@ function effectSend(input, effect, wet) {
export function resetGlobalEffects() { export function resetGlobalEffects() {
delays = {}; delays = {};
reverbs = {}; reverbs = {};
analysers = {};
analysersData = {};
} }
export const superdough = async (value, t, hapDuration) => { export const superdough = async (value, deadline, hapDuration) => {
const ac = getAudioContext(); const ac = getAudioContext();
if (typeof value !== 'object') { if (typeof value !== 'object') {
throw new Error( throw new Error(
@@ -272,13 +270,7 @@ export const superdough = async (value, t, hapDuration) => {
// duration is passed as value too.. // duration is passed as value too..
value.duration = hapDuration; value.duration = hapDuration;
// calculate absolute time // calculate absolute time
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; let t = ac.currentTime + deadline;
if (t < ac.currentTime) {
console.warn(
`[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`,
);
return;
}
// destructure // destructure
let { let {
s = 'triangle', s = 'triangle',
@@ -324,9 +316,6 @@ export const superdough = async (value, t, hapDuration) => {
coarse, coarse,
crush, crush,
shape, shape,
shapevol = 1,
distort,
distortvol = 1,
pan, pan,
vowel, vowel,
delay = 0, delay = 0,
@@ -355,7 +344,7 @@ export const superdough = async (value, t, hapDuration) => {
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior //music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future gain *= velocity; // legacy fix for velocity
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => { const onended = () => {
toDisconnect.forEach((n) => n?.disconnect()); toDisconnect.forEach((n) => n?.disconnect());
@@ -468,8 +457,7 @@ export const superdough = async (value, t, hapDuration) => {
// effects // effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
compressorThreshold !== undefined && compressorThreshold !== undefined &&
chain.push( chain.push(
@@ -520,8 +508,8 @@ export const superdough = async (value, t, hapDuration) => {
// analyser // analyser
let analyserSend; let analyserSend;
if (analyze) { if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); const analyserNode = getAnalyser(/* orbit, */ 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1); analyserSend = effectSend(post, analyserNode, analyze);
} }
// connect chain elements together // connect chain elements together
+86 -133
View File
@@ -1,138 +1,31 @@
import { clamp, midiToFreq, noteToMidi } from './util.mjs'; import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext, getWorklet } from './superdough.mjs'; import { registerSound, getAudioContext } from './superdough.mjs';
import { import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
applyFM,
gainNode,
getADSRValues,
getParamADSR,
getPitchEnvelope,
getVibratoOscillator,
webAudioTimeout,
} from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const getFrequencyFromValue = (value) => { const mod = (freq, range = 1, type = 'sine') => {
let { note, freq } = value; const ctx = getAudioContext();
note = note || 36; const osc = ctx.createOscillator();
if (typeof note === 'string') { osc.type = type;
note = noteToMidi(note); // e.g. c3 => 48 osc.frequency.value = freq;
} osc.start();
// get frequency const g = new GainNode(ctx, { gain: range });
if (!freq && typeof note === 'number') { osc.connect(g); // -range, range
freq = midiToFreq(note); // + 48); return { node: g, stop: (t) => osc.stop(t) };
}
return Number(freq);
}; };
const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
const carrfreq = osc.frequency.value;
const modfreq = carrfreq * harmonicityRatio;
const modgain = modfreq * modulationIndex;
return mod(modfreq, modgain, wave);
};
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
const noises = ['pink', 'white', 'brown', 'crackle']; const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() { export function registerSynthSounds() {
[...waveforms].forEach((s) => { [...waveforms, ...noises].forEach((s) => {
registerSound(
s,
(t, value, onended) => {
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
let sound = getOscillator(s, t, value);
let { node: o, stop, triggerRelease } = sound;
// turn down
const g = gainNode(0.3);
const { duration } = value;
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
const holdEnd = t + duration;
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
const envEnd = holdEnd + release + 0.01;
triggerRelease?.(envEnd);
stop(envEnd);
return {
node,
stop: (releaseTime) => {},
};
},
{ type: 'synth', prebake: true },
);
});
registerSound(
'supersaw',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n, unison = 5, spread = 0.6, detune } = value;
detune = detune ?? n ?? 0.18;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
const voices = clamp(unison, 1, 100);
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
let o = getWorklet(
ac,
'supersaw-oscillator',
{
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
},
{
outputChannelCount: [2],
},
);
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
const fm = applyFM(o.parameters.get('frequency'), value, begin);
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
ac,
() => {
o.disconnect();
envGain.disconnect();
onended();
fm?.stop();
vibratoOscillator?.stop();
},
begin,
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
};
},
{ prebake: true, type: 'synth' },
);
[...noises].forEach((s) => {
registerSound( registerSound(
s, s,
(t, value, onended) => { (t, value, onended) => {
@@ -143,9 +36,12 @@ export function registerSynthSounds() {
); );
let sound; let sound;
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
let { density } = value; let { density } = value;
sound = getNoiseOscillator(s, t, density); sound = getNoiseOscillator(s, t, density);
}
let { node: o, stop, triggerRelease } = sound; let { node: o, stop, triggerRelease } = sound;
@@ -210,7 +106,24 @@ export function waveformN(partials, type) {
// expects one of waveforms as s // expects one of waveforms as s
export function getOscillator(s, t, value) { export function getOscillator(s, t, value) {
let { n: partials, duration, noise = 0 } = value; let {
n: partials,
note,
freq,
noise = 0,
// fm
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'exp',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
duration,
} = value;
let ac = getAudioContext();
let o; let o;
// If no partials are given, use stock waveforms // If no partials are given, use stock waveforms
if (!partials || s === 'sine') { if (!partials || s === 'sine') {
@@ -221,15 +134,55 @@ export function getOscillator(s, t, value) {
else { else {
o = waveformN(partials, s); o = waveformN(partials, s);
} }
// get frequency from note...
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// set frequency // set frequency
o.frequency.value = getFrequencyFromValue(value); o.frequency.value = Number(freq);
o.start(t); o.start(t);
// FM
let stopFm;
let envGain = ac.createGain();
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
const holdEnd = t + duration;
getParamADSR(
envGain.gain,
attack,
decay,
sustain,
release,
0,
1,
t,
holdEnd,
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
);
modulator.connect(envGain);
envGain.connect(o.frequency);
}
stopFm = stop;
}
// Additional oscillator for vibrato effect
let vibratoOscillator = getVibratoOscillator(o.detune, value, t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
// pitch envelope // pitch envelope
getPitchEnvelope(o.detune, value, t, t + duration); getPitchEnvelope(o.detune, value, t, t + duration);
const fmModulator = applyFM(o.frequency, value, t);
let noiseMix; let noiseMix;
if (noise) { if (noise) {
@@ -239,9 +192,9 @@ export function getOscillator(s, t, value) {
return { return {
node: noiseMix?.node || o, node: noiseMix?.node || o,
stop: (time) => { stop: (time) => {
fmModulator.stop(time);
vibratoOscillator?.stop(time); vibratoOscillator?.stop(time);
noiseMix?.stop(time); noiseMix?.stop(time);
stopFm?.(time);
o.stop(time); o.stop(time);
}, },
triggerRelease: (time) => { triggerRelease: (time) => {
+52 -220
View File
@@ -1,5 +1,6 @@
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE // LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
// all the credit goes to dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
// <3
class CoarseProcessor extends AudioWorkletProcessor { class CoarseProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() { static get parameterDescriptors() {
@@ -8,27 +9,28 @@ class CoarseProcessor extends AudioWorkletProcessor {
constructor() { constructor() {
super(); super();
this.notStarted = true;
} }
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
const input = inputs[0]; const input = inputs[0];
const output = outputs[0]; const output = outputs[0];
const coarse = parameters.coarse;
const blockSize = 128; const blockSize = 128;
const hasInput = !(input[0] === undefined);
if (hasInput) {
this.notStarted = false;
output[0][0] = input[0][0];
for (let n = 1; n < blockSize; n++) {
for (let o = 0; o < output.length; o++) {
output[o][n] = n % coarse == 0 ? input[0][n] : output[o][n - 1];
}
}
}
return this.notStarted || hasInput;
}
}
let coarse = parameters.coarse[0] ?? 0;
coarse = Math.max(1, coarse);
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = n % coarse === 0 ? input[i][n] : output[i][n - 1];
}
}
return true;
}
}
registerProcessor('coarse-processor', CoarseProcessor); registerProcessor('coarse-processor', CoarseProcessor);
class CrushProcessor extends AudioWorkletProcessor { class CrushProcessor extends AudioWorkletProcessor {
@@ -38,239 +40,69 @@ class CrushProcessor extends AudioWorkletProcessor {
constructor() { constructor() {
super(); super();
this.notStarted = true;
} }
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
const input = inputs[0]; const input = inputs[0];
const output = outputs[0]; const output = outputs[0];
const crush = parameters.crush;
const blockSize = 128; const blockSize = 128;
const hasInput = !(input[0] === undefined);
let crush = parameters.crush[0] ?? 8; if (hasInput) {
crush = Math.max(1, crush); this.notStarted = false;
if (crush.length === 1) {
if (input[0] == null || output[0] == null) { const x = Math.pow(2, crush[0] - 1);
return false;
}
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) { const value = Math.round(input[0][n] * x) / x;
const x = Math.pow(2, crush - 1); for (let o = 0; o < output.length; o++) {
output[i][n] = Math.round(input[i][n] * x) / x; output[o][n] = value;
} }
} }
return true; } else {
for (let n = 0; n < blockSize; n++) {
let x = Math.pow(2, crush[n] - 1);
const value = Math.round(input[0][n] * x) / x;
for (let o = 0; o < output.length; o++) {
output[o][n] = value;
}
}
}
}
return this.notStarted || hasInput;
} }
} }
registerProcessor('crush-processor', CrushProcessor); registerProcessor('crush-processor', CrushProcessor);
class ShapeProcessor extends AudioWorkletProcessor { class ShapeProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() { static get parameterDescriptors() {
return [ return [{ name: 'shape', defaultValue: 0 }];
{ name: 'shape', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
} }
constructor() { constructor() {
super(); super();
this.notStarted = true;
} }
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
const input = inputs[0]; const input = inputs[0];
const output = outputs[0]; const output = outputs[0];
const shape0 = parameters.shape[0];
const shape1 = shape0 < 1 ? shape0 : 1.0 - 4e-10;
const shape = (2.0 * shape1) / (1.0 - shape1);
const blockSize = 128; const blockSize = 128;
const hasInput = !(input[0] === undefined);
let shape = parameters.shape[0]; if (hasInput) {
shape = shape < 1 ? shape : 1.0 - 4e-10; this.notStarted = false;
shape = (2.0 * shape) / (1.0 - shape);
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) { const value = ((1 + shape) * input[0][n]) / (1 + shape * Math.abs(input[0][n]));
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain; for (let o = 0; o < output.length; o++) {
output[o][n] = value;
} }
} }
return true; }
return this.notStarted || hasInput;
} }
} }
registerProcessor('shape-processor', ShapeProcessor); registerProcessor('shape-processor', ShapeProcessor);
class DistortProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{ name: 'distort', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
}
constructor() {
super();
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const blockSize = 128;
const shape = Math.expm1(parameters.distort[0]);
const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0]));
if (input[0] == null || output[0] == null) {
return false;
}
for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < input.length; i++) {
output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain;
}
}
return true;
}
}
registerProcessor('distort-processor', DistortProcessor);
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
const polyBlep = (phase, dt) => {
// 0 <= phase < 1
if (phase < dt) {
phase /= dt;
// 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1;
}
// -1 < phase < 0
else if (phase > 1 - dt) {
phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5)
return phase * phase + phase + phase + 1;
}
// 0 otherwise
else {
return 0;
}
};
const saw = (phase, dt) => {
const v = 2 * phase - 1;
return v - polyBlep(phase, dt);
};
function lerp(a, b, n) {
return n * (b - a) + a;
}
function getUnisonDetune(unison, detune, voiceIndex) {
if (unison < 2) {
return 0;
}
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
}
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.phase = [];
}
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'panspread',
defaultValue: 0.4,
min: 0,
max: 1,
},
{
name: 'freqspread',
defaultValue: 0.2,
min: 0,
},
{
name: 'detune',
defaultValue: 0,
min: 0,
},
{
name: 'voices',
defaultValue: 5,
min: 1,
},
];
}
process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) {
return true;
}
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' });
return false;
}
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0];
const voices = params.voices[0];
const freqspread = params.freqspread[0];
const panspread = params.panspread[0] * 0.5 + 0.5;
const gain1 = Math.sqrt(1 - panspread);
const gain2 = Math.sqrt(panspread);
for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = frequency * Math.pow(2, getUnisonDetune(voices, freqspread, n) / 12);
let gainL = gain1;
let gainR = gain2;
// invert right and left gain
if (isOdd) {
gainL = gain2;
gainR = gain1;
}
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) {
this.phase[n] = this.phase[n] ?? Math.random();
const v = saw(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR;
this.phase[n] += dt;
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
}
}
}
return true;
}
}
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
-2
View File
@@ -5,5 +5,3 @@ export * from './tonal.mjs';
export * from './voicings.mjs'; export * from './voicings.mjs';
import './ireal.mjs'; import './ireal.mjs';
export const packageName = '@strudel/tonal';
+2 -33
View File
@@ -7,9 +7,10 @@ This program is free software: you can redistribute it and/or modify it under th
// import { strict as assert } from 'assert'; // import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes import '../tonal.mjs'; // need to import this to add prototypes
import { pure, n, seq, note } from '@strudel/core'; import { pure, controls, seq } from '@strudel/core';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { mini } from '../../mini/mini.mjs'; import { mini } from '../../mini/mini.mjs';
const { n } = controls;
describe('tonal', () => { describe('tonal', () => {
it('Should run tonal functions ', () => { it('Should run tonal functions ', () => {
@@ -44,36 +45,4 @@ describe('tonal', () => {
.firstCycleValues.map((h) => h.note), .firstCycleValues.map((h) => h.note),
).toEqual(['C3', 'D3', 'E3']); ).toEqual(['C3', 'D3', 'E3']);
}); });
it('transposes note numbers with interval numbers', () => {
expect(
note(40, 40, 40)
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual([40, 41, 42]);
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
});
it('transposes note numbers with interval strings', () => {
expect(
note(40, 40, 40)
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual([40, 42, 43]);
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
});
it('transposes note strings with interval numbers', () => {
expect(
note('c', 'c', 'c')
.transpose(0, 1, 2)
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'Db', 'D']);
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
});
it('transposes note strings with interval strings', () => {
expect(
note('c', 'c', 'c')
.transpose('1P', '2M', '3m')
.firstCycleValues.map((h) => h.note),
).toEqual(['C', 'D', 'Eb']);
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
});
}); });

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