diff --git a/.gitignore b/.gitignore index b7704c61..59d9940e 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ fabric.properties .idea/caches/build_file_checksums.ser # END JetBrains -> BEGIN JetBrains + +samples/* +!samples/README.md diff --git a/package.json b/package.json index 9d363230..97900913 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "format-check": "prettier --check .", "report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json", "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" }, "repository": { diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index e59249ee..d28bf95d 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -27,6 +27,7 @@ import { persistentAtom } from '@nanostores/persistent'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []), + isBracketClosingEnabled: (on) => (on ? closeBrackets() : []), isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []), theme, isAutoCompletionEnabled, @@ -41,6 +42,7 @@ const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [ke export const defaultSettings = { keybindings: 'codemirror', isBracketMatchingEnabled: false, + isBracketClosingEnabled: true, isLineNumbersDisplayed: true, isActiveLineHighlighted: false, isAutoCompletionEnabled: false, @@ -76,7 +78,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything - closeBrackets(), syntaxHighlighting(defaultHighlightStyle), history(), EditorView.updateListener.of((v) => onChange(v)), @@ -128,6 +129,7 @@ export class StrudelMirror { id, initialCode = '', onDraw, + drawContext, drawTime = [0, 0], autodraw, prebake, @@ -140,13 +142,14 @@ export class StrudelMirror { this.widgets = []; this.painters = []; this.drawTime = drawTime; - this.onDraw = onDraw; + this.drawContext = drawContext; + this.onDraw = onDraw || this.draw; this.id = id || s4(); this.drawer = new Drawer((haps, time) => { - const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped); + const currentFrame = haps.filter((hap) => hap.isActive(time)); this.highlight(currentFrame, time); - this.onDraw?.(haps, time, currentFrame, this.painters); + this.onDraw(haps, time, this.painters); }, drawTime); this.prebaked = prebake(); @@ -236,6 +239,9 @@ export class StrudelMirror { // when no painters are set, [0,0] is enough (just highlighting) 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() { if (!this.onDraw) { return; @@ -246,7 +252,7 @@ export class StrudelMirror { await this.repl.evaluate(this.code, false); this.drawer.invalidate(this.repl.scheduler, -0.001); // 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) { console.warn('first frame could not be painted'); } @@ -304,6 +310,9 @@ export class StrudelMirror { setLineNumbersDisplayed(enabled) { this.reconfigureExtension('isLineNumbersDisplayed', enabled); } + setBracketClosingEnabled(enabled) { + this.reconfigureExtension('isBracketClosingEnabled', enabled); + } setTheme(theme) { this.reconfigureExtension('theme', theme); } diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs index ee3e05bf..f25cf9ce 100644 --- a/packages/codemirror/themes.mjs +++ b/packages/codemirror/themes.mjs @@ -37,6 +37,7 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre import teletext, { settings as teletextSettings } from './themes/teletext'; import algoboy, { settings as algoboySettings } from './themes/algoboy'; import terminal, { settings as terminalSettings } from './themes/terminal'; +import { setTheme } from '@strudel/draw'; export const themes = { strudelTheme, @@ -513,6 +514,7 @@ export function activateTheme(name) { .map(([key, value]) => `--${key}: ${value} !important;`) .join('\n')} }`; + setTheme(themeSettings); // tailwind dark mode if (themeSettings.light) { document.documentElement.classList.remove('dark'); diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 98d78b36..72b4ec65 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -106,23 +106,30 @@ function getCanvasWidget(id, options = {}) { registerWidget('_pianoroll', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); - return pat.id(id).pianoroll({ fold: 1, ...options, ctx, id }); + return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id }); }); registerWidget('_punchcard', (id, options = {}, pat) => { const ctx = getCanvasWidget(id, options).getContext('2d'); - return pat.id(id).punchcard({ fold: 1, ...options, ctx, id }); + 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.id(id).spiral({ ...options, ctx, id }); + 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.id(id).scope({ ...options, ctx, id }); + 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 }); }); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index b0727ee5..08b89375 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -37,12 +37,17 @@ export class Cyclist { 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) { + // avoid querying haps that are in the past anyway + console.log(`skip query: too late`); + return; + } // query the pattern for events const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); - this.lastTick = phase; - haps.forEach((hap) => { if (hap.hasOnset()) { const targetTime = @@ -67,6 +72,9 @@ export class Cyclist { ); } now() { + if (!this.started) { + return 0; + } const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration; return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency; } diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index ab5f0c97..7a9e0a62 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -49,6 +49,27 @@ export class Hap { 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() { return this.whole ? this.whole : this.part; } @@ -70,6 +91,10 @@ export class Hap { return this.whole != undefined && this.whole.begin.equals(this.part.begin); } + hasTag(tag) { + return this.context.tags?.includes(tag); + } + resolveState(state) { if (this.stateful && this.hasOnset()) { console.log('stateful'); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index b1aa852f..daacc946 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -29,22 +29,22 @@ export class Pattern { * @param {function} query - The function that maps a `State` to an array of `Hap`. * @noAutocomplete */ - constructor(query, weight = undefined) { + constructor(query, tactus = undefined) { this.query = query; this._Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another Pattern - this.__weight = weight; // in terms of number of beats per cycle + this.__tactus = tactus; // in terms of number of beats per cycle } - get weight() { - return this.__weight ?? Fraction(1); + get tactus() { + return this.__tactus ?? Fraction(1); } - set weight(weight) { - this.__weight = Fraction(weight); + set tactus(tactus) { + this.__tactus = Fraction(tactus); } - setWeight(weight) { - this.weight = weight; + setTactus(tactus) { + this.tactus = tactus; return this; } @@ -62,7 +62,7 @@ export class Pattern { */ withValue(func) { const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func))); - result.weight = this.weight; + result.tactus = this.tactus; return result; } @@ -130,7 +130,7 @@ export class Pattern { return span_a.intersection_e(span_b); }; const result = pat_func.appWhole(whole_func, pat_val); - result.weight = lcm(pat_val.weight, pat_func.weight); + result.tactus = lcm(pat_val.tactus, pat_func.tactus); return result; } @@ -165,7 +165,7 @@ export class Pattern { return haps; }; const result = new Pattern(query); - result.weight = this.weight; + result.tactus = this.tactus; return result; } @@ -198,7 +198,7 @@ export class Pattern { return haps; }; const result = new Pattern(query); - result.weight = pat_val.weight; + result.tactus = pat_val.tactus; return result; } @@ -452,7 +452,7 @@ export class Pattern { */ withHaps(func) { const result = new Pattern((state) => func(this.query(state), state)); - result.weight = this.weight; + result.tactus = this.tactus; return result; } @@ -542,7 +542,7 @@ export class Pattern { * @noAutocomplete */ filterValues(value_test) { - return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))); + return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setTactus(this.tactus); } /** @@ -1147,7 +1147,7 @@ Pattern.prototype.factories = { slowcat, fastcat, cat, - timeCat, + timecat, sequence, seq, polymeter, @@ -1160,13 +1160,13 @@ Pattern.prototype.factories = { // Elemental patterns /** - * Does absolutely nothing, with a given metrical 'weight' + * Does absolutely nothing, but with a given metrical 'tactus' * @name gap - * @param {number} weight + * @param {number} tactus * @example * gap(3) // "~@3" */ -export const gap = (weight) => new Pattern(() => [], Fraction(weight)); +export const gap = (tactus) => new Pattern(() => [], Fraction(tactus)); /** * Does absolutely nothing.. @@ -1176,7 +1176,7 @@ export const gap = (weight) => new Pattern(() => [], Fraction(weight)); */ export const silence = gap(1); -/* Like silence, but with a 'weight' (relative duration) of 0 */ +/* Like silence, but with a 'tactus' (relative duration) of 0 */ export const nothing = gap(0); /** A discrete value that repeats once per cycle. @@ -1235,7 +1235,7 @@ export function stack(...pats) { pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); const query = (state) => flatten(pats.map((pat) => pat.query(state))); const result = new Pattern(query); - result.weight = lcm(...pats.map((pat) => pat.weight)); + result.tactus = lcm(...pats.map((pat) => pat.tactus)); return result; } @@ -1247,54 +1247,54 @@ function _stackWith(func, pats) { if (pats.length === 1) { return pats[0]; } - const [left, ...right] = pats.map((pat) => pat.weight); - const weight = left.maximum(...right); - return stack(...func(weight, pats)); + const [left, ...right] = pats.map((pat) => pat.tactus); + const tactus = left.maximum(...right); + return stack(...func(tactus, pats)); } export function stackLeft(...pats) { return _stackWith( - (weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(pat, gap(weight.sub(pat.weight))))), + (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(pat, gap(tactus.sub(pat.tactus))))), pats, ); } export function stackRight(...pats) { return _stackWith( - (weight, pats) => pats.map((pat) => (pat.weight.eq(weight) ? pat : timeCat(gap(weight.sub(pat.weight)), pat))), + (tactus, pats) => pats.map((pat) => (pat.tactus.eq(tactus) ? pat : timecat(gap(tactus.sub(pat.tactus)), pat))), pats, ); } export function stackCentre(...pats) { return _stackWith( - (weight, pats) => + (tactus, pats) => pats.map((pat) => { - if (pat.weight.eq(weight)) { + if (pat.tactus.eq(tactus)) { return pat; } - const g = gap(weight.sub(pat.weight).div(2)); - return timeCat(g, pat, g); + const g = gap(tactus.sub(pat.tactus).div(2)); + return timecat(g, pat, g); }), pats, ); } export function stackBy(by, ...pats) { - const [left, ...right] = pats.map((pat) => pat.weight); - const weight = left.maximum(...right); + const [left, ...right] = pats.map((pat) => pat.tactus); + const tactus = left.maximum(...right); const lookup = { centre: stackCentre, left: stackLeft, right: stackRight, expand: stack, - beat: (...args) => polymeterSteps(weight, ...args), + repeat: (...args) => polymeterSteps(tactus, ...args), }; return by .inhabit(lookup) .fmap((func) => func(...pats)) .innerJoin() - .setWeight(weight); + .setTactus(tactus); } /** Concatenation: combines a list of patterns, switching between them successively, one per cycle: @@ -1361,24 +1361,21 @@ export function cat(...pats) { /** Sequences patterns like `seq`, but each pattern has a length, relative to the whole. * This length can either be provided as a [length, pattern] pair, or inferred from - * mininotation as the number of toplevel steps. The latter only works if the mininotation - * hasn't first been modified by another function. + * the pattern's 'tactus', generally inferred by the mininotation. * @return {Pattern} * @example - * timeCat([3,"e3"],[1, "g3"]).note() + * timecat([3,"e3"],[1, "g3"]).note() * // the same as "e3@3 g3".note() * @example - * timeCat("bd sd cp","hh hh").sound() + * timecat("bd sd cp","hh hh").sound() * // the same as "bd sd cp hh hh".sound() */ -export function timeCat(...timepats) { - // Weights may either be provided explicitly in [weight, pattern] pairs, or - // where possible, inferred from the pattern. - const findWeight = (x) => (Array.isArray(x) ? x : [x.weight, x]); - timepats = timepats.map(findWeight); +export function timecat(...timepats) { + const findtactus = (x) => (Array.isArray(x) ? x : [x.tactus, x]); + timepats = timepats.map(findtactus); if (timepats.length == 1) { const result = reify(timepats[0][1]); - result.weight = timepats[0][0]; + result.tactus = timepats[0][0]; return result; } @@ -1391,10 +1388,13 @@ export function timeCat(...timepats) { begin = end; } const result = stack(...pats); - result.weight = total; + result.tactus = total; return result; } +/** Deprecated alias for `timecat` */ +export const timeCat = timecat; + /** * Allows to arrange multiple patterns together over multiple cycles. * Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use. @@ -1409,26 +1409,32 @@ export function timeCat(...timepats) { export function arrange(...sections) { const total = sections.reduce((sum, [cycles]) => sum + cycles, 0); sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]); - return timeCat(...sections).slow(total); + return timecat(...sections).slow(total); } export function fastcat(...pats) { let result = slowcat(...pats); if (pats.length > 1) { result = result._fast(pats.length); - result.weight = pats.length; + result.tactus = pats.length; } return result; } +/** See `fastcat` */ +export function sequence(...pats) { + return fastcat(...pats); +} + /** - * Concatenates patterns beatwise, similar to `timeCat`, but if an argument is a list, the whole pattern will be repeated for each element in the list. + * Concatenates patterns stepwise, according to their 'tactus'. + * Similar to `timecat`, but if an argument is a list, the whole pattern will be repeated for each element in the list. * * @return {Pattern} * @example - * beatCat(["bd cp", "mt"], "bd").sound() + * stepcat(["bd cp", "mt"], "bd").sound() */ -export function beatCat(...groups) { +export function stepcat(...groups) { groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)])); const cycles = lcm(...groups.map((x) => Fraction(x.length))); @@ -1437,18 +1443,13 @@ export function beatCat(...groups) { for (let cycle = 0; cycle < cycles; ++cycle) { result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length]))); } - result = result.filter((x) => x.weight > 0); - const weight = result.reduce((a, b) => a.add(b.weight), Fraction(0)); - result = timeCat(...result); - result.weight = weight; + result = result.filter((x) => x.tactus > 0); + const tactus = result.reduce((a, b) => a.add(b.tactus), Fraction(0)); + result = timecat(...result); + result.tactus = tactus; return result; } -/** See `fastcat` */ -export function sequence(...pats) { - return fastcat(...pats); -} - /** Like **cat**, but the items are crammed into one cycle. * @synonyms fastcat, sequence * @example @@ -1474,13 +1475,13 @@ function _sequenceCount(x) { } /** - * Speeds a pattern up or down, to fit to the given metrical 'weight'. + * Speeds a pattern up or down, to fit to the given metrical 'tactus'. * @example - * s("bd sd cp").reweight(4) + * s("bd sd cp").toTactus(4) * // The same as s("{bd sd cp}%4") */ -export const reweight = register('reweight', function (targetWeight, pat) { - return pat.fast(Fraction(targetWeight).div(pat.weight)); +export const toTactus = register('toTactus', function (targetTactus, pat) { + return pat.fast(Fraction(targetTactus).div(pat.tactus)); }); export function _polymeterListSteps(steps, ...args) { @@ -1525,7 +1526,7 @@ export function polymeterSteps(steps, ...args) { return _polymeterListSteps(steps, ...args); } - return polymeter(...args).reweight(steps); + return polymeter(...args).toTactus(steps); } /** @@ -1545,11 +1546,11 @@ export function polymeter(...args) { if (args.length == 0) { return silence; } - const weight = args[0].weight; + const tactus = args[0].tactus; const [head, ...tail] = args; - const result = stack(head, ...tail.map((pat) => pat._slow(pat.weight.div(weight)))); - result.weight = weight; + const result = stack(head, ...tail.map((pat) => pat._slow(pat.tactus.div(tactus)))); + result.tactus = tactus; return result; } @@ -1592,7 +1593,7 @@ export const func = curry((a, b) => reify(b).func(a)); * @noAutocomplete * */ -export function register(name, func, patternify = true, preserveWeight = false) { +export function register(name, func, patternify = true, preserveTactus = false) { if (Array.isArray(name)) { const result = {}; for (const name_item of name) { @@ -1632,8 +1633,8 @@ export function register(name, func, patternify = true, preserveWeight = false) result = right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin(); } } - if (preserveWeight) { - result.weight = pat.weight; + if (preserveTactus) { + result.tactus = pat.tactus; } return result; }; @@ -1641,8 +1642,8 @@ export function register(name, func, patternify = true, preserveWeight = false) pfunc = function (...args) { args = args.map(reify); const result = func(...args); - if (preserveWeight) { - result.weight = args[args.length - 1].weight; + if (preserveTactus) { + result.tactus = args[args.length - 1].tactus; } return result; }; @@ -1880,7 +1881,7 @@ export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], fun */ export const ply = register('ply', function (factor, pat) { const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin(); - result.weight = pat.weight.mul(factor); + result.tactus = pat.tactus.mul(factor); return result; }); @@ -1902,7 +1903,7 @@ export const { fast, density } = register(['fast', 'density'], function (factor, factor = Fraction(factor); const fastQuery = pat.withQueryTime((t) => t.mul(factor)); const result = fastQuery.withHapTime((t) => t.div(factor)); - result.weight = factor.mul(pat.weight); + result.tactus = factor.mul(pat.tactus); return result; }); @@ -2106,7 +2107,7 @@ export const linger = register( * note(saw.range(40,52).segment(24)) */ export const segment = register('segment', function (rate, pat) { - return pat.struct(pure(true)._fast(rate)).setWeight(rate); + return pat.struct(pure(true)._fast(rate)).setTactus(rate); }); /** @@ -2264,7 +2265,7 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by })); const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }))); - return stack(left, right).setWeight(lcm(left.weight, right.weight)); + return stack(left, right).setTactus(lcm(left.tactus, right.tactus)); }); /** @@ -2477,13 +2478,13 @@ export const hsl = register('hsl', (h, s, l, pat) => { }); /** - * Sets the id of the hap in, for filtering in the future. - * @name id + * Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array). + * @name tag * @noAutocomplete - * @param {string} id anything unique + * @param {string} tag anything unique */ -Pattern.prototype.id = function (id) { - return this.withContext((ctx) => ({ ...ctx, id })); +Pattern.prototype.tag = function (tag) { + return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) })); }; ////////////////////////////////////////////////////////////////////// @@ -2510,7 +2511,7 @@ export const chop = register('chop', function (n, pat) { const func = function (o) { return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o))); }; - return pat.squeezeBind(func).setWeight(pat.weight.mul(n)); + return pat.squeezeBind(func).setTactus(pat.tactus.mul(n)); }); /** @@ -2574,7 +2575,7 @@ export const slice = register( }), ), ) - .setWeight(ipat.weight); + .setTactus(ipat.tactus); }, false, // turns off auto-patternification ); @@ -2605,7 +2606,7 @@ export const splice = register( ...v, })), ); - }).setWeight(ipat.weight); + }).setTactus(ipat.tactus); }, false, // turns off auto-patternification ); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index e073dfe8..3a2e6bd6 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -156,6 +156,7 @@ export function repl({ return pattern; } catch (err) { logger(`[eval] error: ${err.message}`, 'error'); + console.error(err); updateState({ evalError: err, pending: false }); onEvalError?.(err); } diff --git a/packages/core/test/controls.test.mjs b/packages/core/test/controls.test.mjs index 387e506f..3b926685 100644 --- a/packages/core/test/controls.test.mjs +++ b/packages/core/test/controls.test.mjs @@ -30,13 +30,13 @@ describe('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 weight of the left pattern', () => { - expect(s(mini('bd cp mt').pan(mini('1 2 3 4'))).weight).toEqual(Fraction(3)); + 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 weight of the right pattern for .out', () => { - expect(s(mini('bd cp mt').set.out(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(4)); + 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 weight of the pattern for .mix as lcm', () => { - expect(s(mini('bd cp mt').set.mix(pan(mini('1 2 3 4')))).weight).toEqual(Fraction(12)); + 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)); }); }); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index e112a95f..f825c4df 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1119,21 +1119,21 @@ describe('Pattern', () => { ); }); }); - describe('weight', () => { + describe('tactus', () => { it('Is correctly preserved/calculated through transformations', () => { - expect(sequence(0, 1, 2, 3).linger(4).weight).toStrictEqual(Fraction(4)); - expect(sequence(0, 1, 2, 3).iter(4).weight).toStrictEqual(Fraction(4)); - expect(sequence(0, 1, 2, 3).fast(4).weight).toStrictEqual(Fraction(16)); - expect(sequence(0, 1, 2, 3).hurry(4).weight).toStrictEqual(Fraction(16)); - expect(sequence(0, 1, 2, 3).rev().weight).toStrictEqual(Fraction(4)); - expect(sequence(1).segment(10).weight).toStrictEqual(Fraction(10)); - expect(sequence(1, 0, 1).invert().weight).toStrictEqual(Fraction(3)); - expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).chop(4).weight).toStrictEqual(Fraction(8)); - expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).striate(4).weight).toStrictEqual(Fraction(8)); - expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).slice(4, sequence(0, 1, 2, 3)).weight).toStrictEqual( + 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)).weight).toStrictEqual( + expect(sequence({ s: 'bev' }, { s: 'amenbreak' }).splice(4, sequence(0, 1, 2, 3)).tactus).toStrictEqual( Fraction(4), ); }); diff --git a/packages/core/test/value.test.mjs b/packages/core/test/value.test.mjs index c5472607..87cba57d 100644 --- a/packages/core/test/value.test.mjs +++ b/packages/core/test/value.test.mjs @@ -22,8 +22,4 @@ describe('Value', () => { expect(valued(mul).ap(3).ap(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 }]); - }); }); diff --git a/packages/core/value.mjs b/packages/core/value.mjs index f9a963ce..ef98bc37 100644 --- a/packages/core/value.mjs +++ b/packages/core/value.mjs @@ -5,14 +5,13 @@ This program is free software: you can redistribute it and/or modify it under th */ import { curry } from './util.mjs'; +import { logger } from './logger.mjs'; export function unionWithObj(a, b, func) { - if (typeof b?.value === 'number') { - // https://github.com/tidalcycles/strudel/issues/262 - const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number'); - const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value])); - b = Object.assign(b, numerals); - delete b.value; + if (b?.value !== undefined && Object.keys(b).length === 1) { + // https://github.com/tidalcycles/strudel/issues/1026 + logger(`[warn]: Can't do arithmetic on control pattern.`); + return a; } 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])]))); diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs index 0fb4ccc4..a09ace8c 100644 --- a/packages/core/zyklus.mjs +++ b/packages/core/zyklus.mjs @@ -26,8 +26,7 @@ function createClock( // callback as long as we're inside the lookahead while (phase < lookahead) { phase = round ? Math.round(phase * precision) / precision : phase; - phase >= t && callback(phase, duration, tick, t); - phase < t && console.log('TOO LATE', phase); // what if latency is added from outside? + callback(phase, duration, tick, t); // callback has to skip / handle phase < t! phase += duration; // increment phase by duration tick++; } diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index 0b5a1a4f..cfd2ce75 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -39,51 +39,36 @@ function stopAnimationFrame(id) { function stopAllAnimations() { Object.keys(animationFrames).forEach((id) => stopAnimationFrame(id)); } -Pattern.prototype.draw = function (callback, { id = 'std', from, to, onQuery, ctx } = {}) { - if (typeof window === 'undefined') { - return this; - } - stopAnimationFrame(id); - ctx = ctx || getDrawContext(); - let cycle, - events = []; - const animate = (time) => { - const t = getTime(); - if (from !== undefined && to !== undefined) { - const currentCycle = Math.floor(t); - if (cycle !== currentCycle) { - cycle = currentCycle; - const begin = currentCycle + from; - const end = currentCycle + to; - setTimeout(() => { - events = this.query(new State(new TimeSpan(begin, end))) - .filter(Boolean) - .filter((event) => event.part.begin.equals(event.whole.begin)); - onQuery?.(events); - }, 0); - } - } - callback(ctx, events, t, time); - animationFrames[id] = requestAnimationFrame(animate); - }; - requestAnimationFrame(animate); - return this; -}; -// this is a more generic helper to get a rendering callback for the currently active haps -// TODO: this misses events that are prolonged with clip or duration (would need state) -Pattern.prototype.onFrame = function (id, fn, offset = 0) { +let memory = {}; +Pattern.prototype.draw = function (fn, options) { if (typeof window === 'undefined') { return this; } + let { id = 1, lookbehind = 0, lookahead = 0 } = options; + let __t = Math.max(getTime(), 0); stopAnimationFrame(id); + lookbehind = Math.abs(lookbehind); + // init memory, clear future haps of old pattern + memory[id] = (memory[id] || []).filter((h) => !h.isInFuture(__t)); + let newFuture = this.queryArc(__t, __t + lookahead).filter((h) => h.hasOnset()); + memory[id] = memory[id].concat(newFuture); + + let last; const animate = () => { - const t = getTime() + offset; - const haps = this.queryArc(t, t); - fn(haps, t, this); + const _t = getTime(); + const t = _t + lookahead; + // filter out haps that are too far in the past + memory[id] = memory[id].filter((h) => h.isInNearPast(lookbehind, _t)); + // begin where we left off in last frame, but max -0.1s (inactive tab throttles to 1fps) + let begin = Math.max(last || t, t - 1 / 10); + const haps = this.queryArc(begin, t).filter((h) => h.hasOnset()); + memory[id] = memory[id].concat(haps); + last = t; // makes sure no haps are missed + fn(memory[id], _t, t, this); animationFrames[id] = requestAnimationFrame(animate); }; - requestAnimationFrame(animate); + animationFrames[id] = requestAnimationFrame(animate); return this; }; @@ -198,3 +183,18 @@ 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; +} diff --git a/packages/draw/index.mjs b/packages/draw/index.mjs index 89cda805..506c6151 100644 --- a/packages/draw/index.mjs +++ b/packages/draw/index.mjs @@ -3,3 +3,4 @@ export * from './color.mjs'; export * from './draw.mjs'; export * from './pianoroll.mjs'; export * from './spiral.mjs'; +export * from './pitchwheel.mjs'; diff --git a/packages/draw/pianoroll.mjs b/packages/draw/pianoroll.mjs index f24f6114..1bdf81c4 100644 --- a/packages/draw/pianoroll.mjs +++ b/packages/draw/pianoroll.mjs @@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { Pattern, noteToMidi, freqToMidi } from '@strudel/core'; +import { getTheme, getDrawContext } from './draw.mjs'; const scale = (normalized, min, max) => normalized * (max - min) + min; const getValue = (e) => { @@ -36,26 +37,23 @@ const getValue = (e) => { }; Pattern.prototype.pianoroll = function (options = {}) { - let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false, ctx, id } = options; + let { cycles = 4, playhead = 0.5, overscan = 0, hideNegative = false, ctx = getDrawContext(), id = 1 } = options; let from = -cycles * playhead; let to = cycles * (1 - playhead); - + const inFrame = (hap, t) => (!hideNegative || hap.whole.begin >= 0) && hap.isWithinTime(t + from, t + to); this.draw( - (ctx, haps, t) => { - const inFrame = (event) => - (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from; + (haps, time) => { pianoroll({ ...options, - time: t, + time, ctx, - haps: haps.filter(inFrame), + haps: haps.filter((hap) => inFrame(hap, time)), }); }, { - from: from - overscan, - to: to + overscan, - ctx, + lookbehind: from - overscan, + lookahead: to + overscan, id, }, ); @@ -106,11 +104,8 @@ export function pianoroll({ flipTime = 0, flipValues = 0, hideNegative = false, - // inactive = '#C9E597', - // inactive = '#FFCA28', - inactive = '#7491D2', - active = '#FFCA28', - // background = '#2A3236', + inactive = getTheme().foreground, + active = getTheme().foreground, background = 'transparent', smear = 0, playheadColor = 'white', @@ -137,7 +132,7 @@ export function pianoroll({ let to = cycles * (1 - playhead); if (id) { - haps = haps.filter((hap) => hap.context.id === id); + haps = haps.filter((hap) => hap.hasTag(id)); } if (timeframeProp) { @@ -277,8 +272,8 @@ export function getDrawOptions(drawTime, options = {}) { export const getPunchcardPainter = (options = {}) => - (ctx, time, haps, drawTime, paintOptions = {}) => - pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }); + (ctx, time, haps, drawTime) => + pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }); Pattern.prototype.punchcard = function (options) { return this.onPaint(getPunchcardPainter(options)); diff --git a/packages/draw/pitchwheel.mjs b/packages/draw/pitchwheel.mjs new file mode 100644 index 00000000..8d3cfe61 --- /dev/null +++ b/packages/draw/pitchwheel.mjs @@ -0,0 +1,127 @@ +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, + }), + ); +}; diff --git a/packages/draw/spiral.mjs b/packages/draw/spiral.mjs index e0104c29..22f93f5d 100644 --- a/packages/draw/spiral.mjs +++ b/packages/draw/spiral.mjs @@ -1,4 +1,5 @@ import { Pattern } from '@strudel/core'; +import { getTheme } from './draw.mjs'; // polar coords -> xy function fromPolar(angle, radius, cx, cy) { @@ -19,7 +20,7 @@ function spiralSegment(options) { cy = 100, rotate = 0, thickness = margin / 2, - color = 'steelblue', + color = getTheme().foreground, cap = 'round', stretch = 1, fromOpacity = 1, @@ -61,7 +62,8 @@ function drawSpiral(options) { playheadThickness = thickness, padding = 0, steady = 1, - inactiveColor = '#ffffff50', + activeColor = getTheme().foreground, + inactiveColor = getTheme().gutterForeground, colorizeInactive = 0, fade = true, // logSpiral = true, @@ -73,7 +75,7 @@ function drawSpiral(options) { } = options; if (id) { - haps = haps.filter((hap) => hap.context.id === id); + haps = haps.filter((hap) => hap.hasTag(id)); } const [w, h] = [ctx.canvas.width, ctx.canvas.height]; @@ -102,7 +104,8 @@ function drawSpiral(options) { const isActive = hap.whole.begin <= time && hap.endClipped > time; const from = hap.whole.begin - time + inset; const to = hap.endClipped - time + inset - padding; - const color = hap.value?.color; + const hapColor = hap.value?.color || activeColor; + const color = colorizeInactive || isActive ? hapColor : inactiveColor; const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1; spiralSegment({ ctx, @@ -110,7 +113,7 @@ function drawSpiral(options) { from, to, rotate, - color: colorizeInactive || isActive ? color : inactiveColor, + color, fromOpacity: opacity, toOpacity: opacity, }); diff --git a/packages/mini/krill-parser.js b/packages/mini/krill-parser.js index b482d96c..7762242d 100644 --- a/packages/mini/krill-parser.js +++ b/packages/mini/krill-parser.js @@ -308,11 +308,11 @@ function peg$parse(input, options) { } return result; }; - var peg$f17 = function(s) { return new PatternStub(s, 'fastcat'); }; + var peg$f17 = function(tactus, s) { return new PatternStub(s, 'fastcat', undefined, !!tactus); }; var peg$f18 = function(tail) { return { alignment: 'stack', list: tail }; }; 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$f21 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; + var peg$f21 = function(head, tail) {if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }; var peg$f22 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }; var peg$f23 = function(sc) { return sc; }; var peg$f24 = function(s) { return { name: "struct", args: { mini:s }}}; @@ -1477,24 +1477,36 @@ function peg$parse(input, options) { } function peg$parsesequence() { - var s0, s1, s2; + var s0, s1, s2, s3; s0 = peg$currPos; - s1 = []; - s2 = peg$parseslice_with_ops(); - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parseslice_with_ops(); - } + if (input.charCodeAt(peg$currPos) === 94) { + s1 = peg$c9; + peg$currPos++; } else { 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; - s1 = peg$f17(s1); + s0 = peg$f17(s1, s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; } - s0 = s1; return s0; } @@ -2476,10 +2488,10 @@ function peg$parse(input, options) { this.location_ = location(); } - var PatternStub = function(source, alignment, seed) + var PatternStub = function(source, alignment, seed, tactus) { this.type_ = "pattern"; - this.arguments_ = { alignment: alignment }; + this.arguments_ = { alignment: alignment, tactus: tactus }; if (seed !== undefined) { this.arguments_.seed = seed; } diff --git a/packages/mini/krill.pegjs b/packages/mini/krill.pegjs index 18764a34..35d7bdc4 100644 --- a/packages/mini/krill.pegjs +++ b/packages/mini/krill.pegjs @@ -19,10 +19,10 @@ This program is free software: you can redistribute it and/or modify it under th this.location_ = location(); } - var PatternStub = function(source, alignment, seed) + var PatternStub = function(source, alignment, seed, tactus) { this.type_ = "pattern"; - this.arguments_ = { alignment: alignment }; + this.arguments_ = { alignment: alignment, tactus: tactus }; if (seed !== undefined) { 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) -sequence = s:(slice_with_ops)+ - { return new PatternStub(s, 'fastcat'); } +sequence = tactus:'^'? s:(slice_with_ops)+ + { return new PatternStub(s, 'fastcat', undefined, !!tactus); } // a stack is a series of vertically aligned sequence, separated by a comma stack_tail = tail:(comma @sequence)+ @@ -184,7 +184,7 @@ dot_tail = tail:(dot @sequence)+ // if the stack contains only one element, we don't create a stack but return the // underlying element stack_or_choose = head:sequence tail:(stack_tail / choose_tail / dot_tail)? - { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } } + {if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } } polymeter_stack = head:sequence tail:stack_tail? { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); } diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index c032cf12..8c893273 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import * as krill from './krill-parser.js'; import * as strudel from '@strudel/core'; +import Fraction, { lcm } from '@strudel/core/fraction.mjs'; const randOffset = 0.0003; @@ -88,45 +89,75 @@ export function patternifyAST(ast, code, onEnter, offset = 0) { resolveReplications(ast); const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter)); const alignment = ast.arguments_.alignment; - if (alignment === 'stack') { - return strudel.stack(...children); - } - if (alignment === 'polymeter_slowcat') { - const aligned = children.map((child) => child._slow(child.weight)); - return strudel.stack(...aligned); - } - if (alignment === 'polymeter') { - // polymeter - const stepsPerCycle = ast.arguments_.stepsPerCycle - ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) - : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].weight : 1)); + const with_tactus = children.filter((child) => child.__tactus_source); + let pat; + switch (alignment) { + case 'stack': { + pat = strudel.stack(...children); + if (with_tactus.length) { + pat.tactus = lcm(...with_tactus.map((x) => Fraction(x.tactus))); + } + break; + } + case 'polymeter_slowcat': { + 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 + const stepsPerCycle = ast.arguments_.stepsPerCycle + ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) + : 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)))); - return strudel.stack(...aligned); + const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight)))); + pat = strudel.stack(...aligned); + break; + } + case 'rand': { + pat = 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; + } + case 'feet': { + pat = strudel.fastcat(...children); + break; + } + default: { + const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); + if (weightedChildren) { + const weightSum = ast.source_.reduce( + (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)), + strudel.Fraction(0), + ); + 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 (alignment === 'rand') { - return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); + if (with_tactus.length) { + pat.__tactus_source = true; } - if (alignment === 'feet') { - return strudel.fastcat(...children); - } - const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); - if (weightedChildren) { - const weightSum = ast.source_.reduce( - (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)), - strudel.Fraction(0), - ); - const pat = strudel.timeCat( - ...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]), - ); - pat.weight = weightSum; - return pat; - } - const pat = strudel.sequence(...children); - pat.weight = children.length; return pat; } case 'element': { + 1; return enter(ast.source_); } case 'atom': { @@ -166,7 +197,7 @@ export const getLeafLocation = (code, leaf, globalOffset = 0) => { }; // takes quoted mini string, returns ast -export const mini2ast = (code, start, userCode) => { +export const mini2ast = (code, start = 0, userCode = code) => { try { return krill.parse(code); } catch (error) { diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 647512a8..92aa6c12 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { getLeafLocation, getLeafLocations, mini, mini2ast } from '../mini.mjs'; import '@strudel/core/euclid.mjs'; +import { Fraction } from '@strudel/core/index.mjs'; import { describe, expect, it } from 'vitest'; describe('mini', () => { @@ -207,6 +208,15 @@ describe('mini', () => { it('_ and @ are almost interchangeable', () => { 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', () => { diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index 4fa8d6d2..e6e0ee0e 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -41,17 +41,8 @@ if (typeof HTMLElement !== 'undefined') { initialCode: '// LOADING', pattern: silence, drawTime, - 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 }); - }); - }, + drawContext, prebake, - afterEval: ({ code }) => { - // window.location.hash = '#' + code2hash(code); - }, onUpdateState: (state) => { const event = new CustomEvent('update', { detail: state, diff --git a/packages/sampler/README.md b/packages/sampler/README.md new file mode 100644 index 00000000..c495c2ad --- /dev/null +++ b/packages/sampler/README.md @@ -0,0 +1,22 @@ +# @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 +``` diff --git a/packages/sampler/package.json b/packages/sampler/package.json new file mode 100644 index 00000000..c457b315 --- /dev/null +++ b/packages/sampler/package.json @@ -0,0 +1,19 @@ +{ + "name": "@strudel/sampler", + "version": "0.0.8", + "description": "", + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bin": "./sample-server.mjs", + "type": "module", + "dependencies": { + "cowsay": "^1.6.0" + } +} \ No newline at end of file diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs new file mode 100644 index 00000000..b9ae64ed --- /dev/null +++ b/packages/sampler/sample-server.mjs @@ -0,0 +1,117 @@ +#!/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}') +`); +}); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 85fccc97..10087fc6 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -194,6 +194,9 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option if (sampleMap.startsWith('github:')) { sampleMap = githubPath(sampleMap, 'strudel.json'); } + if (sampleMap.startsWith('local:')) { + sampleMap = `http://localhost:5432`; + } if (sampleMap.startsWith('shabda:')) { let [_, path] = sampleMap.split('shabda:'); sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index f899052f..c0ba96e0 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -273,6 +273,12 @@ export const superdough = async (value, t, hapDuration) => { value.duration = hapDuration; // calculate absolute time t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; + if (t < ac.currentTime) { + console.warn( + `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, + ); + return; + } // destructure let { s = 'triangle', diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 66fcd03c..0a08575d 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -235,6 +235,11 @@ Object.keys(simple).forEach((symbol) => { let alias = symbol.replace('^', 'M'); voicingAlias(symbol, alias, [complex, simple]); } + // add aliases for "+" === "aug" + if (symbol.includes('+')) { + let alias = symbol.replace('+', 'aug'); + voicingAlias(symbol, alias, [complex, simple]); + } }); registerVoicings('ireal', simple); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index d4b474d9..95156664 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -53,10 +53,12 @@ export function transpiler(input, options = {}) { return this.replace(sliderWithLocation(node)); } if (isWidgetMethod(node)) { + const type = node.callee.property.name; + const index = widgets.filter((w) => w.type === type).length; const widgetConfig = { to: node.end, - index: widgets.length, - type: node.callee.property.name, + index, + type, }; emitWidgets && widgets.push(widgetConfig); return this.replace(widgetWithLocation(node, widgetConfig)); diff --git a/packages/webaudio/scope.mjs b/packages/webaudio/scope.mjs index c9ee1f33..fbf4c8fc 100644 --- a/packages/webaudio/scope.mjs +++ b/packages/webaudio/scope.mjs @@ -1,5 +1,5 @@ import { Pattern, clamp } from '@strudel/core'; -import { getDrawContext } from '../draw/index.mjs'; +import { getDrawContext, getTheme } from '@strudel/draw'; import { analysers, getAnalyzerData } from 'superdough'; export function drawTimeScope( @@ -132,10 +132,13 @@ Pattern.prototype.fscope = function (config = {}) { * @example * s("sawtooth").scope() */ +let latestColor = {}; Pattern.prototype.tscope = function (config = {}) { let id = config.id ?? 1; return this.analyze(id).draw( - () => { + (haps) => { + config.color = haps[0]?.value?.color || getTheme().foreground; + latestColor[id] = config.color; clearScreen(config.smear, '0,0,0', config.ctx); drawTimeScope(analysers[id], config); }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cef19f74..74697ee6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,6 +361,12 @@ importers: specifier: ^5.0.10 version: 5.0.10 + packages/sampler: + dependencies: + cowsay: + specifier: ^1.6.0 + version: 1.6.0 + packages/serial: dependencies: '@strudel/core': @@ -5417,10 +5423,10 @@ packages: type-fest: 0.21.3 dev: true - /ansi-regex@2.1.1: - resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} - engines: {node: '>=0.10.0'} - dev: true + /ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + dev: false /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -6032,7 +6038,6 @@ packages: /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - dev: true /camelcase@7.0.1: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} @@ -6199,6 +6204,14 @@ packages: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: false + /cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: @@ -6261,11 +6274,6 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true - /code-point-at@1.1.0: - resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} - engines: {node: '>=0.10.0'} - dev: true - /collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} dev: false @@ -6533,6 +6541,17 @@ packages: typescript: 5.3.3 dev: true + /cowsay@1.6.0: + resolution: {integrity: sha512-8C4H1jdrgNusTQr3Yu4SCm+ZKsAlDFbpa0KS0Z3im8ueag+9pGOf3CrioruvmeaW/A5oqg9L0ar6qeftAh03jw==} + engines: {node: '>= 4'} + hasBin: true + dependencies: + get-stdin: 8.0.0 + string-width: 2.1.1 + strip-final-newline: 2.0.0 + yargs: 15.4.1 + dev: false + /crelt@1.0.5: resolution: {integrity: sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==} dev: false @@ -6651,7 +6670,6 @@ packages: /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - dev: true /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -7877,7 +7895,6 @@ packages: /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - dev: true /get-east-asian-width@1.2.0: resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} @@ -7916,6 +7933,11 @@ packages: engines: {node: '>=8'} dev: true + /get-stdin@8.0.0: + resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} + engines: {node: '>=10'} + dev: false + /get-stream@6.0.0: resolution: {integrity: sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==} engines: {node: '>=10'} @@ -8779,12 +8801,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - /is-fullwidth-code-point@1.0.0: - resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} - engines: {node: '>=0.10.0'} - dependencies: - number-is-nan: 1.0.1 - dev: true + /is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + dev: false /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -10893,11 +10913,6 @@ packages: set-blocking: 2.0.0 dev: true - /number-is-nan@1.0.1: - resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} - engines: {node: '>=0.10.0'} - dev: true - /nx@17.2.8: resolution: {integrity: sha512-rM5zXbuXLEuqQqcjVjClyvHwRJwt+NVImR2A6KFNG40Z60HP6X12wAxxeLHF5kXXTDRU0PFhf/yACibrpbPrAw==} hasBin: true @@ -12187,13 +12202,16 @@ packages: /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - dev: true /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} dev: true + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: false + /requirejs-config-file@4.0.0: resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==} engines: {node: '>=10.13.0'} @@ -12517,7 +12535,6 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - dev: true /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} @@ -12890,14 +12907,13 @@ packages: engines: {node: '>=0.10.0'} dev: true - /string-width@1.0.2: - resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} - engines: {node: '>=0.10.0'} + /string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} dependencies: - code-point-at: 1.1.0 - is-fullwidth-code-point: 1.0.0 - strip-ansi: 3.0.1 - dev: true + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + dev: false /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -12999,12 +13015,12 @@ packages: is-regexp: 1.0.0 dev: true - /strip-ansi@3.0.1: - resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} - engines: {node: '>=0.10.0'} + /strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} dependencies: - ansi-regex: 2.1.1 - dev: true + ansi-regex: 3.0.1 + dev: false /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -13039,7 +13055,6 @@ packages: /strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - dev: true /strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} @@ -14211,6 +14226,10 @@ packages: is-symbol: 1.0.4 dev: true + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: false + /which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -14267,7 +14286,7 @@ packages: /wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} dependencies: - string-width: 1.0.2 + string-width: 4.2.3 dev: true /widest-line@4.0.1: @@ -14459,6 +14478,15 @@ packages: worker-timers-worker: 7.0.67 dev: false + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -14551,6 +14579,10 @@ packages: engines: {node: '>=0.4'} dev: true + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -14567,6 +14599,14 @@ packages: engines: {node: '>= 14'} dev: false + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -14576,6 +14616,23 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: false + /yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 00000000..77a3e388 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,5 @@ +# samples folder + +1. copy any samples to this folder +2. run `npx @strudel/sampler` from this folder +3. add `samples('local:')` to your code diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 115a9ef1..27074590 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -954,31 +954,6 @@ exports[`runs examples > example "bank" example index 0 1`] = ` ] `; -exports[`runs examples > example "beatCat" example index 0 1`] = ` -[ - "[ 0/1 → 1/5 | s:bd ]", - "[ 1/5 → 2/5 | s:cp ]", - "[ 2/5 → 3/5 | s:bd ]", - "[ 3/5 → 4/5 | s:mt ]", - "[ 4/5 → 1/1 | s:bd ]", - "[ 1/1 → 6/5 | s:bd ]", - "[ 6/5 → 7/5 | s:cp ]", - "[ 7/5 → 8/5 | s:bd ]", - "[ 8/5 → 9/5 | s:mt ]", - "[ 9/5 → 2/1 | s:bd ]", - "[ 2/1 → 11/5 | s:bd ]", - "[ 11/5 → 12/5 | s:cp ]", - "[ 12/5 → 13/5 | s:bd ]", - "[ 13/5 → 14/5 | s:mt ]", - "[ 14/5 → 3/1 | s:bd ]", - "[ 3/1 → 16/5 | s:bd ]", - "[ 16/5 → 17/5 | s:cp ]", - "[ 17/5 → 18/5 | s:bd ]", - "[ 18/5 → 19/5 | s:mt ]", - "[ 19/5 → 4/1 | s:bd ]", -] -`; - exports[`runs examples > example "begin" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:rave begin:0 ]", @@ -5672,27 +5647,6 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; -exports[`runs examples > example "reweight" example index 0 1`] = ` -[ - "[ 0/1 → 1/4 | s:bd ]", - "[ 1/4 → 1/2 | s:sd ]", - "[ 1/2 → 3/4 | s:cp ]", - "[ 3/4 → 1/1 | s:bd ]", - "[ 1/1 → 5/4 | s:sd ]", - "[ 5/4 → 3/2 | s:cp ]", - "[ 3/2 → 7/4 | s:bd ]", - "[ 7/4 → 2/1 | s:sd ]", - "[ 2/1 → 9/4 | s:cp ]", - "[ 9/4 → 5/2 | s:bd ]", - "[ 5/2 → 11/4 | s:sd ]", - "[ 11/4 → 3/1 | s:cp ]", - "[ 3/1 → 13/4 | s:bd ]", - "[ 13/4 → 7/2 | s:sd ]", - "[ 7/2 → 15/4 | s:cp ]", - "[ 15/4 → 4/1 | s:bd ]", -] -`; - exports[`runs examples > example "ribbon" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:d ]", @@ -7133,6 +7087,31 @@ exports[`runs examples > example "stack" example index 0 2`] = ` ] `; +exports[`runs examples > example "stepcat" example index 0 1`] = ` +[ + "[ 0/1 → 1/5 | s:bd ]", + "[ 1/5 → 2/5 | s:cp ]", + "[ 2/5 → 3/5 | s:bd ]", + "[ 3/5 → 4/5 | s:mt ]", + "[ 4/5 → 1/1 | s:bd ]", + "[ 1/1 → 6/5 | s:bd ]", + "[ 6/5 → 7/5 | s:cp ]", + "[ 7/5 → 8/5 | s:bd ]", + "[ 8/5 → 9/5 | s:mt ]", + "[ 9/5 → 2/1 | s:bd ]", + "[ 2/1 → 11/5 | s:bd ]", + "[ 11/5 → 12/5 | s:cp ]", + "[ 12/5 → 13/5 | s:bd ]", + "[ 13/5 → 14/5 | s:mt ]", + "[ 14/5 → 3/1 | s:bd ]", + "[ 3/1 → 16/5 | s:bd ]", + "[ 16/5 → 17/5 | s:cp ]", + "[ 17/5 → 18/5 | s:bd ]", + "[ 18/5 → 19/5 | s:mt ]", + "[ 19/5 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "striate" example index 0 1`] = ` [ "[ 0/1 → 1/6 | s:numbers n:0 begin:0 end:0.16666666666666666 ]", @@ -7319,7 +7298,7 @@ exports[`runs examples > example "sustain" example index 0 1`] = ` ] `; -exports[`runs examples > example "timeCat" example index 0 1`] = ` +exports[`runs examples > example "timecat" example index 0 1`] = ` [ "[ 0/1 → 3/4 | note:e3 ]", "[ 3/4 → 1/1 | note:g3 ]", @@ -7332,7 +7311,7 @@ exports[`runs examples > example "timeCat" example index 0 1`] = ` ] `; -exports[`runs examples > example "timeCat" example index 1 1`] = ` +exports[`runs examples > example "timecat" example index 1 1`] = ` [ "[ 0/1 → 1/5 | s:bd ]", "[ 1/5 → 2/5 | s:sd ]", @@ -7357,6 +7336,27 @@ exports[`runs examples > example "timeCat" example index 1 1`] = ` ] `; +exports[`runs examples > example "toTactus" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd ]", + "[ 1/4 → 1/2 | s:sd ]", + "[ 1/2 → 3/4 | s:cp ]", + "[ 3/4 → 1/1 | s:bd ]", + "[ 1/1 → 5/4 | s:sd ]", + "[ 5/4 → 3/2 | s:cp ]", + "[ 3/2 → 7/4 | s:bd ]", + "[ 7/4 → 2/1 | s:sd ]", + "[ 2/1 → 9/4 | s:cp ]", + "[ 9/4 → 5/2 | s:bd ]", + "[ 5/2 → 11/4 | s:sd ]", + "[ 11/4 → 3/1 | s:cp ]", + "[ 3/1 → 13/4 | s:bd ]", + "[ 13/4 → 7/2 | s:sd ]", + "[ 7/2 → 15/4 | s:cp ]", + "[ 15/4 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "transpose" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C2 ]", diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 6389debb..774e5234 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -69,7 +69,7 @@ export default defineConfig({ registerType: 'autoUpdate', injectRegister: 'auto', workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg}'], + globPatterns: ['**/*.{js,css,html,ico,png,svg,json,wav,mp3,ogg,ttf,woff2,TTF,otf}'], runtimeCaching: [ { urlPattern: ({ url }) => diff --git a/website/public/fonts/Hack/Hack-Regular.ttf b/website/public/fonts/Hack/Hack-Regular.ttf new file mode 100644 index 00000000..92a90cb0 Binary files /dev/null and b/website/public/fonts/Hack/Hack-Regular.ttf differ diff --git a/website/public/fonts/Hack/license.txt b/website/public/fonts/Hack/license.txt new file mode 100644 index 00000000..7c475e1a --- /dev/null +++ b/website/public/fonts/Hack/license.txt @@ -0,0 +1,45 @@ +The work in the Hack project is Copyright 2018 Source Foundry Authors and licensed under the MIT License + +The work in the DejaVu project was committed to the public domain. + +Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera" + +### MIT License + +Copyright (c) 2018 Source Foundry Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +### BITSTREAM VERA LICENSE + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. \ No newline at end of file diff --git a/website/public/fonts/JetBrains/JetBrainsMono.woff2 b/website/public/fonts/JetBrains/JetBrainsMono.woff2 new file mode 100644 index 00000000..40da4276 Binary files /dev/null and b/website/public/fonts/JetBrains/JetBrainsMono.woff2 differ diff --git a/website/public/fonts/JetBrains/license.txt b/website/public/fonts/JetBrains/license.txt new file mode 100644 index 00000000..4d76c789 --- /dev/null +++ b/website/public/fonts/JetBrains/license.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/website/public/fonts/Monocraft/Monocraft.ttf b/website/public/fonts/Monocraft/Monocraft.ttf new file mode 100644 index 00000000..4066b0a9 Binary files /dev/null and b/website/public/fonts/Monocraft/Monocraft.ttf differ diff --git a/website/public/fonts/Monocraft/license.txt b/website/public/fonts/Monocraft/license.txt new file mode 100644 index 00000000..cd890982 --- /dev/null +++ b/website/public/fonts/Monocraft/license.txt @@ -0,0 +1,93 @@ +Copyright (c) 2022, Idrees Hassan (https://github.com/IdreesInc/Monocraft) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index fe618965..cadebcb3 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -39,16 +39,6 @@ export function MiniRepl({ const init = useCallback(({ code, shouldDraw }) => { const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null; - let onDraw; - if (shouldDraw) { - 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 }); - }); - }; - } const editor = new StrudelMirror({ id, @@ -60,7 +50,7 @@ export function MiniRepl({ initialCode: '// LOADING', pattern: silence, drawTime, - onDraw, + drawContext, editPattern: (pat, id) => { if (onTrigger) { pat = pat.onTrigger(onTrigger, false); diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 786aad66..9c472eb4 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -61,19 +61,12 @@ async function getModule(name) { export function Repl({ embedded = false }) { const isEmbedded = embedded || isIframe; - const { panelPosition, isZen } = useSettings(); + const { panelPosition, isZen, isSyncEnabled } = useSettings(); const init = useCallback(() => { const drawTime = [-2, 2]; const drawContext = getDrawContext(); - const 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 }); - }); - }; const editor = new StrudelMirror({ - sync: true, + sync: isSyncEnabled, defaultOutput: webaudioOutput, getTime: () => getAudioContext().currentTime, setInterval, @@ -84,7 +77,7 @@ export function Repl({ embedded = false }) { initialCode: '// LOADING', pattern: silence, drawTime, - onDraw, + drawContext, prebake: async () => Promise.all([modulesLoading, presets]), onUpdateState: (state) => { setReplState({ ...state }); diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/panel/SettingsTab.jsx index 7b968ad5..ae290189 100644 --- a/website/src/repl/panel/SettingsTab.jsx +++ b/website/src/repl/panel/SettingsTab.jsx @@ -62,21 +62,26 @@ function FormItem({ label, children }) { const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k])); const fontFamilyOptions = { monospace: 'monospace', - BigBlueTerminal: 'BigBlueTerminal', - x3270: 'x3270', - PressStart: 'PressStart2P', - galactico: 'galactico', - 'we-come-in-peace': 'we-come-in-peace', + Courier: 'Courier', + JetBrains: 'JetBrains', + Hack: 'Hack', FiraCode: 'FiraCode', 'FiraCode-SemiBold': 'FiraCode SemiBold', teletext: 'teletext', mode7: 'mode7', + BigBlueTerminal: 'BigBlueTerminal', + x3270: 'x3270', + Monocraft: 'Monocraft', + PressStart: 'PressStart2P', + 'we-come-in-peace': 'we-come-in-peace', + galactico: 'galactico', }; export function SettingsTab({ started }) { const { theme, keybindings, + isBracketClosingEnabled, isBracketMatchingEnabled, isLineNumbersDisplayed, isPatternHighlightingEnabled, @@ -84,6 +89,7 @@ export function SettingsTab({ started }) { isAutoCompletionEnabled, isTooltipEnabled, isFlashEnabled, + isSyncEnabled, isLineWrappingEnabled, fontSize, fontFamily, @@ -143,6 +149,11 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isBracketMatchingEnabled', cbEvent.target.checked)} value={isBracketMatchingEnabled} /> + settingsMap.setKey('isBracketClosingEnabled', cbEvent.target.checked)} + value={isBracketClosingEnabled} + /> settingsMap.setKey('isLineNumbersDisplayed', cbEvent.target.checked)} @@ -178,6 +189,16 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)} value={isFlashEnabled} /> + { + if (confirm('Changing this setting requires the window to reload itself. OK?')) { + settingsMap.setKey('isSyncEnabled', cbEvent.target.checked); + window.location.reload(); + } + }} + value={isSyncEnabled} + /> Try clicking the logo in the top left! diff --git a/website/src/repl/tunes.mjs b/website/src/repl/tunes.mjs index a89bdf6b..cffe0c0e 100644 --- a/website/src/repl/tunes.mjs +++ b/website/src/repl/tunes.mjs @@ -361,8 +361,8 @@ export const echoPiano = `// "Echo piano" // @by Felix Roos n("<0 2 [4 6](3,4,2) 3*2>").color('salmon') -.off(1/4, x=>x.add(2).color('green')) -.off(1/2, x=>x.add(6).color('steelblue')) +.off(1/4, x=>x.add(n(2)).color('green')) +.off(1/2, x=>x.add(n(6)).color('steelblue')) .scale('D minor') .echo(4, 1/8, .5) .clip(.5) diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 34aca701..a70c4202 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -8,11 +8,13 @@ export const defaultSettings = { activeFooter: 'intro', keybindings: 'codemirror', isBracketMatchingEnabled: true, + isBracketClosingEnabled: true, isLineNumbersDisplayed: true, isActiveLineHighlighted: true, isAutoCompletionEnabled: false, isTooltipEnabled: false, isFlashEnabled: true, + isSyncEnabled: false, isLineWrappingEnabled: false, isPatternHighlightingEnabled: true, theme: 'strudelTheme', @@ -29,6 +31,8 @@ export const defaultSettings = { export const settingsMap = persistentMap('strudel-settings', defaultSettings); +const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); + export function useSettings() { const state = useStore(settingsMap); @@ -40,15 +44,17 @@ export function useSettings() { }); return { ...state, - isZen: [true, 'true'].includes(state.isZen) ? true : false, - isBracketMatchingEnabled: [true, 'true'].includes(state.isBracketMatchingEnabled) ? true : false, - isLineNumbersDisplayed: [true, 'true'].includes(state.isLineNumbersDisplayed) ? true : false, - isActiveLineHighlighted: [true, 'true'].includes(state.isActiveLineHighlighted) ? true : false, - isAutoCompletionEnabled: [true, 'true'].includes(state.isAutoCompletionEnabled) ? true : false, - isPatternHighlightingEnabled: [true, 'true'].includes(state.isPatternHighlightingEnabled) ? true : false, - isTooltipEnabled: [true, 'true'].includes(state.isTooltipEnabled) ? true : false, - isLineWrappingEnabled: [true, 'true'].includes(state.isLineWrappingEnabled) ? true : false, - isFlashEnabled: [true, 'true'].includes(state.isFlashEnabled) ? true : false, + isZen: parseBoolean(state.isZen), + isBracketMatchingEnabled: parseBoolean(state.isBracketMatchingEnabled), + isBracketClosingEnabled: parseBoolean(state.isBracketClosingEnabled), + isLineNumbersDisplayed: parseBoolean(state.isLineNumbersDisplayed), + isActiveLineHighlighted: parseBoolean(state.isActiveLineHighlighted), + isAutoCompletionEnabled: parseBoolean(state.isAutoCompletionEnabled), + isPatternHighlightingEnabled: parseBoolean(state.isPatternHighlightingEnabled), + isTooltipEnabled: parseBoolean(state.isTooltipEnabled), + isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled), + isFlashEnabled: parseBoolean(state.isFlashEnabled), + isSyncEnabled: parseBoolean(state.isSyncEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! userPatterns: userPatterns, diff --git a/website/src/styles/index.css b/website/src/styles/index.css index dca41ff0..e50a54cf 100644 --- a/website/src/styles/index.css +++ b/website/src/styles/index.css @@ -14,6 +14,18 @@ font-family: 'galactico'; src: url('/fonts/galactico/Galactico-Basic.otf'); } +@font-face { + font-family: 'JetBrains'; + src: url('/fonts/JetBrains/JetBrainsMono.woff2'); +} +@font-face { + font-family: 'Monocraft'; + src: url('/fonts/Monocraft/Monocraft.ttf'); +} +@font-face { + font-family: 'Hack'; + src: url('/fonts/Hack/Hack-Regular.ttf'); +} @font-face { font-family: 'we-come-in-peace'; src: url('/fonts/we-come-in-peace/we-come-in-peace-bb.regular.ttf');