Merge remote-tracking branch 'origin/HEAD' into paper
This commit is contained in:
commit
0c5903d822
18 changed files with 281 additions and 246 deletions
39
packages/core/drawLine.mjs
Normal file
39
packages/core/drawLine.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import Fraction, { gcd } from './fraction.mjs';
|
||||
|
||||
function drawLine(pat, chars = 60) {
|
||||
let cycle = 0;
|
||||
let pos = Fraction(0);
|
||||
let lines = [''];
|
||||
let emptyLine = ''; // this will be the "reference" empty line, which will be copied into extra lines
|
||||
while (lines[0].length < chars) {
|
||||
const haps = pat.queryArc(cycle, cycle + 1);
|
||||
const durations = haps.filter((hap) => hap.hasOnset()).map((hap) => hap.duration);
|
||||
const charFraction = gcd(...durations);
|
||||
const totalSlots = charFraction.inverse(); // number of character slots for the current cycle
|
||||
lines = lines.map((line) => line + '|'); // add pipe character before each cycle
|
||||
emptyLine += '|';
|
||||
for (let i = 0; i < totalSlots; i++) {
|
||||
const [begin, end] = [pos, pos.add(charFraction)];
|
||||
const matches = haps.filter((hap) => hap.whole.begin.lte(begin) && hap.whole.end.gte(end));
|
||||
const missingLines = matches.length - lines.length;
|
||||
if (missingLines > 0) {
|
||||
lines = lines.concat(Array(missingLines).fill(emptyLine));
|
||||
}
|
||||
lines = lines.map((line, i) => {
|
||||
const hap = matches[i];
|
||||
if (hap) {
|
||||
const isOnset = hap.whole.begin.eq(begin);
|
||||
const char = isOnset ? '' + hap.value : '-';
|
||||
return line + char;
|
||||
}
|
||||
return line + '.';
|
||||
});
|
||||
emptyLine += '.';
|
||||
pos = pos.add(charFraction);
|
||||
}
|
||||
cycle++;
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export default drawLine;
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
const events = pattern.firstCycle(); // query first cycle
|
||||
events.forEach((event) => {
|
||||
ctx.fillStyle = event.value;
|
||||
ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration * canvas.width, canvas.height);
|
||||
ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration.valueOf() * canvas.width, canvas.height);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ const fraction = (n) => {
|
|||
return Fraction(n);
|
||||
};
|
||||
|
||||
export const gcd = (...fractions) => {
|
||||
return fractions.reduce((gcd, fraction) => gcd.gcd(fraction), fraction(1));
|
||||
};
|
||||
|
||||
fraction._original = Fraction;
|
||||
|
||||
export default fraction;
|
||||
|
||||
// "If you concern performance, cache Fraction.js objects and pass arrays/objects.“
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class Hap {
|
|||
}
|
||||
|
||||
get duration() {
|
||||
return this.whole.end.sub(this.whole.begin).valueOf();
|
||||
return this.whole.end.sub(this.whole.begin);
|
||||
}
|
||||
|
||||
wholeOrPart() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Hap from './hap.mjs';
|
|||
import State from './state.mjs';
|
||||
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
|
||||
export class Pattern {
|
||||
// the following functions will get patternFactories as nested functions:
|
||||
|
|
@ -563,6 +564,17 @@ export class Pattern {
|
|||
return this._withContext((context) => ({ ...context, color }));
|
||||
}
|
||||
|
||||
log() {
|
||||
return this._withEvent((e) => {
|
||||
return e.setContext({ ...e.context, logs: (e.context?.logs || []).concat([e.show()]) });
|
||||
});
|
||||
}
|
||||
|
||||
drawLine() {
|
||||
console.log(drawLine(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
_segment(rate) {
|
||||
return this.struct(pure(true)._fast(rate));
|
||||
}
|
||||
|
|
@ -597,10 +609,6 @@ export class Pattern {
|
|||
return slowcatPrime(...pats);
|
||||
}
|
||||
|
||||
append(other) {
|
||||
return fastcat(...[this, other]);
|
||||
}
|
||||
|
||||
rev() {
|
||||
const pat = this;
|
||||
const query = function (state) {
|
||||
|
|
@ -643,14 +651,31 @@ export class Pattern {
|
|||
return this.juxBy(1, func);
|
||||
}
|
||||
|
||||
// is there a different name for those in tidal?
|
||||
stack(...pats) {
|
||||
return stack(this, ...pats);
|
||||
}
|
||||
|
||||
sequence(...pats) {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
// shorthand for sequence
|
||||
seq(...pats) {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
cat(...pats) {
|
||||
return cat(this, ...pats);
|
||||
}
|
||||
|
||||
fastcat(...pats) {
|
||||
return fastcat(this, ...pats);
|
||||
}
|
||||
|
||||
slowcat(...pats) {
|
||||
return slowcat(this, ...pats);
|
||||
}
|
||||
|
||||
superimpose(...funcs) {
|
||||
return this.stack(...funcs.map((func) => func(this)));
|
||||
}
|
||||
|
|
@ -790,7 +815,20 @@ Pattern.prototype.patternified = [
|
|||
'velocity',
|
||||
];
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr };
|
||||
Pattern.prototype.factories = {
|
||||
pure,
|
||||
stack,
|
||||
slowcat,
|
||||
fastcat,
|
||||
cat,
|
||||
timeCat,
|
||||
sequence,
|
||||
seq,
|
||||
polymeter,
|
||||
pm,
|
||||
polyrhythm,
|
||||
pr,
|
||||
};
|
||||
// the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts)
|
||||
|
||||
// Elemental patterns
|
||||
|
|
@ -818,18 +856,23 @@ export function reify(thing) {
|
|||
}
|
||||
return pure(thing);
|
||||
}
|
||||
|
||||
// Basic functions for combining patterns
|
||||
|
||||
export function stack(...pats) {
|
||||
const reified = pats.map((pat) => reify(pat));
|
||||
const query = (state) => flatten(reified.map((pat) => pat.query(state)));
|
||||
// Array test here is to avoid infinite recursions..
|
||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||
const query = (state) => flatten(pats.map((pat) => pat.query(state)));
|
||||
return new Pattern(query);
|
||||
}
|
||||
|
||||
export function slowcat(...pats) {
|
||||
// Concatenation: combines a list of patterns, switching between them
|
||||
// successively, one per cycle.
|
||||
pats = pats.map(reify);
|
||||
|
||||
// Array test here is to avoid infinite recursions..
|
||||
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
|
||||
|
||||
const query = function (state) {
|
||||
const span = state.span;
|
||||
const pat_n = mod(span.begin.sam(), pats.length);
|
||||
|
|
@ -866,7 +909,7 @@ export function fastcat(...pats) {
|
|||
}
|
||||
|
||||
export function cat(...pats) {
|
||||
return fastcat(...pats);
|
||||
return slowcat(...pats);
|
||||
}
|
||||
|
||||
export function timeCat(...timepats) {
|
||||
|
|
@ -882,6 +925,15 @@ export function timeCat(...timepats) {
|
|||
return stack(...pats);
|
||||
}
|
||||
|
||||
export function sequence(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
// shorthand for sequence
|
||||
export function seq(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
function _sequenceCount(x) {
|
||||
if (Array.isArray(x)) {
|
||||
if (x.length == 0) {
|
||||
|
|
@ -895,10 +947,6 @@ function _sequenceCount(x) {
|
|||
return [reify(x), 1];
|
||||
}
|
||||
|
||||
export function sequence(...xs) {
|
||||
return _sequenceCount(xs)[0];
|
||||
}
|
||||
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
const seqs = args.map((a) => _sequenceCount(a));
|
||||
if (seqs.length == 0) {
|
||||
|
|
@ -945,7 +993,6 @@ export function pr(args) {
|
|||
}
|
||||
|
||||
export const add = curry((a, pat) => pat.add(a));
|
||||
export const append = curry((a, pat) => pat.append(a));
|
||||
export const chunk = curry((a, pat) => pat.chunk(a));
|
||||
export const chunkBack = curry((a, pat) => pat.chunkBack(a));
|
||||
export const div = curry((a, pat) => pat.div(a));
|
||||
|
|
|
|||
50
packages/core/test/drawLine.test.mjs
Normal file
50
packages/core/test/drawLine.test.mjs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs';
|
||||
import { strict as assert } from 'assert';
|
||||
import drawLine from '../drawLine.mjs';
|
||||
|
||||
describe('drawLine', () => {
|
||||
it('supports equal lengths', () => {
|
||||
assert.equal(drawLine(fastcat(0), 4), '|0|0');
|
||||
assert.equal(drawLine(fastcat(0, 1), 4), '|01|01');
|
||||
assert.equal(drawLine(fastcat(0, 1, 2), 6), '|012|012');
|
||||
});
|
||||
it('supports unequal lengths', () => {
|
||||
assert.equal(drawLine(fastcat(0, [1, 2]), 10), '|0-12|0-12');
|
||||
assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123');
|
||||
assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23');
|
||||
});
|
||||
it('supports unequal silence', () => {
|
||||
assert.equal(drawLine(fastcat(0, silence, [1, 2]), 10), '|0-..12|0-..12');
|
||||
});
|
||||
it('supports polyrhythms', () => {
|
||||
'0*2 1*3';
|
||||
assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
|
||||
});
|
||||
it('supports multiple lines', () => {
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, stack(1, 2)), 10),
|
||||
`|01|01|01|01
|
||||
|.2|.2|.2|.2`,
|
||||
);
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, 1, stack(2, 3)), 10),
|
||||
`|012|012|012
|
||||
|..3|..3|..3`,
|
||||
);
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, stack(1, 2, 3)), 10),
|
||||
`|01|01|01|01
|
||||
|.2|.2|.2|.2
|
||||
|.3|.3|.3|.3`,
|
||||
);
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, 1, stack(2, 3, 4)), 10),
|
||||
`|012|012|012
|
||||
|..3|..3|..3
|
||||
|..4|..4|..4`,
|
||||
);
|
||||
});
|
||||
it('supports unequal cycle lengths', () => {
|
||||
assert.equal(drawLine(slowcat(0, [1, 2]), 10), `|0|12|0|12`);
|
||||
});
|
||||
});
|
||||
9
packages/core/test/fraction.test.mjs
Normal file
9
packages/core/test/fraction.test.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import Fraction, { gcd } from '../fraction.mjs';
|
||||
import { strict as assert } from 'assert';
|
||||
|
||||
describe('gcd', () => {
|
||||
it('should work', () => {
|
||||
const F = Fraction._original;
|
||||
assert.equal(gcd(F(1 / 6), F(1 / 4)).toFraction(), '1/12');
|
||||
});
|
||||
});
|
||||
|
|
@ -247,6 +247,12 @@ describe('Pattern', function () {
|
|||
['a', 'b', 'c'],
|
||||
);
|
||||
});
|
||||
it('Can stack subpatterns', function () {
|
||||
sameFirst(
|
||||
stack('a', ['b','c']),
|
||||
stack('a', sequence('b', 'c')),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_fast()', function () {
|
||||
it('Makes things faster', function () {
|
||||
|
|
@ -413,6 +419,12 @@ describe('Pattern', function () {
|
|||
['c'],
|
||||
);
|
||||
});
|
||||
it ('Can cat subpatterns', () => {
|
||||
sameFirst(
|
||||
slowcat('a', ['b','c']).fast(4),
|
||||
sequence('a', ['b', 'c']).fast(2)
|
||||
)
|
||||
})
|
||||
});
|
||||
describe('rev()', function () {
|
||||
it('Can reverse things', function () {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { evaluate, extend } from '../evaluate.mjs';
|
|||
import { mini } from '@strudel.cycles/mini';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { cat } = strudel;
|
||||
const { fastcat } = strudel;
|
||||
|
||||
extend({ mini }, strudel);
|
||||
|
||||
|
|
@ -12,11 +12,11 @@ describe('evaluate', () => {
|
|||
it('Should evaluate strudel functions', async () => {
|
||||
assert.deepStrictEqual(await ev("pure('c3')"), ['c3']);
|
||||
assert.deepStrictEqual(await ev('cat(c3)'), ['c3']);
|
||||
assert.deepStrictEqual(await ev('cat(c3, d3)'), ['c3', 'd3']);
|
||||
assert.deepStrictEqual(await ev('fastcat(c3, d3)'), ['c3', 'd3']);
|
||||
assert.deepStrictEqual(await ev('slowcat(c3, d3)'), ['c3']);
|
||||
});
|
||||
it('Should be extendable', async () => {
|
||||
extend({ myFunction: (...x) => cat(...x) });
|
||||
extend({ myFunction: (...x) => fastcat(...x) });
|
||||
assert.deepStrictEqual(await ev('myFunction(c3, d3)'), ['c3', 'd3']);
|
||||
});
|
||||
it('Should evaluate simple double quoted mini notation', async () => {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
|||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: event.duration * 1000 - 5,
|
||||
duration: event.duration.valueOf() * 1000 - 5,
|
||||
velocity,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,14 +54,14 @@ Pattern.prototype.tone = function (instrument) {
|
|||
note = getPlayableNoteValue(event);
|
||||
instrument.triggerAttack(note, time);
|
||||
} else if (instrument instanceof NoiseSynth) {
|
||||
instrument.triggerAttackRelease(event.duration, time); // noise has no value
|
||||
instrument.triggerAttackRelease(event.duration.valueOf(), time); // noise has no value
|
||||
} else if (instrument instanceof Piano) {
|
||||
note = getPlayableNoteValue(event);
|
||||
instrument.keyDown({ note, time, velocity });
|
||||
instrument.keyUp({ note, time: time + event.duration, velocity });
|
||||
instrument.keyUp({ note, time: time + event.duration.valueOf(), velocity });
|
||||
} else if (instrument instanceof Sampler) {
|
||||
note = getPlayableNoteValue(event);
|
||||
instrument.triggerAttackRelease(note, event.duration, time, velocity);
|
||||
instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
} else if (instrument instanceof Players) {
|
||||
if (!instrument.has(event.value)) {
|
||||
throw new Error(`name "${event.value}" not defined for players`);
|
||||
|
|
@ -69,10 +69,10 @@ Pattern.prototype.tone = function (instrument) {
|
|||
const player = instrument.player(event.value);
|
||||
// velocity ?
|
||||
player.start(time);
|
||||
player.stop(time + event.duration);
|
||||
player.stop(time + event.duration.valueOf());
|
||||
} else {
|
||||
note = getPlayableNoteValue(event);
|
||||
instrument.triggerAttackRelease(note, event.duration, time, velocity);
|
||||
instrument.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
}
|
||||
};
|
||||
return event.setContext({ ...event.context, instrument, onTrigger });
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ Pattern.prototype._wave = function (type) {
|
|||
const f = getFrequency(e);
|
||||
osc.frequency.value = f; // expects frequency..
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration;
|
||||
const end = begin + e.valueOf();
|
||||
osc.start(begin);
|
||||
osc.stop(end); // release?
|
||||
return osc;
|
||||
|
|
@ -49,7 +49,7 @@ Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
|
|||
return this.withAudioNode((t, e, node) => {
|
||||
const velocity = e.context?.velocity || 1;
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration + lookahead;
|
||||
const end = begin + e.duration.valueOf() + lookahead;
|
||||
const envelope = adsr(a, d, s, r, velocity, begin, end);
|
||||
node?.connect(envelope);
|
||||
return envelope;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue