Merge branch 'main' of github.com:tidalcycles/strudel into paper

This commit is contained in:
alex 2022-04-13 23:16:07 +01:00
commit 369604aa42
62 changed files with 7782 additions and 3944 deletions

View file

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

View file

@ -1,4 +1,4 @@
import { Pattern, sequence } from '@strudel.cycles/core/strudel.mjs';
import { Pattern, sequence } from './pattern.mjs';
const controls = {};
const generic_params = [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -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": {}
}
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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