Merge branch 'main' into more-functions
This commit is contained in:
commit
f4dd549a41
32 changed files with 518 additions and 3952 deletions
|
|
@ -275,14 +275,14 @@ const generic_params = [
|
|||
|
||||
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
|
||||
|
||||
const _unionise = (func) =>
|
||||
const _setter = (func) =>
|
||||
function (...pats) {
|
||||
return this.union(func(...pats));
|
||||
return this.set(func(...pats));
|
||||
};
|
||||
|
||||
generic_params.forEach(([type, name, description]) => {
|
||||
controls[name] = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _unionise(controls[name]);
|
||||
Pattern.prototype[name] = _setter(controls[name]);
|
||||
});
|
||||
|
||||
export default controls;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Fraction from './fraction.mjs';
|
|||
import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry } from './util.mjs';
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
|
||||
|
||||
export class Pattern {
|
||||
// the following functions will get patternFactories as nested functions:
|
||||
|
|
@ -182,11 +182,13 @@ export class Pattern {
|
|||
const event_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
|
||||
for (const hap_val of event_vals) {
|
||||
const new_whole = hap_func.whole;
|
||||
const new_part = hap_func.part.intersection_e(hap_val.part);
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
const new_part = hap_func.part.intersection(hap_val.part);
|
||||
if (new_part) {
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
}
|
||||
return haps;
|
||||
|
|
@ -203,11 +205,13 @@ export class Pattern {
|
|||
const hap_funcs = pat_func.query(state.setSpan(hap_val.wholeOrPart()));
|
||||
for (const hap_func of hap_funcs) {
|
||||
const new_whole = hap_val.whole;
|
||||
const new_part = hap_func.part.intersection_e(hap_val.part);
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
const new_part = hap_func.part.intersection(hap_val.part);
|
||||
if (new_part) {
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
}
|
||||
return haps;
|
||||
|
|
@ -243,9 +247,24 @@ export class Pattern {
|
|||
);
|
||||
}
|
||||
|
||||
_opleft(other, func) {
|
||||
_opLeft(other, func) {
|
||||
return this.fmap(func).appLeft(reify(other));
|
||||
}
|
||||
_opRight(other, func) {
|
||||
return this.fmap(func).appRight(reify(other));
|
||||
}
|
||||
_opBoth(other, func) {
|
||||
return this.fmap(func).appBoth(reify(other));
|
||||
}
|
||||
_opSqueeze(other, func) {
|
||||
const otherPat = reify(other);
|
||||
return this.fmap((a) => otherPat.fmap((b) => func(a)(b)))._squeezeJoin();
|
||||
}
|
||||
_opSqueezeFlip(other, func) {
|
||||
const thisPat = this;
|
||||
const otherPat = reify(other);
|
||||
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a)))._squeezeJoin();
|
||||
}
|
||||
|
||||
_asNumber(silent = false) {
|
||||
return this._withEvent((event) => {
|
||||
|
|
@ -271,22 +290,6 @@ export class Pattern {
|
|||
})._removeUndefineds();
|
||||
}
|
||||
|
||||
add(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a + b);
|
||||
}
|
||||
|
||||
sub(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a - b);
|
||||
}
|
||||
|
||||
mul(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a * b);
|
||||
}
|
||||
|
||||
div(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a / b);
|
||||
}
|
||||
|
||||
round() {
|
||||
return this._asNumber().fmap((v) => Math.round(v));
|
||||
}
|
||||
|
|
@ -320,10 +323,6 @@ export class Pattern {
|
|||
return this._fromBipolar().range(min, max);
|
||||
}
|
||||
|
||||
union(other) {
|
||||
return this._opleft(other, (a) => (b) => Object.assign({}, a, b));
|
||||
}
|
||||
|
||||
_bindWhole(choose_whole, func) {
|
||||
const pat_val = this;
|
||||
const query = function (state) {
|
||||
|
|
@ -464,7 +463,7 @@ export class Pattern {
|
|||
}
|
||||
|
||||
_compress(b, e) {
|
||||
if (b > e || b > 1 || e > 1 || b < 0 || e < 0) {
|
||||
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
|
||||
return silence;
|
||||
}
|
||||
return this._fastGap(Fraction(1).div(e.sub(b)))._late(b);
|
||||
|
|
@ -500,7 +499,7 @@ export class Pattern {
|
|||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
const slicePat = slowcat(...slice_objects);
|
||||
return this.union(slicePat)._fast(n);
|
||||
return this.set(slicePat)._fast(n);
|
||||
}
|
||||
|
||||
// cpm = cycles per minute
|
||||
|
|
@ -669,7 +668,7 @@ export class Pattern {
|
|||
}
|
||||
|
||||
stutWith(times, time, func) {
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
|
||||
}
|
||||
|
||||
stut(times, feedback, time) {
|
||||
|
|
@ -678,7 +677,7 @@ export class Pattern {
|
|||
|
||||
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
|
||||
_echoWith(times, time, func) {
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
|
||||
}
|
||||
|
||||
_echo(times, time, feedback) {
|
||||
|
|
@ -736,6 +735,49 @@ export class Pattern {
|
|||
}
|
||||
}
|
||||
|
||||
// pattern composers
|
||||
const composers = {
|
||||
set: [
|
||||
(a) => (b) => {
|
||||
// If an object is involved, do a union, discarding matching keys from a.
|
||||
// Otherwise, just return b.
|
||||
if (a instanceof Object || b instanceof Object) {
|
||||
if (!a instanceof Object) {
|
||||
a = { value: a };
|
||||
}
|
||||
if (!b instanceof Object) {
|
||||
b = { value: b };
|
||||
}
|
||||
return Object.assign({}, a, b);
|
||||
}
|
||||
return b;
|
||||
},
|
||||
id,
|
||||
],
|
||||
add: [(a) => (b) => a + b, (x) => x._asNumber()],
|
||||
sub: [(a) => (b) => a - b, (x) => x._asNumber()],
|
||||
mul: [(a) => (b) => a * b, (x) => x._asNumber()],
|
||||
div: [(a) => (b) => a / b, (x) => x._asNumber()],
|
||||
};
|
||||
|
||||
for (const [name, op] of Object.entries(composers)) {
|
||||
Pattern.prototype[name] = function (...other) {
|
||||
return op[1](this)._opLeft(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'Flip'] = function (...other) {
|
||||
return op[1](this)._opRight(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'Sect'] = function (...other) {
|
||||
return op[1](this)._opBoth(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'Squeeze'] = function (...other) {
|
||||
return op[1](this)._opSqueeze(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'SqueezeFlip'] = function (...other) {
|
||||
return op[1](this)._opSqueezeFlip(sequence(other), op[0]);
|
||||
};
|
||||
}
|
||||
|
||||
// methods of Pattern that get callable factories
|
||||
Pattern.prototype.patternified = [
|
||||
'apply',
|
||||
|
|
@ -798,7 +840,7 @@ export function slowcat(...pats) {
|
|||
pats = pats.map(reify);
|
||||
const query = function (state) {
|
||||
const span = state.span;
|
||||
const pat_n = Math.floor(span.begin) % pats.length;
|
||||
const pat_n = mod(span.begin.sam(), pats.length);
|
||||
const pat = pats[pat_n];
|
||||
if (!pat) {
|
||||
// pat_n can be negative, if the span is in the past..
|
||||
|
|
@ -938,7 +980,7 @@ export const slow = curry((a, pat) => pat.slow(a));
|
|||
export const struct = curry((a, pat) => pat.struct(a));
|
||||
export const sub = curry((a, pat) => pat.sub(a));
|
||||
export const superimpose = curry((array, pat) => pat.superimpose(...array));
|
||||
export const union = curry((a, pat) => pat.union(a));
|
||||
export const set = curry((a, pat) => pat.set(a));
|
||||
export const when = curry((binary, f, pat) => pat.when(binary, f));
|
||||
|
||||
// problem: curried functions with spread arguments must have pat at the beginning
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { id } from './util.mjs';
|
|||
|
||||
export function steady(value) {
|
||||
// A continuous value
|
||||
return new Pattern((span) => Hap(undefined, span, value));
|
||||
return new Pattern((state) => [new Hap(undefined, state.span, value)]);
|
||||
}
|
||||
|
||||
export const signal = (func) => {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ import {
|
|||
id,
|
||||
ply,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
//import { Time } from 'tone';
|
||||
import pkg from 'tone';
|
||||
const { Time } = pkg;
|
||||
|
|
@ -47,6 +50,10 @@ const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, co
|
|||
const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
const sameFirst = (a, b) => {
|
||||
return assert.deepStrictEqual(a._sortEventsByPart().firstCycle(), b._sortEventsByPart().firstCycle());
|
||||
};
|
||||
|
||||
describe('TimeSpan', function () {
|
||||
describe('equals()', function () {
|
||||
it('Should be equal to the same value', function () {
|
||||
|
|
@ -108,6 +115,18 @@ describe('Hap', function () {
|
|||
assert.deepStrictEqual(state3, { incrementme: 12 });
|
||||
});
|
||||
});
|
||||
describe('wholeOrPart()', () => {
|
||||
const ts1 = new TimeSpan(Fraction(0), Fraction(1));
|
||||
const ts0_5 = new TimeSpan(Fraction(0), Fraction(0.5));
|
||||
const continuousHap = new Hap(undefined, ts1, 'hello');
|
||||
const discreteHap = new Hap(ts1, ts0_5, 'hello');
|
||||
it('Can pick a whole', () => {
|
||||
assert.deepStrictEqual(discreteHap.wholeOrPart(), ts1);
|
||||
});
|
||||
it('Can pick a part', () => {
|
||||
assert.deepStrictEqual(continuousHap.wholeOrPart(), ts1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pattern', function () {
|
||||
|
|
@ -131,6 +150,33 @@ describe('Pattern', function () {
|
|||
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
|
||||
});
|
||||
});
|
||||
describe('addFlip()', () => {
|
||||
it('Can add things with structure from second pattern', () => {
|
||||
sameFirst(sequence(1, 2).addFlip(4), sequence(5, 6).struct(true));
|
||||
});
|
||||
});
|
||||
describe('addSqueeze()', () => {
|
||||
it('Can add while squeezing the second pattern inside the events of the first', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
|
||||
sequence(
|
||||
[11, 21, 31],
|
||||
[
|
||||
[12, 22, 32],
|
||||
[13, 23, 33],
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('addSqueezeFlip()', () => {
|
||||
it('Can add while squeezing the first pattern inside the events of the second', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).addSqueezeFlip(10, 20, 30),
|
||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('sub()', function () {
|
||||
it('Can subtract things', function () {
|
||||
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
|
||||
|
|
@ -146,14 +192,50 @@ describe('Pattern', function () {
|
|||
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
|
||||
});
|
||||
});
|
||||
describe('union()', function () {
|
||||
it('Can union things', function () {
|
||||
describe('set()', function () {
|
||||
it('Can set things in objects', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 4, b: 6 })
|
||||
.union(pure({ c: 7 }))
|
||||
.set(pure({ c: 7 }))
|
||||
.firstCycle()[0].value,
|
||||
{ a: 4, b: 6, c: 7 },
|
||||
);
|
||||
sameFirst(
|
||||
sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
|
||||
sequence({ a: 4, b: 2, c: 5 }).fast(3),
|
||||
);
|
||||
});
|
||||
it('Can set things with plain values', function () {
|
||||
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
|
||||
});
|
||||
describe('setFlip()', () => {
|
||||
it('Can set things with structure from second pattern', () => {
|
||||
sameFirst(sequence(1, 2).setFlip(4), pure(4).mask(true, true));
|
||||
});
|
||||
});
|
||||
describe('setSqueeze()', () => {
|
||||
it('Can squeeze one pattern inside the events of another', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
|
||||
sequence(
|
||||
['a', 'b', 'c'],
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
['a', 'b', 'c'],
|
||||
],
|
||||
),
|
||||
);
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
|
||||
sequence(
|
||||
['a', 'b', 'c'],
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
['a', 'b', 'c'],
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('stack()', function () {
|
||||
|
|
@ -292,6 +374,15 @@ describe('Pattern', function () {
|
|||
);
|
||||
});
|
||||
});
|
||||
describe('fastcat()', function () {
|
||||
it('Can go into negative time', function () {
|
||||
sameFirst(
|
||||
fastcat('a','b','c')
|
||||
.late(1000000),
|
||||
fastcat('a','b','c'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('slowcat()', function () {
|
||||
it('Can concatenate things slowly', function () {
|
||||
assert.deepStrictEqual(
|
||||
|
|
@ -399,7 +490,7 @@ describe('Pattern', function () {
|
|||
});
|
||||
});
|
||||
describe('struct()', function () {
|
||||
it('Can restructure a pattern', function () {
|
||||
it('Can restructure a discrete pattern', function () {
|
||||
assert.deepStrictEqual(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle(), [
|
||||
hap(ts(0, third), ts(0, third), 'a'),
|
||||
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
||||
|
|
@ -425,6 +516,12 @@ describe('Pattern', function () {
|
|||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can structure a continuous pattern', () => {
|
||||
assert.deepStrictEqual(
|
||||
steady('a').struct(true, [true, true]).firstCycle(),
|
||||
sequence('a', ['a', 'a']).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mask()', function () {
|
||||
it('Can fragment a pattern', function () {
|
||||
|
|
@ -616,6 +713,18 @@ describe('Pattern', function () {
|
|||
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Doesnt drop events in the 9th cycle', () => {
|
||||
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
|
||||
});
|
||||
});
|
||||
describe('striate', () => {
|
||||
it('Can striate(2)', () => {
|
||||
sameFirst(
|
||||
sequence({ sound: 'a' }).striate(2),
|
||||
sequence({ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0.5, end: 1 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chop', () => {
|
||||
it('Can _chop(2)', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { strict as assert } from 'assert';
|
||||
import { isNote, tokenizeNote, toMidi, mod, compose } from '../util.mjs';
|
||||
import { pure } from '../pattern.mjs';
|
||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should recognize notes without accidentals', () => {
|
||||
|
|
@ -64,6 +65,21 @@ describe('toMidi', () => {
|
|||
assert.equal(toMidi('C##3'), 50);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
assert.equal(fromMidi(69), 440);
|
||||
assert.equal(fromMidi(57), 220);
|
||||
});
|
||||
});
|
||||
describe('getFrequency', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
assert.equal(getFrequency(happify('a4')), 440);
|
||||
assert.equal(getFrequency(happify('a3')), 220);
|
||||
assert.equal(getFrequency(happify(440, { type: 'frequency' })), 440); // TODO: migrate when values are objects..
|
||||
assert.equal(getFrequency(happify(432, { type: 'frequency' })), 432);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mod', () => {
|
||||
it('should work like regular modulo with positive numbers', () => {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export class TimeSpan {
|
|||
// (Note that the output timespan probably does not start *at* Time 0 --
|
||||
// that only happens when the input Arc starts at an integral Time.)
|
||||
const b = this.begin.cyclePos();
|
||||
const e = b + (this.end - this.begin);
|
||||
const e = b.add(this.end.sub(this.begin));
|
||||
return new TimeSpan(b, e);
|
||||
}
|
||||
|
||||
|
|
@ -81,8 +81,7 @@ export class TimeSpan {
|
|||
// Like 'sect', but raises an exception if the timespans don't intersect.
|
||||
const result = this.intersection(other);
|
||||
if (result == undefined) {
|
||||
// TODO - raise exception
|
||||
// raise ValueError(f'TimeSpan {self} and TimeSpan {other} do not intersect')
|
||||
throw 'TimeSpans do not intersect';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const fromMidi = (n) => {
|
|||
|
||||
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
||||
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
||||
export const mod = (n, m) => (n < 0 ? mod(n + m, m) : n % m);
|
||||
export const mod = (n, m) => (n % m + m) % m;
|
||||
|
||||
export const getPlayableNoteValue = (event) => {
|
||||
let { value: note, context } = event;
|
||||
|
|
@ -40,6 +40,22 @@ export const getPlayableNoteValue = (event) => {
|
|||
return note;
|
||||
};
|
||||
|
||||
export const getFrequency = (event) => {
|
||||
let { value, context } = event;
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof value === 'object' && value.freq) {
|
||||
return value.freq;
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = fromMidi(event.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(event.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency:' + value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// rotate array by n steps (to the left)
|
||||
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue