Merge branch 'main' of github.com:tidalcycles/strudel into paper
This commit is contained in:
commit
369604aa42
62 changed files with 7782 additions and 3944 deletions
|
|
@ -15,14 +15,14 @@ import { sequence, State, TimeSpan } from '@strudel.cycles/core';
|
|||
|
||||
const pattern = sequence('a', ['b', 'c']);
|
||||
|
||||
const events = pattern.query(new State(new TimeSpan(0, 2)));
|
||||
const events = pattern.queryArc(0, 1);
|
||||
|
||||
const spans = events.map(
|
||||
(event) => `${event.value}: ${event.whole.begin.toFraction()} - ${event.whole.end.toFraction()} `,
|
||||
);
|
||||
```
|
||||
|
||||
spans:
|
||||
yields:
|
||||
|
||||
```log
|
||||
a: 0 - 1/2
|
||||
|
|
@ -33,4 +33,6 @@ b: 3/2 - 7/4
|
|||
c: 7/4 - 2
|
||||
```
|
||||
|
||||
[play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
|
||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
|
||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/canvas.html)
|
||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/metro.html)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Pattern, sequence } from '@strudel.cycles/core/strudel.mjs';
|
||||
import { Pattern, sequence } from './pattern.mjs';
|
||||
|
||||
const controls = {};
|
||||
const generic_params = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Pattern, timeCat } from './strudel.mjs';
|
||||
import { Pattern, timeCat } from './pattern.mjs';
|
||||
import bjork from 'bjork';
|
||||
import { rotate } from './util.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Fraction from 'fraction.js';
|
||||
import { TimeSpan } from './strudel.mjs';
|
||||
import { TimeSpan } from './timespan.mjs';
|
||||
|
||||
// Returns the start of the cycle.
|
||||
Fraction.prototype.sam = function () {
|
||||
|
|
@ -16,6 +16,11 @@ Fraction.prototype.wholeCycle = function () {
|
|||
return new TimeSpan(this.sam(), this.nextSam());
|
||||
};
|
||||
|
||||
// The position of a time value relative to the start of its cycle.
|
||||
Fraction.prototype.cyclePos = function () {
|
||||
return this.sub(this.sam());
|
||||
};
|
||||
|
||||
Fraction.prototype.lt = function (other) {
|
||||
return this.compare(other) < 0;
|
||||
};
|
||||
|
|
|
|||
90
packages/core/hap.mjs
Normal file
90
packages/core/hap.mjs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
export class Hap {
|
||||
/*
|
||||
Event class, representing a value active during the timespan
|
||||
'part'. This might be a fragment of an event, in which case the
|
||||
timespan will be smaller than the 'whole' timespan, otherwise the
|
||||
two timespans will be the same. The 'part' must never extend outside of the
|
||||
'whole'. If the event represents a continuously changing value
|
||||
then the whole will be returned as None, in which case the given
|
||||
value will have been sampled from the point halfway between the
|
||||
start and end of the 'part' timespan.
|
||||
The context is to store a list of source code locations causing the event
|
||||
*/
|
||||
|
||||
constructor(whole, part, value, context = {}, stateful = false) {
|
||||
this.whole = whole;
|
||||
this.part = part;
|
||||
this.value = value;
|
||||
this.context = context;
|
||||
this.stateful = stateful;
|
||||
if (stateful) {
|
||||
console.assert(typeof this.value === 'function', 'Stateful values must be functions');
|
||||
}
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.whole.end.sub(this.whole.begin).valueOf();
|
||||
}
|
||||
|
||||
wholeOrPart() {
|
||||
return this.whole ? this.whole : this.part;
|
||||
}
|
||||
|
||||
withSpan(func) {
|
||||
// Returns a new event with the function f applies to the event timespan.
|
||||
const whole = this.whole ? func(this.whole) : undefined;
|
||||
return new Hap(whole, func(this.part), this.value, this.context);
|
||||
}
|
||||
|
||||
withValue(func) {
|
||||
// Returns a new event with the function f applies to the event value.
|
||||
return new Hap(this.whole, this.part, func(this.value), this.context);
|
||||
}
|
||||
|
||||
hasOnset() {
|
||||
// Test whether the event contains the onset, i.e that
|
||||
// the beginning of the part is the same as that of the whole timespan."""
|
||||
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
||||
}
|
||||
|
||||
resolveState(state) {
|
||||
if (this.stateful && this.hasOnset()) {
|
||||
console.log('stateful');
|
||||
const func = this.value;
|
||||
const [newState, newValue] = func(state);
|
||||
return [newState, new Hap(this.whole, this.part, newValue, this.context, false)];
|
||||
}
|
||||
return [state, this];
|
||||
}
|
||||
|
||||
spanEquals(other) {
|
||||
return (this.whole == undefined && other.whole == undefined) || this.whole.equals(other.whole);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return (
|
||||
this.spanEquals(other) &&
|
||||
this.part.equals(other.part) &&
|
||||
// TODO would == be better ??
|
||||
this.value === other.value
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
return (
|
||||
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
|
||||
);
|
||||
}
|
||||
|
||||
combineContext(b) {
|
||||
const a = this;
|
||||
return { ...a.context, ...b.context, locations: (a.context.locations || []).concat(b.context.locations || []) };
|
||||
}
|
||||
|
||||
setContext(context) {
|
||||
return new Hap(this.whole, this.part, this.value, context);
|
||||
}
|
||||
}
|
||||
|
||||
export default Hap;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export * from './controls.mjs';
|
||||
export * from './euclid.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
export {Fraction};
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
// export * from './value.mjs';
|
||||
19
packages/core/package-lock.json
generated
19
packages/core/package-lock.json
generated
|
|
@ -6,12 +6,11 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"bjork": "^0.0.1",
|
||||
"fraction.js": "^4.2.0",
|
||||
"ramda": "^0.28.0"
|
||||
"fraction.js": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.2.2"
|
||||
|
|
@ -733,15 +732,6 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/ramda": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz",
|
||||
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ramda"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
|
|
@ -1485,11 +1475,6 @@
|
|||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"ramda": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz",
|
||||
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA=="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.3",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "strudel.mjs",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
|
|
@ -26,8 +26,7 @@
|
|||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"dependencies": {
|
||||
"bjork": "^0.0.1",
|
||||
"fraction.js": "^4.2.0",
|
||||
"ramda": "^0.28.0"
|
||||
"fraction.js": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.2.2"
|
||||
|
|
|
|||
|
|
@ -1,224 +1,20 @@
|
|||
import TimeSpan from './timespan.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { compose } from 'ramda'; // will remove this as soon as compose is implemented here
|
||||
import { isNote, toMidi } from './util.mjs';
|
||||
import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
|
||||
// Removes 'None' values from given list
|
||||
const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry } from './util.mjs';
|
||||
|
||||
const flatten = (arr) => [].concat(...arr);
|
||||
|
||||
const id = (a) => a;
|
||||
|
||||
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||
|
||||
export function curry(func, overload) {
|
||||
const fn = function curried(...args) {
|
||||
if (args.length >= func.length) {
|
||||
return func.apply(this, args);
|
||||
} else {
|
||||
const partial = function (...args2) {
|
||||
return curried.apply(this, args.concat(args2));
|
||||
};
|
||||
if (overload) {
|
||||
overload(partial, args);
|
||||
}
|
||||
return partial;
|
||||
}
|
||||
};
|
||||
if (overload) {
|
||||
// overload function without args... needed for chordBass.transpose(2)
|
||||
overload(fn, []);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
class TimeSpan {
|
||||
constructor(begin, end) {
|
||||
this.begin = Fraction(begin);
|
||||
this.end = Fraction(end);
|
||||
}
|
||||
|
||||
get spanCycles() {
|
||||
const spans = [];
|
||||
var begin = this.begin;
|
||||
const end = this.end;
|
||||
const end_sam = end.sam();
|
||||
|
||||
while (end.gt(begin)) {
|
||||
// If begin and end are in the same cycle, we're done.
|
||||
if (begin.sam().equals(end_sam)) {
|
||||
spans.push(new TimeSpan(begin, this.end));
|
||||
break;
|
||||
}
|
||||
// add a timespan up to the next sam
|
||||
const next_begin = begin.nextSam();
|
||||
spans.push(new TimeSpan(begin, next_begin));
|
||||
|
||||
// continue with the next cycle
|
||||
begin = next_begin;
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
withTime(func_time) {
|
||||
// Applies given function to both the begin and end time value of the timespan"""
|
||||
return new TimeSpan(func_time(this.begin), func_time(this.end));
|
||||
}
|
||||
withEnd(func_time) {
|
||||
// Applies given function to both the begin and end time value of the timespan"""
|
||||
return new TimeSpan(this.begin, func_time(this.end));
|
||||
}
|
||||
|
||||
intersection(other) {
|
||||
// Intersection of two timespans, returns None if they don't intersect.
|
||||
const intersect_begin = this.begin.max(other.begin);
|
||||
const intersect_end = this.end.min(other.end);
|
||||
|
||||
if (intersect_begin.gt(intersect_end)) {
|
||||
return undefined;
|
||||
}
|
||||
if (intersect_begin.equals(intersect_end)) {
|
||||
// Zero-width (point) intersection - doesn't intersect if it's at the end of a
|
||||
// non-zero-width timespan.
|
||||
if (intersect_begin.equals(this.end) && this.begin.lt(this.end)) {
|
||||
return undefined;
|
||||
}
|
||||
if (intersect_begin.equals(other.end) && other.begin.lt(other.end)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return new TimeSpan(intersect_begin, intersect_end);
|
||||
}
|
||||
|
||||
intersection_e(other) {
|
||||
// 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')
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
midpoint() {
|
||||
return this.begin.add(this.end.sub(this.begin).div(Fraction(2)));
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return this.begin.equals(other.begin) && this.end.equals(other.end);
|
||||
}
|
||||
|
||||
show() {
|
||||
return this.begin.show() + ' -> ' + this.end.show();
|
||||
}
|
||||
}
|
||||
|
||||
class Hap {
|
||||
/*
|
||||
Event class, representing a value active during the timespan
|
||||
'part'. This might be a fragment of an event, in which case the
|
||||
timespan will be smaller than the 'whole' timespan, otherwise the
|
||||
two timespans will be the same. The 'part' must never extend outside of the
|
||||
'whole'. If the event represents a continuously changing value
|
||||
then the whole will be returned as None, in which case the given
|
||||
value will have been sampled from the point halfway between the
|
||||
start and end of the 'part' timespan.
|
||||
The context is to store a list of source code locations causing the event
|
||||
*/
|
||||
|
||||
constructor(whole, part, value, context = {}, stateful = false) {
|
||||
this.whole = whole;
|
||||
this.part = part;
|
||||
this.value = value;
|
||||
this.context = context;
|
||||
this.stateful = stateful;
|
||||
if (stateful) {
|
||||
console.assert(typeof this.value === 'function', 'Stateful values must be functions');
|
||||
}
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.whole.end.sub(this.whole.begin).valueOf();
|
||||
}
|
||||
|
||||
withSpan(func) {
|
||||
// Returns a new event with the function f applies to the event timespan.
|
||||
const whole = this.whole ? func(this.whole) : undefined;
|
||||
return new Hap(whole, func(this.part), this.value, this.context);
|
||||
}
|
||||
|
||||
withValue(func) {
|
||||
// Returns a new event with the function f applies to the event value.
|
||||
return new Hap(this.whole, this.part, func(this.value), this.context);
|
||||
}
|
||||
|
||||
hasOnset() {
|
||||
// Test whether the event contains the onset, i.e that
|
||||
// the beginning of the part is the same as that of the whole timespan."""
|
||||
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
|
||||
}
|
||||
|
||||
resolveState(state) {
|
||||
if (this.stateful && this.hasOnset()) {
|
||||
console.log('stateful');
|
||||
const func = this.value;
|
||||
const [newState, newValue] = func(state);
|
||||
return [newState, new Hap(this.whole, this.part, newValue, this.context, false)];
|
||||
}
|
||||
return [state, this];
|
||||
}
|
||||
|
||||
spanEquals(other) {
|
||||
return (this.whole == undefined && other.whole == undefined) || this.whole.equals(other.whole);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return (
|
||||
this.spanEquals(other) &&
|
||||
this.part.equals(other.part) &&
|
||||
// TODO would == be better ??
|
||||
this.value === other.value
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
return (
|
||||
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
|
||||
);
|
||||
}
|
||||
|
||||
setContext(context) {
|
||||
return new Hap(this.whole, this.part, this.value, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class State {
|
||||
constructor(span, controls = {}) {
|
||||
this.span = span;
|
||||
this.controls = controls;
|
||||
}
|
||||
|
||||
// Returns new State with different span
|
||||
setSpan(span) {
|
||||
return new State(span, this.controls);
|
||||
}
|
||||
|
||||
withSpan(func) {
|
||||
return this.setSpan(func(this.span));
|
||||
}
|
||||
|
||||
// Returns new State with different controls
|
||||
setControls(controls) {
|
||||
return new State(this.span, controls);
|
||||
}
|
||||
}
|
||||
|
||||
class Pattern {
|
||||
export class Pattern {
|
||||
// the following functions will get patternFactories as nested functions:
|
||||
constructor(query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
queryArc(begin, end) {
|
||||
return this.query(new State(new TimeSpan(begin, end)));
|
||||
}
|
||||
|
||||
_splitQueries() {
|
||||
// Splits queries at cycle boundaries. This makes some calculations
|
||||
// easier to express, as all events are then constrained to happen within
|
||||
|
|
@ -352,12 +148,11 @@ class Pattern {
|
|||
if (s == undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// TODO: is it right to add event_val.context here?
|
||||
return new Hap(
|
||||
whole_func(event_func.whole, event_val.whole),
|
||||
s,
|
||||
event_func.value(event_val.value),
|
||||
event_val.context,
|
||||
event_val.combineContext(event_func),
|
||||
);
|
||||
};
|
||||
return flatten(
|
||||
|
|
@ -389,11 +184,8 @@ class Pattern {
|
|||
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 hap = new Hap(new_whole, new_part, new_value, {
|
||||
...hap_val.context,
|
||||
...hap_func.context,
|
||||
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || []),
|
||||
});
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
|
|
@ -413,11 +205,8 @@ class Pattern {
|
|||
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 hap = new Hap(new_whole, new_part, new_value, {
|
||||
...hap_func.context,
|
||||
...hap_val.context,
|
||||
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || []),
|
||||
});
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
|
|
@ -510,6 +299,23 @@ class Pattern {
|
|||
return this._asNumber().fmap((v) => Math.ceil(v));
|
||||
}
|
||||
|
||||
_toBipolar() {
|
||||
return this.fmap((x) => x * 2 - 1);
|
||||
}
|
||||
_fromBipolar() {
|
||||
return this.fmap((x) => (x + 1) / 2);
|
||||
}
|
||||
|
||||
// Assumes source pattern of numbers in range 0..1
|
||||
range(min, max) {
|
||||
return this.mul(max - min).add(min);
|
||||
}
|
||||
|
||||
// Assumes source pattern of numbers in range -1..1
|
||||
range2(min, max) {
|
||||
return this._fromBipolar().range(min, max);
|
||||
}
|
||||
|
||||
union(other) {
|
||||
return this._opleft(other, (a) => (b) => Object.assign({}, a, b));
|
||||
}
|
||||
|
|
@ -553,24 +359,61 @@ class Pattern {
|
|||
return this.bind(id);
|
||||
}
|
||||
|
||||
innerBind(func) {
|
||||
outerBind(func) {
|
||||
return this._bindWhole((a, _) => a, func);
|
||||
}
|
||||
|
||||
outerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
return this.outerBind(id);
|
||||
}
|
||||
|
||||
innerBind(func) {
|
||||
return this._bindWhole((_, b) => b, func);
|
||||
}
|
||||
|
||||
innerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
return this.innerBind(id);
|
||||
}
|
||||
|
||||
outerBind(func) {
|
||||
return this._bindWhole((_, b) => b, func);
|
||||
_squeezeJoin() {
|
||||
const pat_of_pats = this;
|
||||
function query(state) {
|
||||
const haps = pat_of_pats.query(state);
|
||||
function flatHap(outerHap) {
|
||||
const pat = outerHap.value._compressSpan(outerHap.wholeOrPart().cycleArc());
|
||||
const innerHaps = pat.query(state.setSpan(outerHap.part));
|
||||
function munge(outer, inner) {
|
||||
let whole = undefined;
|
||||
if (inner.whole && outer.whole) {
|
||||
whole = inner.whole.intersection(outer.whole);
|
||||
if (!whole) {
|
||||
// The wholes are present, but don't intersect
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const part = inner.part.intersection(outer.part);
|
||||
if (!part) {
|
||||
// The parts don't intersect
|
||||
return undefined;
|
||||
}
|
||||
const context = inner.combineContext(outer);
|
||||
return new Hap(whole, part, inner.value, context);
|
||||
}
|
||||
return innerHaps.map((innerHap) => munge(outerHap, innerHap));
|
||||
}
|
||||
const result = flatten(haps.map(flatHap));
|
||||
// remove undefineds
|
||||
return result.filter((x) => x);
|
||||
}
|
||||
return new Pattern(query);
|
||||
}
|
||||
|
||||
outerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
return this.outerBind(id);
|
||||
_squeezeBind(func) {
|
||||
return this.fmap(func)._squeezeJoin();
|
||||
}
|
||||
|
||||
_apply(func) {
|
||||
|
|
@ -590,7 +433,7 @@ class Pattern {
|
|||
args = args.map((arg) => (isPattern(arg) ? arg.fmap((value) => value.value || value) : arg));
|
||||
const pat_arg = sequence(...args);
|
||||
// arg.locations has to go somewhere..
|
||||
return pat_arg.fmap((arg) => func.call(pat, arg)).outerJoin();
|
||||
return pat_arg.fmap((arg) => func.call(pat, arg)).innerJoin();
|
||||
};
|
||||
return patterned;
|
||||
}
|
||||
|
|
@ -616,15 +459,17 @@ class Pattern {
|
|||
return this.withQuerySpan(qf).withEventSpan(ef)._splitQueries();
|
||||
}
|
||||
|
||||
_compressSpan(span) {
|
||||
const b = span.begin;
|
||||
const e = span.end;
|
||||
_compress(b, e) {
|
||||
if (b > e || b > 1 || e > 1 || b < 0 || e < 0) {
|
||||
return silence;
|
||||
}
|
||||
return this._fastGap(Fraction(1).div(e.sub(b)))._late(b);
|
||||
}
|
||||
|
||||
_compressSpan(span) {
|
||||
return this._compress(span.begin, span.end);
|
||||
}
|
||||
|
||||
_fast(factor) {
|
||||
const fastQuery = this.withQueryTime((t) => t.mul(factor));
|
||||
return fastQuery.withEventTime((t) => t.div(factor));
|
||||
|
|
@ -634,6 +479,19 @@ class Pattern {
|
|||
return this._fast(Fraction(1).div(factor));
|
||||
}
|
||||
|
||||
_ply(factor) {
|
||||
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
|
||||
}
|
||||
|
||||
_chop(n) {
|
||||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
const func = function (o) {
|
||||
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
|
||||
};
|
||||
return this._squeezeBind(func);
|
||||
}
|
||||
|
||||
// cpm = cycles per minute
|
||||
_cpm(cpm) {
|
||||
return this._fast(cpm / 60);
|
||||
|
|
@ -651,6 +509,28 @@ class Pattern {
|
|||
return this._early(Fraction(0).sub(offset));
|
||||
}
|
||||
|
||||
_zoom(s, e) {
|
||||
e = Fraction(e);
|
||||
s = Fraction(s);
|
||||
const d = e.sub(s);
|
||||
return this.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||
.withEventSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
||||
._splitQueries();
|
||||
}
|
||||
|
||||
_zoomArc(a) {
|
||||
return this.zoom(a.begin, a.end);
|
||||
}
|
||||
|
||||
_linger(t) {
|
||||
if (t == 0) {
|
||||
return silence;
|
||||
} else if (t < 0) {
|
||||
return this._zoom(t.add(1), 1)._slow(t);
|
||||
}
|
||||
return this._zoom(0, t)._slow(t);
|
||||
}
|
||||
|
||||
struct(...binary_pats) {
|
||||
// Re structure the pattern according to a binary pattern (false values are dropped)
|
||||
const binary_pat = sequence(binary_pats);
|
||||
|
|
@ -731,7 +611,7 @@ class Pattern {
|
|||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
jux(func, by = 1) {
|
||||
juxBy(by, func) {
|
||||
by /= 2;
|
||||
const elem_or = function (dict, key, dflt) {
|
||||
if (key in dict) {
|
||||
|
|
@ -745,6 +625,10 @@ class Pattern {
|
|||
return stack(left, func(right));
|
||||
}
|
||||
|
||||
_jux(func) {
|
||||
return this.juxBy(1, func);
|
||||
}
|
||||
|
||||
// is there a different name for those in tidal?
|
||||
stack(...pats) {
|
||||
return stack(this, ...pats);
|
||||
|
|
@ -758,7 +642,7 @@ class Pattern {
|
|||
}
|
||||
|
||||
stutWith(times, time, func) {
|
||||
return stack(...range(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
}
|
||||
|
||||
stut(times, feedback, time) {
|
||||
|
|
@ -767,7 +651,7 @@ class Pattern {
|
|||
|
||||
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
|
||||
_echoWith(times, time, func) {
|
||||
return stack(...range(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
}
|
||||
|
||||
_echo(times, time, feedback) {
|
||||
|
|
@ -775,7 +659,7 @@ class Pattern {
|
|||
}
|
||||
|
||||
iter(times, back = false) {
|
||||
return slowcat(...range(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
|
||||
return slowcat(...listRange(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
|
||||
}
|
||||
|
||||
// known as iter' in tidalcycles
|
||||
|
|
@ -832,16 +716,20 @@ class Pattern {
|
|||
// methods of Pattern that get callable factories
|
||||
Pattern.prototype.patternified = [
|
||||
'apply',
|
||||
'fast',
|
||||
'slow',
|
||||
'cpm',
|
||||
'early',
|
||||
'late',
|
||||
'duration',
|
||||
'legato',
|
||||
'velocity',
|
||||
'segment',
|
||||
'chop',
|
||||
'color',
|
||||
'cpm',
|
||||
'duration',
|
||||
'early',
|
||||
'fast',
|
||||
'jux',
|
||||
'late',
|
||||
'legato',
|
||||
'linger',
|
||||
'ply',
|
||||
'segment',
|
||||
'slow',
|
||||
'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 };
|
||||
|
|
@ -850,9 +738,9 @@ Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, seq
|
|||
// Elemental patterns
|
||||
|
||||
// Nothing
|
||||
const silence = new Pattern((_) => []);
|
||||
export const silence = new Pattern((_) => []);
|
||||
|
||||
function pure(value) {
|
||||
export function pure(value) {
|
||||
// A discrete value that repeats once per cycle
|
||||
function query(state) {
|
||||
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
|
||||
|
|
@ -860,43 +748,12 @@ function pure(value) {
|
|||
return new Pattern(query);
|
||||
}
|
||||
|
||||
function steady(value) {
|
||||
// A continuous value
|
||||
return new Pattern((span) => Hap(undefined, span, value));
|
||||
}
|
||||
|
||||
export const signal = (func) => {
|
||||
const query = (state) => [new Hap(undefined, state.span, func(state.span.midpoint()))];
|
||||
return new Pattern(query);
|
||||
};
|
||||
|
||||
const _toBipolar = (pat) => pat.fmap((x) => x * 2 - 1);
|
||||
const _fromBipolar = (pat) => pat.fmap((x) => (x + 1) / 2);
|
||||
|
||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
export const sine = _fromBipolar(sine2);
|
||||
|
||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
export const cosine = sine._early(Fraction(1).div(4));
|
||||
|
||||
export const saw = signal((t) => t % 1);
|
||||
export const saw2 = _toBipolar(saw);
|
||||
|
||||
export const isaw = signal((t) => 1 - (t % 1));
|
||||
export const isaw2 = _toBipolar(isaw);
|
||||
|
||||
export const tri2 = fastcat(isaw2, saw2);
|
||||
export const tri = fastcat(isaw, saw);
|
||||
|
||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
export const square2 = _toBipolar(square);
|
||||
|
||||
export function isPattern(thing) {
|
||||
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
|
||||
return thing instanceof Pattern;
|
||||
}
|
||||
|
||||
function reify(thing) {
|
||||
export function reify(thing) {
|
||||
// Turns something into a pattern, unless it's already a pattern
|
||||
if (isPattern(thing)) {
|
||||
return thing;
|
||||
|
|
@ -905,13 +762,13 @@ function reify(thing) {
|
|||
}
|
||||
// Basic functions for combining patterns
|
||||
|
||||
function stack(...pats) {
|
||||
export function stack(...pats) {
|
||||
const reified = pats.map((pat) => reify(pat));
|
||||
const query = (state) => flatten(reified.map((pat) => pat.query(state)));
|
||||
return new Pattern(query);
|
||||
}
|
||||
|
||||
function slowcat(...pats) {
|
||||
export function slowcat(...pats) {
|
||||
// Concatenation: combines a list of patterns, switching between them
|
||||
// successively, one per cycle.
|
||||
pats = pats.map(reify);
|
||||
|
|
@ -932,7 +789,7 @@ function slowcat(...pats) {
|
|||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
function slowcatPrime(...pats) {
|
||||
export function slowcatPrime(...pats) {
|
||||
// Concatenation: combines a list of patterns, switching between them
|
||||
// successively, one per cycle. Unlike slowcat, this version will skip cycles.
|
||||
pats = pats.map(reify);
|
||||
|
|
@ -944,24 +801,24 @@ function slowcatPrime(...pats) {
|
|||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
function fastcat(...pats) {
|
||||
export function fastcat(...pats) {
|
||||
// Concatenation: as with slowcat, but squashes a cycle from each
|
||||
// pattern into one cycle
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
|
||||
function cat(...pats) {
|
||||
export function cat(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
function timeCat(...timepats) {
|
||||
export function timeCat(...timepats) {
|
||||
// Like cat, but where each step has a temporal 'weight'
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
let begin = Fraction(0);
|
||||
const pats = [];
|
||||
for (const [time, pat] of timepats) {
|
||||
const end = begin.add(time);
|
||||
pats.push(reify(pat)._compressSpan(new TimeSpan(begin.div(total), end.div(total))));
|
||||
pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
|
||||
begin = end;
|
||||
}
|
||||
return stack(...pats);
|
||||
|
|
@ -980,11 +837,11 @@ function _sequenceCount(x) {
|
|||
return [reify(x), 1];
|
||||
}
|
||||
|
||||
function sequence(...xs) {
|
||||
export function sequence(...xs) {
|
||||
return _sequenceCount(xs)[0];
|
||||
}
|
||||
|
||||
function polymeter(steps = 0, ...args) {
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
const seqs = args.map((a) => _sequenceCount(a));
|
||||
if (seqs.length == 0) {
|
||||
return silence;
|
||||
|
|
@ -1003,15 +860,19 @@ function polymeter(steps = 0, ...args) {
|
|||
pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1]))));
|
||||
}
|
||||
}
|
||||
return stack(pats);
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
export function polymeter(...args) {
|
||||
return polymeterSteps(0, ...args);
|
||||
}
|
||||
|
||||
// alias
|
||||
function pm(args) {
|
||||
polymeter(args);
|
||||
export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
function polyrhythm(...xs) {
|
||||
export function polyrhythm(...xs) {
|
||||
const seqs = xs.map((a) => sequence(a));
|
||||
|
||||
if (seqs.length == 0) {
|
||||
|
|
@ -1021,35 +882,40 @@ function polyrhythm(...xs) {
|
|||
}
|
||||
|
||||
// alias
|
||||
function pr(args) {
|
||||
export function pr(args) {
|
||||
polyrhythm(args);
|
||||
}
|
||||
|
||||
const fast = curry((a, pat) => pat.fast(a));
|
||||
const slow = curry((a, pat) => pat.slow(a));
|
||||
const early = curry((a, pat) => pat.early(a));
|
||||
const late = curry((a, pat) => pat.late(a));
|
||||
const rev = (pat) => pat.rev();
|
||||
const add = curry((a, pat) => pat.add(a));
|
||||
const sub = curry((a, pat) => pat.sub(a));
|
||||
const mul = curry((a, pat) => pat.mul(a));
|
||||
const div = curry((a, pat) => pat.div(a));
|
||||
const union = curry((a, pat) => pat.union(a));
|
||||
const every = curry((i, f, pat) => pat.every(i, f));
|
||||
const when = curry((binary, f, pat) => pat.when(binary, f));
|
||||
const off = curry((t, f, pat) => pat.off(t, f));
|
||||
const jux = curry((f, pat) => pat.jux(f));
|
||||
const append = curry((a, pat) => pat.append(a));
|
||||
const superimpose = curry((array, pat) => pat.superimpose(...array));
|
||||
const struct = curry((a, pat) => pat.struct(a));
|
||||
const mask = curry((a, pat) => pat.mask(a));
|
||||
const echo = curry((a, b, c, pat) => pat.echo(a, b, c));
|
||||
const invert = (pat) => pat.invert();
|
||||
const inv = (pat) => pat.inv();
|
||||
const iter = curry((a, pat) => pat.iter(a));
|
||||
const iterBack = curry((a, pat) => pat.iter(a));
|
||||
const chunk = curry((a, pat) => pat.chunk(a));
|
||||
const chunkBack = curry((a, pat) => pat.chunkBack(a));
|
||||
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));
|
||||
export const early = curry((a, pat) => pat.early(a));
|
||||
export const echo = curry((a, b, c, pat) => pat.echo(a, b, c));
|
||||
export const every = curry((i, f, pat) => pat.every(i, f));
|
||||
export const fast = curry((a, pat) => pat.fast(a));
|
||||
export const inv = (pat) => pat.inv();
|
||||
export const invert = (pat) => pat.invert();
|
||||
export const iter = curry((a, pat) => pat.iter(a));
|
||||
export const iterBack = curry((a, pat) => pat.iter(a));
|
||||
export const jux = curry((f, pat) => pat.jux(f));
|
||||
export const juxBy = curry((by, f, pat) => pat.juxBy(by, f));
|
||||
export const late = curry((a, pat) => pat.late(a));
|
||||
export const linger = curry((a, pat) => pat.linger(a));
|
||||
export const mask = curry((a, pat) => pat.mask(a));
|
||||
export const mul = curry((a, pat) => pat.mul(a));
|
||||
export const off = curry((t, f, pat) => pat.off(t, f));
|
||||
export const ply = curry((a, pat) => pat.ply(a));
|
||||
export const range = curry((a, b, pat) => pat.range(a, b));
|
||||
export const range2 = curry((a, b, pat) => pat.range2(a, b));
|
||||
export const rev = (pat) => pat.rev();
|
||||
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 when = curry((binary, f, pat) => pat.when(binary, f));
|
||||
|
||||
// problem: curried functions with spread arguments must have pat at the beginning
|
||||
// with this, we cannot keep the pattern open at the end.. solution for now: use array to keep using pat as last arg
|
||||
|
|
@ -1073,24 +939,24 @@ export function makeComposable(func) {
|
|||
return func;
|
||||
}
|
||||
|
||||
const patternify2 = (f) => (pata, patb, pat) =>
|
||||
export const patternify2 = (f) => (pata, patb, pat) =>
|
||||
pata
|
||||
.fmap((a) => (b) => f.call(pat, a, b))
|
||||
.appLeft(patb)
|
||||
.outerJoin();
|
||||
const patternify3 = (f) => (pata, patb, patc, pat) =>
|
||||
.innerJoin();
|
||||
export const patternify3 = (f) => (pata, patb, patc, pat) =>
|
||||
pata
|
||||
.fmap((a) => (b) => (c) => f.call(pat, a, b, c))
|
||||
.appLeft(patb)
|
||||
.appLeft(patc)
|
||||
.outerJoin();
|
||||
const patternify4 = (f) => (pata, patb, patc, patd, pat) =>
|
||||
.innerJoin();
|
||||
export const patternify4 = (f) => (pata, patb, patc, patd, pat) =>
|
||||
pata
|
||||
.fmap((a) => (b) => (c) => (d) => f.call(pat, a, b, c, d))
|
||||
.appLeft(patb)
|
||||
.appLeft(patc)
|
||||
.appLeft(patd)
|
||||
.outerJoin();
|
||||
.innerJoin();
|
||||
|
||||
Pattern.prototype.echo = function (...args) {
|
||||
args = args.map(reify);
|
||||
|
|
@ -1108,6 +974,14 @@ Pattern.prototype.chunkBack = function (...args) {
|
|||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._chunkBack)(...args, this);
|
||||
};
|
||||
Pattern.prototype.zoom = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._zoom)(...args, this);
|
||||
};
|
||||
Pattern.prototype.compress = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._compress)(...args, this);
|
||||
};
|
||||
|
||||
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
|
||||
Pattern.prototype.bootstrap = function () {
|
||||
|
|
@ -1160,50 +1034,3 @@ Pattern.prototype.define = (name, func, options = {}) => {
|
|||
// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true });
|
||||
Pattern.prototype.define('hush', (pat) => pat.hush(), { patternified: false, composable: true });
|
||||
Pattern.prototype.define('bypass', (pat) => pat.bypass(on), { patternified: true, composable: true });
|
||||
|
||||
export {
|
||||
Fraction,
|
||||
TimeSpan,
|
||||
Hap,
|
||||
Pattern,
|
||||
pure,
|
||||
stack,
|
||||
slowcat,
|
||||
fastcat,
|
||||
cat,
|
||||
timeCat,
|
||||
sequence,
|
||||
polymeter,
|
||||
pm,
|
||||
polyrhythm,
|
||||
pr,
|
||||
reify,
|
||||
silence,
|
||||
fast,
|
||||
slow,
|
||||
early,
|
||||
late,
|
||||
rev,
|
||||
add,
|
||||
sub,
|
||||
mul,
|
||||
div,
|
||||
union,
|
||||
every,
|
||||
when,
|
||||
off,
|
||||
jux,
|
||||
append,
|
||||
superimpose,
|
||||
struct,
|
||||
mask,
|
||||
invert,
|
||||
inv,
|
||||
id,
|
||||
range,
|
||||
echo,
|
||||
iter,
|
||||
iterBack,
|
||||
chunk,
|
||||
chunkBack,
|
||||
};
|
||||
30
packages/core/signal.mjs
Normal file
30
packages/core/signal.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
|
||||
export function steady(value) {
|
||||
// A continuous value
|
||||
return new Pattern((span) => Hap(undefined, span, value));
|
||||
}
|
||||
|
||||
export const signal = (func) => {
|
||||
const query = (state) => [new Hap(undefined, state.span, func(state.span.midpoint()))];
|
||||
return new Pattern(query);
|
||||
};
|
||||
|
||||
export const isaw = signal((t) => 1 - (t % 1));
|
||||
export const isaw2 = isaw._toBipolar();
|
||||
|
||||
export const saw = signal((t) => t % 1);
|
||||
export const saw2 = saw._toBipolar();
|
||||
|
||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
export const sine = sine2._fromBipolar();
|
||||
export const cosine = sine._early(Fraction(1).div(4));
|
||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
|
||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
export const square2 = square._toBipolar();
|
||||
|
||||
export const tri = fastcat(isaw, saw);
|
||||
export const tri2 = fastcat(isaw2, saw2);
|
||||
33
packages/core/speak.mjs
Normal file
33
packages/core/speak.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Pattern, patternify2 } from './index.mjs';
|
||||
|
||||
const synth = window?.speechSynthesis;
|
||||
let allVoices = synth?.getVoices();
|
||||
// console.log('voices', allVoices);
|
||||
|
||||
function speak(words, lang, voice) {
|
||||
synth.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(words);
|
||||
utterance.lang = lang;
|
||||
allVoices = synth.getVoices();
|
||||
const voices = allVoices.filter((v) => v.lang.includes(lang));
|
||||
if (typeof voice === 'number') {
|
||||
utterance.voice = voices[voice % voices.length];
|
||||
} else if (typeof voice === 'string') {
|
||||
utterance.voice = voices.find((voice) => voice.name === voice);
|
||||
}
|
||||
// console.log(utterance.voice?.name, utterance.voice?.lang);
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
|
||||
Pattern.prototype._speak = function (lang, voice) {
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event) => {
|
||||
speak(event.value, lang, voice);
|
||||
};
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.speak = function (lang, voice) {
|
||||
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
|
||||
};
|
||||
22
packages/core/state.mjs
Normal file
22
packages/core/state.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export class State {
|
||||
constructor(span, controls = {}) {
|
||||
this.span = span;
|
||||
this.controls = controls;
|
||||
}
|
||||
|
||||
// Returns new State with different span
|
||||
setSpan(span) {
|
||||
return new State(span, this.controls);
|
||||
}
|
||||
|
||||
withSpan(func) {
|
||||
return this.setSpan(func(this.span));
|
||||
}
|
||||
|
||||
// Returns new State with different controls
|
||||
setControls(controls) {
|
||||
return new State(this.span, controls);
|
||||
}
|
||||
}
|
||||
|
||||
export default State;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Fraction from 'fraction.js'
|
||||
import Fraction from 'fraction.js';
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
import { deepStrictEqual, strict as assert } from 'assert';
|
||||
|
||||
import {
|
||||
TimeSpan,
|
||||
|
|
@ -13,6 +13,8 @@ import {
|
|||
slowcat,
|
||||
cat,
|
||||
sequence,
|
||||
polymeter,
|
||||
polymeterSteps,
|
||||
polyrhythm,
|
||||
silence,
|
||||
fast,
|
||||
|
|
@ -31,194 +33,210 @@ import {
|
|||
square2,
|
||||
tri,
|
||||
tri2,
|
||||
} from '../strudel.mjs';
|
||||
id,
|
||||
ply,
|
||||
} from '../index.mjs';
|
||||
//import { Time } from 'tone';
|
||||
import pkg from 'tone';
|
||||
const { Time } = pkg;
|
||||
|
||||
const st = (begin, end) => new State(ts(begin, end))
|
||||
const st = (begin, end) => new State(ts(begin, end));
|
||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||
const hap = (whole, part, value, context={}) => new Hap(whole, part, value, context)
|
||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||
|
||||
const third = Fraction(1,3)
|
||||
const twothirds = Fraction(2,3)
|
||||
const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
describe('TimeSpan', function() {
|
||||
describe('equals()', function() {
|
||||
it('Should be equal to the same value', function() {
|
||||
assert.equal((new TimeSpan(0,4)).equals(new TimeSpan(0,4)), true);
|
||||
describe('TimeSpan', function () {
|
||||
describe('equals()', function () {
|
||||
it('Should be equal to the same value', function () {
|
||||
assert.equal(new TimeSpan(0, 4).equals(new TimeSpan(0, 4)), true);
|
||||
});
|
||||
});
|
||||
describe('splitCycles', function () {
|
||||
it('Should split two cycles into two', function () {
|
||||
assert.equal(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length, 2);
|
||||
});
|
||||
});
|
||||
describe('splitCycles', function() {
|
||||
it('Should split two cycles into two', function() {
|
||||
assert.equal(new TimeSpan(Fraction(0),Fraction(2)).spanCycles.length, 2)
|
||||
})
|
||||
})
|
||||
describe('intersection_e', function () {
|
||||
var a = new TimeSpan(Fraction(0), Fraction(2))
|
||||
var b = new TimeSpan(Fraction(1), Fraction(3))
|
||||
var c = new TimeSpan(Fraction(1), Fraction(2))
|
||||
var d = new TimeSpan(Fraction(1), Fraction(2))
|
||||
it('Should create an intersection', function () {
|
||||
assert.equal(a.intersection_e(b).equals(c), true)
|
||||
})
|
||||
})
|
||||
var a = new TimeSpan(Fraction(0), Fraction(2));
|
||||
var b = new TimeSpan(Fraction(1), Fraction(3));
|
||||
var c = new TimeSpan(Fraction(1), Fraction(2));
|
||||
var d = new TimeSpan(Fraction(1), Fraction(2));
|
||||
it('Should create an intersection', function () {
|
||||
assert.equal(a.intersection_e(b).equals(c), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hap', function() {
|
||||
describe('hasOnset()', function() {
|
||||
it('True if part includes onset from whole', function() {
|
||||
assert.equal(new Hap(new TimeSpan(0,1), new TimeSpan(0,1), "thing").hasOnset(), true);
|
||||
});
|
||||
describe('Hap', function () {
|
||||
describe('hasOnset()', function () {
|
||||
it('True if part includes onset from whole', function () {
|
||||
assert.equal(new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), 'thing').hasOnset(), true);
|
||||
});
|
||||
var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), "a")
|
||||
var b = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), "b")
|
||||
var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), "c")
|
||||
var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), "d")
|
||||
var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), "e")
|
||||
describe('spanEquals', function() {
|
||||
it('True if two haps have the same whole and part', function() {
|
||||
assert.equal(a.spanEquals(b), true)
|
||||
})
|
||||
it('False if two haps don\'t the same whole and part', function() {
|
||||
assert.equal(a.spanEquals(c), false)
|
||||
})
|
||||
it('True if two haps have the same part and undefined wholes', function() {
|
||||
assert.equal(d.spanEquals(e), true)
|
||||
})
|
||||
})
|
||||
describe('resolveState()', () => {
|
||||
it('Can increment some state', function() {
|
||||
const stateful_value = state => {
|
||||
const newValue = state["incrementme"]
|
||||
// TODO Does the state *need* duplicating here?
|
||||
const newState = {...state}
|
||||
newState["incrementme"]++
|
||||
return [newState, newValue]
|
||||
}
|
||||
const state = {incrementme: 10}
|
||||
const ev1 = new Hap(ts(0,1), ts(0,1), stateful_value, {}, true)
|
||||
const [state2, ev2] = ev1.resolveState(state)
|
||||
const [state3, ev3] = ev1.resolveState(state2)
|
||||
assert.deepStrictEqual(
|
||||
ev3, new Hap(ts(0,1),ts(0,1),11,{},false)
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
state3, {incrementme: 12}
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
describe('Pattern', function() {
|
||||
var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'a');
|
||||
var b = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'b');
|
||||
var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), 'c');
|
||||
var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'd');
|
||||
var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'e');
|
||||
describe('spanEquals', function () {
|
||||
it('True if two haps have the same whole and part', function () {
|
||||
assert.equal(a.spanEquals(b), true);
|
||||
});
|
||||
it("False if two haps don't the same whole and part", function () {
|
||||
assert.equal(a.spanEquals(c), false);
|
||||
});
|
||||
it('True if two haps have the same part and undefined wholes', function () {
|
||||
assert.equal(d.spanEquals(e), true);
|
||||
});
|
||||
});
|
||||
describe('resolveState()', () => {
|
||||
it('Can increment some state', function () {
|
||||
const stateful_value = (state) => {
|
||||
const newValue = state['incrementme'];
|
||||
// TODO Does the state *need* duplicating here?
|
||||
const newState = { ...state };
|
||||
newState['incrementme']++;
|
||||
return [newState, newValue];
|
||||
};
|
||||
const state = { incrementme: 10 };
|
||||
const ev1 = new Hap(ts(0, 1), ts(0, 1), stateful_value, {}, true);
|
||||
const [state2, ev2] = ev1.resolveState(state);
|
||||
const [state3, ev3] = ev1.resolveState(state2);
|
||||
assert.deepStrictEqual(ev3, new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
|
||||
assert.deepStrictEqual(state3, { incrementme: 12 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pattern', function () {
|
||||
describe('pure', function () {
|
||||
it('Can make a pattern', function() {
|
||||
assert.equal(pure("hello").query(st(0.5, 2.5)).length, 3)
|
||||
})
|
||||
})
|
||||
it('Can make a pattern', function () {
|
||||
assert.equal(pure('hello').query(st(0.5, 2.5)).length, 3);
|
||||
});
|
||||
});
|
||||
describe('fmap()', function () {
|
||||
it('Can add things', function () {
|
||||
assert.equal(pure(3).fmap(x => x + 4).firstCycle()[0].value, 7)
|
||||
})
|
||||
})
|
||||
assert.equal(
|
||||
pure(3)
|
||||
.fmap((x) => x + 4)
|
||||
.firstCycle()[0].value,
|
||||
7,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('add()', function () {
|
||||
it('Can add things', function() {
|
||||
assert.equal(pure(3).add(pure(4)).query(st(0,1))[0].value, 7)
|
||||
})
|
||||
})
|
||||
it('Can add things', function () {
|
||||
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
|
||||
});
|
||||
});
|
||||
describe('sub()', function () {
|
||||
it('Can subtract things', function() {
|
||||
assert.equal(pure(3).sub(pure(4)).query(st(0,1))[0].value, -1)
|
||||
})
|
||||
})
|
||||
it('Can subtract things', function () {
|
||||
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
|
||||
});
|
||||
});
|
||||
describe('mul()', function () {
|
||||
it('Can multiply things', function() {
|
||||
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6)
|
||||
})
|
||||
})
|
||||
it('Can multiply things', function () {
|
||||
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6);
|
||||
});
|
||||
});
|
||||
describe('div()', function () {
|
||||
it('Can divide things', function() {
|
||||
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5)
|
||||
})
|
||||
})
|
||||
it('Can divide things', function () {
|
||||
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
|
||||
});
|
||||
});
|
||||
describe('union()', function () {
|
||||
it('Can union things', function () {
|
||||
assert.deepStrictEqual(pure({a: 4, b: 6}).union(pure({c: 7})).firstCycle()[0].value, {a: 4, b: 6, c: 7})
|
||||
})
|
||||
})
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 4, b: 6 })
|
||||
.union(pure({ c: 7 }))
|
||||
.firstCycle()[0].value,
|
||||
{ a: 4, b: 6, c: 7 },
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('stack()', function () {
|
||||
it('Can stack things', function () {
|
||||
assert.deepStrictEqual(stack(pure("a"), pure("b"), pure("c")).firstCycle().map(h => h.value), ["a", "b", "c"])
|
||||
})
|
||||
})
|
||||
assert.deepStrictEqual(
|
||||
stack(pure('a'), pure('b'), pure('c'))
|
||||
.firstCycle()
|
||||
.map((h) => h.value),
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_fast()', function () {
|
||||
it('Makes things faster', function () {
|
||||
assert.equal(pure("a")._fast(2).firstCycle().length, 2)
|
||||
})
|
||||
})
|
||||
assert.equal(pure('a')._fast(2).firstCycle().length, 2);
|
||||
});
|
||||
});
|
||||
describe('_fastGap()', function () {
|
||||
it('Makes things faster, with a gap', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", "b", "c")._fastGap(2).firstCycle(),
|
||||
sequence(["a","b","c"], silence).firstCycle()
|
||||
)
|
||||
sequence('a', 'b', 'c')._fastGap(2).firstCycle(),
|
||||
sequence(['a', 'b', 'c'], silence).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", "b", "c")._fastGap(3).firstCycle(),
|
||||
sequence(["a","b","c"], silence, silence).firstCycle()
|
||||
)
|
||||
})
|
||||
sequence('a', 'b', 'c')._fastGap(3).firstCycle(),
|
||||
sequence(['a', 'b', 'c'], silence, silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Makes things faster, with a gap, when speeded up further', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", "b", "c")._fastGap(2).fast(2).firstCycle(),
|
||||
sequence(["a","b","c"], silence, ["a","b","c"], silence).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle(),
|
||||
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_compressSpan()', function () {
|
||||
it('Can squash cycles of a pattern into a given timespan', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure("a")._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
|
||||
sequence(silence, "a", silence, silence).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
pure('a')._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
|
||||
sequence(silence, 'a', silence, silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fast()', function () {
|
||||
it('Makes things faster', function () {
|
||||
assert.equal(pure("a").fast(2).firstCycle().length, 2)
|
||||
})
|
||||
assert.equal(pure('a').fast(2).firstCycle().length, 2);
|
||||
});
|
||||
it('Makes things faster, with a pattern of factors', function () {
|
||||
assert.equal(pure("a").fast(sequence(1,4)).firstCycle().length, 3)
|
||||
assert.equal(pure('a').fast(sequence(1, 4)).firstCycle().length, 3);
|
||||
// .fast(sequence(1,silence) is a quick hack to cut an event in two..
|
||||
assert.deepStrictEqual(pure("a").fast(sequence(1,4)).firstCycle(), stack(pure("a").fast(sequence(1,silence)), sequence(silence, ["a","a"])).firstCycle())
|
||||
})
|
||||
assert.deepStrictEqual(
|
||||
pure('a').fast(sequence(1, 4)).firstCycle(),
|
||||
stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('defaults to accepting sequences', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1,2,3).fast(sequence(1.5,2)).firstCycle(),
|
||||
sequence(1,2,3).fast(1.5,2).firstCycle()
|
||||
)
|
||||
})
|
||||
it("works as a static function", function () {
|
||||
sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle(),
|
||||
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works as a static function', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1,2,3).fast(1,2).firstCycle(),
|
||||
fast(sequence(1,2), sequence(1,2,3)).firstCycle()
|
||||
)
|
||||
})
|
||||
it("works as a curried static function", function () {
|
||||
sequence(1, 2, 3).fast(1, 2).firstCycle(),
|
||||
fast(sequence(1, 2), sequence(1, 2, 3)).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works as a curried static function', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1,2,3).fast(1,2).firstCycle(),
|
||||
fast(sequence(1,2))(sequence(1,2,3)).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
sequence(1, 2, 3).fast(1, 2).firstCycle(),
|
||||
fast(sequence(1, 2))(sequence(1, 2, 3)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_slow()', function () {
|
||||
it('Makes things slower', function () {
|
||||
assert.deepStrictEqual(pure("a")._slow(2).firstCycle()[0], new Hap(new TimeSpan(Fraction(0),Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), "a"))
|
||||
assert.deepStrictEqual(
|
||||
pure('a')._slow(2).firstCycle()[0],
|
||||
new Hap(new TimeSpan(Fraction(0), Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), 'a'),
|
||||
);
|
||||
|
||||
const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
|
||||
assert.deepStrictEqual(
|
||||
pat.query(st(0,1))[1],
|
||||
hap(ts(0.5,1.5), ts(1/2,1), "eb3")
|
||||
)
|
||||
assert.deepStrictEqual(pat.query(st(0, 1))[1], hap(ts(0.5, 1.5), ts(1 / 2, 1), 'eb3'));
|
||||
// the following test fails
|
||||
/* assert.deepStrictEqual(
|
||||
pat.query(ts(1,2))[1], undefined
|
||||
|
|
@ -228,248 +246,422 @@ describe('Pattern', function() {
|
|||
// notable examples:
|
||||
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
|
||||
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
describe('_filterValues()', function () {
|
||||
it('Filters true', function () {
|
||||
assert.equal(pure(true)._filterValues(x => x).firstCycle().length, 1)
|
||||
})
|
||||
})
|
||||
assert.equal(
|
||||
pure(true)
|
||||
._filterValues((x) => x)
|
||||
.firstCycle().length,
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('when()', function () {
|
||||
it('Always faster', function () {
|
||||
assert.equal(pure("a").when(pure(true), x => x._fast(2)).firstCycle().length, 2)
|
||||
})
|
||||
assert.equal(
|
||||
pure('a')
|
||||
.when(pure(true), (x) => x._fast(2))
|
||||
.firstCycle().length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
it('Never faster', function () {
|
||||
assert.equal(pure("a").when(pure(false), x => x._fast(2)).firstCycle().length, 1)
|
||||
})
|
||||
assert.equal(
|
||||
pure('a')
|
||||
.when(pure(false), (x) => x._fast(2))
|
||||
.firstCycle().length,
|
||||
1,
|
||||
);
|
||||
});
|
||||
it('Can alternate', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure(10).when(slowcat(true,false),add(3)).fast(4)._sortEventsByPart().firstCycle(),
|
||||
fastcat(13,10,13,10).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortEventsByPart().firstCycle(),
|
||||
fastcat(13, 10, 13, 10).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fastcat()', function () {
|
||||
it('Can concatenate two things', function () {
|
||||
assert.deepStrictEqual(fastcat(pure("a"), pure("b")).firstCycle().map(x => x.value), ["a", "b"])
|
||||
})
|
||||
})
|
||||
assert.deepStrictEqual(
|
||||
fastcat(pure('a'), pure('b'))
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['a', 'b'],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('slowcat()', function () {
|
||||
it('Can concatenate things slowly', function () {
|
||||
assert.deepStrictEqual(slowcat("a", "b").firstCycle().map(x => x.value), ["a"])
|
||||
assert.deepStrictEqual(slowcat("a", "b")._early(1).firstCycle().map(x => x.value), ["b"])
|
||||
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(1).firstCycle().map(x => x.value), ["b"])
|
||||
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(3).firstCycle().map(x => x.value), ["c"])
|
||||
})
|
||||
})
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', 'b')
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['a'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', 'b')
|
||||
._early(1)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['b'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', slowcat('b', 'c'))
|
||||
._early(1)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['b'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
slowcat('a', slowcat('b', 'c'))
|
||||
._early(3)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['c'],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('rev()', function () {
|
||||
it('Can reverse things', function () {
|
||||
assert.deepStrictEqual(fastcat("a","b","c").rev().firstCycle().sort((a,b) => a.part.begin.sub(b.part.begin)).map(a => a.value), ["c", "b","a"])
|
||||
})
|
||||
})
|
||||
assert.deepStrictEqual(
|
||||
fastcat('a', 'b', 'c')
|
||||
.rev()
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.value),
|
||||
['c', 'b', 'a'],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('sequence()', () => {
|
||||
it('Can work like fastcat', () => {
|
||||
assert.deepStrictEqual(sequence(1,2,3).firstCycle(), fastcat(1,2,3).firstCycle())
|
||||
})
|
||||
})
|
||||
assert.deepStrictEqual(sequence(1, 2, 3).firstCycle(), fastcat(1, 2, 3).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('polyrhythm()', () => {
|
||||
it('Can layer up cycles', () => {
|
||||
assert.deepStrictEqual(
|
||||
polyrhythm(["a","b"],["c"]).firstCycle(),
|
||||
stack(fastcat(pure("a"),pure("b")),pure("c")).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
polyrhythm(['a', 'b'], ['c']).firstCycle(),
|
||||
stack(fastcat(pure('a'), pure('b')), pure('c')).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('polymeter()', () => {
|
||||
it('Can layer up cycles, stepwise', () => {
|
||||
assert.deepStrictEqual(
|
||||
polymeterSteps(3, ['d', 'e']).firstCycle(),
|
||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle(),
|
||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('every()', () => {
|
||||
it('Can apply a function every 3rd time', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure("a").every(3, x => x._fast(2))._fast(3).firstCycle(),
|
||||
sequence(sequence("a", "a"), "a", "a").firstCycle()
|
||||
)
|
||||
})
|
||||
it("works with currying", () => {
|
||||
pure('a')
|
||||
.every(3, (x) => x._fast(2))
|
||||
._fast(3)
|
||||
.firstCycle(),
|
||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works with currying', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure("a").every(3, fast(2))._fast(3).firstCycle(),
|
||||
sequence(sequence("a", "a"), "a", "a").firstCycle()
|
||||
)
|
||||
pure('a').every(3, fast(2))._fast(3).firstCycle(),
|
||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence(3,4,5).every(3, add(3)).fast(5).firstCycle(),
|
||||
sequence(6,7,8,3,4,5,3,4,5,6,7,8,3,4,5).firstCycle()
|
||||
)
|
||||
sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle(),
|
||||
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence(3,4,5).every(2, sub(1)).fast(5).firstCycle(),
|
||||
sequence(2,3,4,3,4,5,2,3,4,3,4,5,2,3,4).firstCycle()
|
||||
)
|
||||
sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle(),
|
||||
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence(3,4,5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
|
||||
sequence(5,6,7,3,4,5).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('timeCat()', function() {
|
||||
it('Can concatenate patterns with different relative durations', function() {
|
||||
sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
|
||||
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('timeCat()', function () {
|
||||
it('Can concatenate patterns with different relative durations', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", ["a", "a"]).firstCycle(),
|
||||
timeCat([1,"a"], [0.5, "a"], [0.5, "a"]).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('struct()', function() {
|
||||
it('Can restructure a pattern', function() {
|
||||
sequence('a', ['a', 'a']).firstCycle(),
|
||||
timeCat([1, 'a'], [0.5, 'a'], [0.5, 'a']).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('struct()', function () {
|
||||
it('Can restructure a 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'),
|
||||
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
||||
hap(ts(twothirds, 1), ts(twothirds, 1), 'b'),
|
||||
]);
|
||||
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"),
|
||||
hap(ts(third, twothirds), ts(0.5, twothirds), "b"),
|
||||
hap(ts(twothirds, 1), ts(twothirds, 1), "b")
|
||||
]
|
||||
)
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, false], true))
|
||||
.firstCycle(),
|
||||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
pure("a").struct(sequence(true, [true,false], true)).firstCycle(),
|
||||
sequence("a", ["a", silence], "a").firstCycle(),
|
||||
)
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, false], true).invert())
|
||||
.firstCycle(),
|
||||
sequence(silence, [silence, 'a'], silence).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
pure("a").struct(sequence(true, [true,false], true).invert()).firstCycle(),
|
||||
sequence(silence, [silence, "a"], silence).firstCycle(),
|
||||
)
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, silence], true))
|
||||
.firstCycle(),
|
||||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mask()', function () {
|
||||
it('Can fragment a pattern', function () {
|
||||
assert.deepStrictEqual(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle(), [
|
||||
hap(ts(0, 0.5), ts(0, third), 'a'),
|
||||
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
||||
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
|
||||
hap(ts(0.5, 1), ts(twothirds, 1), 'b'),
|
||||
]);
|
||||
});
|
||||
it('Can mask off parts of a pattern', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure("a").struct(sequence(true, [true,silence], true)).firstCycle(),
|
||||
sequence("a", ["a", silence], "a").firstCycle(),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('mask()', function() {
|
||||
it('Can fragment a pattern', function() {
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", "b").mask(sequence(true, true, true)).firstCycle(),
|
||||
[hap(ts(0, 0.5), ts(0,third), "a"),
|
||||
hap(ts(0, 0.5), ts(third, 0.5), "a"),
|
||||
hap(ts(0.5, 1), ts(0.5, twothirds), "b"),
|
||||
hap(ts(0.5, 1), ts(twothirds, 1), "b")
|
||||
]
|
||||
)
|
||||
})
|
||||
it('Can mask off parts of a pattern', function() {
|
||||
assert.deepStrictEqual(
|
||||
sequence(["a", "b"], "c").mask(sequence(true, false)).firstCycle(),
|
||||
sequence(["a","b"], silence).firstCycle()
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
sequence("a").mask(sequence(true, false)).firstCycle(),
|
||||
[hap(ts(0,1),ts(0,0.5), "a")]
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('invert()', function() {
|
||||
it('Can invert a binary pattern', function() {
|
||||
sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle(),
|
||||
sequence(['a', 'b'], silence).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(sequence('a').mask(sequence(true, false)).firstCycle(), [hap(ts(0, 1), ts(0, 0.5), 'a')]);
|
||||
});
|
||||
});
|
||||
describe('invert()', function () {
|
||||
it('Can invert a binary pattern', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(true, false, [true, false]).invert().firstCycle(),
|
||||
sequence(false, true, [false, true]).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('signal()', function() {
|
||||
it('Can make saw/saw2', function() {
|
||||
sequence(false, true, [false, true]).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('signal()', function () {
|
||||
it('Can make saw/saw2', function () {
|
||||
assert.deepStrictEqual(
|
||||
saw.struct(true,true,true,true).firstCycle(),
|
||||
sequence(1/8,3/8,5/8,7/8).firstCycle()
|
||||
)
|
||||
saw.struct(true, true, true, true).firstCycle(),
|
||||
sequence(1 / 8, 3 / 8, 5 / 8, 7 / 8).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
saw2.struct(true,true,true,true).firstCycle(),
|
||||
sequence(-3/4,-1/4,1/4,3/4).firstCycle()
|
||||
)
|
||||
})
|
||||
it('Can make isaw/isaw2', function() {
|
||||
saw2.struct(true, true, true, true).firstCycle(),
|
||||
sequence(-3 / 4, -1 / 4, 1 / 4, 3 / 4).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can make isaw/isaw2', function () {
|
||||
assert.deepStrictEqual(
|
||||
isaw.struct(true,true,true,true).firstCycle(),
|
||||
sequence(7/8,5/8,3/8,1/8).firstCycle()
|
||||
)
|
||||
isaw.struct(true, true, true, true).firstCycle(),
|
||||
sequence(7 / 8, 5 / 8, 3 / 8, 1 / 8).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
isaw2.struct(true,true,true,true).firstCycle(),
|
||||
sequence(3/4,1/4,-1/4,-3/4).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
isaw2.struct(true, true, true, true).firstCycle(),
|
||||
sequence(3 / 4, 1 / 4, -1 / 4, -3 / 4).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_setContext()', () => {
|
||||
it('Can set the event context', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure("a")._setContext([[[0,1],[1,2]]]).firstCycle(true),
|
||||
[hap(ts(0,1),
|
||||
ts(0,1),
|
||||
"a",
|
||||
[[[0,1],[1,2]]]
|
||||
)
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
pure('a')
|
||||
._setContext([
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
])
|
||||
.firstCycle(true),
|
||||
[
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
]),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_withContext()', () => {
|
||||
it('Can update the event context', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure("a")._setContext([[[0,1],[1,2]]])._withContext(c => [...c,[[3,4],[3,4]]]).firstCycle(true),
|
||||
[hap(ts(0,1),
|
||||
ts(0,1),
|
||||
"a",
|
||||
[[[0,1],[1,2]],[[3,4],[3,4]]]
|
||||
)
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("apply", () => {
|
||||
pure('a')
|
||||
._setContext([
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
])
|
||||
._withContext((c) => [
|
||||
...c,
|
||||
[
|
||||
[3, 4],
|
||||
[3, 4],
|
||||
],
|
||||
])
|
||||
.firstCycle(true),
|
||||
[
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
[3, 4],
|
||||
[3, 4],
|
||||
],
|
||||
]),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('apply', () => {
|
||||
it('Can apply a function', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", "b")._apply(fast(2)).firstCycle(),
|
||||
sequence("a", "b").fast(2).firstCycle()
|
||||
)
|
||||
assert.deepStrictEqual(sequence('a', 'b')._apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
|
||||
}),
|
||||
it('Can apply a pattern of functions', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", "b").apply(fast(2)).firstCycle(),
|
||||
sequence("a", "b").fast(2).firstCycle()
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
sequence("a", "b").apply(fast(2),fast(3)).firstCycle(),
|
||||
sequence("a", "b").fast(2,3).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("layer", () => {
|
||||
it('Can apply a pattern of functions', () => {
|
||||
assert.deepStrictEqual(sequence('a', 'b').apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b').apply(fast(2), fast(3)).firstCycle(),
|
||||
sequence('a', 'b').fast(2, 3).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('layer', () => {
|
||||
it('Can layer up multiple functions', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1,2,3).layer(fast(2), pat => pat.add(3,4)).firstCycle(),
|
||||
stack(sequence(1,2,3).fast(2), sequence(1,2,3).add(3,4)).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("early", () => {
|
||||
it("Can shift an event earlier", () => {
|
||||
assert.deepStrictEqual(
|
||||
pure(30)._late(0.25).query(st(1,2)),
|
||||
[hap(ts(1/4,5/4), ts(1,5/4), 30), hap(ts(5/4,9/4), ts(5/4,2), 30)]
|
||||
)
|
||||
})
|
||||
it("Can shift an event earlier, into negative time", () => {
|
||||
assert.deepStrictEqual(
|
||||
pure(30)._late(0.25).query(st(0,1)),
|
||||
[hap(ts(-3/4,1/4), ts(0,1/4), 30), hap(ts(1/4,5/4), ts(1/4,1), 30)]
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("off", () => {
|
||||
it("Can offset a transformed pattern from the original", () => {
|
||||
sequence(1, 2, 3)
|
||||
.layer(fast(2), (pat) => pat.add(3, 4))
|
||||
.firstCycle(),
|
||||
stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('early', () => {
|
||||
it('Can shift an event earlier', () => {
|
||||
assert.deepStrictEqual(pure(30)._late(0.25).query(st(1, 2)), [
|
||||
hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30),
|
||||
hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30),
|
||||
]);
|
||||
});
|
||||
it('Can shift an event earlier, into negative time', () => {
|
||||
assert.deepStrictEqual(pure(30)._late(0.25).query(st(0, 1)), [
|
||||
hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
|
||||
hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('off', () => {
|
||||
it('Can offset a transformed pattern from the original', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure(30).off(0.25, add(2)).firstCycle(),
|
||||
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("jux", () => {
|
||||
it("Can juxtapose", () => {
|
||||
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('jux', () => {
|
||||
it('Can juxtapose', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure({a: 1}).jux(fast(2))._sortEventsByPart().firstCycle(),
|
||||
stack(pure({a:1, pan: 0}), pure({a:1, pan: 1}).fast(2))._sortEventsByPart().firstCycle()
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
pure({ a: 1 }).jux(fast(2))._sortEventsByPart().firstCycle(),
|
||||
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
||||
._sortEventsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('juxBy', () => {
|
||||
it('Can juxtapose by half', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 1 }).juxBy(0.5, fast(2))._sortEventsByPart().firstCycle(),
|
||||
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
||||
._sortEventsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_squeezeJoin', () => {
|
||||
it('Can squeeze', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', ['a', 'a'])
|
||||
.fmap((a) => fastcat('b', 'c'))
|
||||
._squeezeJoin()
|
||||
.firstCycle(),
|
||||
sequence(
|
||||
['b', 'c'],
|
||||
[
|
||||
['b', 'c'],
|
||||
['b', 'c'],
|
||||
],
|
||||
).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('ply', () => {
|
||||
it('Can ply(3)', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', ['b', 'c']).ply(3).firstCycle(),
|
||||
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chop', () => {
|
||||
it('Can _chop(2)', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle(),
|
||||
sequence(
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
{ sound: 'a', begin: 0.5, end: 1 },
|
||||
{ sound: 'b', begin: 0, end: 0.5 },
|
||||
{ sound: 'b', begin: 0.5, end: 1 },
|
||||
).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can chop(2,3)', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortEventsByPart().firstCycle(),
|
||||
sequence(
|
||||
[
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
{ sound: 'a', begin: 0.5, end: 1 },
|
||||
],
|
||||
[
|
||||
{ sound: 'a', begin: 0, end: 1 / 3 },
|
||||
{ sound: 'a', begin: 1 / 3, end: 2 / 3 },
|
||||
{ sound: 'a', begin: 2 / 3, end: 1 },
|
||||
],
|
||||
)
|
||||
._sortEventsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('range2', () => {
|
||||
it('Can change the range of a bipolar pattern', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle(),
|
||||
sequence(1000, 1025, 1050, 1075).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('linger', () => {
|
||||
it('Can linger on the first quarter of a cycle', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle(),
|
||||
sequence(0, 1, 0, 1, 0, 1, 0, 1).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { strict as assert } from 'assert';
|
||||
import { isNote, tokenizeNote, toMidi, mod } from '../util.mjs';
|
||||
import { isNote, tokenizeNote, toMidi, mod, compose } from '../util.mjs';
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should recognize notes without accidentals', () => {
|
||||
|
|
@ -83,3 +83,16 @@ describe('mod', () => {
|
|||
assert.equal(mod(-3, 2), 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compose', () => {
|
||||
const add1 = (a) => a + 1;
|
||||
it('should compose', () => {
|
||||
assert.equal(compose(add1, add1)(0), 2);
|
||||
assert.equal(compose(add1)(0), 1);
|
||||
});
|
||||
const addS = (s) => (a) => a + s;
|
||||
it('should compose left to right', () => {
|
||||
assert.equal(compose(addS('a'), addS('b'))(''), 'ab');
|
||||
assert.equal(compose(addS('a'), addS('b'))('x'), 'xab');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
103
packages/core/timespan.mjs
Normal file
103
packages/core/timespan.mjs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import Fraction from './fraction.mjs';
|
||||
|
||||
export class TimeSpan {
|
||||
constructor(begin, end) {
|
||||
this.begin = Fraction(begin);
|
||||
this.end = Fraction(end);
|
||||
}
|
||||
|
||||
get spanCycles() {
|
||||
const spans = [];
|
||||
var begin = this.begin;
|
||||
const end = this.end;
|
||||
const end_sam = end.sam();
|
||||
|
||||
while (end.gt(begin)) {
|
||||
// If begin and end are in the same cycle, we're done.
|
||||
if (begin.sam().equals(end_sam)) {
|
||||
spans.push(new TimeSpan(begin, this.end));
|
||||
break;
|
||||
}
|
||||
// add a timespan up to the next sam
|
||||
const next_begin = begin.nextSam();
|
||||
spans.push(new TimeSpan(begin, next_begin));
|
||||
|
||||
// continue with the next cycle
|
||||
begin = next_begin;
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
cycleArc() {
|
||||
// Shifts a timespan to one of equal duration that starts within cycle zero.
|
||||
// (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);
|
||||
return new TimeSpan(b, e);
|
||||
}
|
||||
|
||||
withTime(func_time) {
|
||||
// Applies given function to both the begin and end time of the timespan"""
|
||||
return new TimeSpan(func_time(this.begin), func_time(this.end));
|
||||
}
|
||||
|
||||
withEnd(func_time) {
|
||||
// Applies given function to the end time of the timespan"""
|
||||
return new TimeSpan(this.begin, func_time(this.end));
|
||||
}
|
||||
|
||||
withCycle(func_time) {
|
||||
// Like withTime, but time is relative to relative to the cycle (i.e. the
|
||||
// sam of the start of the timespan)
|
||||
const sam = this.begin.sam();
|
||||
const b = sam.add(func_time(this.begin.sub(sam)));
|
||||
const e = sam.add(func_time(this.end.sub(sam)));
|
||||
return new TimeSpan(b, e);
|
||||
}
|
||||
|
||||
intersection(other) {
|
||||
// Intersection of two timespans, returns None if they don't intersect.
|
||||
const intersect_begin = this.begin.max(other.begin);
|
||||
const intersect_end = this.end.min(other.end);
|
||||
|
||||
if (intersect_begin.gt(intersect_end)) {
|
||||
return undefined;
|
||||
}
|
||||
if (intersect_begin.equals(intersect_end)) {
|
||||
// Zero-width (point) intersection - doesn't intersect if it's at the end of a
|
||||
// non-zero-width timespan.
|
||||
if (intersect_begin.equals(this.end) && this.begin.lt(this.end)) {
|
||||
return undefined;
|
||||
}
|
||||
if (intersect_begin.equals(other.end) && other.begin.lt(other.end)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return new TimeSpan(intersect_begin, intersect_end);
|
||||
}
|
||||
|
||||
intersection_e(other) {
|
||||
// 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')
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
midpoint() {
|
||||
return this.begin.add(this.end.sub(this.begin).div(Fraction(2)));
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return this.begin.equals(other.begin) && this.end.equals(other.end);
|
||||
}
|
||||
|
||||
show() {
|
||||
return this.begin.show() + ' -> ' + this.end.show();
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeSpan;
|
||||
|
|
@ -42,3 +42,44 @@ export const getPlayableNoteValue = (event) => {
|
|||
|
||||
// rotate array by n steps (to the left)
|
||||
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));
|
||||
|
||||
export const pipe = (...funcs) => {
|
||||
return funcs.reduce(
|
||||
(f, g) =>
|
||||
(...args) =>
|
||||
f(g(...args)),
|
||||
(x) => x,
|
||||
);
|
||||
};
|
||||
|
||||
export const compose = (...funcs) => pipe(...funcs.reverse());
|
||||
|
||||
// Removes 'None' values from given list
|
||||
export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
||||
|
||||
export const flatten = (arr) => [].concat(...arr);
|
||||
|
||||
export const id = (a) => a;
|
||||
|
||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||
|
||||
export function curry(func, overload) {
|
||||
const fn = function curried(...args) {
|
||||
if (args.length >= func.length) {
|
||||
return func.apply(this, args);
|
||||
} else {
|
||||
const partial = function (...args2) {
|
||||
return curried.apply(this, args.concat(args2));
|
||||
};
|
||||
if (overload) {
|
||||
overload(partial, args);
|
||||
}
|
||||
return partial;
|
||||
}
|
||||
};
|
||||
if (overload) {
|
||||
// overload function without args... needed for chordBass.transpose(2)
|
||||
overload(fn, []);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { curry } from 'ramda';
|
||||
import { curry } from './util.mjs';
|
||||
|
||||
function unionWithObj(a, b, func) {
|
||||
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
||||
|
|
|
|||
2
packages/eval/package-lock.json
generated
2
packages/eval/package-lock.json
generated
|
|
@ -6,7 +6,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.3",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.3.0",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import shiftCodegen from 'shift-codegen';
|
||||
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
|
||||
|
||||
import * as strudel from '@strudel.cycles/core/strudel.mjs';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { Pattern } = strudel;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { isNote } from 'tone';
|
||||
import _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core/strudel.mjs';
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
|
|
|
|||
2
packages/midi/package-lock.json
generated
2
packages/midi/package-lock.json
generated
|
|
@ -6,7 +6,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.4",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"tone": "^14.7.77",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as krill from './krill-parser.js';
|
||||
import * as strudel from '@strudel.cycles/core/strudel.mjs';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
|
||||
|
||||
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
|
||||
|
|
|
|||
|
|
@ -1,33 +1,37 @@
|
|||
# @strudel.cycles/osc
|
||||
|
||||
OSC messaging between strudel and super collider?
|
||||
OSC output for strudel patterns! Currently only tested with super collider / super dirt.
|
||||
|
||||
## Sniffing Tidal Messages
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
npm run tidal-sniffer
|
||||
OSC will only work if you run the REPL locally + the OSC server besides it:
|
||||
|
||||
From the project root:
|
||||
|
||||
```js
|
||||
npm run repl
|
||||
```
|
||||
|
||||
Now open a .tidal file and play something. There should be logs like:
|
||||
and in a seperate shell:
|
||||
|
||||
```js
|
||||
npm run osc
|
||||
```
|
||||
|
||||
This should give you
|
||||
|
||||
```log
|
||||
received: /dirt/play [
|
||||
'_id_', '1',
|
||||
'cps', 0.5625,
|
||||
'cutoff', 100,
|
||||
'cycle', 724.1875,
|
||||
'delta', 0.11111068725585938,
|
||||
'orbit', 0,
|
||||
's', 'arpy'
|
||||
]
|
||||
osc client running on port 57120
|
||||
osc server running on port 57121
|
||||
websocket server running on port 8080
|
||||
```
|
||||
|
||||
## Web Client + Server (WIP)
|
||||
Now open Supercollider (with the super dirt startup file)
|
||||
|
||||
```sh
|
||||
npm run client
|
||||
npm run server # another terminal
|
||||
Now open the REPL and type:
|
||||
|
||||
```js
|
||||
s("<bd sd> hh").osc()
|
||||
```
|
||||
|
||||
Then go to [http://localhost:4321](localhost:4321) and push the button.
|
||||
In your server terminal, there should be a log.
|
||||
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
||||
|
|
|
|||
1726
packages/osc/osc.js
Normal file
1726
packages/osc/osc.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
|||
import OSC from './lib/osc-js.js';
|
||||
import { Pattern } from '@strudel.cycles/core/strudel.mjs';
|
||||
import OSC from './osc.js';
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
|
||||
const comm = new OSC();
|
||||
comm.open();
|
||||
|
|
|
|||
148
packages/osc/package-lock.json
generated
148
packages/osc/package-lock.json
generated
|
|
@ -1,76 +1,76 @@
|
|||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"osc-js": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/osc-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
|
||||
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"ws": "8.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"requires": {}
|
||||
},
|
||||
"osc-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
|
||||
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
|
||||
"requires": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"ws": "8.5.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"osc-js": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/osc-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
|
||||
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"ws": "8.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"requires": {}
|
||||
},
|
||||
"osc-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
|
||||
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
|
||||
"requires": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"ws": "8.5.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5446
packages/tonal/package-lock.json
generated
5446
packages/tonal/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
import { strict as assert } from 'assert';
|
||||
import '../tonal.mjs'; // need to import this to add prototypes
|
||||
import { pure } from '@strudel.cycles/core/strudel.mjs';
|
||||
import { pure } from '@strudel.cycles/core';
|
||||
|
||||
describe('tonal', () => {
|
||||
it('Should run tonal functions ', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
import { mod } from '@strudel.cycles/core/util.mjs';
|
||||
import { Pattern, mod } from '@strudel.cycles/core';
|
||||
|
||||
// transpose note inside scale by offset steps
|
||||
// function scaleTranspose(scale: string, offset: number, note: string) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core/strudel.mjs';
|
||||
import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core';
|
||||
import _voicings from 'chord-voicings';
|
||||
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
|
||||
|
||||
|
|
|
|||
2
packages/tone/package-lock.json
generated
2
packages/tone/package-lock.json
generated
|
|
@ -6,7 +6,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.4",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { strict as assert } from 'assert';
|
||||
import '../tone.mjs';
|
||||
import { pure } from '@strudel.cycles/core/strudel.mjs';
|
||||
import { pure } from '@strudel.cycles/core';
|
||||
import Tone from 'tone';
|
||||
|
||||
describe('tone', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Tune from './tunejs.js';
|
||||
import { Pattern } from '@strudel.cycles/core/strudel.mjs';
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
|
||||
Pattern.prototype._tune = function (scale, tonic = 220) {
|
||||
const tune = new Tune();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Pattern } from '@strudel.cycles/core/strudel.mjs';
|
||||
import { mod } from '@strudel.cycles/core/util.mjs';
|
||||
import { Pattern, mod } from '@strudel.cycles/core';
|
||||
|
||||
export function edo(name) {
|
||||
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue