mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Merge branch 'main' into paper
This commit is contained in:
@@ -11,7 +11,7 @@ npm i @strudel.cycles/core --save
|
||||
## Example
|
||||
|
||||
```js
|
||||
import { sequence, State, TimeSpan } from '@strudel.cycles/core';
|
||||
import { sequence } from '@strudel.cycles/core';
|
||||
|
||||
const pattern = sequence('a', ['b', 'c']);
|
||||
|
||||
|
||||
@@ -276,14 +276,14 @@ const generic_params = [
|
||||
|
||||
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
|
||||
|
||||
const _unionise = (func) =>
|
||||
const _setter = (func) =>
|
||||
function (...pats) {
|
||||
return this.union(func(...pats));
|
||||
return this.set(func(...pats));
|
||||
};
|
||||
|
||||
generic_params.forEach(([type, name, description]) => {
|
||||
controls[name] = (...pats) => _name(name, ...pats);
|
||||
Pattern.prototype[name] = _unionise(controls[name]);
|
||||
Pattern.prototype[name] = _setter(controls[name]);
|
||||
});
|
||||
|
||||
export default controls;
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -30,5 +30,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.2.2"
|
||||
}
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
||||
}
|
||||
|
||||
+98
-40
@@ -3,7 +3,7 @@ import Fraction from './fraction.mjs';
|
||||
import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry } from './util.mjs';
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
|
||||
|
||||
export class Pattern {
|
||||
// the following functions will get patternFactories as nested functions:
|
||||
@@ -179,14 +179,16 @@ export class Pattern {
|
||||
const query = function (state) {
|
||||
const haps = [];
|
||||
for (const hap_func of pat_func.query(state)) {
|
||||
const event_vals = pat_val.query(state.setSpan(hap_func.part));
|
||||
const event_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
|
||||
for (const hap_val of event_vals) {
|
||||
const new_whole = hap_func.whole;
|
||||
const new_part = hap_func.part.intersection_e(hap_val.part);
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
const new_part = hap_func.part.intersection(hap_val.part);
|
||||
if (new_part) {
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
}
|
||||
return haps;
|
||||
@@ -200,14 +202,16 @@ export class Pattern {
|
||||
const query = function (state) {
|
||||
const haps = [];
|
||||
for (const hap_val of pat_val.query(state)) {
|
||||
const hap_funcs = pat_func.query(state.setSpan(hap_val.part));
|
||||
const hap_funcs = pat_func.query(state.setSpan(hap_val.wholeOrPart()));
|
||||
for (const hap_func of hap_funcs) {
|
||||
const new_whole = hap_val.whole;
|
||||
const new_part = hap_func.part.intersection_e(hap_val.part);
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
const new_part = hap_func.part.intersection(hap_val.part);
|
||||
if (new_part) {
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
}
|
||||
return haps;
|
||||
@@ -243,9 +247,24 @@ export class Pattern {
|
||||
);
|
||||
}
|
||||
|
||||
_opleft(other, func) {
|
||||
_opLeft(other, func) {
|
||||
return this.fmap(func).appLeft(reify(other));
|
||||
}
|
||||
_opRight(other, func) {
|
||||
return this.fmap(func).appRight(reify(other));
|
||||
}
|
||||
_opBoth(other, func) {
|
||||
return this.fmap(func).appBoth(reify(other));
|
||||
}
|
||||
_opSqueeze(other, func) {
|
||||
const otherPat = reify(other);
|
||||
return this.fmap((a) => otherPat.fmap((b) => func(a)(b)))._squeezeJoin();
|
||||
}
|
||||
_opSqueezeFlip(other, func) {
|
||||
const thisPat = this;
|
||||
const otherPat = reify(other);
|
||||
return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a)))._squeezeJoin();
|
||||
}
|
||||
|
||||
_asNumber(silent = false) {
|
||||
return this._withEvent((event) => {
|
||||
@@ -271,22 +290,6 @@ export class Pattern {
|
||||
})._removeUndefineds();
|
||||
}
|
||||
|
||||
add(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a + b);
|
||||
}
|
||||
|
||||
sub(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a - b);
|
||||
}
|
||||
|
||||
mul(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a * b);
|
||||
}
|
||||
|
||||
div(other) {
|
||||
return this._asNumber()._opleft(other, (a) => (b) => a / b);
|
||||
}
|
||||
|
||||
round() {
|
||||
return this._asNumber().fmap((v) => Math.round(v));
|
||||
}
|
||||
@@ -311,15 +314,15 @@ export class Pattern {
|
||||
return this.mul(max - min).add(min);
|
||||
}
|
||||
|
||||
rangex(min, max) {
|
||||
return this.range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
_bindWhole(choose_whole, func) {
|
||||
const pat_val = this;
|
||||
const query = function (state) {
|
||||
@@ -460,7 +463,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
_compress(b, e) {
|
||||
if (b > e || b > 1 || e > 1 || b < 0 || e < 0) {
|
||||
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
|
||||
return silence;
|
||||
}
|
||||
return this._fastGap(Fraction(1).div(e.sub(b)))._late(b);
|
||||
@@ -492,6 +495,13 @@ export class Pattern {
|
||||
return this._squeezeBind(func);
|
||||
}
|
||||
|
||||
_striate(n) {
|
||||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
const slicePat = slowcat(...slice_objects);
|
||||
return this.set(slicePat)._fast(n);
|
||||
}
|
||||
|
||||
// cpm = cycles per minute
|
||||
_cpm(cpm) {
|
||||
return this._fast(cpm / 60);
|
||||
@@ -554,7 +564,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
_segment(rate) {
|
||||
return this.struct(pure(true).fast(rate));
|
||||
return this.struct(pure(true)._fast(rate));
|
||||
}
|
||||
|
||||
invert() {
|
||||
@@ -611,6 +621,10 @@ export class Pattern {
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
palindrome() {
|
||||
return this.every(2, rev);
|
||||
}
|
||||
|
||||
juxBy(by, func) {
|
||||
by /= 2;
|
||||
const elem_or = function (dict, key, dflt) {
|
||||
@@ -642,7 +656,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
stutWith(times, time, func) {
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
|
||||
}
|
||||
|
||||
stut(times, feedback, time) {
|
||||
@@ -651,7 +665,7 @@ export class Pattern {
|
||||
|
||||
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
|
||||
_echoWith(times, time, func) {
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
|
||||
}
|
||||
|
||||
_echo(times, time, feedback) {
|
||||
@@ -713,6 +727,49 @@ export class Pattern {
|
||||
}
|
||||
}
|
||||
|
||||
// pattern composers
|
||||
const composers = {
|
||||
set: [
|
||||
(a) => (b) => {
|
||||
// If an object is involved, do a union, discarding matching keys from a.
|
||||
// Otherwise, just return b.
|
||||
if (a instanceof Object || b instanceof Object) {
|
||||
if (!a instanceof Object) {
|
||||
a = { value: a };
|
||||
}
|
||||
if (!b instanceof Object) {
|
||||
b = { value: b };
|
||||
}
|
||||
return Object.assign({}, a, b);
|
||||
}
|
||||
return b;
|
||||
},
|
||||
id,
|
||||
],
|
||||
add: [(a) => (b) => a + b, (x) => x._asNumber()],
|
||||
sub: [(a) => (b) => a - b, (x) => x._asNumber()],
|
||||
mul: [(a) => (b) => a * b, (x) => x._asNumber()],
|
||||
div: [(a) => (b) => a / b, (x) => x._asNumber()],
|
||||
};
|
||||
|
||||
for (const [name, op] of Object.entries(composers)) {
|
||||
Pattern.prototype[name] = function (...other) {
|
||||
return op[1](this)._opLeft(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'Flip'] = function (...other) {
|
||||
return op[1](this)._opRight(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'Sect'] = function (...other) {
|
||||
return op[1](this)._opBoth(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'Squeeze'] = function (...other) {
|
||||
return op[1](this)._opSqueeze(sequence(other), op[0]);
|
||||
};
|
||||
Pattern.prototype[name + 'SqueezeFlip'] = function (...other) {
|
||||
return op[1](this)._opSqueezeFlip(sequence(other), op[0]);
|
||||
};
|
||||
}
|
||||
|
||||
// methods of Pattern that get callable factories
|
||||
Pattern.prototype.patternified = [
|
||||
'apply',
|
||||
@@ -728,6 +785,7 @@ Pattern.prototype.patternified = [
|
||||
'linger',
|
||||
'ply',
|
||||
'segment',
|
||||
'striate',
|
||||
'slow',
|
||||
'velocity',
|
||||
];
|
||||
@@ -774,7 +832,7 @@ export function slowcat(...pats) {
|
||||
pats = pats.map(reify);
|
||||
const query = function (state) {
|
||||
const span = state.span;
|
||||
const pat_n = Math.floor(span.begin) % pats.length;
|
||||
const pat_n = mod(span.begin.sam(), pats.length);
|
||||
const pat = pats[pat_n];
|
||||
if (!pat) {
|
||||
// pat_n can be negative, if the span is in the past..
|
||||
@@ -914,7 +972,7 @@ export const slow = curry((a, pat) => pat.slow(a));
|
||||
export const struct = curry((a, pat) => pat.struct(a));
|
||||
export const sub = curry((a, pat) => pat.sub(a));
|
||||
export const superimpose = curry((array, pat) => pat.superimpose(...array));
|
||||
export const union = curry((a, pat) => pat.union(a));
|
||||
export const set = curry((a, pat) => pat.set(a));
|
||||
export const when = curry((binary, f, pat) => pat.when(binary, f));
|
||||
|
||||
// problem: curried functions with spread arguments must have pat at the beginning
|
||||
|
||||
+181
-2
@@ -1,10 +1,11 @@
|
||||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat } from './pattern.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id } from './util.mjs';
|
||||
|
||||
export function steady(value) {
|
||||
// A continuous value
|
||||
return new Pattern((span) => Hap(undefined, span, value));
|
||||
return new Pattern((state) => [new Hap(undefined, state.span, value)]);
|
||||
}
|
||||
|
||||
export const signal = (func) => {
|
||||
@@ -28,3 +29,181 @@ export const square2 = square._toBipolar();
|
||||
|
||||
export const tri = fastcat(isaw, saw);
|
||||
export const tri2 = fastcat(isaw2, saw2);
|
||||
|
||||
export const time = signal(id);
|
||||
|
||||
// random signals
|
||||
|
||||
const xorwise = (x) => {
|
||||
const a = (x << 13) ^ x;
|
||||
const b = (a >> 17) ^ a;
|
||||
return (b << 5) ^ b;
|
||||
};
|
||||
|
||||
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
|
||||
const _frac = (x) => x - Math.trunc(x);
|
||||
|
||||
const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
|
||||
|
||||
const intSeedToRand = (x) => (x % 536870912) / 536870912;
|
||||
|
||||
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
||||
|
||||
const timeToRandsPrime = (seed, n) => {
|
||||
const result = [];
|
||||
for (let i = 0; i < n; ++n) {
|
||||
result.push(intSeedToRand(seed));
|
||||
seed = xorwise(seed);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
|
||||
export const rand = signal(timeToRand);
|
||||
|
||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||
export const brand = _brandBy(0.5);
|
||||
|
||||
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||
|
||||
export const chooseWith = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]).outerJoin();
|
||||
};
|
||||
|
||||
export const choose = (...xs) => chooseWith(rand, xs);
|
||||
|
||||
const _wchooseWith = function (pat, ...pairs) {
|
||||
const values = pairs.map((pair) => reify(pair[0]));
|
||||
const weights = [];
|
||||
let accum = 0;
|
||||
for (const pair of pairs) {
|
||||
accum += pair[1];
|
||||
weights.push(accum);
|
||||
}
|
||||
const total = accum;
|
||||
const match = function(r) {
|
||||
const find = r * total;
|
||||
return values[weights.findIndex((x) => x > find, weights)];
|
||||
};
|
||||
return pat.fmap(match);
|
||||
};
|
||||
|
||||
const wchooseWith = (...args) => _wchooseWith(...args).outerJoin()
|
||||
|
||||
export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||
|
||||
export const perlinWith = (pat) => {
|
||||
const pata = pat.fmap(Math.floor);
|
||||
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
||||
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
|
||||
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
|
||||
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
||||
};
|
||||
|
||||
export const perlin = perlinWith(time);
|
||||
|
||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
||||
};
|
||||
|
||||
Pattern.prototype._degradeBy = function (x) {
|
||||
return this._degradeByWith(rand, x);
|
||||
};
|
||||
|
||||
Pattern.prototype.degrade = function () {
|
||||
return this._degradeBy(0.5);
|
||||
};
|
||||
|
||||
Pattern.prototype._undegradeBy = function (x) {
|
||||
return this._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
x,
|
||||
);
|
||||
};
|
||||
|
||||
Pattern.prototype.undegrade = function () {
|
||||
return this._undegradeBy(0.5);
|
||||
};
|
||||
|
||||
Pattern.prototype._sometimesBy = function (x, func) {
|
||||
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
|
||||
};
|
||||
|
||||
Pattern.prototype.sometimesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
.fmap((x) => pat._sometimesBy(x, func))
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
Pattern.prototype._sometimesByPre = function (x, func) {
|
||||
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
|
||||
};
|
||||
|
||||
Pattern.prototype.sometimesByPre = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
.fmap((x) => pat._sometimesByPre(x, func))
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
Pattern.prototype.sometimes = function (func) {
|
||||
return this._sometimesBy(0.5, func);
|
||||
};
|
||||
|
||||
Pattern.prototype.sometimesPre = function (func) {
|
||||
return this._sometimesByPre(0.5, func);
|
||||
};
|
||||
|
||||
Pattern.prototype._someCyclesBy = function (x, func) {
|
||||
return stack(
|
||||
this._degradeByWith(rand._segment(1), x),
|
||||
func(this._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
||||
);
|
||||
};
|
||||
|
||||
Pattern.prototype.someCyclesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
.fmap((x) => pat._someCyclesBy(x, func))
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
Pattern.prototype.someCycles = function (func) {
|
||||
return this._someCyclesBy(0.5, func);
|
||||
};
|
||||
|
||||
Pattern.prototype.often = function (func) {
|
||||
return this.sometimesBy(0.75, func);
|
||||
};
|
||||
|
||||
Pattern.prototype.rarely = function (func) {
|
||||
return this.sometimesBy(0.25, func);
|
||||
};
|
||||
|
||||
Pattern.prototype.almostNever = function (func) {
|
||||
return this.sometimesBy(0.1, func);
|
||||
};
|
||||
|
||||
Pattern.prototype.almostAlways = function (func) {
|
||||
return this.sometimesBy(0.9, func);
|
||||
};
|
||||
|
||||
Pattern.prototype.never = function (func) {
|
||||
return this;
|
||||
};
|
||||
|
||||
Pattern.prototype.always = function (func) {
|
||||
return func(this);
|
||||
};
|
||||
|
||||
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
|
||||
|
||||
@@ -36,6 +36,9 @@ import {
|
||||
id,
|
||||
ply,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
//import { Time } from 'tone';
|
||||
import pkg from 'tone';
|
||||
const { Time } = pkg;
|
||||
@@ -47,6 +50,10 @@ const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, co
|
||||
const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
const sameFirst = (a, b) => {
|
||||
return assert.deepStrictEqual(a._sortEventsByPart().firstCycle(), b._sortEventsByPart().firstCycle());
|
||||
};
|
||||
|
||||
describe('TimeSpan', function () {
|
||||
describe('equals()', function () {
|
||||
it('Should be equal to the same value', function () {
|
||||
@@ -108,6 +115,18 @@ describe('Hap', function () {
|
||||
assert.deepStrictEqual(state3, { incrementme: 12 });
|
||||
});
|
||||
});
|
||||
describe('wholeOrPart()', () => {
|
||||
const ts1 = new TimeSpan(Fraction(0), Fraction(1));
|
||||
const ts0_5 = new TimeSpan(Fraction(0), Fraction(0.5));
|
||||
const continuousHap = new Hap(undefined, ts1, 'hello');
|
||||
const discreteHap = new Hap(ts1, ts0_5, 'hello');
|
||||
it('Can pick a whole', () => {
|
||||
assert.deepStrictEqual(discreteHap.wholeOrPart(), ts1);
|
||||
});
|
||||
it('Can pick a part', () => {
|
||||
assert.deepStrictEqual(continuousHap.wholeOrPart(), ts1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pattern', function () {
|
||||
@@ -131,6 +150,33 @@ describe('Pattern', function () {
|
||||
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
|
||||
});
|
||||
});
|
||||
describe('addFlip()', () => {
|
||||
it('Can add things with structure from second pattern', () => {
|
||||
sameFirst(sequence(1, 2).addFlip(4), sequence(5, 6).struct(true));
|
||||
});
|
||||
});
|
||||
describe('addSqueeze()', () => {
|
||||
it('Can add while squeezing the second pattern inside the events of the first', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
|
||||
sequence(
|
||||
[11, 21, 31],
|
||||
[
|
||||
[12, 22, 32],
|
||||
[13, 23, 33],
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('addSqueezeFlip()', () => {
|
||||
it('Can add while squeezing the first pattern inside the events of the second', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).addSqueezeFlip(10, 20, 30),
|
||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('sub()', function () {
|
||||
it('Can subtract things', function () {
|
||||
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
|
||||
@@ -146,14 +192,50 @@ describe('Pattern', function () {
|
||||
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
|
||||
});
|
||||
});
|
||||
describe('union()', function () {
|
||||
it('Can union things', function () {
|
||||
describe('set()', function () {
|
||||
it('Can set things in objects', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 4, b: 6 })
|
||||
.union(pure({ c: 7 }))
|
||||
.set(pure({ c: 7 }))
|
||||
.firstCycle()[0].value,
|
||||
{ a: 4, b: 6, c: 7 },
|
||||
);
|
||||
sameFirst(
|
||||
sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
|
||||
sequence({ a: 4, b: 2, c: 5 }).fast(3),
|
||||
);
|
||||
});
|
||||
it('Can set things with plain values', function () {
|
||||
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
|
||||
});
|
||||
describe('setFlip()', () => {
|
||||
it('Can set things with structure from second pattern', () => {
|
||||
sameFirst(sequence(1, 2).setFlip(4), pure(4).mask(true, true));
|
||||
});
|
||||
});
|
||||
describe('setSqueeze()', () => {
|
||||
it('Can squeeze one pattern inside the events of another', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
|
||||
sequence(
|
||||
['a', 'b', 'c'],
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
['a', 'b', 'c'],
|
||||
],
|
||||
),
|
||||
);
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
|
||||
sequence(
|
||||
['a', 'b', 'c'],
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
['a', 'b', 'c'],
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('stack()', function () {
|
||||
@@ -292,6 +374,15 @@ describe('Pattern', function () {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fastcat()', function () {
|
||||
it('Can go into negative time', function () {
|
||||
sameFirst(
|
||||
fastcat('a','b','c')
|
||||
.late(1000000),
|
||||
fastcat('a','b','c'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('slowcat()', function () {
|
||||
it('Can concatenate things slowly', function () {
|
||||
assert.deepStrictEqual(
|
||||
@@ -399,7 +490,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
});
|
||||
describe('struct()', function () {
|
||||
it('Can restructure a pattern', function () {
|
||||
it('Can restructure a discrete pattern', function () {
|
||||
assert.deepStrictEqual(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle(), [
|
||||
hap(ts(0, third), ts(0, third), 'a'),
|
||||
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
||||
@@ -425,6 +516,12 @@ describe('Pattern', function () {
|
||||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can structure a continuous pattern', () => {
|
||||
assert.deepStrictEqual(
|
||||
steady('a').struct(true, [true, true]).firstCycle(),
|
||||
sequence('a', ['a', 'a']).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mask()', function () {
|
||||
it('Can fragment a pattern', function () {
|
||||
@@ -616,6 +713,18 @@ describe('Pattern', function () {
|
||||
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Doesnt drop events in the 9th cycle', () => {
|
||||
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
|
||||
});
|
||||
});
|
||||
describe('striate', () => {
|
||||
it('Can striate(2)', () => {
|
||||
sameFirst(
|
||||
sequence({ sound: 'a' }).striate(2),
|
||||
sequence({ sound: 'a', begin: 0, end: 0.5 }, { sound: 'a', begin: 0.5, end: 1 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chop', () => {
|
||||
it('Can _chop(2)', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { isNote, tokenizeNote, toMidi, mod, compose } from '../util.mjs';
|
||||
import { pure } from '../pattern.mjs';
|
||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should recognize notes without accidentals', () => {
|
||||
@@ -64,6 +65,21 @@ describe('toMidi', () => {
|
||||
assert.equal(toMidi('C##3'), 50);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
assert.equal(fromMidi(69), 440);
|
||||
assert.equal(fromMidi(57), 220);
|
||||
});
|
||||
});
|
||||
describe('getFrequency', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
assert.equal(getFrequency(happify('a4')), 440);
|
||||
assert.equal(getFrequency(happify('a3')), 220);
|
||||
assert.equal(getFrequency(happify(440, { type: 'frequency' })), 440); // TODO: migrate when values are objects..
|
||||
assert.equal(getFrequency(happify(432, { type: 'frequency' })), 432);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mod', () => {
|
||||
it('should work like regular modulo with positive numbers', () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class TimeSpan {
|
||||
// (Note that the output timespan probably does not start *at* Time 0 --
|
||||
// that only happens when the input Arc starts at an integral Time.)
|
||||
const b = this.begin.cyclePos();
|
||||
const e = b + (this.end - this.begin);
|
||||
const e = b.add(this.end.sub(this.begin));
|
||||
return new TimeSpan(b, e);
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ export class TimeSpan {
|
||||
// Like 'sect', but raises an exception if the timespans don't intersect.
|
||||
const result = this.intersection(other);
|
||||
if (result == undefined) {
|
||||
// TODO - raise exception
|
||||
// raise ValueError(f'TimeSpan {self} and TimeSpan {other} do not intersect')
|
||||
throw 'TimeSpans do not intersect';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+18
-1
@@ -27,7 +27,7 @@ export const fromMidi = (n) => {
|
||||
|
||||
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
||||
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
||||
export const mod = (n, m) => (n < 0 ? mod(n + m, m) : n % m);
|
||||
export const mod = (n, m) => (n % m + m) % m;
|
||||
|
||||
export const getPlayableNoteValue = (event) => {
|
||||
let { value: note, context } = event;
|
||||
@@ -40,6 +40,22 @@ export const getPlayableNoteValue = (event) => {
|
||||
return note;
|
||||
};
|
||||
|
||||
export const getFrequency = (event) => {
|
||||
let { value, context } = event;
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof value === 'object' && value.freq) {
|
||||
return value.freq;
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = fromMidi(event.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(event.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency:' + value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// rotate array by n steps (to the left)
|
||||
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));
|
||||
|
||||
@@ -60,6 +76,7 @@ export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
||||
export const flatten = (arr) => [].concat(...arr);
|
||||
|
||||
export const id = (a) => a;
|
||||
export const constant = (a, b) => a;
|
||||
|
||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "evaluate.mjs",
|
||||
"directories": {
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.3",
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "midi.mjs",
|
||||
"repository": {
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.0.4",
|
||||
"@strudel.cycles/tone": "^0.0.6",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.7",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "mini.mjs",
|
||||
"type": "module",
|
||||
@@ -25,7 +25,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/eval": "^0.0.3",
|
||||
"@strudel.cycles/tone": "^0.0.4"
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"@strudel.cycles/eval": "^0.0.5",
|
||||
"@strudel.cycles/tone": "^0.0.6"
|
||||
}
|
||||
}
|
||||
|
||||
-1726
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import OSC from './osc.js';
|
||||
import OSC from 'osc-js';
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
|
||||
const comm = new OSC();
|
||||
|
||||
Generated
+10
-26
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -9,24 +9,15 @@
|
||||
"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": "*"
|
||||
"osc-js": "^2.3.2"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
|
||||
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"ws": "8.5.0"
|
||||
"ws": "^8.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
@@ -51,19 +42,12 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
|
||||
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
|
||||
"requires": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"ws": "8.5.0"
|
||||
"ws": "^8.5.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"scripts": {
|
||||
@@ -21,12 +21,13 @@
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"contributors": ["Alex McLean <alex@slab.org>"],
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"osc-js": "^2.3.0"
|
||||
"osc-js": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# @strudel.cycles/serial
|
||||
|
||||
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.0.6",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"titdalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Alex McLean <alex@slab.org>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
|
||||
var serialWriter;
|
||||
var choosing = false;
|
||||
|
||||
export async function getWriter(br=115200) {
|
||||
if (choosing) {
|
||||
return;
|
||||
}
|
||||
choosing = true;
|
||||
if (serialWriter) {
|
||||
return serialWriter;
|
||||
}
|
||||
if ('serial' in navigator) {
|
||||
const port = await navigator.serial.requestPort();
|
||||
await port.open({ baudRate: br });
|
||||
const textEncoder = new TextEncoderStream();
|
||||
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
||||
const writer = textEncoder.writable.getWriter();
|
||||
serialWriter = function (message) {
|
||||
writer.write(message)
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw('Webserial is not available in this browser.')
|
||||
}
|
||||
}
|
||||
|
||||
const latency = 0.1;
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.serial = async function (...args) {
|
||||
return this._withEvent((event) => {
|
||||
if (!serialWriter) {
|
||||
getWriter(...args);
|
||||
}
|
||||
const onTrigger = (time, event, currentTime) => {
|
||||
var message = "";
|
||||
if (typeof event.value === 'object') {
|
||||
for (const [key, val] of Object.entries(event.value).flat()) {
|
||||
message += `${key}:${val};`
|
||||
}
|
||||
}
|
||||
else {
|
||||
message = event.value;
|
||||
}
|
||||
const offset = (time - currentTime + latency) * 1000;
|
||||
window.setTimeout(serialWriter, offset, message);
|
||||
};
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
});
|
||||
};
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "tonal.mjs",
|
||||
"type": "module",
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.3",
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "tone.mjs",
|
||||
"type": "module",
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.3",
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"tone": "^14.7.77"
|
||||
|
||||
@@ -117,157 +117,3 @@ export const highpass = (v) => new Filter(v, 'highpass');
|
||||
export const adsr = (a, d = 0.1, s = 0.4, r = 0.01) => ({ envelope: { attack: a, decay: d, sustain: s, release: r } });
|
||||
export const osc = (type) => ({ oscillator: { type } });
|
||||
export const out = () => getDestination();
|
||||
|
||||
/*
|
||||
|
||||
You are entering experimental zone
|
||||
|
||||
*/
|
||||
|
||||
// the following code is an attempt to minimize tonejs code.. it is still an experiment
|
||||
|
||||
const chainable = function (instr) {
|
||||
const _chain = instr.chain.bind(instr);
|
||||
let chained = [];
|
||||
instr.chain = (...args) => {
|
||||
chained = chained.concat(args);
|
||||
instr.disconnect(); // disconnect from destination / previous chain
|
||||
return _chain(...chained, getDestination());
|
||||
};
|
||||
// shortcuts: chaining multiple won't work forn now.. like filter(1000).gain(0.5). use chain + native Tone calls instead
|
||||
// instr.filter = (freq = 1000, type: BiquadFilterType = 'lowpass') =>
|
||||
instr.filter = (freq = 1000, type = 'lowpass') =>
|
||||
instr.chain(
|
||||
new Filter(freq, type), // .Q.setValueAtTime(q, time);
|
||||
);
|
||||
instr.gain = (gain = 0.9) => instr.chain(new Gain(gain));
|
||||
return instr;
|
||||
};
|
||||
|
||||
// helpers
|
||||
export const poly = (type) => {
|
||||
const s = new PolySynth(Synth, { oscillator: { type } }).toDestination();
|
||||
return chainable(s);
|
||||
};
|
||||
|
||||
Pattern.prototype._poly = function (type = 'triangle') {
|
||||
const instrumentConfig = {
|
||||
oscillator: { type },
|
||||
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
|
||||
};
|
||||
if (!this.instrument) {
|
||||
// create only once to keep the js heap happy
|
||||
// this.instrument = new PolySynth(Synth, instrumentConfig).toDestination();
|
||||
this.instrument = poly(type);
|
||||
}
|
||||
return this._withEvent((event) => {
|
||||
const onTrigger = (time, event) => {
|
||||
this.instrument.set(instrumentConfig);
|
||||
this.instrument.triggerAttackRelease(event.value, event.duration, time);
|
||||
};
|
||||
return event.setContext({ ...event.context, instrumentConfig, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.define('poly', (type, pat) => pat.poly(type), { composable: true, patternified: true });
|
||||
|
||||
/*
|
||||
|
||||
You are entering danger zone
|
||||
|
||||
*/
|
||||
|
||||
// everything below is nice in theory, but not healthy for the JS heap, as nodes get recreated on every call
|
||||
|
||||
const getTrigger = (getChain, value) => (time, event) => {
|
||||
const chain = getChain(); // make sure this returns a node that is connected toDestination // time
|
||||
if (!isNote(value)) {
|
||||
throw new Error('not a note: ' + value);
|
||||
}
|
||||
chain.triggerAttackRelease(value, event.duration, time);
|
||||
setTimeout(() => {
|
||||
// setTimeout is a little bit better compared to Transport.scheduleOnce
|
||||
chain.dispose(); // mark for garbage collection
|
||||
}, event.duration * 2000);
|
||||
};
|
||||
|
||||
Pattern.prototype._synth = function (type = 'triangle') {
|
||||
return this._withEvent((event) => {
|
||||
const instrumentConfig = {
|
||||
oscillator: { type },
|
||||
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
|
||||
};
|
||||
const getInstrument = () => {
|
||||
const instrument = new Synth();
|
||||
instrument.set(instrumentConfig);
|
||||
return instrument;
|
||||
};
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
|
||||
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
|
||||
return this._withEvent((event) => {
|
||||
if (!event.context.getInstrument) {
|
||||
throw new Error('cannot chain adsr: need instrument first (like synth)');
|
||||
}
|
||||
const instrumentConfig = { ...event.context.instrumentConfig, envelope: { attack, decay, sustain, release } };
|
||||
const getInstrument = () => {
|
||||
const instrument = event.context.getInstrument();
|
||||
instrument.set(instrumentConfig);
|
||||
return instrument;
|
||||
};
|
||||
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
|
||||
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.chain = function (...effectGetters) {
|
||||
return this._withEvent((event) => {
|
||||
if (!event.context?.getInstrument) {
|
||||
throw new Error('cannot chain: need instrument first (like synth)');
|
||||
}
|
||||
const chain = (event.context.chain || []).concat(effectGetters);
|
||||
const getChain = () => {
|
||||
const effects = chain.map((getEffect) => getEffect());
|
||||
return event.context.getInstrument().chain(...effects, getDestination());
|
||||
};
|
||||
const onTrigger = getTrigger(getChain, event.value);
|
||||
return event.setContext({ ...event.context, getChain, onTrigger, chain });
|
||||
});
|
||||
};
|
||||
|
||||
export const autofilter =
|
||||
(freq = 1) =>
|
||||
() =>
|
||||
new AutoFilter(freq).start();
|
||||
|
||||
export const filter =
|
||||
// (freq = 1, q = 1, type: BiquadFilterType = 'lowpass') =>
|
||||
|
||||
|
||||
(freq = 1, q = 1, type = 'lowpass') =>
|
||||
() =>
|
||||
new Filter(freq, type); // .Q.setValueAtTime(q, time);
|
||||
|
||||
export const gain =
|
||||
(gain = 0.9) =>
|
||||
() =>
|
||||
new Gain(gain);
|
||||
|
||||
Pattern.prototype._gain = function (g) {
|
||||
return this.chain(gain(g));
|
||||
};
|
||||
// Pattern.prototype._filter = function (freq: number, q: number, type: BiquadFilterType = 'lowpass') {
|
||||
Pattern.prototype._filter = function (freq, q, type = 'lowpass') {
|
||||
return this.chain(filter(freq, q, type));
|
||||
};
|
||||
Pattern.prototype._autofilter = function (g) {
|
||||
return this.chain(autofilter(g));
|
||||
};
|
||||
|
||||
Pattern.prototype.define('synth', (type, pat) => pat.synth(type), { composable: true, patternified: true });
|
||||
Pattern.prototype.define('gain', (gain, pat) => pat.synth(gain), { composable: true, patternified: true });
|
||||
Pattern.prototype.define('filter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });
|
||||
Pattern.prototype.define('autofilter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# @strudel.cycles/webaudio
|
||||
|
||||
This package contains a scheduler + a clockworker and synths based on the Web Audio API.
|
||||
It's an alternative to `@strudel.cycles/tone`, with better performance, but less features.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel.cycles/webaudio --save
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import { Scheduler, getAudioContext } from '@strudel.cycles/webaudio';
|
||||
|
||||
const scheduler = new Scheduler({
|
||||
audioContext: getAudioContext(),
|
||||
interval: 0.1,
|
||||
onEvent: (e) => e.context?.createAudioNode?.(e),
|
||||
});
|
||||
const pattern = sequence([55, 99], 110).osc('sawtooth').out()
|
||||
scheduler.setPattern(pattern);
|
||||
scheduler.start()
|
||||
//scheduler.stop()
|
||||
```
|
||||
|
||||
A more sophisticated example can be found in [examples/repl.html](./examples/repl.html).
|
||||
You can run it by opening the html file with your browser, or by clicking [this link](https://raw.githack.com/tidalcycles/strudel/main/packages/webaudio/examples/repl.html)
|
||||
@@ -0,0 +1,69 @@
|
||||
// helpers to create a worker dynamically without needing a server / extra file
|
||||
const stringifyFunction = (func) => '(' + func + ')();';
|
||||
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
|
||||
const createWorker = (func) => new Worker(urlifyFunction(func));
|
||||
|
||||
// this class is basically the tale of two clocks
|
||||
class ClockWorker {
|
||||
worker;
|
||||
audioContext;
|
||||
interval = 0.2; // query span
|
||||
lastEnd = 0;
|
||||
constructor(audioContext, callback, interval = this.interval) {
|
||||
this.audioContext = audioContext;
|
||||
this.interval = interval;
|
||||
this.worker = createWorker(() => {
|
||||
// we cannot use closures here!
|
||||
let interval;
|
||||
let timerID = null; // this is clock #1 (the sloppy js clock)
|
||||
const clear = () => {
|
||||
if (timerID) {
|
||||
clearInterval(timerID);
|
||||
timerID = null;
|
||||
}
|
||||
};
|
||||
const start = () => {
|
||||
clear();
|
||||
if (!interval) {
|
||||
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
|
||||
}
|
||||
timerID = setInterval(() => postMessage('tick'), interval * 1000);
|
||||
};
|
||||
self.onmessage = function (e) {
|
||||
if (e.data == 'start') {
|
||||
start();
|
||||
} else if (e.data.interval) {
|
||||
interval = e.data.interval;
|
||||
if (timerID) {
|
||||
start();
|
||||
}
|
||||
} else if (e.data == 'stop') {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
});
|
||||
this.worker.postMessage({ interval });
|
||||
// const round = (n, d) => Math.round(n * d) / d;
|
||||
const precision = 100;
|
||||
this.worker.onmessage = (e) => {
|
||||
if (e.data === 'tick') {
|
||||
const begin = this.lastEnd || this.audioContext.currentTime;
|
||||
const end = this.audioContext.currentTime + this.interval; // DONT reference begin here!
|
||||
this.lastEnd = end;
|
||||
// callback with query span, using clock #2 (the audio clock)
|
||||
callback(begin, end);
|
||||
}
|
||||
};
|
||||
}
|
||||
start() {
|
||||
console.log('start...');
|
||||
this.audioContext.resume();
|
||||
this.worker.postMessage('start');
|
||||
}
|
||||
stop() {
|
||||
console.log('stop...');
|
||||
this.worker.postMessage('stop');
|
||||
}
|
||||
}
|
||||
|
||||
export default ClockWorker;
|
||||
@@ -0,0 +1,64 @@
|
||||
<div style="position: absolute; top: 0; right: 0; padding: 4px">
|
||||
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br />
|
||||
<button id="stop" style="font-size: 2em">stop</button>
|
||||
</div>
|
||||
<textarea
|
||||
style="font-size: 2em; background: #bce865; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
|
||||
id="text"
|
||||
spellcheck="false"
|
||||
>
|
||||
Loading...</textarea
|
||||
>
|
||||
<script type="module">
|
||||
document.body.style = 'margin: 0';
|
||||
// import * as strudel from '@strudel.cycles/core';
|
||||
import * as strudel from '../../core/index.mjs';
|
||||
import * as util from '../../core/util.mjs';
|
||||
import '@strudel.cycles/core/euclid.mjs';
|
||||
// import { Scheduler, getAudioContext } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.0.4';
|
||||
import { Scheduler, getAudioContext } from '../index.mjs';
|
||||
|
||||
const { cat, State, TimeSpan } = strudel;
|
||||
Object.assign(window, strudel); // add strudel to eval scope
|
||||
|
||||
const scheduler = new Scheduler({
|
||||
audioContext: getAudioContext(),
|
||||
interval: 0.1,
|
||||
onEvent: (e) => {
|
||||
e.context?.createAudioNode?.(e);
|
||||
},
|
||||
});
|
||||
|
||||
let initialCode = `sequence(1,2).mul(55/2) // frequencies
|
||||
.mul(slowcat(1,2))
|
||||
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
|
||||
.fast(3)
|
||||
.velocity(.5)
|
||||
.wave(cat('sawtooth','square').fast(2))
|
||||
.adsr(0.01,.02,.5,0.1)
|
||||
.filter('lowshelf',800,25)
|
||||
.out()`;
|
||||
|
||||
try {
|
||||
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
|
||||
initialCode = atob(base64);
|
||||
} catch (err) {
|
||||
console.warn('failed to decode', err);
|
||||
}
|
||||
const input = document.getElementById('text');
|
||||
input.value = initialCode;
|
||||
const evaluate = () => {
|
||||
try {
|
||||
const pattern = eval(input.value);
|
||||
scheduler.setPattern(pattern);
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
evaluate();
|
||||
input.addEventListener('input', () => evaluate());
|
||||
|
||||
document.getElementById('start').addEventListener('click', () => scheduler.start());
|
||||
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as ClockWorker } from './clockworker.mjs';
|
||||
export { default as Scheduler } from './scheduler.mjs';
|
||||
export * from './webaudio.mjs';
|
||||
Generated
+3851
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.0.6",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"example": "npx parcel examples/repl.html"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import ClockWorker from './clockworker.mjs';
|
||||
import { State, TimeSpan } from '@strudel.cycles/core';
|
||||
|
||||
class Scheduler {
|
||||
worker;
|
||||
pattern;
|
||||
constructor({ audioContext, interval = 0.2, onEvent }) {
|
||||
this.worker = new ClockWorker(
|
||||
audioContext,
|
||||
(begin, end) => {
|
||||
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
|
||||
if (!e.part.begin.equals(e.whole.begin)) {
|
||||
return;
|
||||
}
|
||||
if (onEvent) {
|
||||
onEvent?.(e);
|
||||
} else {
|
||||
console.warn('unplayable event: no audio node nor onEvent callback', e);
|
||||
}
|
||||
});
|
||||
},
|
||||
interval,
|
||||
);
|
||||
}
|
||||
start() {
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
this.worker.start();
|
||||
}
|
||||
stop() {
|
||||
this.worker.stop();
|
||||
}
|
||||
setPattern(pat) {
|
||||
this.pattern = pat;
|
||||
}
|
||||
}
|
||||
|
||||
export default Scheduler;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
|
||||
// let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
return Tone.getContext().rawContext;
|
||||
/* if (!audioContext) {
|
||||
audioContext = new AudioContext();
|
||||
}
|
||||
return audioContext; */
|
||||
};
|
||||
|
||||
const lookahead = 0.2;
|
||||
|
||||
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
|
||||
const gainNode = getAudioContext().createGain();
|
||||
gainNode.gain.setValueAtTime(0, begin);
|
||||
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
|
||||
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
|
||||
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
|
||||
// for some reason, using exponential ramping creates little cracklings
|
||||
return gainNode;
|
||||
};
|
||||
|
||||
Pattern.prototype.withAudioNode = function (createAudioNode) {
|
||||
return this._withEvent((event) => {
|
||||
return event.setContext({
|
||||
...event.context,
|
||||
createAudioNode: (t, e) => createAudioNode(t, e, event.context.createAudioNode?.(t, event)),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype._wave = function (type) {
|
||||
return this.withAudioNode((t, e) => {
|
||||
const osc = getAudioContext().createOscillator();
|
||||
osc.type = type;
|
||||
const f = getFrequency(e);
|
||||
osc.frequency.value = f; // expects frequency..
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration;
|
||||
osc.start(begin);
|
||||
osc.stop(end); // release?
|
||||
return osc;
|
||||
});
|
||||
};
|
||||
Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
|
||||
return this.withAudioNode((t, e, node) => {
|
||||
const velocity = e.context?.velocity || 1;
|
||||
const begin = t ?? e.whole.begin.valueOf() + lookahead;
|
||||
const end = begin + e.duration + lookahead;
|
||||
const envelope = adsr(a, d, s, r, velocity, begin, end);
|
||||
node?.connect(envelope);
|
||||
return envelope;
|
||||
});
|
||||
};
|
||||
Pattern.prototype._filter = function (type = 'lowpass', frequency = 1000) {
|
||||
return this.withAudioNode((t, e, node) => {
|
||||
const filter = getAudioContext().createBiquadFilter();
|
||||
filter.type = type;
|
||||
filter.frequency.value = frequency;
|
||||
node?.connect(filter);
|
||||
return filter;
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.filter = function (type, frequency) {
|
||||
return patternify2(Pattern.prototype._filter)(reify(type), reify(frequency), this);
|
||||
};
|
||||
|
||||
Pattern.prototype.out = function () {
|
||||
const master = getAudioContext().createGain();
|
||||
master.gain.value = 0.1;
|
||||
master.connect(getAudioContext().destination);
|
||||
return this.withAudioNode((t, e, node) => {
|
||||
if (!node) {
|
||||
console.warn('out: no source! call .osc() first');
|
||||
}
|
||||
node?.connect(master);
|
||||
})._withEvent((event) => {
|
||||
const onTrigger = (time, e) => e.context?.createAudioNode?.(time, e);
|
||||
return event.setContext({ ...event.context, onTrigger });
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.define('wave', (type, pat) => pat.wave(type), { patternified: true });
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.5",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "xen.mjs",
|
||||
"scripts": {
|
||||
@@ -24,6 +24,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.3"
|
||||
"@strudel.cycles/core": "^0.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user