Compare commits

..

1 Commits

Author SHA1 Message Date
Felix Roos 0b7ff17e01 move samples to dedicated samples folder 2022-07-28 22:35:27 +02:00
174 changed files with 6724 additions and 24924 deletions
+1 -2
View File
@@ -34,5 +34,4 @@ repl_old
tutorial.rendered.mdx
doc.json
talk/public/EmuSP12
talk/public/samples
server/samples/old
talk/public/samples
-6
View File
@@ -38,12 +38,6 @@ Click on the package names to find out more about each one.
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
<a href="https://github.com/tidalcycles/strudel/graphs/contributors">
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
## Community
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
+1208 -2522
View File
File diff suppressed because it is too large Load Diff
+2 -5
View File
@@ -4,8 +4,7 @@
"private": true,
"description": "Port of tidalcycles to javascript",
"scripts": {
"test": "vitest run --version",
"test-ui": "vitest --ui",
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
"snapshot": "cd repl && npm run snapshot",
@@ -38,14 +37,12 @@
},
"homepage": "https://strudel.tidalcycles.org",
"devDependencies": {
"@vitest/ui": "^0.21.1",
"events": "^3.3.0",
"gh-pages": "^4.0.0",
"happy-dom": "^6.0.4",
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^7.1.1",
"lerna": "^4.0.0",
"vitest": "^0.21.1"
"mocha": "^9.1.4"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.2.0",
"version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+5 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel.cycles/core",
"version": "0.2.0",
"version": "0.1.2",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "vitest run"
"test": "mocha --colors"
},
"repository": {
"type": "git",
@@ -28,5 +28,8 @@
"bjork": "^0.0.1",
"fraction.js": "^4.2.0"
},
"devDependencies": {
"mocha": "^9.2.2"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
}
+2 -18
View File
@@ -1046,22 +1046,6 @@ export class Pattern {
onTrigger(onTrigger) {
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
}
log(func = id) {
return this._withHap((hap) =>
hap.setContext({
...hap.context,
onTrigger: (...args) => {
if (hap.context.onTrigger) {
hap.context.onTrigger(...args);
}
console.log(func(...args));
},
}),
);
}
logValues(func = id) {
return this.log((_, hap) => func(hap.value));
}
}
// TODO - adopt value.mjs fully..
@@ -1294,8 +1278,8 @@ export function slowcatPrime(...pats) {
pats = pats.map(reify);
const query = function (state) {
const pat_n = Math.floor(state.span.begin) % pats.length;
const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
return pat?.query(state) || [];
const pat = pats[pat_n];
return pat.query(state);
};
return new Pattern(query)._splitQueries();
}
+7 -60
View File
@@ -47,6 +47,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
*/
export const sine = sine2._fromBipolar();
/**
* A cosine signal between 0 and 1.
*
@@ -58,6 +59,7 @@ export const sine = sine2._fromBipolar();
export const cosine = sine._early(Fraction(1).div(4));
export const cosine2 = sine2._early(Fraction(1).div(4));
/**
* A square signal between 0 and 1.
*
@@ -110,14 +112,7 @@ const timeToRandsPrime = (seed, n) => {
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/**
* A continuous pattern of random numbers, between 0 and 1
*/
export const rand = signal(timeToRand);
/**
* A continuous pattern of random numbers, between -1 and 1
*/
export const rand2 = rand._toBipolar();
export const _brandBy = (p) => rand.fmap((x) => x < p);
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
@@ -126,67 +121,19 @@ 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) => {
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)]);
};
/**
* Choose from the list of values (or patterns of values) using the given
* pattern of numbers, which should be in the range of 0..1
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const chooseWith = (pat, xs) => {
return __chooseWith(pat, xs).outerJoin();
return pat
.range(0, xs.length)
.fmap((i) => xs[Math.floor(i)])
.outerJoin();
};
/**
* As with {chooseWith}, but the structure comes from the chosen values, rather
* than the pattern you're using to choose with.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const chooseInWith = (pat, xs) => {
return __chooseWith(pat, xs).innerJoin();
};
/**
* Chooses randomly from the given list of values.
* @param {...any} xs
* @returns {Pattern} - a continuous pattern.
*/
export const choose = (...xs) => chooseWith(rand, xs);
/**
* Chooses from the given list of values (or patterns of values), according
* to the pattern that the method is called on. The pattern should be in
* the range 0 .. 1.
* @param {...any} xs
* @returns {Pattern}
*/
Pattern.prototype.choose = function (...xs) {
return chooseWith(this, xs);
};
/**
* As with choose, but the pattern that this method is called on should be
* in the range -1 .. 1
* @param {...any} xs
* @returns {Pattern}
*/
Pattern.prototype.choose2 = function (...xs) {
return chooseWith(this._fromBipolar(), xs);
};
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
export const randcat = chooseCycles;
const _wchooseWith = function (pat, ...pairs) {
const values = pairs.map((pair) => reify(pair[0]));
const weights = [];
+31 -21
View File
@@ -5,42 +5,52 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs';
import { describe, it, expect } from 'vitest';
import { strict as assert } from 'assert';
import drawLine from '../drawLine.mjs';
describe('drawLine', () => {
it('supports equal lengths', () => {
expect(drawLine(fastcat(0), 4)).toEqual('|0|0');
expect(drawLine(fastcat(0, 1), 4)).toEqual('|01|01');
expect(drawLine(fastcat(0, 1, 2), 6)).toEqual('|012|012');
assert.equal(drawLine(fastcat(0), 4), '|0|0');
assert.equal(drawLine(fastcat(0, 1), 4), '|01|01');
assert.equal(drawLine(fastcat(0, 1, 2), 6), '|012|012');
});
it('supports unequal lengths', () => {
expect(drawLine(fastcat(0, [1, 2]), 10)).toEqual('|0-12|0-12');
expect(drawLine(fastcat(0, [1, 2, 3]), 10)).toEqual('|0--123|0--123');
expect(drawLine(fastcat(0, 1, [2, 3]), 10)).toEqual('|0-1-23|0-1-23');
assert.equal(drawLine(fastcat(0, [1, 2]), 10), '|0-12|0-12');
assert.equal(drawLine(fastcat(0, [1, 2, 3]), 10), '|0--123|0--123');
assert.equal(drawLine(fastcat(0, 1, [2, 3]), 10), '|0-1-23|0-1-23');
});
it('supports unequal silence', () => {
expect(drawLine(fastcat(0, silence, [1, 2]), 10)).toEqual('|0-..12|0-..12');
assert.equal(drawLine(fastcat(0, silence, [1, 2]), 10), '|0-..12|0-..12');
});
it('supports polyrhythms', () => {
// assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
expect(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10)).toEqual('|0--0--1-1-1-');
'0*2 1*3';
assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
});
it('supports multiple lines', () => {
expect(drawLine(fastcat(0, stack(1, 2)), 10)).toEqual(`|01|01|01|01
|.2|.2|.2|.2`);
expect(drawLine(fastcat(0, 1, stack(2, 3)), 10)).toEqual(`|012|012|012
|..3|..3|..3`);
expect(drawLine(fastcat(0, stack(1, 2, 3)), 10)).toEqual(`|01|01|01|01
assert.equal(
drawLine(fastcat(0, stack(1, 2)), 10),
`|01|01|01|01
|.2|.2|.2|.2`,
);
assert.equal(
drawLine(fastcat(0, 1, stack(2, 3)), 10),
`|012|012|012
|..3|..3|..3`,
);
assert.equal(
drawLine(fastcat(0, stack(1, 2, 3)), 10),
`|01|01|01|01
|.2|.2|.2|.2
|.3|.3|.3|.3`);
expect(drawLine(fastcat(0, 1, stack(2, 3, 4)), 10)).toEqual(`|012|012|012
|.3|.3|.3|.3`,
);
assert.equal(
drawLine(fastcat(0, 1, stack(2, 3, 4)), 10),
`|012|012|012
|..3|..3|..3
|..4|..4|..4`);
|..4|..4|..4`,
);
});
it('supports unequal cycle lengths', () => {
expect(drawLine(slowcat(0, [1, 2]), 10)).toEqual(`|0|12|0|12`);
assert.equal(drawLine(slowcat(0, [1, 2]), 10), `|0|12|0|12`);
});
});
+2 -2
View File
@@ -5,11 +5,11 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import Fraction, { gcd } from '../fraction.mjs';
import { describe, it, expect } from 'vitest';
import { strict as assert } from 'assert';
describe('gcd', () => {
it('should work', () => {
const F = Fraction._original;
expect(gcd(F(1 / 6), F(1 / 4)).toFraction()).toEqual('1/12');
assert.equal(gcd(F(1 / 6), F(1 / 4)).toFraction(), '1/12');
});
});
+290 -246
View File
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js';
import { describe, it, expect } from 'vitest';
import { deepStrictEqual, strict as assert } from 'assert';
import {
TimeSpan,
@@ -41,11 +41,15 @@ import {
tri2,
id,
ply,
rev,
rev
} from '../index.mjs';
import { steady } from '../signal.mjs';
//import { Time } from 'tone';
import pkg from 'tone';
const { Time } = pkg;
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);
@@ -54,35 +58,35 @@ const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
return assert.deepStrictEqual(a._sortHapsByPart().firstCycle(), b._sortHapsByPart().firstCycle());
};
describe('TimeSpan', () => {
describe('equals()', () => {
it('Should be equal to the same value', () => {
expect(new TimeSpan(0, 4).equals(new TimeSpan(0, 4))).toBe(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', () => {
it('Should split two cycles into two', () => {
expect(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length).toBe(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', () => {
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', () => {
expect(a.intersection_e(b).equals(c)).toBe(true);
it('Should create an intersection', function () {
assert.equal(a.intersection_e(b).equals(c), true);
});
});
});
describe('Hap', () => {
describe('hasOnset()', () => {
it('True if part includes onset from whole', () => {
expect(new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), 'thing').hasOnset()).toBe(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');
@@ -90,19 +94,19 @@ describe('Hap', () => {
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', () => {
it('True if two haps have the same whole and part', () => {
expect(a.spanEquals(b)).toBe(true);
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", () => {
expect(a.spanEquals(c)).toBe(false);
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', () => {
expect(d.spanEquals(e)).toBe(true);
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', () => {
it('Can increment some state', function () {
const stateful_value = (state) => {
const newValue = state['incrementme'];
// TODO Does the state *need* duplicating here?
@@ -114,8 +118,8 @@ describe('Hap', () => {
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);
expect(ev3).toStrictEqual(new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
expect(state3).toStrictEqual({ incrementme: 12 });
assert.deepStrictEqual(ev3, new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
assert.deepStrictEqual(state3, { incrementme: 12 });
});
});
describe('wholeOrPart()', () => {
@@ -124,40 +128,42 @@ describe('Hap', () => {
const continuousHap = new Hap(undefined, ts1, 'hello');
const discreteHap = new Hap(ts1, ts0_5, 'hello');
it('Can pick a whole', () => {
expect(discreteHap.wholeOrPart()).toStrictEqual(ts1);
assert.deepStrictEqual(discreteHap.wholeOrPart(), ts1);
});
it('Can pick a part', () => {
expect(continuousHap.wholeOrPart()).toStrictEqual(ts1);
assert.deepStrictEqual(continuousHap.wholeOrPart(), ts1);
});
});
});
describe('Pattern', () => {
describe('pure', () => {
it('Can make a pattern', () => {
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
describe('Pattern', function () {
describe('pure', function () {
it('Can make a pattern', function () {
assert.equal(pure('hello').query(st(0.5, 2.5)).length, 3);
});
});
describe('fmap()', () => {
it('Can add things', () => {
expect(
describe('fmap()', function () {
it('Can add things', function () {
assert.equal(
pure(3)
.fmap((x) => x + 4)
.firstCycle()[0].value,
).toBe(7);
7,
);
});
});
describe('add()', () => {
it('can structure In()', () => {
expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
expect(pure(3).addIn(pure(4)).query(st(0, 1))[0].value).toBe(7);
describe('add()', function () {
it('can structure In()', function () {
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
assert.equal(pure(3).addIn(pure(4)).query(st(0, 1))[0].value, 7);
});
it('can structure Out()', () => {
sameFirst(sequence(1, 2).addOut(4), sequence(5, 6).struct(true));
});
it('can Mix() structure', () => {
expect(sequence(1, 2).addMix(silence, 5, silence).firstCycle()).toStrictEqual([
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
assert.deepStrictEqual(sequence(1, 2).addMix(silence, 5, silence).firstCycle(), [
hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
]);
});
it('can Trig() structure', () => {
@@ -191,18 +197,18 @@ describe('Pattern', () => {
);
});
});
describe('keep()', () => {
it('can structure In()', () => {
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
expect(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value).toBe(3);
describe('keep()', function () {
it('can structure In()', function () {
assert.equal(pure(3).keep(pure(4)).query(st(0, 1))[0].value, 3);
assert.equal(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value, 3);
});
it('can structure Out()', () => {
sameFirst(sequence(1, 2).keepOut(4), sequence(1, 2).struct(true));
});
it('can Mix() structure', () => {
expect(sequence(1, 2).keepMix(silence, 5, silence).firstCycle()).toStrictEqual([
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
assert.deepStrictEqual(sequence(1, 2).keepMix(silence, 5, silence).firstCycle(), [
hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
@@ -233,8 +239,8 @@ describe('Pattern', () => {
sameFirst(sequence(1, [2, 3]).keepSqueezeOut(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
});
});
describe('keepif()', () => {
it('can structure In()', () => {
describe('keepif()', function () {
it('can structure In()', function () {
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
sameFirst(sequence(3, 4).keepifIn(true, false), sequence(3, silence));
});
@@ -242,9 +248,9 @@ describe('Pattern', () => {
sameFirst(pure(1).keepifOut(true, false), sequence(1, silence));
});
it('can Mix() structure', () => {
expect(sequence(1, 2).keepifMix(false, true, false).firstCycle()).toStrictEqual([
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
assert.deepStrictEqual(sequence(1, 2).keepifMix(false, true, false).firstCycle(), [
hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
]);
});
it('can Trig() structure', () => {
@@ -275,39 +281,35 @@ describe('Pattern', () => {
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
});
});
describe('sub()', () => {
it('Can subtract things', () => {
expect(pure(3).sub(pure(4)).query(st(0, 1))[0].value).toBe(-1);
describe('sub()', function () {
it('Can subtract things', function () {
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
});
});
describe('mul()', () => {
it('Can multiply things', () => {
expect(pure(3).mul(pure(2)).firstCycle()[0].value).toBe(6);
describe('mul()', function () {
it('Can multiply things', function () {
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6);
});
});
describe('div()', () => {
it('Can divide things', () => {
expect(pure(3).div(pure(2)).firstCycle()[0].value).toBe(1.5);
describe('div()', function () {
it('Can divide things', function () {
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
});
});
describe('set()', () => {
it('Can set things in objects', () => {
expect(
describe('set()', function () {
it('Can set things in objects', function () {
assert.deepStrictEqual(
pure({ a: 4, b: 6 })
.set(pure({ c: 7 }))
.firstCycle()[0].value,
).toStrictEqual({
a: 4,
b: 6,
c: 7,
});
{ 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', () => {
it('Can set things with plain values', function () {
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
});
describe('setOut()', () => {
@@ -340,84 +342,94 @@ describe('Pattern', () => {
});
});
});
describe('stack()', () => {
it('Can stack things', () => {
expect(
describe('stack()', function () {
it('Can stack things', function () {
assert.deepStrictEqual(
stack(pure('a'), pure('b'), pure('c'))
.firstCycle()
.map((h) => h.value),
).toStrictEqual(['a', 'b', 'c']);
['a', 'b', 'c'],
);
});
it('Can stack subpatterns', () => {
it('Can stack subpatterns', function () {
sameFirst(stack('a', ['b', 'c']), stack('a', sequence('b', 'c')));
});
});
describe('_fast()', () => {
it('Makes things faster', () => {
expect(pure('a')._fast(2).firstCycle().length).toBe(2);
describe('_fast()', function () {
it('Makes things faster', function () {
assert.equal(pure('a')._fast(2).firstCycle().length, 2);
});
});
describe('_fastGap()', () => {
it('Makes things faster, with a gap', () => {
expect(sequence('a', 'b', 'c')._fastGap(2).firstCycle()).toStrictEqual(
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(),
);
expect(sequence('a', 'b', 'c')._fastGap(3).firstCycle()).toStrictEqual(
assert.deepStrictEqual(
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', () => {
expect(sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle()).toStrictEqual(
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(),
);
});
});
describe('_compressSpan()', () => {
it('Can squash cycles of a pattern into a given timespan', () => {
expect(pure('a')._compressSpan(ts(0.25, 0.5)).firstCycle()).toStrictEqual(
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(),
);
});
});
describe('fast()', () => {
it('Makes things faster', () => {
expect(pure('a').fast(2).firstCycle().length).toBe(2);
describe('fast()', function () {
it('Makes things faster', function () {
assert.equal(pure('a').fast(2).firstCycle().length, 2);
});
it('Makes things faster, with a pattern of factors', () => {
expect(pure('a').fast(sequence(1, 4)).firstCycle().length).toBe(3);
expect(pure('a').fast(sequence(1, 4)).firstCycle()).toStrictEqual(
it('Makes things faster, with a pattern of factors', function () {
assert.equal(pure('a').fast(sequence(1, 4)).firstCycle().length, 3);
// .fast(sequence(1,silence) is a quick hack to cut a hap in two..
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', () => {
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
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', () => {
expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
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', () => {
expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
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(),
);
});
});
describe('_slow()', () => {
it('Makes things slower', () => {
expect(pure('a')._slow(2).firstCycle()[0]).toStrictEqual(hap(ts(0, 2), ts(0, 1), 'a'));
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'),
);
const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
expect(pat.query(st(0, 1))[1]).toStrictEqual(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
// )
/* assert.deepStrictEqual(
pat.query(ts(1,2))[1], undefined
) */
// expecting [c3 eb3] [c3 ~]
// what happens [c3 eb3] [c3 eb3]
// notable examples:
@@ -427,125 +439,134 @@ describe('Pattern', () => {
});
describe('inside', () => {
it('can rev inside a cycle', () => {
sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev), sequence('b', 'a', 'd', 'c'));
sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev),
sequence('b', 'a', 'd', 'c')
);
});
});
describe('outside', () => {
it('can rev outside a cycle', () => {
sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev), sequence('d', 'c'));
sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev),
sequence('d', 'c')
);
});
});
describe('_filterValues()', () => {
it('Filters true', () => {
expect(
describe('_filterValues()', function () {
it('Filters true', function () {
assert.equal(
pure(true)
._filterValues((x) => x)
.firstCycle().length,
).toBe(1);
1,
);
});
});
describe('when()', () => {
it('Always faster', () => {
expect(
describe('when()', function () {
it('Always faster', function () {
assert.equal(
pure('a')
.when(pure(true), (x) => x._fast(2))
.firstCycle().length,
).toBe(2);
2,
);
});
it('Never faster', () => {
expect(
it('Never faster', function () {
assert.equal(
pure('a')
.when(pure(false), (x) => x._fast(2))
.firstCycle().length,
).toBe(1);
1,
);
});
it('Can alternate', () => {
expect(
pure(10)
.when(slowcat(true, false), (x) => x.add(3))
.fast(4)
._sortHapsByPart()
.firstCycle(),
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
it('Can alternate', function () {
assert.deepStrictEqual(
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortHapsByPart().firstCycle(),
fastcat(13, 10, 13, 10).firstCycle(),
);
});
});
describe('fastcat()', () => {
it('Can concatenate two things', () => {
expect(
describe('fastcat()', function () {
it('Can concatenate two things', function () {
assert.deepStrictEqual(
fastcat(pure('a'), pure('b'))
.firstCycle()
.map((x) => x.value),
).toStrictEqual(['a', 'b']);
['a', 'b'],
);
});
});
describe('fastcat()', () => {
it('Can go into negative time', () => {
describe('fastcat()', function () {
it('Can go into negative time', function () {
sameFirst(fastcat('a', 'b', 'c').late(1000000), fastcat('a', 'b', 'c'));
});
});
describe('slowcat()', () => {
it('Can concatenate things slowly', () => {
expect(
describe('slowcat()', function () {
it('Can concatenate things slowly', function () {
assert.deepStrictEqual(
slowcat('a', 'b')
.firstCycle()
.map((x) => x.value),
).toStrictEqual(['a']);
expect(
['a'],
);
assert.deepStrictEqual(
slowcat('a', 'b')
._early(1)
.firstCycle()
.map((x) => x.value),
).toStrictEqual(['b']);
expect(
['b'],
);
assert.deepStrictEqual(
slowcat('a', slowcat('b', 'c'))
._early(1)
.firstCycle()
.map((x) => x.value),
).toStrictEqual(['b']);
expect(
['b'],
);
assert.deepStrictEqual(
slowcat('a', slowcat('b', 'c'))
._early(3)
.firstCycle()
.map((x) => x.value),
).toStrictEqual(['c']);
['c'],
);
});
it('Can cat subpatterns', () => {
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
});
});
describe('rev()', () => {
it('Can reverse things', () => {
expect(
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),
).toStrictEqual(['c', 'b', 'a']);
['c', 'b', 'a'],
);
});
});
describe('sequence()', () => {
it('Can work like fastcat', () => {
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
assert.deepStrictEqual(sequence(1, 2, 3).firstCycle(), fastcat(1, 2, 3).firstCycle());
});
});
describe('polyrhythm()', () => {
it('Can layer up cycles', () => {
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
assert.deepStrictEqual(
polyrhythm(['a', 'b'], ['c']).firstCycle(),
stack(fastcat(pure('a'), pure('b')), pure('c')).firstCycle(),
);
});
});
describe('polymeter()', () => {
it('Can layer up cycles, stepwise', () => {
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
assert.deepStrictEqual(
polymeterSteps(3, ['d', 'e']).firstCycle(),
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
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(),
);
});
@@ -553,24 +574,29 @@ describe('Pattern', () => {
describe('every()', () => {
it('Can apply a function every 3rd time', () => {
expect(
assert.deepStrictEqual(
pure('a')
.every(3, (x) => x._fast(2))
._fast(3)
.firstCycle(),
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
});
it('works with currying', () => {
expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
);
expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
});
it('works with currying', () => {
assert.deepStrictEqual(
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(),
);
expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
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(),
);
expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
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(),
);
});
@@ -580,91 +606,98 @@ describe('Pattern', () => {
sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
});
});
describe('timeCat()', () => {
it('Can concatenate patterns with different relative durations', () => {
expect(sequence('a', ['a', 'a']).firstCycle()).toStrictEqual(
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()', () => {
it('Can restructure a discrete pattern', () => {
expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
describe('struct()', 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'),
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
hap(ts(twothirds, 1), ts(twothirds, 1), 'b'),
]);
expect(
assert.deepStrictEqual(
pure('a')
.struct(sequence(true, [true, false], true))
.firstCycle(),
).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
expect(
sequence('a', ['a', silence], 'a').firstCycle(),
);
assert.deepStrictEqual(
pure('a')
.struct(sequence(true, [true, false], true).invert())
.firstCycle(),
).toStrictEqual(sequence(silence, [silence, 'a'], silence).firstCycle());
expect(
sequence(silence, [silence, 'a'], silence).firstCycle(),
);
assert.deepStrictEqual(
pure('a')
.struct(sequence(true, [true, silence], true))
.firstCycle(),
).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
sequence('a', ['a', silence], 'a').firstCycle(),
);
});
it('Can structure a continuous pattern', () => {
expect(steady('a').struct(true, [true, true]).firstCycle()).toStrictEqual(sequence('a', ['a', 'a']).firstCycle());
assert.deepStrictEqual(
steady('a').struct(true, [true, true]).firstCycle(),
sequence('a', ['a', 'a']).firstCycle(),
);
});
});
describe('mask()', () => {
it('Can fragment a pattern', () => {
expect(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle()).toStrictEqual([
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', () => {
expect(sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle()).toStrictEqual(
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(),
);
expect(sequence('a').mask(sequence(true, false)).firstCycle()).toStrictEqual([hap(ts(0, 1), ts(0, 0.5), 'a')]);
assert.deepStrictEqual(sequence('a').mask(sequence(true, false)).firstCycle(), [hap(ts(0, 1), ts(0, 0.5), 'a')]);
});
});
describe('invert()', () => {
it('Can invert a binary pattern', () => {
expect(sequence(true, false, [true, false]).invert().firstCycle()).toStrictEqual(
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()', () => {
it('Can make saw/saw2', () => {
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
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(),
);
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(
assert.deepStrictEqual(
saw2.struct(true, true, true, true).firstCycle(),
sequence(-3 / 4, -1 / 4, 1 / 4, 3 / 4).firstCycle(),
);
});
it('Can make isaw/isaw2', () => {
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(
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(),
);
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(
assert.deepStrictEqual(
isaw2.struct(true, true, true, true).firstCycle(),
sequence(3 / 4, 1 / 4, -1 / 4, -3 / 4).firstCycle(),
);
});
});
describe('_setContext()', () => {
it('Can set the hap context', () => {
expect(
assert.deepStrictEqual(
pure('a')
._setContext([
[
@@ -673,19 +706,20 @@ describe('Pattern', () => {
],
])
.firstCycle(true),
).toStrictEqual([
hap(ts(0, 1), ts(0, 1), 'a', [
[
[0, 1],
[1, 2],
],
]),
]);
[
hap(ts(0, 1), ts(0, 1), 'a', [
[
[0, 1],
[1, 2],
],
]),
],
);
});
});
describe('_withContext()', () => {
it('Can update the hap context', () => {
expect(
assert.deepStrictEqual(
pure('a')
._setContext([
[
@@ -701,49 +735,52 @@ describe('Pattern', () => {
],
])
.firstCycle(true),
).toStrictEqual([
hap(ts(0, 1), ts(0, 1), 'a', [
[
[0, 1],
[1, 2],
],
[
[3, 4],
[3, 4],
],
]),
]);
[
hap(ts(0, 1), ts(0, 1), 'a', [
[
[0, 1],
[1, 2],
],
[
[3, 4],
[3, 4],
],
]),
],
);
});
});
describe('apply', () => {
it('Can apply a function', () => {
expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(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', () => {
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual(
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', () => {
expect(
assert.deepStrictEqual(
sequence(1, 2, 3)
.layer(fast(2), (pat) => pat.add(3, 4))
.firstCycle(),
).toStrictEqual(stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle());
stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle(),
);
});
});
describe('early', () => {
it('Can shift a hap earlier', () => {
expect(pure(30)._late(0.25).query(st(1, 2))).toStrictEqual([
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 a hap earlier, into negative time', () => {
expect(pure(30)._late(0.25).query(st(0, 1))).toStrictEqual([
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),
]);
@@ -751,14 +788,16 @@ describe('Pattern', () => {
});
describe('off', () => {
it('Can offset a transformed pattern from the original', () => {
expect(pure(30).off(0.25, add(2)).firstCycle()).toStrictEqual(
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', () => {
expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
assert.deepStrictEqual(
pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
._sortHapsByPart()
.firstCycle(),
@@ -767,7 +806,8 @@ describe('Pattern', () => {
});
describe('juxBy', () => {
it('Can juxtapose by half', () => {
expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
assert.deepStrictEqual(
pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
._sortHapsByPart()
.firstCycle(),
@@ -776,12 +816,11 @@ describe('Pattern', () => {
});
describe('_squeezeJoin', () => {
it('Can squeeze', () => {
expect(
assert.deepStrictEqual(
sequence('a', ['a', 'a'])
.fmap((a) => fastcat('b', 'c'))
._squeezeJoin()
.firstCycle(),
).toStrictEqual(
sequence(
['b', 'c'],
[
@@ -794,13 +833,14 @@ describe('Pattern', () => {
});
describe('ply', () => {
it('Can ply(3)', () => {
expect(sequence('a', ['b', 'c']).ply(3).firstCycle()).toStrictEqual(
assert.deepStrictEqual(
sequence('a', ['b', 'c']).ply(3).firstCycle(),
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
);
});
it('Doesnt drop haps in the 9th cycle', () => {
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
});
});
describe('striate', () => {
@@ -813,7 +853,8 @@ describe('Pattern', () => {
});
describe('chop', () => {
it('Can _chop(2)', () => {
expect(sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle()).toStrictEqual(
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 },
@@ -823,7 +864,8 @@ describe('Pattern', () => {
);
});
it('Can chop(2,3)', () => {
expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
assert.deepStrictEqual(
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle(),
sequence(
[
{ sound: 'a', begin: 0, end: 0.5 },
@@ -842,14 +884,16 @@ describe('Pattern', () => {
});
describe('range2', () => {
it('Can change the range of a bipolar pattern', () => {
expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
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', () => {
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
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(),
);
});
+59 -59
View File
@@ -4,117 +4,117 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { strict as assert } from 'assert';
import { pure } from '../pattern.mjs';
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
import { describe, it, expect } from 'vitest';
describe('isNote', () => {
it('should recognize notes without accidentals', () => {
'C3 D3 E3 F3 G3 A3 B3 C4 D5 c5 d5 e5'.split(' ').forEach((note) => {
expect(isNote(note)).toBe(true);
assert.equal(isNote(note), true);
});
});
it('should recognize notes with accidentals', () => {
'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => {
expect(isNote(note)).toBe(true);
assert.equal(isNote(note), true);
});
});
it('should not recognize invalid notes', () => {
expect(isNote('H5')).toBe(false);
expect(isNote('C')).toBe(false);
expect(isNote('X')).toBe(false);
expect(isNote(1)).toBe(false);
assert.equal(isNote('H5'), false);
assert.equal(isNote('C'), false);
assert.equal(isNote('X'), false);
assert.equal(isNote(1), false);
});
});
describe('isNote', () => {
it('should tokenize notes without accidentals', () => {
expect(tokenizeNote('C3')).toStrictEqual(['C', '', 3]);
expect(tokenizeNote('D3')).toStrictEqual(['D', '', 3]);
expect(tokenizeNote('E3')).toStrictEqual(['E', '', 3]);
expect(tokenizeNote('F3')).toStrictEqual(['F', '', 3]);
expect(tokenizeNote('G3')).toStrictEqual(['G', '', 3]);
expect(tokenizeNote('A3')).toStrictEqual(['A', '', 3]);
expect(tokenizeNote('B3')).toStrictEqual(['B', '', 3]);
expect(tokenizeNote('C4')).toStrictEqual(['C', '', 4]);
expect(tokenizeNote('D5')).toStrictEqual(['D', '', 5]);
assert.deepStrictEqual(tokenizeNote('C3'), ['C', '', 3]);
assert.deepStrictEqual(tokenizeNote('D3'), ['D', '', 3]);
assert.deepStrictEqual(tokenizeNote('E3'), ['E', '', 3]);
assert.deepStrictEqual(tokenizeNote('F3'), ['F', '', 3]);
assert.deepStrictEqual(tokenizeNote('G3'), ['G', '', 3]);
assert.deepStrictEqual(tokenizeNote('A3'), ['A', '', 3]);
assert.deepStrictEqual(tokenizeNote('B3'), ['B', '', 3]);
assert.deepStrictEqual(tokenizeNote('C4'), ['C', '', 4]);
assert.deepStrictEqual(tokenizeNote('D5'), ['D', '', 5]);
});
it('should tokenize notes with accidentals', () => {
expect(tokenizeNote('C#3')).toStrictEqual(['C', '#', 3]);
expect(tokenizeNote('D##3')).toStrictEqual(['D', '##', 3]);
expect(tokenizeNote('Eb3')).toStrictEqual(['E', 'b', 3]);
expect(tokenizeNote('Fbb3')).toStrictEqual(['F', 'bb', 3]);
expect(tokenizeNote('Bb5')).toStrictEqual(['B', 'b', 5]);
assert.deepStrictEqual(tokenizeNote('C#3'), ['C', '#', 3]);
assert.deepStrictEqual(tokenizeNote('D##3'), ['D', '##', 3]);
assert.deepStrictEqual(tokenizeNote('Eb3'), ['E', 'b', 3]);
assert.deepStrictEqual(tokenizeNote('Fbb3'), ['F', 'bb', 3]);
assert.deepStrictEqual(tokenizeNote('Bb5'), ['B', 'b', 5]);
});
it('should tokenize notes without octave', () => {
expect(tokenizeNote('C')).toStrictEqual(['C', '', undefined]);
expect(tokenizeNote('C#')).toStrictEqual(['C', '#', undefined]);
expect(tokenizeNote('Bb')).toStrictEqual(['B', 'b', undefined]);
expect(tokenizeNote('Bbb')).toStrictEqual(['B', 'bb', undefined]);
assert.deepStrictEqual(tokenizeNote('C'), ['C', '', undefined]);
assert.deepStrictEqual(tokenizeNote('C#'), ['C', '#', undefined]);
assert.deepStrictEqual(tokenizeNote('Bb'), ['B', 'b', undefined]);
assert.deepStrictEqual(tokenizeNote('Bbb'), ['B', 'bb', undefined]);
});
it('should not tokenize invalid notes', () => {
expect(tokenizeNote('X')).toStrictEqual([]);
expect(tokenizeNote('asfasf')).toStrictEqual([]);
expect(tokenizeNote(123)).toStrictEqual([]);
assert.deepStrictEqual(tokenizeNote('X'), []);
assert.deepStrictEqual(tokenizeNote('asfasf'), []);
assert.deepStrictEqual(tokenizeNote(123), []);
});
});
describe('toMidi', () => {
it('should turn notes into midi', () => {
expect(toMidi('A4')).toEqual(69);
expect(toMidi('C4')).toEqual(60);
expect(toMidi('Db4')).toEqual(61);
expect(toMidi('C3')).toEqual(48);
expect(toMidi('Cb3')).toEqual(47);
expect(toMidi('Cbb3')).toEqual(46);
expect(toMidi('C#3')).toEqual(49);
expect(toMidi('C#3')).toEqual(49);
expect(toMidi('C##3')).toEqual(50);
assert.equal(toMidi('A4'), 69);
assert.equal(toMidi('C4'), 60);
assert.equal(toMidi('Db4'), 61);
assert.equal(toMidi('C3'), 48);
assert.equal(toMidi('Cb3'), 47);
assert.equal(toMidi('Cbb3'), 46);
assert.equal(toMidi('C#3'), 49);
assert.equal(toMidi('C#3'), 49);
assert.equal(toMidi('C##3'), 50);
});
});
describe('fromMidi', () => {
it('should turn midi into frequency', () => {
expect(fromMidi(69)).toEqual(440);
expect(fromMidi(57)).toEqual(220);
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);
expect(getFrequency(happify('a4'))).toEqual(440);
expect(getFrequency(happify('a3'))).toEqual(220);
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440); // TODO: migrate when values are objects..
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
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', () => {
expect(mod(0, 3)).toEqual(0);
expect(mod(1, 3)).toEqual(1);
expect(mod(2, 3)).toEqual(2);
expect(mod(3, 3)).toEqual(0);
expect(mod(4, 3)).toEqual(1);
expect(mod(4, 2)).toEqual(0);
assert.equal(mod(0, 3), 0);
assert.equal(mod(1, 3), 1);
assert.equal(mod(2, 3), 2);
assert.equal(mod(3, 3), 0);
assert.equal(mod(4, 3), 1);
assert.equal(mod(4, 2), 0);
});
it('should work with negative numbers', () => {
expect(mod(-1, 3)).toEqual(2);
expect(mod(-2, 3)).toEqual(1);
expect(mod(-3, 3)).toEqual(0);
expect(mod(-4, 3)).toEqual(2);
expect(mod(-5, 3)).toEqual(1);
expect(mod(-3, 2)).toEqual(1);
assert.equal(mod(-1, 3), 2);
assert.equal(mod(-2, 3), 1);
assert.equal(mod(-3, 3), 0);
assert.equal(mod(-4, 3), 2);
assert.equal(mod(-5, 3), 1);
assert.equal(mod(-3, 2), 1);
});
});
describe('compose', () => {
const add1 = (a) => a + 1;
it('should compose', () => {
expect(compose(add1, add1)(0)).toEqual(2);
expect(compose(add1)(0)).toEqual(1);
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', () => {
expect(compose(addS('a'), addS('b'))('')).toEqual('ab');
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
assert.equal(compose(addS('a'), addS('b'))(''), 'ab');
assert.equal(compose(addS('a'), addS('b'))('x'), 'xab');
});
});
+8 -8
View File
@@ -4,21 +4,21 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it, expect } from 'vitest';
import { strict as assert } from 'assert';
import { map, valued, mul } from '../value.mjs';
describe('Value', () => {
it('unionWith', () => {
const { value } = valued({ freq: 2000, distortion: 1.2 }).unionWith({ distortion: 2 }, mul);
expect(value).toStrictEqual({ freq: 2000, distortion: 2.4 });
assert.deepStrictEqual(value, { freq: 2000, distortion: 2.4 });
});
it('experiments', () => {
expect(map(mul(5), valued(3)).value).toEqual(15);
expect(map(mul(null), valued(3)).value).toEqual(0);
expect(map(mul(3), valued(null)).value).toEqual(null);
expect(valued(3).map(mul).ap(3).value).toEqual(9);
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
expect(valued(3).mul(3).value).toEqual(9);
assert.equal(map(mul(5), valued(3)).value, 15);
assert.equal(map(mul(null), valued(3)).value, 0);
assert.equal(map(mul(3), valued(null)).value, null);
assert.equal(valued(3).map(mul).ap(3).value, 9);
assert.equal(valued(mul).ap(3).ap(3).value, 9);
assert.equal(valued(3).mul(3).value, 9);
});
});
+2 -20
View File
@@ -31,35 +31,17 @@ export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
}
return fromMidi(toMidi(noteOrMidi));
};
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
return pc + oct;
};
// 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 % m) + m) % m;
export const getPlayableNoteValue = (hap) => {
let { value: note, context } = hap;
if (typeof note === 'object' && !Array.isArray(note)) {
note = note.note || note.n || note.value;
}
// if value is number => interpret as midi number as long as its not marked as frequency
if (typeof note === 'number' && context.type !== 'frequency') {
note = fromMidi(hap.value);
} else if (typeof note === 'number' && context.type === 'frequency') {
note = hap.value; // legacy workaround.. will be removed in the future
} else if (typeof note !== 'string' || !isNote(note)) {
throw new Error('not a note: ' + JSON.stringify(note));
} else if (typeof note === 'string' && !isNote(note)) {
throw new Error('not a note: ' + note);
}
return note;
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.2.0",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.2.0",
"version": "0.1.3",
"description": "Code evaluator for strudel",
"main": "index.mjs",
"type": "module",
@@ -8,7 +8,7 @@
"test": "test"
},
"scripts": {
"test": "vitest run"
"test": "mocha --colors"
},
"repository": {
"type": "git",
@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/core": "^0.1.2",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
+2 -14
View File
@@ -34,7 +34,7 @@ export const wrappedAsync = true;
export default (_code) => {
const { code, addReturn } = wrapAsync(_code);
const ast = parseScriptWithLocation(disguiseImports(code));
const ast = parseScriptWithLocation(code);
const artificialNodes = [];
const parents = [];
const shifted = replace(ast.tree, {
@@ -128,22 +128,10 @@ export default (_code) => {
if (wrappedAsync) {
addReturn(shifted);
}
const generated = undisguiseImports(codegen(shifted));
const generated = codegen(shifted);
return generated;
};
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
// there shouldn't be any side-effects from this as this change does not affect
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
function disguiseImports(code) {
return code.replaceAll('import', '_mport'); // Must be the same length!
}
// Rename the renamed import statements back to "import"
function undisguiseImports(code) {
return code.replaceAll('_mport', 'import');
}
function wrapAsync(code) {
// wrap code in async to make await work on top level => this will create 1 line offset to locations
// this is why line offset is -1 in getLocationObject calls below
+17 -15
View File
@@ -4,29 +4,31 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { expect, describe, it } from 'vitest';
import { evaluate, evalScope } from '../evaluate.mjs';
import { strict as assert } from 'assert';
import { evaluate, extend } from '../evaluate.mjs';
import { mini } from '@strudel.cycles/mini';
import * as strudel from '@strudel.cycles/core';
const { fastcat } = strudel;
describe('evaluate', async () => {
await evalScope({ mini }, strudel);
extend({ mini }, strudel);
// TODO: test evalScope
describe('evaluate', () => {
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
it('Should evaluate strudel functions', async () => {
expect(await ev('pure("c3")')).toEqual(['c3']);
expect(await ev('cat("c3")')).toEqual(['c3']);
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
assert.deepStrictEqual(await ev("pure('c3')"), ['c3']);
assert.deepStrictEqual(await ev('cat(c3)'), ['c3']);
assert.deepStrictEqual(await ev('fastcat(c3, d3)'), ['c3', 'd3']);
assert.deepStrictEqual(await ev('slowcat(c3, d3)'), ['c3']);
});
it('Scope should be extendable', async () => {
await evalScope({ myFunction: (...x) => fastcat(...x) });
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
it('Should be extendable', async () => {
extend({ myFunction: (...x) => fastcat(...x) });
assert.deepStrictEqual(await ev('myFunction(c3, d3)'), ['c3', 'd3']);
});
it('Should evaluate simple double quoted mini notation', async () => {
expect(await ev('"c3"')).toEqual(['c3']);
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
assert.deepStrictEqual(await ev('"c3"'), ['c3']);
assert.deepStrictEqual(await ev('"c3 d3"'), ['c3', 'd3']);
assert.deepStrictEqual(await ev('"<c3 d3>"'), ['c3']);
});
});
+2 -7
View File
@@ -4,16 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, it, expect } from 'vitest';
import { strict as assert } from 'assert';
import shapeshifter from '../shapeshifter.mjs';
describe('shapeshifter', () => {
it('Should shift simple double quote string', () => {
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
});
it('Should handle dynamic imports', () => {
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
'(async()=>{const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])})()',
);
assert.equal(shapeshifter('"c3"'), '(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.2.0",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.2.0",
"version": "0.1.3",
"description": "Midi API for strudel",
"main": "index.mjs",
"repository": {
@@ -21,7 +21,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.2.0",
"@strudel.cycles/tone": "^0.1.3",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}
File diff suppressed because it is too large Load Diff
+7 -20
View File
@@ -8,7 +8,6 @@ This program is free software: you can redistribute it and/or modify it under th
// a sequence = a serie of elements placed between quotes
// a stack = a serie of vertically aligned slices sharing the same overall length
// a slice = a serie of horizontally aligned elements
// a choose = a serie of elements, one of which is chosen at random
{
@@ -82,8 +81,7 @@ DIGIT = [0-9]
// ------------------ delimiters ---------------------------
ws "whitespace" = [ \n\r\t]*
comma = ws "," ws
pipe = ws "|" ws
comma = ws "," ws;
quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
@@ -93,7 +91,7 @@ step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
step = ws chars:step_char+ ws { return chars.join("") }
// define a sub cycle e.g. [1 2, 3 [4]]
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s}
sub_cycle = ws "[" ws s:stack ws "]" ws { return s}
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
timeline = ws "<" ws sc:single_cycle ws ">" ws
@@ -104,7 +102,7 @@ slice = step / sub_cycle / timeline
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate
slice_weight = "@" a:number
{ return { weight: a} }
@@ -124,9 +122,6 @@ slice_fast = "*"a:number
slice_fixed_step = "%"a:number
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
slice_degrade = "?"a:number?
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_modifier = s:slice o:slice_modifier?
{ return new ElementStub(s, o);}
@@ -137,22 +132,14 @@ single_cycle = s:(slice_with_modifier)+
{ return new PatternStub(s,"h"); }
// a stack is a serie of vertically aligned single cycles, separated by a comma
stack_tail = tail:(comma @single_cycle)+
{ return { alignment: 'v', list: tail }; }
// a choose is a serie of pipe-separated single cycles, one of which is chosen
// at random each time through the pattern
choose_tail = tail:(pipe @single_cycle)+
{ return { alignment: 'r', list: tail }; }
// if the stack contains only one element, we don't create a stack but return the
// underlying element
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
stack = c:single_cycle cs:(comma v:single_cycle { return v})*
{ if (cs.length == 0 && c instanceof Object) { return c;} else { cs.unshift(c); return new PatternStub(cs,"v");} }
// a sequence is a quoted stack
sequence = ws quote sc:stack_or_choose quote
{ return sc; }
sequence = ws quote s:stack quote
{ return s; }
// ------------------ operators ---------------------------
-12
View File
@@ -10,13 +10,6 @@ import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
var _seedState = 0;
const randOffset = 0.0002;
function _nextSeed() {
return _seedState++;
}
const applyOptions = (parent) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
@@ -28,8 +21,6 @@ const applyOptions = (parent) => (pat, i) => {
return reify(pat).fast(speed);
case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
case 'degradeBy':
return reify(pat)._degradeByWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), operator.arguments_.amount);
// TODO: case 'fixed-step': "%"
}
console.warn(`operator "${operator.type_}" not implemented`);
@@ -91,9 +82,6 @@ export function patternifyAST(ast) {
if (alignment === 'v') {
return stack(...children);
}
if (alignment === 'r') {
return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 't') {
return slowcat(...children);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.2.0",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
+5 -5
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel.cycles/mini",
"version": "0.2.0",
"version": "0.1.3",
"description": "Mini notation for strudel",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "vitest run",
"test": "mocha --colors",
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs"
},
"repository": {
@@ -26,9 +26,9 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/eval": "^0.2.0",
"@strudel.cycles/tone": "^0.2.0"
"@strudel.cycles/core": "^0.1.2",
"@strudel.cycles/eval": "^0.1.3",
"@strudel.cycles/tone": "^0.1.3"
},
"devDependencies": {
"peggy": "^2.0.1"
+18 -72
View File
@@ -4,104 +4,50 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { strict as assert } from 'assert';
import { mini } from '../mini.mjs';
import '@strudel.cycles/core/euclid.mjs';
import { describe, expect, it } from 'vitest';
describe('mini', () => {
const minV = (v) => mini(v)._firstCycleValues;
const minS = (v) => mini(v)._showFirstCycle;
it('supports single elements', () => {
expect(minV('a')).toEqual(['a']);
assert.deepStrictEqual(minV('a'), ['a']);
});
it('supports rest', () => {
expect(minV('~')).toEqual([]);
assert.deepStrictEqual(minV('~'), []);
});
it('supports cat', () => {
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
assert.deepStrictEqual(minS('a b'), ['a: 0 - 1/2', 'b: 1/2 - 1']);
assert.deepStrictEqual(minS('a b c'), ['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
});
it('supports slowcat', () => {
expect(minV('<a b>')).toEqual(['a']);
assert.deepStrictEqual(minV('<a b>'), ['a']);
});
it('supports division', () => {
expect(minS('a/2')).toEqual(['a: 0 - 2']);
expect(minS('[c3 d3]/2')).toEqual(['c3: 0 - 1']);
assert.deepStrictEqual(minS('a/2'), ['a: 0 - 2']);
assert.deepStrictEqual(minS('[c3 d3]/2'), ['c3: 0 - 1']);
});
it('supports multiplication', () => {
expect(minS('c3*2')).toEqual(['c3: 0 - 1/2', 'c3: 1/2 - 1']);
expect(minV('[c3 d3]*2')).toEqual(['c3', 'd3', 'c3', 'd3']);
assert.deepStrictEqual(minS('c3*2'), ['c3: 0 - 1/2', 'c3: 1/2 - 1']);
assert.deepStrictEqual(minV('[c3 d3]*2'), ['c3', 'd3', 'c3', 'd3']);
});
it('supports brackets', () => {
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
assert.deepStrictEqual(minS('c3 [d3 e3]'), ['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
assert.deepStrictEqual(minS('c3 [d3 [e3 f3]]'), ['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
});
it('supports commas', () => {
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
assert.deepStrictEqual(minS('c3,e3,g3'), ['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
assert.deepStrictEqual(minS('[c3,e3,g3] f3'), ['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
});
it('supports elongation', () => {
expect(minS('a@3 b')).toEqual(['a: 0 - 3/4', 'b: 3/4 - 1']);
expect(minS('a@2 b@3')).toEqual(['a: 0 - 2/5', 'b: 2/5 - 1']);
assert.deepStrictEqual(minS('a@3 b'), ['a: 0 - 3/4', 'b: 3/4 - 1']);
assert.deepStrictEqual(minS('a@2 b@3'), ['a: 0 - 2/5', 'b: 2/5 - 1']);
});
it('supports replication', () => {
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
assert.deepStrictEqual(minS('a!3 b'), ['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
});
it('supports euclidean rhythms', () => {
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
});
it('supports the ? operator', () => {
expect(
mini('a?')
.queryArc(0, 20)
.map((hap) => hap.whole.begin),
).toEqual(
mini('a')
.degradeBy(0.5)
.queryArc(0, 20)
.map((hap) => hap.whole.begin),
);
});
// testing things that involve pseudo-randomness, so there's a probability we could fail by chance.
// these next few tests work with the current PRNG, and are intended to succeed with p > 0.99 even if the PRNG changes
// (as long as the PRNG has a relatively-uniform distribution of values)
it('supports degradeBy with default of 50%', () => {
const haps = mini('a?').queryArc(0, 1000);
expect(459 <= haps.length && haps.length <= 541).toBe(true);
// 'Number of elements did not fall in 99% confidence interval for binomial with p=0.5',
});
it('supports degradeBy with an argument', () => {
const haps = mini('a?0.8').queryArc(0, 1000);
expect(haps.length > 0).toBe(true);
// 'Should have had at least one element when degradeBy was set at 0.8');
expect(haps.length < 230).toBe(true);
// 'Had too many cycles remaining after degradeBy 0.8');
});
it('supports the random choice operator ("|") with nesting', () => {
const numCycles = 900;
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
// Evaluating this distribution with a chi-squared test.
// Note: this just evaluates the overall distribution, not things like correlation/runs of values
const observed = haps.reduce((acc, hap) => {
acc[hap.value] = (acc[hap.value] || 0) + 1;
return acc;
}, {});
const expected = {
a: numCycles / 3,
b: numCycles / 6,
c: numCycles / 6,
d: numCycles / 9,
e: numCycles / 9,
f: numCycles / 9,
};
let chisq = -numCycles;
for (let k in expected) {
chisq += (observed[k] * observed[k]) / expected[k];
}
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
// PRNG, this test should succeed
expect(chisq <= 15.086).toBe(true);
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
assert.deepStrictEqual(minS('a(3, 8)'), ['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
});
});
+2 -2
View File
File diff suppressed because one or more lines are too long
+176 -119
View File
@@ -1,10 +1,9 @@
import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { Decoration, EditorView } from '@codemirror/view';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { tags } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import { HighlightStyle, tags } from '@codemirror/highlight';
import { useInView } from 'react-hook-inview';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
@@ -12,42 +11,139 @@ import { Tone } from '@strudel.cycles/tone';
import { TimeSpan, State } from '@strudel.cycles/core';
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
var strudelTheme = createTheme({
theme: 'dark',
settings: {
background: '#222',
foreground: '#75baff', // whats that?
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
lineHighlight: '#8a91991a',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#676e95',
},
styles: [
{ tag: tags.keyword, color: '#c792ea' },
{ tag: tags.operator, color: '#89ddff' },
{ tag: tags.special(tags.variableName), color: '#eeffff' },
{ tag: tags.typeName, color: '#f07178' },
{ tag: tags.atom, color: '#f78c6c' },
{ tag: tags.number, color: '#ff5370' },
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
{ tag: tags.string, color: '#c3e88d' },
{ tag: tags.special(tags.string), color: '#f07178' },
{ tag: tags.comment, color: '#7d8799' },
{ tag: tags.variableName, color: '#f07178' },
{ tag: tags.tagName, color: '#ff5370' },
{ tag: tags.bracket, color: '#a2a1a4' },
{ tag: tags.meta, color: '#ffcb6b' },
{ tag: tags.attributeName, color: '#c792ea' },
{ tag: tags.propertyName, color: '#c792ea' },
{ tag: tags.className, color: '#decb6b' },
{ tag: tags.invalid, color: '#ffffff' },
],
});
/*
Credits for color palette:
var style = '';
Author: Mattia Astorino (http://github.com/equinusocio)
Website: https://material-theme.site/
*/
const ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
invalid = '#ffffff',
darkBackground = '#21252b',
highlightBackground = 'rgba(0, 0, 0, 0.5)',
// background = '#292d3e',
background = 'transparent',
tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00';
/// The editor theme styles for Material Palenight.
const materialPalenightTheme = EditorView.theme(
{
// done
'&': {
color: '#ffffff',
backgroundColor: background,
fontSize: '15px',
'z-index': 11,
},
// done
'.cm-content': {
caretColor: cursor,
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
backgroundColor: cursor,
width: '3px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
backgroundColor: selection,
},
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
// done, use onedarktheme
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
outline: '1px solid #515a6b',
},
// done
'.cm-gutters': {
background: 'transparent',
color: '#676e95',
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the Material Palenight theme.
const materialPalenightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: '#c792ea' },
{ tag: tags.operator, color: '#89ddff' },
{ tag: tags.special(tags.variableName), color: '#eeffff' },
{ tag: tags.typeName, color: '#f07178' },
{ tag: tags.atom, color: '#f78c6c' },
{ tag: tags.number, color: '#ff5370' },
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
{ tag: tags.string, color: '#c3e88d' },
{ tag: tags.special(tags.string), color: '#f07178' },
{ tag: tags.comment, color: stone },
{ tag: tags.variableName, color: '#f07178' },
{ tag: tags.tagName, color: '#ff5370' },
{ tag: tags.bracket, color: '#a2a1a4' },
{ tag: tags.meta, color: '#ffcb6b' },
{ tag: tags.attributeName, color: '#c792ea' },
{ tag: tags.propertyName, color: '#c792ea' },
{ tag: tags.className, color: '#decb6b' },
{ tag: tags.invalid, color: invalid },
]);
/// Extension to enable the Material Palenight theme (both the editor theme and
/// the highlight style).
// : Extension
const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
const setFlash = StateEffect.define();
const flashField = StateField.define({
@@ -89,57 +185,42 @@ const highlightField = StateField.define({
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const marks = e.value.map(
(hap) => (hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || "#FFCA28";
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
return mark.range(from, to);
})
).flat().filter(Boolean) || [];
highlights = Decoration.set(marks, true);
highlights = Decoration.set(e.value.flatMap((hap) => (hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || "#FFCA28";
let from = tr.newDoc.line(start.line).from + start.column;
let to = tr.newDoc.line(end.line).from + end.column;
const l = tr.newDoc.length;
if (from > l || to > l) {
return;
}
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
return mark.range(from, to);
})).filter(Boolean), true);
}
}
return highlights;
} catch (err) {
return Decoration.set([]);
return highlights;
}
},
provide: (f) => EditorView.decorations.from(f)
});
const extensions = [javascript(), strudelTheme, highlightField, flashField];
function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
const handleOnChange = useCallback(
(value2) => {
onChange?.(value2);
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
onViewChange: onViewChanged,
style: {
display: "flex",
flexDirection: "column",
flex: "1 0 auto"
},
[onChange]
);
const handleOnCreateEditor = useCallback(
(view) => {
onViewChanged?.(view);
},
[onViewChanged]
);
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
},
[onSelectionChange]
);
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(_CodeMirror, {
value,
onChange: handleOnChange,
onCreateEditor: handleOnCreateEditor,
onUpdate: handleOnUpdate,
extensions
onChange,
extensions: [
javascript(),
materialPalenight,
highlightField,
flashField
]
}));
}
@@ -270,8 +351,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
@@ -316,7 +395,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
},
[pattern],
),
onSchedule: useCallback((_events, cycle) => logCycle(_events), []),
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
ready: !!pattern && !!activeCode,
});
@@ -370,8 +449,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
};
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -442,13 +519,15 @@ function useHighlighting({ view, pattern, active }) {
var tailwind = '';
const container = "_container_3i85k_1";
const header = "_header_3i85k_5";
const buttons = "_buttons_3i85k_9";
const button = "_button_3i85k_9";
const buttonDisabled = "_buttonDisabled_3i85k_17";
const error = "_error_3i85k_21";
const body = "_body_3i85k_25";
var style = '';
const container = "_container_10e1g_1";
const header = "_header_10e1g_5";
const buttons = "_buttons_10e1g_9";
const button = "_button_10e1g_9";
const buttonDisabled = "_buttonDisabled_10e1g_17";
const error = "_error_10e1g_21";
const body = "_body_10e1g_25";
var styles = {
container: container,
header: header,
@@ -484,16 +563,12 @@ function Icon({ type }) {
}[type]);
}
function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
onEvent
autolink: false
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01
@@ -505,25 +580,7 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, on
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === "Enter") {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === "Period") {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener("keydown", handleKeyPress, true);
return () => window.removeEventListener("keydown", handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
useHighlighting({ view, pattern, active: cycle.started });
return /* @__PURE__ */ React.createElement("div", {
className: styles.container,
ref
+1 -1
View File
@@ -1 +1 @@
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cm-editor{background-color:transparent!important}._container_10e1g_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(68 76 87 / var(--tw-bg-opacity))}._header_10e1g_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_10e1g_9{display:flex}._button_10e1g_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_10e1g_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_10e1g_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_10e1g_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_10e1g_25{position:relative;overflow:auto}
+3984
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/react",
"version": "0.2.0",
"version": "0.1.4",
"description": "React components for strudel",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
@@ -37,12 +37,11 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/lang-javascript": "^6.0.2",
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/eval": "^0.2.0",
"@strudel.cycles/tone": "^0.2.0",
"@uiw/codemirror-themes": "^4.11.4",
"@uiw/react-codemirror": "^4.11.4",
"@codemirror/lang-javascript": "^0.19.0",
"@strudel.cycles/core": "^0.1.2",
"@strudel.cycles/eval": "^0.1.3",
"@strudel.cycles/tone": "^0.1.3",
"react-codemirror6": "^1.1.0",
"react-hook-inview": "^4.5.0"
},
"peerDependencies": {
+26 -39
View File
@@ -1,11 +1,11 @@
import React from 'react';
import _CodeMirror from '@uiw/react-codemirror';
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
// import { CodeMirrorLite as _CodeMirror } from 'react-codemirror6/dist/lite';
import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import strudelTheme from '../themes/strudel-theme';
import './style.css';
import { useCallback } from 'react';
// import { materialPalenight } from 'codemirror6-themes';
import { materialPalenight } from '../themes/material-palenight';
export const setFlash = StateEffect.define();
const flashField = StateField.define({
@@ -49,9 +49,9 @@ const highlightField = StateField.define({
try {
for (let e of tr.effects) {
if (e.is(setHighlights)) {
const marks =
highlights = Decoration.set(
e.value
.map((hap) =>
.flatMap((hap) =>
(hap.context.locations || []).map(({ start, end }) => {
const color = hap.context.color || '#FFCA28';
let from = tr.newDoc.line(start.line).from + start.column;
@@ -60,56 +60,43 @@ const highlightField = StateField.define({
if (from > l || to > l) {
return; // dont mark outside of range, as it will throw an error
}
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
return mark.range(from, to);
}),
)
.flat()
.filter(Boolean) || [];
highlights = Decoration.set(marks, true);
.filter(Boolean),
true,
);
}
}
return highlights;
} catch (err) {
// console.warn('highlighting error', err);
return Decoration.set([]);
return highlights;
}
},
provide: (f) => EditorView.decorations.from(f),
});
const extensions = [javascript(), strudelTheme, highlightField, flashField];
export default function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
const handleOnChange = useCallback(
(value) => {
onChange?.(value);
},
[onChange],
);
const handleOnCreateEditor = useCallback(
(view) => {
onViewChanged?.(view);
},
[onViewChanged],
);
const handleOnUpdate = useCallback(
(viewUpdate) => {
if (viewUpdate.selectionSet && onSelectionChange) {
onSelectionChange?.(viewUpdate.state.selection);
}
},
[onSelectionChange],
);
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
return (
<>
<_CodeMirror
onViewChange={onViewChanged}
style={{
display: 'flex',
flexDirection: 'column',
flex: '1 0 auto',
}}
value={value}
onChange={handleOnChange}
onCreateEditor={handleOnCreateEditor}
onUpdate={handleOnUpdate}
extensions={extensions}
onChange={onChange}
extensions={[
javascript(),
materialPalenight,
highlightField,
flashField,
// theme, language, ...
]}
/>
</>
);
+10 -36
View File
@@ -1,25 +1,20 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import React, { useState, useMemo, useRef } from 'react';
import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
import CodeMirror6 from './CodeMirror6';
import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
useRepl({
tune,
defaultSynth,
autolink: false,
onEvent,
});
useEffect(() => {
init && evaluateOnly();
}, [tune, init]);
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
tune,
defaultSynth,
autolink: false,
});
const [view, setView] = useState();
const [ref, isVisible] = useInView({
threshold: 0.01,
@@ -31,28 +26,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
}
return isVisible || wasVisible.current;
}, [isVisible, hideOutsideView]);
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
// set active pattern on ctrl+enter
useLayoutEffect(() => {
if (enableKeyboard) {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.code === 'Period') {
cycle.stop();
e.preventDefault();
}
}
};
window.addEventListener('keydown', handleKeyPress, true);
return () => window.removeEventListener('keydown', handleKeyPress, true);
}
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
useHighlighting({ view, pattern, active: cycle.started });
return (
<div className={styles.container} ref={ref}>
<div className={styles.header}>
@@ -66,7 +40,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
</div>
{error && <div className={styles.error}>{error.message}</div>}
</div>
<div className={styles.body}>
<div className={styles.body} >
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
@@ -1,5 +1,5 @@
.container {
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#222222];
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#444C57];
}
.header {
-11
View File
@@ -1,14 +1,3 @@
.cm-editor {
background-color: transparent !important;
height: 100%;
z-index: 11;
font-size: 16px;
}
.cm-theme-light {
width: 100%;
}
.cm-line > * {
background: #00000095;
}
-4
View File
@@ -36,8 +36,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
@@ -138,8 +136,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
};
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
@@ -0,0 +1,136 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, tags as t } from '@codemirror/highlight';
/*
Credits for color palette:
Author: Mattia Astorino (http://github.com/equinusocio)
Website: https://material-theme.site/
*/
const ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
invalid = '#ffffff',
darkBackground = '#21252b',
highlightBackground = 'rgba(0, 0, 0, 0.5)',
// background = '#292d3e',
background = 'transparent',
tooltipBackground = '#353a42',
selection = 'rgba(128, 203, 196, 0.5)',
cursor = '#ffcc00';
/// The editor theme styles for Material Palenight.
export const materialPalenightTheme = EditorView.theme(
{
// done
'&': {
color: '#ffffff',
backgroundColor: background,
fontSize: '15px',
'z-index': 11,
},
// done
'.cm-content': {
caretColor: cursor,
lineHeight: '22px',
},
'.cm-line': {
background: '#2C323699',
},
// done
'&.cm-focused .cm-cursor': {
backgroundColor: cursor,
width: '3px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
backgroundColor: selection,
},
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
// done, use onedarktheme
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
outline: '1px solid #515a6b',
},
// done
'.cm-gutters': {
background: 'transparent',
color: '#676e95',
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the Material Palenight theme.
export const materialPalenightHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
{ tag: t.typeName, color: '#f07178' },
{ tag: t.atom, color: '#f78c6c' },
{ tag: t.number, color: '#ff5370' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
{ tag: t.special(t.string), color: '#f07178' },
{ tag: t.comment, color: stone },
{ tag: t.variableName, color: '#f07178' },
{ tag: t.tagName, color: '#ff5370' },
{ tag: t.bracket, color: '#a2a1a4' },
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the Material Palenight theme (both the editor theme and
/// the highlight style).
// : Extension
export const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
@@ -1,36 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export default createTheme({
theme: 'dark',
settings: {
background: '#222',
foreground: '#75baff', // whats that?
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
lineHighlight: '#8a91991a',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#676e95',
},
styles: [
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
{ tag: t.typeName, color: '#f07178' },
{ tag: t.atom, color: '#f78c6c' },
{ tag: t.number, color: '#ff5370' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
{ tag: t.special(t.string), color: '#f07178' },
{ tag: t.comment, color: '#7d8799' },
{ tag: t.variableName, color: '#f07178' },
{ tag: t.tagName, color: '#ff5370' },
{ tag: t.bracket, color: '#a2a1a4' },
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
],
});
+2 -6
View File
@@ -33,12 +33,8 @@ export default defineConfig({
'@strudel.cycles/serial',
'@strudel.cycles/webaudio',
'@codemirror/view',
'@codemirror/lang-javascript',
'@codemirror/state',
'@codemirror/commands',
'@lezer/highlight',
'@codemirror/language',
'@uiw/codemirror-themes'
'@codemirror/highlight',
'@codemirror/state'
],
},
target: 'esnext',
+3 -3
View File
@@ -1,6 +1,6 @@
import { getFontBufferSource } from './fontloader.mjs';
import * as soundfontList from './list.mjs';
import { startPresetNote } from 'sfumato';
import { loadSoundfont } from './sfumato.mjs';
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
globalThis.getFontBufferSource = getFontBufferSource;
globalThis.soundfontList = soundfontList;
globalThis.soundfontList = soundfontList;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.2.0",
"version": "0.1.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/soundfonts",
"version": "0.2.0",
"version": "0.1.1",
"description": "Soundsfont support for strudel",
"main": "index.mjs",
"type": "module",
@@ -22,10 +22,8 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/webaudio": "^0.2.0",
"sfumato": "^0.1.2",
"soundfont2": "^0.4.0"
"@strudel.cycles/core": "*",
"@strudel.cycles/webaudio": "^0.1.4"
},
"devDependencies": {
"node-fetch": "^3.2.6"
-24
View File
@@ -1,24 +0,0 @@
import { Pattern } from '@strudel.cycles/core';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) {
return this.onTrigger((t, h, ct) => {
const ctx = getAudioContext();
const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length];
const deadline = ctx.currentTime + t - ct;
const args = [ctx, preset, toMidi(note), deadline];
const stop = startPresetNote(...args);
stop(deadline + h.duration);
});
};
const soundfontCache = new Map();
export function loadSoundfont(url) {
if (soundfontCache.get(url)) {
return soundfontCache.get(url);
}
const sf = _loadSoundfont(url);
soundfontCache.set(url, sf);
return sf;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.2.0",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
+3 -3
View File
@@ -1,11 +1,11 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.2.0",
"version": "0.1.3",
"description": "Tonal functions for strudel",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "vitest run"
"test": "mocha --colors"
},
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/core": "^0.1.2",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
}
+2 -4
View File
@@ -4,14 +4,12 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// import { strict as assert } from 'assert';
import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
describe('tonal', () => {
it('Should run tonal functions ', () => {
expect(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues).toEqual(['D3']);
assert.deepStrictEqual(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues, ['D3']);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.2.0",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.2.0",
"version": "0.1.3",
"description": "Tone.js API for strudel",
"main": "index.mjs",
"type": "module",
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/core": "^0.1.2",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
+13 -23
View File
@@ -22,14 +22,10 @@ Pattern.prototype.pianoroll = function ({
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
inactive = '#C9E597',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
@@ -62,14 +58,12 @@ Pattern.prototype.pianoroll = function ({
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
const playheadPosition = scale(-from / timeExtent, ...timeRange);
this.draw(
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
events.filter(inFrame).forEach((event) => {
@@ -77,6 +71,15 @@ Pattern.prototype.pianoroll = function ({
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
@@ -104,19 +107,6 @@ Pattern.prototype.pianoroll = function ({
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
});
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
},
{
from: from - overscan,
+5 -5
View File
@@ -4,15 +4,15 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { strict as assert } from 'assert';
import '../tone.mjs';
import { pure } from '@strudel.cycles/core';
import { describe, it, expect } from 'vitest';
import Tone from 'tone';
describe('tone', () => {
it('Should have working tone function', () => {
// const s = synth().chain(out()); // TODO: mock audio context?
// assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
const s = {};
expect(pure('c3').tone(s)._firstCycleValues).toEqual(['c3']);
const s = synth().chain(out());
assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
assert.deepStrictEqual(pure('c3').tone(s)._firstCycleValues, ['c3']);
});
});
+15 -12
View File
@@ -9,12 +9,14 @@ const stringifyFunction = (func) => '(' + func + ')();';
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
const createWorker = (func) => new Worker(urlifyFunction(func));
// this is just a setInterval with a counter, running in a worker
// this class is basically the tale of two clocks
export class ClockWorker {
worker;
interval = 0.1; // query span
tick = 0;
constructor(callback, interval = this.interval) {
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!
@@ -31,7 +33,6 @@ export class ClockWorker {
if (!interval) {
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
}
postMessage('tick');
timerID = setInterval(() => postMessage('tick'), interval * 1000);
};
self.onmessage = function (e) {
@@ -49,23 +50,25 @@ export class ClockWorker {
});
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(this.tick++, this.interval);
callback(begin, end);
}
};
}
start() {
// console.log('start...');
console.log('start...');
this.audioContext.resume();
this.worker.postMessage('start');
}
stop() {
// console.log('stop...');
console.log('stop...');
this.worker.postMessage('stop');
this.tick = 0;
}
setInterval(interval) {
this.worker.postMessage({ interval });
}
}
+10 -13
View File
@@ -1,11 +1,9 @@
<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="margin-bottom: 4px; font-size: 2em">stop</button><br />
<button id="slower" style="font-size: 2em">-</button>
<button id="faster" style="font-size: 2em">+</button>
<button id="stop" style="font-size: 2em">stop</button>
</div>
<textarea
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
style="font-size: 2em; background: #bce865; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
id="text"
spellcheck="false"
>
@@ -13,9 +11,11 @@ Loading...</textarea
>
<script type="module">
document.body.style = 'margin: 0';
import * as strudel from '@strudel.cycles/core';
import * as util from '@strudel.cycles/core/util.mjs';
// 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;
@@ -33,12 +33,11 @@ Loading...</textarea
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.freq()
.velocity(.5)
.s('sawtooth')
.cutoff(800)
.out()
`;
.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]);
@@ -62,6 +61,4 @@ Loading...</textarea
document.getElementById('start').addEventListener('click', () => scheduler.start());
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.2.0",
"version": "0.1.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.2.0",
"version": "0.1.4",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"type": "module",
@@ -28,6 +28,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0"
"@strudel.cycles/core": "^0.1.2"
}
}
+20 -31
View File
@@ -5,43 +5,35 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import { ClockWorker } from './clockworker.mjs';
import { State, TimeSpan } from '@strudel.cycles/core';
export class Scheduler {
worker;
pattern;
phase;
audioContext;
cps = 1;
constructor({ audioContext, interval = 0.1, onEvent, latency = 0.1 }) {
this.audioContext = audioContext;
this.worker = new ClockWorker((tick, interval) => {
const begin = this.phase;
const end = this.phase + interval * this.cps;
this.phase = end;
const haps = this.pattern.queryArc(begin, end);
haps.forEach((e) => {
if (typeof e.value?.cps === 'number') {
this.setCps(e.value?.cps);
}
if (!e.part.begin.equals(e.whole.begin)) {
return;
}
if (e.context.onTrigger) {
const ctxTime = (e.whole.begin - begin) / this.cps + this.audioContext.currentTime + latency;
e.context.onTrigger(ctxTime, e, this.audioContext.currentTime, this.cps);
}
if (onEvent) {
onEvent?.(e);
}
});
}, interval);
constructor({ audioContext, interval = 0.2, onEvent, latency = 0.2 }) {
this.worker = new ClockWorker(
audioContext,
(begin, end) => {
this.pattern.query(new State(new TimeSpan(begin + latency, end + latency))).forEach((e) => {
if (!e.part.begin.equals(e.whole.begin)) {
return;
}
if (e.context.onTrigger) {
// TODO: kill first param, as it's contained in e
e.context.onTrigger(e.whole.begin, e, audioContext.currentTime, 1 /* cps */);
}
if (onEvent) {
onEvent?.(e);
}
});
},
interval,
);
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
this.audioContext.resume();
this.phase = 0;
this.worker.start();
}
stop() {
@@ -50,7 +42,4 @@ export class Scheduler {
setPattern(pat) {
this.pattern = pat;
}
setCps(cps = 1) {
this.cps = cps;
}
}
+134 -139
View File
@@ -125,152 +125,147 @@ const splitSN = (s, n) => {
};
Pattern.prototype.out = function () {
return this.onTrigger(async (t, hap, ct, cps) => {
const hapDuration = hap.duration / cps;
try {
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
t = ac.currentTime + t - ct;
// destructure value
let {
freq,
s,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 1,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
pan,
attack = 0.001,
decay = 0.05,
sustain = 0.5,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
return this.onTrigger(async (t, hap, ct) => {
const ac = getAudioContext();
// calculate correct time (tone.js workaround)
t = ac.currentTime + t - ct;
// destructure value
let {
freq,
s,
sf,
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 1,
cutoff,
resonance = 1,
hcutoff,
hresonance = 1,
bandf,
bandq = 1,
pan,
attack = 0.001,
decay = 0.05,
sustain = 0.5,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
} = hap.value;
const { velocity = 1 } = hap.context;
gain *= velocity; // legacy fix for velocity
// the chain will hold all audio nodes that connect to each other
const chain = [];
if (typeof s === 'string') {
[s, n] = splitSN(s, n);
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// with synths, n and note are the same thing
n = note || n;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
}
if (typeof note === 'string') {
[note, n] = splitSN(note, n);
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// with synths, n and note are the same thing
n = note || n;
if (typeof n === 'string') {
n = toMidi(n); // e.g. c3 => 48
// make oscillator
const o = getOscillator({ t, s, freq, duration: hap.duration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
g.gain.value = 0.3;
chain.push(g);
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration);
chain.push(adsr);
} else {
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
try {
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note);
}
// get frequency
if (!freq && typeof n === 'number') {
freq = fromMidi(n); // + 48);
}
// make oscillator
const o = getOscillator({ t, s, freq, duration: hapDuration, release });
chain.push(o);
// level down oscillators as they are really loud compared to samples i've tested
const g = ac.createGain();
g.gain.value = 0.3;
chain.push(g);
// TODO: make adsr work with samples without pops
// envelope
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
chain.push(adsr);
} catch (err) {
console.warn(err);
return;
}
// asny stuff above took too long?
if (ac.currentTime > t) {
console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop
let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration;
// let duration = bufferSource.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);
if (soundfont || clip) {
bufferSource.start(t, offset); // duration does not work here for some reason
} else {
// load sample
if (speed === 0) {
// no playback
return;
}
if (!s) {
console.warn('no sample specified');
return;
}
const soundfont = getSoundfontKey(s);
let bufferSource;
try {
if (soundfont) {
// is soundfont
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
} else {
// is sample from loaded samples(..)
bufferSource = await getSampleBufferSource(s, n, note);
}
} catch (err) {
console.warn(err);
return;
}
// asny stuff above took too long?
if (ac.currentTime > t) {
console.warn('sample still loading:', s, n);
return;
}
if (!bufferSource) {
console.warn('no buffer source');
return;
}
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
// TODO: nudge, unit, cut, loop
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration;
// let duration = bufferSource.buffer.duration;
const offset = begin * duration;
duration = ((end - begin) * duration) / Math.abs(speed);
if (soundfont || clip) {
bufferSource.start(t, offset); // duration does not work here for some reason
} else {
bufferSource.start(t, offset, duration);
}
chain.push(bufferSource);
if (soundfont || clip) {
const env = ac.createGain();
const releaseLength = 0.1;
env.gain.value = 0.6;
env.gain.setValueAtTime(env.gain.value, t + duration);
env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
// env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
chain.push(env);
bufferSource.stop(t + duration + releaseLength);
} else {
bufferSource.stop(t + duration);
}
bufferSource.start(t, offset, duration);
}
// master out
const master = ac.createGain();
master.gain.value = gain;
chain.push(master);
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
// TODO vowel
// TODO delay / delaytime / delayfeedback
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
chain.push(bufferSource);
if (soundfont || clip) {
const env = ac.createGain();
const releaseLength = 0.1;
env.gain.value = 0.6;
env.gain.setValueAtTime(env.gain.value, t + duration);
env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
// env.gain.linearRampToValueAtTime(0, t + duration + releaseLength);
chain.push(env);
bufferSource.stop(t + duration + releaseLength);
} else {
bufferSource.stop(t + duration);
}
// master out
/* const master = ac.createGain();
}
// master out
const master = ac.createGain();
master.gain.value = gain;
chain.push(master);
// filters
cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance));
hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance));
bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq));
// TODO vowel
// TODO delay / delaytime / delayfeedback
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// master out
/* const master = ac.createGain();
master.gain.value = 0.8 * gain;
chain.push(master); */
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
} catch (e) {
console.warn('.out error:', e);
}
chain.push(ac.destination);
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
});
};
+2 -2
View File
@@ -6,6 +6,6 @@ This package adds [webdirt](https://github.com/dktr0/WebDirt) support to strudel
Add default samples to repl:
1. move samples to `repl/public` folder. the samples folder is expected to have subfolders, with samples in it. the subfolders will be the names of the samples.
2. run `./makeSampleMap.sh ../../repl/public/EmuSP12 > ../../repl/public/EmuSP12.json`
1. move samples to `repl/public/samples` folder. the samples folder is expected to have subfolders, with samples in it. the subfolders will be the names of the samples.
2. run `./makeSampleMap.sh ../../repl/public/samples/EmuSP12 > ../../repl/public/samples/EmuSP12.json`
3. adapt `loadWebDirt` in App.jsx + MiniRepl.jsx to use the generated json file
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.2.0",
"version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/webdirt",
"version": "0.2.0",
"version": "0.1.2",
"description": "WebDirt integration for Strudel",
"main": "index.mjs",
"type": "module",
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0",
"@strudel.cycles/core": "^0.1.2",
"WebDirt": "github:dktr0/WebDirt"
}
}
+3 -3
View File
@@ -1,10 +1,10 @@
{
"name": "@strudel.cycles/xen",
"version": "0.2.0",
"version": "0.1.3",
"description": "Xenharmonic API for strudel",
"main": "index.mjs",
"scripts": {
"test": "vitest run"
"test": "mocha --colors"
},
"repository": {
"type": "git",
@@ -24,6 +24,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.2.0"
"@strudel.cycles/core": "^0.1.2"
}
}
+2 -2
View File
@@ -4,11 +4,11 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { strict as assert } from 'assert';
import { edo } from '../xen.mjs';
import { describe, it, expect } from 'vitest';
describe('xen', () => {
it('edo', () => {
expect(edo('3edo')).toEqual([1, Math.pow(2, 1 / 3), Math.pow(2, 2 / 3)]);
assert.deepStrictEqual(edo('3edo'), [1, Math.pow(2, 1 / 3), Math.pow(2, 2 / 3)]);
});
});
+1 -2
View File
@@ -23,5 +23,4 @@ dist-ssr
*.sln
*.sw?
oldtunes.mjs
public/samples/EMU World/
oldtunes.mjs
+2 -2
View File
@@ -7,8 +7,8 @@
"start": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run --reporter verbose -v --no-isolate",
"snapshot": "vitest run -u --silent",
"test": "NODE_OPTIONS='--experimental-fetch' mocha ./src/test --colors",
"snapshot": "cd ./src/ && rm -f ./tunes.snapshot.mjs && node ./shoot.mjs > ./tunes.snapshot.mjs",
"add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt",
"predeploy": "npm run build",
"deploy": "gh-pages -d ../docs",

Some files were not shown because too many files have changed in this diff Show More