improve api for web package

This commit is contained in:
Felix Roos 2023-05-07 22:36:26 +02:00
parent 3b631cb6af
commit 12228c56d9
10 changed files with 160 additions and 73 deletions

View File

@ -18,6 +18,7 @@ export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './draw.mjs';
export * from './animate.mjs';

View File

@ -17,23 +17,15 @@ export function repl({
}) {
const scheduler = new Cyclist({
interval,
onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle,
});
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
const evaluate = async (code, autostart = true) => {
if (!code) {
throw new Error('no code to evaluate');
@ -43,8 +35,7 @@ export function repl({
let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`);
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
setPattern(pattern, autostart);
afterEval?.({ code, pattern });
return pattern;
} catch (err) {
@ -61,5 +52,21 @@ export function repl({
setCps,
setcps: setCps,
});
return { scheduler, evaluate, start, stop, pause, setCps };
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
}
export const getTrigger =
({ getTime, defaultOutput }) =>
async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};

View File

@ -190,3 +190,8 @@ export function minify(thing) {
}
return strudel.reify(thing);
}
// calling this function will cause patterns to parse strings as mini notation by default
export function miniAllStrings() {
strudel.setStringParser(mini);
}

View File

@ -4,16 +4,20 @@ This package provides an easy to use bundle of multiple strudel packages for the
## Usage
```js
import { repl } from '@strudel/web';
Minimal example:
const strudel = repl();
```js
import '@strudel/web';
document.getElementById('play').addEventListener('click',
() => strudel.evaluate('note("c a f e").jux(rev)')
);
() => note("c a f e").play()
)
```
As soon as you `import '@strudel/web'`, all strudel functions will be available in the global scope.
In this case, we are using `note` to create a pattern.
To actually play the pattern, you have to append `.play()` to the end.
Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event.
### Loading samples
@ -21,15 +25,28 @@ Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/
By default, no external samples are loaded, but you can add them like this:
```js
import { repl, samples } from '@strudel/web';
import { prebake } from '@strudel/web';
const strudel = repl({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
prebake(() => samples('github:tidalcycles/Dirt-Samples/master'))
document.getElementById('play').addEventListener('click',
() => strudel.evaluate('s("bd,jvbass(3,8)").jux(rev)')
);
() => s("bd sd").play()
)
```
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
### Evaluating Code
Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern.
This is called evaluation: Taking a piece of code and executing it on the fly.
To do that, you can use the `evaluate` function:
```js
import '@strudel/web';
document.getElementById('play').addEventListener('click',
() => evaluate('note("c a f e").jux(rev)')
);
```

View File

@ -13,17 +13,16 @@
<button id="c">C</button>
<button id="stop">stop</button>
<script type="module">
import { repl, samples } from '@strudel/web';
const strudel = repl({
import { init } from '@strudel/web';
init({
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
});
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
click('a', () => strudel.evaluate('s("bd,jvbass(3,8)").jux(rev)'));
click('b', () => strudel.evaluate('s("bd*2,hh(3,4),jvbass(5,8,1)").jux(rev)'));
click('c', () => strudel.evaluate('s("bd*2,hh(3,4),jvbass:[0 4](5,8,1)").jux(rev).stack(s("~ sd"))'));
click('stop', () => strudel.stop());
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
click('stop', () => hush());
</script>
</body>
</html>

View File

@ -2,7 +2,7 @@
"name": "@strudel/web",
"version": "0.8.0",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"main": "repl.mjs",
"main": "web.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"

View File

@ -1,38 +0,0 @@
export * from '@strudel.cycles/core';
export * from '@strudel.cycles/webaudio';
export * from '@strudel.cycles/soundfonts';
export * from '@strudel.cycles/transpiler';
export * from '@strudel.cycles/mini';
export * from '@strudel.cycles/tonal';
export * from '@strudel.cycles/webaudio';
import { repl as _repl, evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick, getAudioContext, registerSynthSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { transpiler } from '@strudel.cycles/transpiler';
async function prebake(userPrebake) {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts(), userPrebake?.()]);
}
export function repl(options = {}) {
const prebaked = prebake(options?.prebake);
initAudioOnFirstClick();
return _repl({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
...options,
beforeEval: async (args) => {
options?.beforeEval?.(args);
await prebaked;
},
});
}

View File

@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'web.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});

64
packages/web/web.mjs Normal file
View File

@ -0,0 +1,64 @@
export * from '@strudel.cycles/core';
export * from '@strudel.cycles/webaudio';
export * from '@strudel.cycles/soundfonts';
export * from '@strudel.cycles/transpiler';
export * from '@strudel.cycles/mini';
export * from '@strudel.cycles/tonal';
export * from '@strudel.cycles/webaudio';
import { Pattern, evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioScheduler } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
import { evaluate as _evaluate } from '@strudel.cycles/transpiler';
import { miniAllStrings } from '@strudel.cycles/mini';
// init logic
export async function defaultPrebake() {
const loadModules = evalScope(
evalScope,
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
{ hush, evaluate },
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
}
// when this function finishes, everything is initialized
let initDone;
let scheduler;
export function init(options = {}) {
initAudioOnFirstClick();
miniAllStrings();
const { prebake, ...schedulerOptions } = options;
initDone = (async () => {
await defaultPrebake();
await prebake?.();
})();
scheduler = webaudioScheduler(schedulerOptions);
}
// this method will play the pattern on the default scheduler
Pattern.prototype.play = function () {
if (!scheduler) {
throw new Error('.play: no scheduler found. Have you called init?');
}
initDone.then(() => {
scheduler.setPattern(this, true);
});
return this;
};
// stop playback
export function hush() {
scheduler.stop();
}
// evaluate and play the given code using the transpiler
export async function evaluate(code, autoplay = true) {
const { pattern } = await _evaluate(code);
autoplay && pattern.play();
}

View File

@ -243,3 +243,16 @@ Pattern.prototype.webaudio = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger(webaudioOutputTrigger);
};
export function webaudioScheduler(options = {}) {
options = {
getTime: () => getAudioContext().currentTime,
defaultOutput: webaudioOutput,
...options,
};
const { defaultOutput, getTime } = options;
return new strudel.Cyclist({
...options,
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}