mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-24 20:18:34 +00:00
improve api for web package
This commit is contained in:
parent
3b631cb6af
commit
12228c56d9
@ -18,6 +18,7 @@ export * from './util.mjs';
|
|||||||
export * from './speak.mjs';
|
export * from './speak.mjs';
|
||||||
export * from './evaluate.mjs';
|
export * from './evaluate.mjs';
|
||||||
export * from './repl.mjs';
|
export * from './repl.mjs';
|
||||||
|
export * from './cyclist.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './draw.mjs';
|
export * from './draw.mjs';
|
||||||
export * from './animate.mjs';
|
export * from './animate.mjs';
|
||||||
|
|||||||
@ -17,23 +17,15 @@ export function repl({
|
|||||||
}) {
|
}) {
|
||||||
const scheduler = new Cyclist({
|
const scheduler = new Cyclist({
|
||||||
interval,
|
interval,
|
||||||
onTrigger: async (hap, deadline, duration, cps) => {
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
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');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: onSchedulerError,
|
onError: onSchedulerError,
|
||||||
getTime,
|
getTime,
|
||||||
onToggle,
|
onToggle,
|
||||||
});
|
});
|
||||||
|
const setPattern = (pattern, autostart = true) => {
|
||||||
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
|
scheduler.setPattern(pattern, autostart);
|
||||||
|
};
|
||||||
const evaluate = async (code, autostart = true) => {
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
@ -43,8 +35,7 @@ export function repl({
|
|||||||
let { pattern } = await _evaluate(code, transpiler);
|
let { pattern } = await _evaluate(code, transpiler);
|
||||||
|
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
setPattern(pattern, autostart);
|
||||||
scheduler.setPattern(pattern, autostart);
|
|
||||||
afterEval?.({ code, pattern });
|
afterEval?.({ code, pattern });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -61,5 +52,21 @@ export function repl({
|
|||||||
setCps,
|
setCps,
|
||||||
setcps: 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -190,3 +190,8 @@ export function minify(thing) {
|
|||||||
}
|
}
|
||||||
return strudel.reify(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);
|
||||||
|
}
|
||||||
|
|||||||
@ -4,16 +4,20 @@ This package provides an easy to use bundle of multiple strudel packages for the
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```js
|
Minimal example:
|
||||||
import { repl } from '@strudel/web';
|
|
||||||
|
|
||||||
const strudel = repl();
|
```js
|
||||||
|
import '@strudel/web';
|
||||||
|
|
||||||
document.getElementById('play').addEventListener('click',
|
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.
|
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
|
### 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:
|
By default, no external samples are loaded, but you can add them like this:
|
||||||
|
|
||||||
```js
|
```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',
|
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).
|
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)')
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|||||||
@ -13,17 +13,16 @@
|
|||||||
<button id="c">C</button>
|
<button id="c">C</button>
|
||||||
<button id="stop">stop</button>
|
<button id="stop">stop</button>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { repl, samples } from '@strudel/web';
|
import { init } from '@strudel/web';
|
||||||
|
init({
|
||||||
const strudel = repl({
|
|
||||||
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||||
click('a', () => strudel.evaluate('s("bd,jvbass(3,8)").jux(rev)'));
|
click('a', () => 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('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
|
||||||
click('c', () => strudel.evaluate('s("bd*2,hh(3,4),jvbass:[0 4](5,8,1)").jux(rev).stack(s("~ sd"))'));
|
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
|
||||||
click('stop', () => strudel.stop());
|
click('stop', () => hush());
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "@strudel/web",
|
"name": "@strudel/web",
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||||
"main": "repl.mjs",
|
"main": "web.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs"
|
"module": "dist/index.mjs"
|
||||||
|
|||||||
@ -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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
19
packages/web/vite.config.js
Normal file
19
packages/web/vite.config.js
Normal 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
64
packages/web/web.mjs
Normal 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();
|
||||||
|
}
|
||||||
@ -243,3 +243,16 @@ Pattern.prototype.webaudio = function () {
|
|||||||
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
||||||
return this.onTrigger(webaudioOutputTrigger);
|
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 }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user