From 12228c56d97d004cb2ca536ea8514c02c690882c Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sun, 7 May 2023 22:36:26 +0200 Subject: [PATCH] improve api for web package --- packages/core/index.mjs | 1 + packages/core/repl.mjs | 39 ++++++----- packages/mini/mini.mjs | 5 ++ packages/web/README.md | 39 +++++++---- packages/web/examples/repl-example/index.html | 13 ++-- packages/web/package.json | 2 +- packages/web/repl.mjs | 38 ----------- packages/web/vite.config.js | 19 ++++++ packages/web/web.mjs | 64 +++++++++++++++++++ packages/webaudio/webaudio.mjs | 13 ++++ 10 files changed, 160 insertions(+), 73 deletions(-) delete mode 100644 packages/web/repl.mjs create mode 100644 packages/web/vite.config.js create mode 100644 packages/web/web.mjs diff --git a/packages/core/index.mjs b/packages/core/index.mjs index 30504a5b..78241f74 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -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'; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index de979258..abb9ef80 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -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'); + } + }; diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 648499e6..3f9228d9 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -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); +} diff --git a/packages/web/README.md b/packages/web/README.md index c70a5df7..53609c3e 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -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)') +); +``` diff --git a/packages/web/examples/repl-example/index.html b/packages/web/examples/repl-example/index.html index a7954e64..853e02db 100644 --- a/packages/web/examples/repl-example/index.html +++ b/packages/web/examples/repl-example/index.html @@ -13,17 +13,16 @@ diff --git a/packages/web/package.json b/packages/web/package.json index 11e9153e..be1a4bb9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -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" diff --git a/packages/web/repl.mjs b/packages/web/repl.mjs deleted file mode 100644 index f86459b1..00000000 --- a/packages/web/repl.mjs +++ /dev/null @@ -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; - }, - }); -} diff --git a/packages/web/vite.config.js b/packages/web/vite.config.js new file mode 100644 index 00000000..ffa2ad27 --- /dev/null +++ b/packages/web/vite.config.js @@ -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', + }, +}); diff --git a/packages/web/web.mjs b/packages/web/web.mjs new file mode 100644 index 00000000..e546a8f9 --- /dev/null +++ b/packages/web/web.mjs @@ -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(); +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c75bde0b..6b069a85 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -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 }), + }); +}