Merge branch 'mfix' of github.com:daslyfe/strudel into mfix
This commit is contained in:
commit
0f24e97855
48 changed files with 1090 additions and 377 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
2
packages/codemirror/themes.mjs
vendored
2
packages/codemirror/themes.mjs
vendored
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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])])));
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export * from './color.mjs';
|
|||
export * from './draw.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './pitchwheel.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));
|
||||
|
|
|
|||
127
packages/draw/pitchwheel.mjs
Normal file
127
packages/draw/pitchwheel.mjs
Normal file
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'); }
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
22
packages/sampler/README.md
Normal file
22
packages/sampler/README.md
Normal file
|
|
@ -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
|
||||
```
|
||||
19
packages/sampler/package.json
Normal file
19
packages/sampler/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@strudel/sampler",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bin": "./sample-server.mjs",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"cowsay": "^1.6.0"
|
||||
}
|
||||
}
|
||||
117
packages/sampler/sample-server.mjs
Normal file
117
packages/sampler/sample-server.mjs
Normal file
|
|
@ -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}')
|
||||
`);
|
||||
});
|
||||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue