Merge branch 'more-functions' of github.com:tidalcycles/strudel into more-functions

This commit is contained in:
alex 2022-04-24 17:51:41 +01:00
commit 1a5c95d084
16 changed files with 230 additions and 229 deletions

View 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;

View file

@ -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>

View file

@ -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.“

View file

@ -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() {

View file

@ -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));
}
@ -799,7 +811,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

View 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`);
});
});

View 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');
});
});

View file

@ -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,
});
};

View file

@ -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 });

View file

@ -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;