mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 14:53:45 -04:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9e25ef8d8 | |||
| a08325c975 | |||
| 4d8adde9d5 | |||
| f2b2f4f226 | |||
| 40bbc57269 | |||
| 48526824d6 | |||
| 745c37f2d1 | |||
| 0e185b7335 | |||
| 596ae696f9 | |||
| b5e2bdf0f9 | |||
| 4e09cc14b6 | |||
| 58dc46b445 | |||
| f34d0a29e8 | |||
| 365a90e72e | |||
| 72f4d3efa8 | |||
| 98f4d4de8b | |||
| 57f84748ec | |||
| 4fb57d2818 | |||
| aff819069f | |||
| af585588af | |||
| eaf6617a43 | |||
| 4b294c4aff | |||
| c2354bffa2 | |||
| ea1ffca4ae | |||
| 86a68d78c1 | |||
| 2e4a3516c8 | |||
| e71fec0e95 | |||
| 9478915cf4 | |||
| f955d77d71 | |||
| c1255bfa88 | |||
| 9aaeee2026 | |||
| 27b3a4083b | |||
| 2f1d963126 | |||
| 235e82a348 | |||
| 17d0b7d291 | |||
| 22c60fb894 | |||
| dc3a873abc | |||
| 543d5f0619 | |||
| 8b088ab6c2 | |||
| 29009e14f4 | |||
| bf3e90baf5 | |||
| 3986c407ff | |||
| 8bd56d29d6 | |||
| 467b3db824 | |||
| 6025dd6f79 | |||
| 984f3d2781 | |||
| 0a558b963d | |||
| 1227d98904 | |||
| f533e4a5d0 | |||
| 0b00ad98e6 | |||
| 8988408d18 | |||
| f858d89f7b | |||
| 964ee68974 | |||
| 8244ba7754 | |||
| 21b2184750 | |||
| 8d1c98f03e | |||
| 684bab99a9 | |||
| 7a4a8a4581 | |||
| b8843a0eb6 | |||
| 563dfbea43 | |||
| 488ebf8f1a | |||
| 03158bd689 | |||
| 765f2df18e | |||
| a15d8553e0 | |||
| 3a790b03ac | |||
| 90e5e03531 | |||
| a4158c16f0 | |||
| ffc0b364ca | |||
| 1efb8f6681 | |||
| 55b2d7f68f | |||
| 7f0e98ea93 | |||
| 67c21c18cf | |||
| 0cd82c7e57 | |||
| 1942c36443 | |||
| 93251582ab | |||
| decd2d6625 | |||
| 6824dcb89b | |||
| eeac075a29 | |||
| b864b1dec3 | |||
| 130fb9c217 | |||
| 688a3d29fd | |||
| 800f2f466b | |||
| 655a9ff1a4 | |||
| 12f8902c5f | |||
| 9cdb1a53f9 | |||
| f5efa18e96 | |||
| fcebde5abd | |||
| 7fbd4527e5 | |||
| d79716c3f8 | |||
| 884811fd99 | |||
| 712a2ae018 | |||
| 4f460eeb32 | |||
| 913416a9d5 | |||
| fd61001b4d | |||
| ce4d2c17a7 | |||
| a42ebd7aaf | |||
| f38718004f | |||
| 1ddf968750 | |||
| 71dc108b2d | |||
| 82728be44b | |||
| bebfa23cc9 | |||
| 0d325a3ec4 |
+2
-1
@@ -34,4 +34,5 @@ repl_old
|
||||
tutorial.rendered.mdx
|
||||
doc.json
|
||||
talk/public/EmuSP12
|
||||
talk/public/samples
|
||||
talk/public/samples
|
||||
server/samples/old
|
||||
@@ -38,6 +38,12 @@ 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>
|
||||
|
||||
Generated
+2517
-1203
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"description": "Port of tidalcycles to javascript",
|
||||
"scripts": {
|
||||
"test": "npm run test --workspaces --if-present && cd repl && npm run test",
|
||||
"test": "vitest run --version",
|
||||
"test-ui": "vitest --ui",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
||||
"snapshot": "cd repl && npm run snapshot",
|
||||
@@ -37,12 +38,14 @@
|
||||
},
|
||||
"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",
|
||||
"mocha": "^9.1.4"
|
||||
"vitest": "^0.21.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,8 +28,5 @@
|
||||
"bjork": "^0.0.1",
|
||||
"fraction.js": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.2.2"
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
||||
}
|
||||
|
||||
@@ -1014,13 +1014,6 @@ export class Pattern {
|
||||
return this._chunk(n, func, true);
|
||||
}
|
||||
|
||||
edit(...funcs) {
|
||||
return stack(...funcs.map((func) => func(this)));
|
||||
}
|
||||
pipe(func) {
|
||||
return func(this);
|
||||
}
|
||||
|
||||
_bypass(on) {
|
||||
on = Boolean(parseInt(on));
|
||||
return on ? silence : this;
|
||||
@@ -1053,6 +1046,22 @@ 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..
|
||||
@@ -1285,8 +1294,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];
|
||||
return pat.query(state);
|
||||
const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
|
||||
return pat?.query(state) || [];
|
||||
};
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
*/
|
||||
export const sine = sine2._fromBipolar();
|
||||
|
||||
|
||||
/**
|
||||
* A cosine signal between 0 and 1.
|
||||
*
|
||||
@@ -59,7 +58,6 @@ 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.
|
||||
*
|
||||
@@ -112,7 +110,14 @@ 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();
|
||||
@@ -121,19 +126,67 @@ 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)])
|
||||
.outerJoin();
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
@@ -5,52 +5,42 @@ 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 { strict as assert } from 'assert';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import drawLine from '../drawLine.mjs';
|
||||
|
||||
describe('drawLine', () => {
|
||||
it('supports equal lengths', () => {
|
||||
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');
|
||||
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');
|
||||
});
|
||||
it('supports unequal lengths', () => {
|
||||
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');
|
||||
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');
|
||||
});
|
||||
it('supports unequal silence', () => {
|
||||
assert.equal(drawLine(fastcat(0, silence, [1, 2]), 10), '|0-..12|0-..12');
|
||||
expect(drawLine(fastcat(0, silence, [1, 2]), 10)).toEqual('|0-..12|0-..12');
|
||||
});
|
||||
it('supports polyrhythms', () => {
|
||||
'0*2 1*3';
|
||||
assert.equal(drawLine(fastcat(pure(0).fast(2), pure(1).fast(3)), 10), '|0--0--1-1-1-');
|
||||
// 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-');
|
||||
});
|
||||
it('supports multiple lines', () => {
|
||||
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
|
||||
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
|
||||
|.2|.2|.2|.2
|
||||
|.3|.3|.3|.3`,
|
||||
);
|
||||
assert.equal(
|
||||
drawLine(fastcat(0, 1, stack(2, 3, 4)), 10),
|
||||
`|012|012|012
|
||||
|.3|.3|.3|.3`);
|
||||
expect(drawLine(fastcat(0, 1, stack(2, 3, 4)), 10)).toEqual(`|012|012|012
|
||||
|..3|..3|..3
|
||||
|..4|..4|..4`,
|
||||
);
|
||||
|..4|..4|..4`);
|
||||
});
|
||||
it('supports unequal cycle lengths', () => {
|
||||
assert.equal(drawLine(slowcat(0, [1, 2]), 10), `|0|12|0|12`);
|
||||
expect(drawLine(slowcat(0, [1, 2]), 10)).toEqual(`|0|12|0|12`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { strict as assert } from 'assert';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('gcd', () => {
|
||||
it('should work', () => {
|
||||
const F = Fraction._original;
|
||||
assert.equal(gcd(F(1 / 6), F(1 / 4)).toFraction(), '1/12');
|
||||
expect(gcd(F(1 / 6), F(1 / 4)).toFraction()).toEqual('1/12');
|
||||
});
|
||||
});
|
||||
|
||||
+244
-288
@@ -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 { deepStrictEqual, strict as assert } from 'assert';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
TimeSpan,
|
||||
@@ -41,15 +41,11 @@ 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);
|
||||
@@ -58,35 +54,35 @@ const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
const sameFirst = (a, b) => {
|
||||
return assert.deepStrictEqual(a._sortHapsByPart().firstCycle(), b._sortHapsByPart().firstCycle());
|
||||
return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
|
||||
};
|
||||
|
||||
describe('TimeSpan', function () {
|
||||
describe('equals()', function () {
|
||||
it('Should be equal to the same value', function () {
|
||||
assert.equal(new TimeSpan(0, 4).equals(new TimeSpan(0, 4)), true);
|
||||
describe('TimeSpan', () => {
|
||||
describe('equals()', () => {
|
||||
it('Should be equal to the same value', () => {
|
||||
expect(new TimeSpan(0, 4).equals(new TimeSpan(0, 4))).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('splitCycles', function () {
|
||||
it('Should split two cycles into two', function () {
|
||||
assert.equal(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length, 2);
|
||||
describe('splitCycles', () => {
|
||||
it('Should split two cycles into two', () => {
|
||||
expect(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length).toBe(2);
|
||||
});
|
||||
});
|
||||
describe('intersection_e', function () {
|
||||
describe('intersection_e', () => {
|
||||
var a = new TimeSpan(Fraction(0), Fraction(2));
|
||||
var b = new TimeSpan(Fraction(1), Fraction(3));
|
||||
var c = new TimeSpan(Fraction(1), Fraction(2));
|
||||
var d = new TimeSpan(Fraction(1), Fraction(2));
|
||||
it('Should create an intersection', function () {
|
||||
assert.equal(a.intersection_e(b).equals(c), true);
|
||||
it('Should create an intersection', () => {
|
||||
expect(a.intersection_e(b).equals(c)).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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'a');
|
||||
@@ -94,19 +90,19 @@ describe('Hap', function () {
|
||||
var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), 'c');
|
||||
var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'd');
|
||||
var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'e');
|
||||
describe('spanEquals', function () {
|
||||
it('True if two haps have the same whole and part', function () {
|
||||
assert.equal(a.spanEquals(b), true);
|
||||
describe('spanEquals', () => {
|
||||
it('True if two haps have the same whole and part', () => {
|
||||
expect(a.spanEquals(b)).toBe(true);
|
||||
});
|
||||
it("False if two haps don't the same whole and part", function () {
|
||||
assert.equal(a.spanEquals(c), false);
|
||||
it("False if two haps don't the same whole and part", () => {
|
||||
expect(a.spanEquals(c)).toBe(false);
|
||||
});
|
||||
it('True if two haps have the same part and undefined wholes', function () {
|
||||
assert.equal(d.spanEquals(e), true);
|
||||
it('True if two haps have the same part and undefined wholes', () => {
|
||||
expect(d.spanEquals(e)).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('resolveState()', () => {
|
||||
it('Can increment some state', function () {
|
||||
it('Can increment some state', () => {
|
||||
const stateful_value = (state) => {
|
||||
const newValue = state['incrementme'];
|
||||
// TODO Does the state *need* duplicating here?
|
||||
@@ -118,8 +114,8 @@ describe('Hap', function () {
|
||||
const ev1 = new Hap(ts(0, 1), ts(0, 1), stateful_value, {}, true);
|
||||
const [state2, ev2] = ev1.resolveState(state);
|
||||
const [state3, ev3] = ev1.resolveState(state2);
|
||||
assert.deepStrictEqual(ev3, new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
|
||||
assert.deepStrictEqual(state3, { incrementme: 12 });
|
||||
expect(ev3).toStrictEqual(new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
|
||||
expect(state3).toStrictEqual({ incrementme: 12 });
|
||||
});
|
||||
});
|
||||
describe('wholeOrPart()', () => {
|
||||
@@ -128,42 +124,40 @@ describe('Hap', function () {
|
||||
const continuousHap = new Hap(undefined, ts1, 'hello');
|
||||
const discreteHap = new Hap(ts1, ts0_5, 'hello');
|
||||
it('Can pick a whole', () => {
|
||||
assert.deepStrictEqual(discreteHap.wholeOrPart(), ts1);
|
||||
expect(discreteHap.wholeOrPart()).toStrictEqual(ts1);
|
||||
});
|
||||
it('Can pick a part', () => {
|
||||
assert.deepStrictEqual(continuousHap.wholeOrPart(), ts1);
|
||||
expect(continuousHap.wholeOrPart()).toStrictEqual(ts1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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('Pattern', () => {
|
||||
describe('pure', () => {
|
||||
it('Can make a pattern', () => {
|
||||
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
|
||||
});
|
||||
});
|
||||
describe('fmap()', function () {
|
||||
it('Can add things', function () {
|
||||
assert.equal(
|
||||
describe('fmap()', () => {
|
||||
it('Can add things', () => {
|
||||
expect(
|
||||
pure(3)
|
||||
.fmap((x) => x + 4)
|
||||
.firstCycle()[0].value,
|
||||
7,
|
||||
);
|
||||
).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);
|
||||
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);
|
||||
});
|
||||
it('can structure Out()', () => {
|
||||
sameFirst(sequence(1, 2).addOut(4), sequence(5, 6).struct(true));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
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),
|
||||
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),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
@@ -197,18 +191,18 @@ describe('Pattern', function () {
|
||||
);
|
||||
});
|
||||
});
|
||||
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);
|
||||
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);
|
||||
});
|
||||
it('can structure Out()', () => {
|
||||
sameFirst(sequence(1, 2).keepOut(4), sequence(1, 2).struct(true));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
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),
|
||||
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),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
@@ -239,8 +233,8 @@ describe('Pattern', function () {
|
||||
sameFirst(sequence(1, [2, 3]).keepSqueezeOut(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
||||
});
|
||||
});
|
||||
describe('keepif()', function () {
|
||||
it('can structure In()', function () {
|
||||
describe('keepif()', () => {
|
||||
it('can structure In()', () => {
|
||||
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
|
||||
sameFirst(sequence(3, 4).keepifIn(true, false), sequence(3, silence));
|
||||
});
|
||||
@@ -248,9 +242,9 @@ describe('Pattern', function () {
|
||||
sameFirst(pure(1).keepifOut(true, false), sequence(1, silence));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
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),
|
||||
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),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
@@ -281,35 +275,39 @@ describe('Pattern', function () {
|
||||
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||
});
|
||||
});
|
||||
describe('sub()', function () {
|
||||
it('Can subtract things', function () {
|
||||
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
|
||||
describe('sub()', () => {
|
||||
it('Can subtract things', () => {
|
||||
expect(pure(3).sub(pure(4)).query(st(0, 1))[0].value).toBe(-1);
|
||||
});
|
||||
});
|
||||
describe('mul()', function () {
|
||||
it('Can multiply things', function () {
|
||||
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6);
|
||||
describe('mul()', () => {
|
||||
it('Can multiply things', () => {
|
||||
expect(pure(3).mul(pure(2)).firstCycle()[0].value).toBe(6);
|
||||
});
|
||||
});
|
||||
describe('div()', function () {
|
||||
it('Can divide things', function () {
|
||||
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
|
||||
describe('div()', () => {
|
||||
it('Can divide things', () => {
|
||||
expect(pure(3).div(pure(2)).firstCycle()[0].value).toBe(1.5);
|
||||
});
|
||||
});
|
||||
describe('set()', function () {
|
||||
it('Can set things in objects', function () {
|
||||
assert.deepStrictEqual(
|
||||
describe('set()', () => {
|
||||
it('Can set things in objects', () => {
|
||||
expect(
|
||||
pure({ a: 4, b: 6 })
|
||||
.set(pure({ c: 7 }))
|
||||
.firstCycle()[0].value,
|
||||
{ a: 4, b: 6, c: 7 },
|
||||
);
|
||||
).toStrictEqual({
|
||||
a: 4,
|
||||
b: 6,
|
||||
c: 7,
|
||||
});
|
||||
|
||||
sameFirst(
|
||||
sequence({ a: 1, b: 2 }, { a: 2, b: 2 }, { a: 3, b: 2 }).set({ a: 4, c: 5 }),
|
||||
sequence({ a: 4, b: 2, c: 5 }).fast(3),
|
||||
);
|
||||
});
|
||||
it('Can set things with plain values', function () {
|
||||
it('Can set things with plain values', () => {
|
||||
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
|
||||
});
|
||||
describe('setOut()', () => {
|
||||
@@ -342,94 +340,84 @@ describe('Pattern', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('stack()', function () {
|
||||
it('Can stack things', function () {
|
||||
assert.deepStrictEqual(
|
||||
describe('stack()', () => {
|
||||
it('Can stack things', () => {
|
||||
expect(
|
||||
stack(pure('a'), pure('b'), pure('c'))
|
||||
.firstCycle()
|
||||
.map((h) => h.value),
|
||||
['a', 'b', 'c'],
|
||||
);
|
||||
).toStrictEqual(['a', 'b', 'c']);
|
||||
});
|
||||
it('Can stack subpatterns', function () {
|
||||
it('Can stack subpatterns', () => {
|
||||
sameFirst(stack('a', ['b', 'c']), stack('a', sequence('b', 'c')));
|
||||
});
|
||||
});
|
||||
describe('_fast()', function () {
|
||||
it('Makes things faster', function () {
|
||||
assert.equal(pure('a')._fast(2).firstCycle().length, 2);
|
||||
describe('_fast()', () => {
|
||||
it('Makes things faster', () => {
|
||||
expect(pure('a')._fast(2).firstCycle().length).toBe(2);
|
||||
});
|
||||
});
|
||||
describe('_fastGap()', function () {
|
||||
it('Makes things faster, with a gap', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b', 'c')._fastGap(2).firstCycle(),
|
||||
describe('_fastGap()', () => {
|
||||
it('Makes things faster, with a gap', () => {
|
||||
expect(sequence('a', 'b', 'c')._fastGap(2).firstCycle()).toStrictEqual(
|
||||
sequence(['a', 'b', 'c'], silence).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b', 'c')._fastGap(3).firstCycle(),
|
||||
expect(sequence('a', 'b', 'c')._fastGap(3).firstCycle()).toStrictEqual(
|
||||
sequence(['a', 'b', 'c'], silence, silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Makes things faster, with a gap, when speeded up further', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle(),
|
||||
it('Makes things faster, with a gap, when speeded up further', () => {
|
||||
expect(sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle()).toStrictEqual(
|
||||
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_compressSpan()', function () {
|
||||
it('Can squash cycles of a pattern into a given timespan', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure('a')._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
|
||||
describe('_compressSpan()', () => {
|
||||
it('Can squash cycles of a pattern into a given timespan', () => {
|
||||
expect(pure('a')._compressSpan(ts(0.25, 0.5)).firstCycle()).toStrictEqual(
|
||||
sequence(silence, 'a', silence, silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('fast()', function () {
|
||||
it('Makes things faster', function () {
|
||||
assert.equal(pure('a').fast(2).firstCycle().length, 2);
|
||||
describe('fast()', () => {
|
||||
it('Makes things faster', () => {
|
||||
expect(pure('a').fast(2).firstCycle().length).toBe(2);
|
||||
});
|
||||
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(),
|
||||
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(
|
||||
stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('defaults to accepting sequences', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle(),
|
||||
it('defaults to accepting sequences', () => {
|
||||
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
|
||||
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works as a static function', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1, 2, 3).fast(1, 2).firstCycle(),
|
||||
it('works as a static function', () => {
|
||||
expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
|
||||
fast(sequence(1, 2), sequence(1, 2, 3)).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works as a curried static function', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(1, 2, 3).fast(1, 2).firstCycle(),
|
||||
it('works as a curried static function', () => {
|
||||
expect(sequence(1, 2, 3).fast(1, 2).firstCycle()).toStrictEqual(
|
||||
fast(sequence(1, 2))(sequence(1, 2, 3)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_slow()', function () {
|
||||
it('Makes things slower', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure('a')._slow(2).firstCycle()[0],
|
||||
new Hap(new TimeSpan(Fraction(0), Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), 'a'),
|
||||
);
|
||||
|
||||
describe('_slow()', () => {
|
||||
it('Makes things slower', () => {
|
||||
expect(pure('a')._slow(2).firstCycle()[0]).toStrictEqual(hap(ts(0, 2), ts(0, 1), 'a'));
|
||||
|
||||
const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
|
||||
assert.deepStrictEqual(pat.query(st(0, 1))[1], hap(ts(0.5, 1.5), ts(1 / 2, 1), 'eb3'));
|
||||
|
||||
expect(pat.query(st(0, 1))[1]).toStrictEqual(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:
|
||||
@@ -439,134 +427,125 @@ describe('Pattern', function () {
|
||||
});
|
||||
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()', function () {
|
||||
it('Filters true', function () {
|
||||
assert.equal(
|
||||
describe('_filterValues()', () => {
|
||||
it('Filters true', () => {
|
||||
expect(
|
||||
pure(true)
|
||||
._filterValues((x) => x)
|
||||
.firstCycle().length,
|
||||
1,
|
||||
);
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
describe('when()', function () {
|
||||
it('Always faster', function () {
|
||||
assert.equal(
|
||||
describe('when()', () => {
|
||||
it('Always faster', () => {
|
||||
expect(
|
||||
pure('a')
|
||||
.when(pure(true), (x) => x._fast(2))
|
||||
.firstCycle().length,
|
||||
2,
|
||||
);
|
||||
).toBe(2);
|
||||
});
|
||||
it('Never faster', function () {
|
||||
assert.equal(
|
||||
it('Never faster', () => {
|
||||
expect(
|
||||
pure('a')
|
||||
.when(pure(false), (x) => x._fast(2))
|
||||
.firstCycle().length,
|
||||
1,
|
||||
);
|
||||
).toBe(1);
|
||||
});
|
||||
it('Can alternate', function () {
|
||||
assert.deepStrictEqual(
|
||||
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortHapsByPart().firstCycle(),
|
||||
fastcat(13, 10, 13, 10).firstCycle(),
|
||||
);
|
||||
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());
|
||||
});
|
||||
});
|
||||
describe('fastcat()', function () {
|
||||
it('Can concatenate two things', function () {
|
||||
assert.deepStrictEqual(
|
||||
describe('fastcat()', () => {
|
||||
it('Can concatenate two things', () => {
|
||||
expect(
|
||||
fastcat(pure('a'), pure('b'))
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['a', 'b'],
|
||||
);
|
||||
).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
});
|
||||
describe('fastcat()', function () {
|
||||
it('Can go into negative time', function () {
|
||||
describe('fastcat()', () => {
|
||||
it('Can go into negative time', () => {
|
||||
sameFirst(fastcat('a', 'b', 'c').late(1000000), fastcat('a', 'b', 'c'));
|
||||
});
|
||||
});
|
||||
describe('slowcat()', function () {
|
||||
it('Can concatenate things slowly', function () {
|
||||
assert.deepStrictEqual(
|
||||
describe('slowcat()', () => {
|
||||
it('Can concatenate things slowly', () => {
|
||||
expect(
|
||||
slowcat('a', 'b')
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['a'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
).toStrictEqual(['a']);
|
||||
|
||||
expect(
|
||||
slowcat('a', 'b')
|
||||
._early(1)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['b'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
).toStrictEqual(['b']);
|
||||
|
||||
expect(
|
||||
slowcat('a', slowcat('b', 'c'))
|
||||
._early(1)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['b'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
).toStrictEqual(['b']);
|
||||
|
||||
expect(
|
||||
slowcat('a', slowcat('b', 'c'))
|
||||
._early(3)
|
||||
.firstCycle()
|
||||
.map((x) => x.value),
|
||||
['c'],
|
||||
);
|
||||
).toStrictEqual(['c']);
|
||||
});
|
||||
it('Can cat subpatterns', () => {
|
||||
sameFirst(slowcat('a', ['b', 'c']).fast(4), sequence('a', ['b', 'c']).fast(2));
|
||||
});
|
||||
});
|
||||
describe('rev()', function () {
|
||||
it('Can reverse things', function () {
|
||||
assert.deepStrictEqual(
|
||||
describe('rev()', () => {
|
||||
it('Can reverse things', () => {
|
||||
expect(
|
||||
fastcat('a', 'b', 'c')
|
||||
.rev()
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.value),
|
||||
['c', 'b', 'a'],
|
||||
);
|
||||
).toStrictEqual(['c', 'b', 'a']);
|
||||
});
|
||||
});
|
||||
describe('sequence()', () => {
|
||||
it('Can work like fastcat', () => {
|
||||
assert.deepStrictEqual(sequence(1, 2, 3).firstCycle(), fastcat(1, 2, 3).firstCycle());
|
||||
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('polyrhythm()', () => {
|
||||
it('Can layer up cycles', () => {
|
||||
assert.deepStrictEqual(
|
||||
polyrhythm(['a', 'b'], ['c']).firstCycle(),
|
||||
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
||||
stack(fastcat(pure('a'), pure('b')), pure('c')).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('polymeter()', () => {
|
||||
it('Can layer up cycles, stepwise', () => {
|
||||
assert.deepStrictEqual(
|
||||
polymeterSteps(3, ['d', 'e']).firstCycle(),
|
||||
expect(polymeterSteps(3, ['d', 'e']).firstCycle()).toStrictEqual(
|
||||
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle(),
|
||||
|
||||
expect(polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle()).toStrictEqual(
|
||||
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
|
||||
);
|
||||
});
|
||||
@@ -574,29 +553,24 @@ describe('Pattern', function () {
|
||||
|
||||
describe('every()', () => {
|
||||
it('Can apply a function every 3rd time', () => {
|
||||
assert.deepStrictEqual(
|
||||
expect(
|
||||
pure('a')
|
||||
.every(3, (x) => x._fast(2))
|
||||
._fast(3)
|
||||
.firstCycle(),
|
||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||
);
|
||||
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
||||
});
|
||||
it('works with currying', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure('a').every(3, fast(2))._fast(3).firstCycle(),
|
||||
expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle(),
|
||||
expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
||||
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle(),
|
||||
expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
||||
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
|
||||
expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
||||
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
});
|
||||
@@ -606,98 +580,91 @@ describe('Pattern', function () {
|
||||
sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
|
||||
});
|
||||
});
|
||||
describe('timeCat()', function () {
|
||||
it('Can concatenate patterns with different relative durations', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', ['a', 'a']).firstCycle(),
|
||||
describe('timeCat()', () => {
|
||||
it('Can concatenate patterns with different relative durations', () => {
|
||||
expect(sequence('a', ['a', 'a']).firstCycle()).toStrictEqual(
|
||||
timeCat([1, 'a'], [0.5, 'a'], [0.5, 'a']).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('struct()', function () {
|
||||
it('Can restructure a discrete pattern', function () {
|
||||
assert.deepStrictEqual(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle(), [
|
||||
describe('struct()', () => {
|
||||
it('Can restructure a discrete pattern', () => {
|
||||
expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
||||
hap(ts(0, third), ts(0, third), 'a'),
|
||||
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
||||
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
||||
hap(ts(twothirds, 1), ts(twothirds, 1), 'b'),
|
||||
]);
|
||||
assert.deepStrictEqual(
|
||||
|
||||
expect(
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, false], true))
|
||||
.firstCycle(),
|
||||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
|
||||
|
||||
expect(
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, false], true).invert())
|
||||
.firstCycle(),
|
||||
sequence(silence, [silence, 'a'], silence).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
).toStrictEqual(sequence(silence, [silence, 'a'], silence).firstCycle());
|
||||
|
||||
expect(
|
||||
pure('a')
|
||||
.struct(sequence(true, [true, silence], true))
|
||||
.firstCycle(),
|
||||
sequence('a', ['a', silence], 'a').firstCycle(),
|
||||
);
|
||||
).toStrictEqual(sequence('a', ['a', silence], 'a').firstCycle());
|
||||
});
|
||||
it('Can structure a continuous pattern', () => {
|
||||
assert.deepStrictEqual(
|
||||
steady('a').struct(true, [true, true]).firstCycle(),
|
||||
sequence('a', ['a', 'a']).firstCycle(),
|
||||
);
|
||||
expect(steady('a').struct(true, [true, true]).firstCycle()).toStrictEqual(sequence('a', ['a', 'a']).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('mask()', function () {
|
||||
it('Can fragment a pattern', function () {
|
||||
assert.deepStrictEqual(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle(), [
|
||||
describe('mask()', () => {
|
||||
it('Can fragment a pattern', () => {
|
||||
expect(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
||||
hap(ts(0, 0.5), ts(0, third), 'a'),
|
||||
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
||||
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
|
||||
hap(ts(0.5, 1), ts(twothirds, 1), 'b'),
|
||||
]);
|
||||
});
|
||||
it('Can mask off parts of a pattern', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle(),
|
||||
it('Can mask off parts of a pattern', () => {
|
||||
expect(sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle()).toStrictEqual(
|
||||
sequence(['a', 'b'], silence).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(sequence('a').mask(sequence(true, false)).firstCycle(), [hap(ts(0, 1), ts(0, 0.5), 'a')]);
|
||||
|
||||
expect(sequence('a').mask(sequence(true, false)).firstCycle()).toStrictEqual([hap(ts(0, 1), ts(0, 0.5), 'a')]);
|
||||
});
|
||||
});
|
||||
describe('invert()', function () {
|
||||
it('Can invert a binary pattern', function () {
|
||||
assert.deepStrictEqual(
|
||||
sequence(true, false, [true, false]).invert().firstCycle(),
|
||||
describe('invert()', () => {
|
||||
it('Can invert a binary pattern', () => {
|
||||
expect(sequence(true, false, [true, false]).invert().firstCycle()).toStrictEqual(
|
||||
sequence(false, true, [false, true]).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('signal()', function () {
|
||||
it('Can make saw/saw2', function () {
|
||||
assert.deepStrictEqual(
|
||||
saw.struct(true, true, true, true).firstCycle(),
|
||||
describe('signal()', () => {
|
||||
it('Can make saw/saw2', () => {
|
||||
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
sequence(1 / 8, 3 / 8, 5 / 8, 7 / 8).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
saw2.struct(true, true, true, true).firstCycle(),
|
||||
|
||||
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
sequence(-3 / 4, -1 / 4, 1 / 4, 3 / 4).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Can make isaw/isaw2', function () {
|
||||
assert.deepStrictEqual(
|
||||
isaw.struct(true, true, true, true).firstCycle(),
|
||||
it('Can make isaw/isaw2', () => {
|
||||
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
sequence(7 / 8, 5 / 8, 3 / 8, 1 / 8).firstCycle(),
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
isaw2.struct(true, true, true, true).firstCycle(),
|
||||
|
||||
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
sequence(3 / 4, 1 / 4, -1 / 4, -3 / 4).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_setContext()', () => {
|
||||
it('Can set the hap context', () => {
|
||||
assert.deepStrictEqual(
|
||||
expect(
|
||||
pure('a')
|
||||
._setContext([
|
||||
[
|
||||
@@ -706,20 +673,19 @@ describe('Pattern', function () {
|
||||
],
|
||||
])
|
||||
.firstCycle(true),
|
||||
[
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
]),
|
||||
],
|
||||
);
|
||||
).toStrictEqual([
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('_withContext()', () => {
|
||||
it('Can update the hap context', () => {
|
||||
assert.deepStrictEqual(
|
||||
expect(
|
||||
pure('a')
|
||||
._setContext([
|
||||
[
|
||||
@@ -735,52 +701,49 @@ describe('Pattern', function () {
|
||||
],
|
||||
])
|
||||
.firstCycle(true),
|
||||
[
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
[3, 4],
|
||||
[3, 4],
|
||||
],
|
||||
]),
|
||||
],
|
||||
);
|
||||
).toStrictEqual([
|
||||
hap(ts(0, 1), ts(0, 1), 'a', [
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
[
|
||||
[3, 4],
|
||||
[3, 4],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('apply', () => {
|
||||
it('Can apply a function', () => {
|
||||
assert.deepStrictEqual(sequence('a', 'b')._apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
|
||||
expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||
}),
|
||||
it('Can apply a pattern of functions', () => {
|
||||
assert.deepStrictEqual(sequence('a', 'b').apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', 'b').apply(fast(2), fast(3)).firstCycle(),
|
||||
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(
|
||||
sequence('a', 'b').fast(2, 3).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('layer', () => {
|
||||
it('Can layer up multiple functions', () => {
|
||||
assert.deepStrictEqual(
|
||||
expect(
|
||||
sequence(1, 2, 3)
|
||||
.layer(fast(2), (pat) => pat.add(3, 4))
|
||||
.firstCycle(),
|
||||
stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle(),
|
||||
);
|
||||
).toStrictEqual(stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('early', () => {
|
||||
it('Can shift a hap earlier', () => {
|
||||
assert.deepStrictEqual(pure(30)._late(0.25).query(st(1, 2)), [
|
||||
expect(pure(30)._late(0.25).query(st(1, 2))).toStrictEqual([
|
||||
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', () => {
|
||||
assert.deepStrictEqual(pure(30)._late(0.25).query(st(0, 1)), [
|
||||
expect(pure(30)._late(0.25).query(st(0, 1))).toStrictEqual([
|
||||
hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
|
||||
hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
|
||||
]);
|
||||
@@ -788,16 +751,14 @@ describe('Pattern', function () {
|
||||
});
|
||||
describe('off', () => {
|
||||
it('Can offset a transformed pattern from the original', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure(30).off(0.25, add(2)).firstCycle(),
|
||||
expect(pure(30).off(0.25, add(2)).firstCycle()).toStrictEqual(
|
||||
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('jux', () => {
|
||||
it('Can juxtapose', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle(),
|
||||
expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
@@ -806,8 +767,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
describe('juxBy', () => {
|
||||
it('Can juxtapose by half', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle(),
|
||||
expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
@@ -816,11 +776,12 @@ describe('Pattern', function () {
|
||||
});
|
||||
describe('_squeezeJoin', () => {
|
||||
it('Can squeeze', () => {
|
||||
assert.deepStrictEqual(
|
||||
expect(
|
||||
sequence('a', ['a', 'a'])
|
||||
.fmap((a) => fastcat('b', 'c'))
|
||||
._squeezeJoin()
|
||||
.firstCycle(),
|
||||
).toStrictEqual(
|
||||
sequence(
|
||||
['b', 'c'],
|
||||
[
|
||||
@@ -833,14 +794,13 @@ describe('Pattern', function () {
|
||||
});
|
||||
describe('ply', () => {
|
||||
it('Can ply(3)', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence('a', ['b', 'c']).ply(3).firstCycle(),
|
||||
expect(sequence('a', ['b', 'c']).ply(3).firstCycle()).toStrictEqual(
|
||||
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
|
||||
assert.equal(sequence(1, 2, 3).ply(2).early(8).firstCycle().length, 6);
|
||||
expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
|
||||
});
|
||||
});
|
||||
describe('striate', () => {
|
||||
@@ -853,8 +813,7 @@ describe('Pattern', function () {
|
||||
});
|
||||
describe('chop', () => {
|
||||
it('Can _chop(2)', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle(),
|
||||
expect(sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle()).toStrictEqual(
|
||||
sequence(
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
{ sound: 'a', begin: 0.5, end: 1 },
|
||||
@@ -864,8 +823,7 @@ describe('Pattern', function () {
|
||||
);
|
||||
});
|
||||
it('Can chop(2,3)', () => {
|
||||
assert.deepStrictEqual(
|
||||
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle(),
|
||||
expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
sequence(
|
||||
[
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
@@ -884,16 +842,14 @@ describe('Pattern', function () {
|
||||
});
|
||||
describe('range2', () => {
|
||||
it('Can change the range of a bipolar pattern', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle(),
|
||||
expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
|
||||
sequence(1000, 1025, 1050, 1075).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('linger', () => {
|
||||
it('Can linger on the first quarter of a cycle', () => {
|
||||
assert.deepStrictEqual(
|
||||
sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle(),
|
||||
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
|
||||
sequence(0, 1, 0, 1, 0, 1, 0, 1).firstCycle(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
assert.equal(isNote(note), true);
|
||||
expect(isNote(note)).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should recognize notes with accidentals', () => {
|
||||
'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => {
|
||||
assert.equal(isNote(note), true);
|
||||
expect(isNote(note)).toBe(true);
|
||||
});
|
||||
});
|
||||
it('should not recognize invalid notes', () => {
|
||||
assert.equal(isNote('H5'), false);
|
||||
assert.equal(isNote('C'), false);
|
||||
assert.equal(isNote('X'), false);
|
||||
assert.equal(isNote(1), false);
|
||||
expect(isNote('H5')).toBe(false);
|
||||
expect(isNote('C')).toBe(false);
|
||||
expect(isNote('X')).toBe(false);
|
||||
expect(isNote(1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should tokenize notes without accidentals', () => {
|
||||
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]);
|
||||
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]);
|
||||
});
|
||||
it('should tokenize notes with accidentals', () => {
|
||||
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]);
|
||||
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]);
|
||||
});
|
||||
it('should tokenize notes without octave', () => {
|
||||
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]);
|
||||
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]);
|
||||
});
|
||||
it('should not tokenize invalid notes', () => {
|
||||
assert.deepStrictEqual(tokenizeNote('X'), []);
|
||||
assert.deepStrictEqual(tokenizeNote('asfasf'), []);
|
||||
assert.deepStrictEqual(tokenizeNote(123), []);
|
||||
expect(tokenizeNote('X')).toStrictEqual([]);
|
||||
expect(tokenizeNote('asfasf')).toStrictEqual([]);
|
||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
describe('toMidi', () => {
|
||||
it('should turn notes into midi', () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
assert.equal(fromMidi(69), 440);
|
||||
assert.equal(fromMidi(57), 220);
|
||||
expect(fromMidi(69)).toEqual(440);
|
||||
expect(fromMidi(57)).toEqual(220);
|
||||
});
|
||||
});
|
||||
describe('getFrequency', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
assert.equal(getFrequency(happify('a4')), 440);
|
||||
assert.equal(getFrequency(happify('a3')), 220);
|
||||
assert.equal(getFrequency(happify(440, { type: 'frequency' })), 440); // TODO: migrate when values are objects..
|
||||
assert.equal(getFrequency(happify(432, { type: 'frequency' })), 432);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mod', () => {
|
||||
it('should work like regular modulo with positive numbers', () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
it('should work with negative numbers', () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compose', () => {
|
||||
const add1 = (a) => a + 1;
|
||||
it('should compose', () => {
|
||||
assert.equal(compose(add1, add1)(0), 2);
|
||||
assert.equal(compose(add1)(0), 1);
|
||||
expect(compose(add1, add1)(0)).toEqual(2);
|
||||
expect(compose(add1)(0)).toEqual(1);
|
||||
});
|
||||
const addS = (s) => (a) => a + s;
|
||||
it('should compose left to right', () => {
|
||||
assert.equal(compose(addS('a'), addS('b'))(''), 'ab');
|
||||
assert.equal(compose(addS('a'), addS('b'))('x'), 'xab');
|
||||
expect(compose(addS('a'), addS('b'))('')).toEqual('ab');
|
||||
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { strict as assert } from 'assert';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { map, valued, mul } from '../value.mjs';
|
||||
|
||||
describe('Value', () => {
|
||||
it('unionWith', () => {
|
||||
const { value } = valued({ freq: 2000, distortion: 1.2 }).unionWith({ distortion: 2 }, mul);
|
||||
assert.deepStrictEqual(value, { freq: 2000, distortion: 2.4 });
|
||||
expect(value).toStrictEqual({ freq: 2000, distortion: 2.4 });
|
||||
});
|
||||
|
||||
it('experiments', () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
+20
-2
@@ -31,17 +31,35 @@ 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 === 'string' && !isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
} 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));
|
||||
}
|
||||
return note;
|
||||
};
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -8,7 +8,7 @@
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
|
||||
@@ -34,7 +34,7 @@ export const wrappedAsync = true;
|
||||
|
||||
export default (_code) => {
|
||||
const { code, addReturn } = wrapAsync(_code);
|
||||
const ast = parseScriptWithLocation(code);
|
||||
const ast = parseScriptWithLocation(disguiseImports(code));
|
||||
const artificialNodes = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
@@ -128,10 +128,22 @@ export default (_code) => {
|
||||
if (wrappedAsync) {
|
||||
addReturn(shifted);
|
||||
}
|
||||
const generated = codegen(shifted);
|
||||
const generated = undisguiseImports(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
|
||||
|
||||
@@ -4,31 +4,29 @@ 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 { evaluate, extend } from '../evaluate.mjs';
|
||||
import { expect, describe, it } from 'vitest';
|
||||
|
||||
import { evaluate, evalScope } from '../evaluate.mjs';
|
||||
import { mini } from '@strudel.cycles/mini';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { fastcat } = strudel;
|
||||
|
||||
extend({ mini }, strudel);
|
||||
// TODO: test evalScope
|
||||
|
||||
describe('evaluate', () => {
|
||||
describe('evaluate', async () => {
|
||||
await evalScope({ mini }, strudel);
|
||||
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
|
||||
it('Should evaluate strudel functions', async () => {
|
||||
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']);
|
||||
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']);
|
||||
});
|
||||
it('Should be extendable', async () => {
|
||||
extend({ myFunction: (...x) => fastcat(...x) });
|
||||
assert.deepStrictEqual(await ev('myFunction(c3, d3)'), ['c3', 'd3']);
|
||||
it('Scope should be extendable', async () => {
|
||||
await evalScope({ myFunction: (...x) => fastcat(...x) });
|
||||
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||
});
|
||||
it('Should evaluate simple double quoted mini notation', async () => {
|
||||
assert.deepStrictEqual(await ev('"c3"'), ['c3']);
|
||||
assert.deepStrictEqual(await ev('"c3 d3"'), ['c3', 'd3']);
|
||||
assert.deepStrictEqual(await ev('"<c3 d3>"'), ['c3']);
|
||||
expect(await ev('"c3"')).toEqual(['c3']);
|
||||
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
|
||||
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,16 @@ 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 { describe, it, expect } from 'vitest';
|
||||
import shapeshifter from '../shapeshifter.mjs';
|
||||
|
||||
describe('shapeshifter', () => {
|
||||
it('Should shift simple double quote string', () => {
|
||||
assert.equal(shapeshifter('"c3"'), '(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
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])})()',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"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.1.3",
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
|
||||
+399
-269
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ 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
|
||||
|
||||
|
||||
{
|
||||
@@ -81,7 +82,8 @@ DIGIT = [0-9]
|
||||
// ------------------ delimiters ---------------------------
|
||||
|
||||
ws "whitespace" = [ \n\r\t]*
|
||||
comma = ws "," ws;
|
||||
comma = ws "," ws
|
||||
pipe = ws "|" ws
|
||||
quote = '"' / "'"
|
||||
|
||||
// ------------------ steps and cycles ---------------------------
|
||||
@@ -91,7 +93,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 ws "]" ws { return s}
|
||||
sub_cycle = ws "[" ws s:stack_or_choose 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
|
||||
@@ -102,7 +104,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_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
|
||||
|
||||
slice_weight = "@" a:number
|
||||
{ return { weight: a} }
|
||||
@@ -122,6 +124,9 @@ 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);}
|
||||
@@ -132,14 +137,22 @@ 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 = 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");} }
|
||||
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; } }
|
||||
|
||||
// a sequence is a quoted stack
|
||||
sequence = ws quote s:stack quote
|
||||
{ return s; }
|
||||
sequence = ws quote sc:stack_or_choose quote
|
||||
{ return sc; }
|
||||
|
||||
// ------------------ operators ---------------------------
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ 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_;
|
||||
@@ -21,6 +28,8 @@ 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`);
|
||||
@@ -82,6 +91,9 @@ 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);
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --colors",
|
||||
"test": "vitest run",
|
||||
"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.1.2",
|
||||
"@strudel.cycles/eval": "^0.1.3",
|
||||
"@strudel.cycles/tone": "^0.1.3"
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/eval": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"peggy": "^2.0.1"
|
||||
|
||||
@@ -4,50 +4,104 @@ 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', () => {
|
||||
assert.deepStrictEqual(minV('a'), ['a']);
|
||||
expect(minV('a')).toEqual(['a']);
|
||||
});
|
||||
it('supports rest', () => {
|
||||
assert.deepStrictEqual(minV('~'), []);
|
||||
expect(minV('~')).toEqual([]);
|
||||
});
|
||||
it('supports cat', () => {
|
||||
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']);
|
||||
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']);
|
||||
});
|
||||
it('supports slowcat', () => {
|
||||
assert.deepStrictEqual(minV('<a b>'), ['a']);
|
||||
expect(minV('<a b>')).toEqual(['a']);
|
||||
});
|
||||
it('supports division', () => {
|
||||
assert.deepStrictEqual(minS('a/2'), ['a: 0 - 2']);
|
||||
assert.deepStrictEqual(minS('[c3 d3]/2'), ['c3: 0 - 1']);
|
||||
expect(minS('a/2')).toEqual(['a: 0 - 2']);
|
||||
expect(minS('[c3 d3]/2')).toEqual(['c3: 0 - 1']);
|
||||
});
|
||||
it('supports multiplication', () => {
|
||||
assert.deepStrictEqual(minS('c3*2'), ['c3: 0 - 1/2', 'c3: 1/2 - 1']);
|
||||
assert.deepStrictEqual(minV('[c3 d3]*2'), ['c3', 'd3', 'c3', 'd3']);
|
||||
expect(minS('c3*2')).toEqual(['c3: 0 - 1/2', 'c3: 1/2 - 1']);
|
||||
expect(minV('[c3 d3]*2')).toEqual(['c3', 'd3', 'c3', 'd3']);
|
||||
});
|
||||
it('supports brackets', () => {
|
||||
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']);
|
||||
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']);
|
||||
});
|
||||
it('supports commas', () => {
|
||||
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']);
|
||||
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']);
|
||||
});
|
||||
it('supports elongation', () => {
|
||||
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']);
|
||||
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']);
|
||||
});
|
||||
it('supports replication', () => {
|
||||
assert.deepStrictEqual(minS('a!3 b'), ['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
||||
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
||||
});
|
||||
it('supports euclidean rhythms', () => {
|
||||
assert.deepStrictEqual(minS('a(3, 8)'), ['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+118
-175
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { HighlightStyle, tags } from '@codemirror/highlight';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
@@ -11,139 +12,42 @@ import { Tone } from '@strudel.cycles/tone';
|
||||
import { TimeSpan, State } from '@strudel.cycles/core';
|
||||
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
|
||||
|
||||
/*
|
||||
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.
|
||||
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,
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
{ dark: true },
|
||||
);
|
||||
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' },
|
||||
],
|
||||
});
|
||||
|
||||
/// 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];
|
||||
var style = '';
|
||||
|
||||
const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
@@ -185,42 +89,57 @@ const highlightField = StateField.define({
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
return highlights;
|
||||
return Decoration.set([]);
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
});
|
||||
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"
|
||||
const extensions = [javascript(), strudelTheme, highlightField, flashField];
|
||||
function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
|
||||
const handleOnChange = useCallback(
|
||||
(value2) => {
|
||||
onChange?.(value2);
|
||||
},
|
||||
[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,
|
||||
extensions: [
|
||||
javascript(),
|
||||
materialPalenight,
|
||||
highlightField,
|
||||
flashField
|
||||
]
|
||||
onChange: handleOnChange,
|
||||
onCreateEditor: handleOnCreateEditor,
|
||||
onUpdate: handleOnUpdate,
|
||||
extensions
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -351,6 +270,8 @@ 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,
|
||||
@@ -395,7 +316,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
||||
},
|
||||
[pattern],
|
||||
),
|
||||
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
|
||||
onSchedule: useCallback((_events, cycle) => logCycle(_events), []),
|
||||
ready: !!pattern && !!activeCode,
|
||||
});
|
||||
|
||||
@@ -449,6 +370,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
||||
};
|
||||
|
||||
return {
|
||||
hideHeader,
|
||||
hideConsole,
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
@@ -519,15 +442,13 @@ function useHighlighting({ view, pattern, active }) {
|
||||
|
||||
var tailwind = '';
|
||||
|
||||
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";
|
||||
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 styles = {
|
||||
container: container,
|
||||
header: header,
|
||||
@@ -563,12 +484,16 @@ function Icon({ type }) {
|
||||
}[type]);
|
||||
}
|
||||
|
||||
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
|
||||
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
|
||||
autolink: false,
|
||||
onEvent
|
||||
});
|
||||
useEffect(() => {
|
||||
init && evaluateOnly();
|
||||
}, [tune, init]);
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01
|
||||
@@ -580,7 +505,25 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started });
|
||||
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]);
|
||||
return /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.container,
|
||||
ref
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
*,: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}
|
||||
.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}
|
||||
|
||||
Generated
-3984
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.1.4",
|
||||
"version": "0.2.0",
|
||||
"description": "React components for strudel",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
@@ -37,11 +37,12 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
|
||||
// import { CodeMirrorLite as _CodeMirror } from 'react-codemirror6/dist/lite';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
// import { materialPalenight } from 'codemirror6-themes';
|
||||
import { materialPalenight } from '../themes/material-palenight';
|
||||
import strudelTheme from '../themes/strudel-theme';
|
||||
import './style.css';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
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)) {
|
||||
highlights = Decoration.set(
|
||||
const marks =
|
||||
e.value
|
||||
.flatMap((hap) =>
|
||||
.map((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || '#FFCA28';
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
@@ -60,43 +60,56 @@ 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: 1px solid ${color}` } });
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
.filter(Boolean),
|
||||
true,
|
||||
);
|
||||
.flat()
|
||||
.filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
// console.warn('highlighting error', err);
|
||||
return highlights;
|
||||
return Decoration.set([]);
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
||||
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],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<_CodeMirror
|
||||
onViewChange={onViewChanged}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 0 auto',
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
extensions={[
|
||||
javascript(),
|
||||
materialPalenight,
|
||||
highlightField,
|
||||
flashField,
|
||||
// theme, language, ...
|
||||
]}
|
||||
onChange={handleOnChange}
|
||||
onCreateEditor={handleOnCreateEditor}
|
||||
onUpdate={handleOnUpdate}
|
||||
extensions={extensions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } 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 from './CodeMirror6';
|
||||
import CodeMirror6, { flash } 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 }) {
|
||||
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
|
||||
tune,
|
||||
defaultSynth,
|
||||
autolink: false,
|
||||
});
|
||||
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]);
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01,
|
||||
@@ -26,7 +31,28 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started });
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<div className={styles.header}>
|
||||
@@ -40,7 +66,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
</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-[#444C57];
|
||||
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#222222];
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
.cm-editor {
|
||||
background-color: transparent !important;
|
||||
height: 100%;
|
||||
z-index: 11;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.cm-theme-light {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cm-line > * {
|
||||
background: #00000095;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ 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,
|
||||
@@ -136,6 +138,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
|
||||
};
|
||||
|
||||
return {
|
||||
hideHeader,
|
||||
hideConsole,
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
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];
|
||||
@@ -0,0 +1,36 @@
|
||||
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' },
|
||||
],
|
||||
});
|
||||
@@ -33,8 +33,12 @@ export default defineConfig({
|
||||
'@strudel.cycles/serial',
|
||||
'@strudel.cycles/webaudio',
|
||||
'@codemirror/view',
|
||||
'@codemirror/highlight',
|
||||
'@codemirror/state'
|
||||
'@codemirror/lang-javascript',
|
||||
'@codemirror/state',
|
||||
'@codemirror/commands',
|
||||
'@lezer/highlight',
|
||||
'@codemirror/language',
|
||||
'@uiw/codemirror-themes'
|
||||
],
|
||||
},
|
||||
target: 'esnext',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getFontBufferSource } from './fontloader.mjs';
|
||||
import * as soundfontList from './list.mjs';
|
||||
import { startPresetNote } from 'sfumato';
|
||||
import { loadSoundfont } from './sfumato.mjs';
|
||||
|
||||
globalThis.getFontBufferSource = getFontBufferSource;
|
||||
globalThis.soundfontList = soundfontList;
|
||||
globalThis.soundfontList = soundfontList;
|
||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/soundfonts",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -22,8 +22,10 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "*",
|
||||
"@strudel.cycles/webaudio": "^0.1.4"
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/webaudio": "^0.2.0",
|
||||
"sfumato": "^0.1.2",
|
||||
"soundfont2": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-fetch": "^3.2.6"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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;
|
||||
}
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.2",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ 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 ', () => {
|
||||
assert.deepStrictEqual(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues, ['D3']);
|
||||
expect(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues).toEqual(['D3']);
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"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.1.2",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"tone": "^14.7.77"
|
||||
|
||||
+23
-13
@@ -22,10 +22,14 @@ Pattern.prototype.pianoroll = function ({
|
||||
flipTime = 0,
|
||||
flipValues = 0,
|
||||
hideNegative = false,
|
||||
inactive = '#C9E597',
|
||||
// inactive = '#C9E597',
|
||||
// inactive = '#FFCA28',
|
||||
inactive = '#7491D2',
|
||||
active = '#FFCA28',
|
||||
// background = '#2A3236',
|
||||
background = 'transparent',
|
||||
smear = 0,
|
||||
playheadColor = 'white',
|
||||
minMidi = 10,
|
||||
maxMidi = 90,
|
||||
autorange = 0,
|
||||
@@ -58,12 +62,14 @@ 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.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
if (!smear) {
|
||||
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) => {
|
||||
@@ -71,15 +77,6 @@ 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);
|
||||
@@ -107,6 +104,19 @@ 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,
|
||||
|
||||
@@ -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 Tone from 'tone';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('tone', () => {
|
||||
it('Should have working tone function', () => {
|
||||
const s = synth().chain(out());
|
||||
assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
|
||||
assert.deepStrictEqual(pure('c3').tone(s)._firstCycleValues, ['c3']);
|
||||
// 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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,14 +9,12 @@ const stringifyFunction = (func) => '(' + func + ')();';
|
||||
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
|
||||
const createWorker = (func) => new Worker(urlifyFunction(func));
|
||||
|
||||
// this class is basically the tale of two clocks
|
||||
// this is just a setInterval with a counter, running in a worker
|
||||
export class ClockWorker {
|
||||
worker;
|
||||
audioContext;
|
||||
interval = 0.2; // query span
|
||||
lastEnd = 0;
|
||||
constructor(audioContext, callback, interval = this.interval) {
|
||||
this.audioContext = audioContext;
|
||||
interval = 0.1; // query span
|
||||
tick = 0;
|
||||
constructor(callback, interval = this.interval) {
|
||||
this.interval = interval;
|
||||
this.worker = createWorker(() => {
|
||||
// we cannot use closures here!
|
||||
@@ -33,6 +31,7 @@ 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) {
|
||||
@@ -50,25 +49,23 @@ 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(begin, end);
|
||||
callback(this.tick++, this.interval);
|
||||
}
|
||||
};
|
||||
}
|
||||
start() {
|
||||
console.log('start...');
|
||||
this.audioContext.resume();
|
||||
// console.log('start...');
|
||||
this.worker.postMessage('start');
|
||||
}
|
||||
stop() {
|
||||
console.log('stop...');
|
||||
// console.log('stop...');
|
||||
this.worker.postMessage('stop');
|
||||
this.tick = 0;
|
||||
}
|
||||
setInterval(interval) {
|
||||
this.worker.postMessage({ interval });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div style="position: absolute; top: 0; right: 0; padding: 4px">
|
||||
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br />
|
||||
<button id="stop" style="font-size: 2em">stop</button>
|
||||
<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>
|
||||
</div>
|
||||
<textarea
|
||||
style="font-size: 2em; background: #bce865; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
|
||||
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
|
||||
id="text"
|
||||
spellcheck="false"
|
||||
>
|
||||
@@ -11,11 +13,9 @@ Loading...</textarea
|
||||
>
|
||||
<script type="module">
|
||||
document.body.style = 'margin: 0';
|
||||
// import * as strudel from '@strudel.cycles/core';
|
||||
import * as strudel from '../../core/index.mjs';
|
||||
import * as util from '../../core/util.mjs';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import * as util from '@strudel.cycles/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,11 +33,12 @@ Loading...</textarea
|
||||
.mul(slowcat(1,2))
|
||||
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
|
||||
.fast(3)
|
||||
.freq()
|
||||
.velocity(.5)
|
||||
.wave(cat('sawtooth','square').fast(2))
|
||||
.adsr(0.01,.02,.5,0.1)
|
||||
.filter('lowshelf',800,25)
|
||||
.out()`;
|
||||
.s('sawtooth')
|
||||
.cutoff(800)
|
||||
.out()
|
||||
`;
|
||||
|
||||
try {
|
||||
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
|
||||
@@ -61,4 +62,6 @@ 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>
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.1.4",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.1.4",
|
||||
"version": "0.2.0",
|
||||
"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.1.2"
|
||||
"@strudel.cycles/core": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,35 +5,43 @@ 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;
|
||||
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,
|
||||
);
|
||||
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);
|
||||
}
|
||||
start() {
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
this.audioContext.resume();
|
||||
this.phase = 0;
|
||||
this.worker.start();
|
||||
}
|
||||
stop() {
|
||||
@@ -42,4 +50,7 @@ export class Scheduler {
|
||||
setPattern(pat) {
|
||||
this.pattern = pat;
|
||||
}
|
||||
setCps(cps = 1) {
|
||||
this.cps = cps;
|
||||
}
|
||||
}
|
||||
|
||||
+140
-135
@@ -125,147 +125,152 @@ const splitSN = (s, n) => {
|
||||
};
|
||||
|
||||
Pattern.prototype.out = function () {
|
||||
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
|
||||
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);
|
||||
}
|
||||
// get frequency
|
||||
if (!freq && typeof n === 'number') {
|
||||
freq = fromMidi(n); // + 48);
|
||||
if (typeof note === 'string') {
|
||||
[note, n] = splitSN(note, n);
|
||||
}
|
||||
// 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);
|
||||
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
|
||||
}
|
||||
} 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
|
||||
// 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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
// master out
|
||||
const master = ac.createGain();
|
||||
master.gain.value = gain;
|
||||
chain.push(master);
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
// no playback
|
||||
return;
|
||||
}
|
||||
if (!s) {
|
||||
console.warn('no sample specified');
|
||||
return;
|
||||
}
|
||||
const soundfont = getSoundfontKey(s);
|
||||
let bufferSource;
|
||||
|
||||
// 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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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]);
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/webdirt",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"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.1.2",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"WebDirt": "github:dktr0/WebDirt"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,6 +24,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.1.2"
|
||||
"@strudel.cycles/core": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
assert.deepStrictEqual(edo('3edo'), [1, Math.pow(2, 1 / 3), Math.pow(2, 2 / 3)]);
|
||||
expect(edo('3edo')).toEqual([1, Math.pow(2, 1 / 3), Math.pow(2, 2 / 3)]);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -23,4 +23,5 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
oldtunes.mjs
|
||||
oldtunes.mjs
|
||||
public/samples/EMU World/
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "NODE_OPTIONS='--experimental-fetch' mocha ./src/test --colors",
|
||||
"snapshot": "cd ./src/ && rm -f ./tunes.snapshot.mjs && node ./shoot.mjs > ./tunes.snapshot.mjs",
|
||||
"test": "vitest run --reporter verbose -v --no-isolate",
|
||||
"snapshot": "vitest run -u --silent",
|
||||
"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",
|
||||
|
||||
+4
-15
@@ -1,26 +1,15 @@
|
||||
@import '@strudel.cycles/react/dist/style.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
background-color: #2a3236;
|
||||
}
|
||||
|
||||
.react-codemirror2,
|
||||
.CodeMirror {
|
||||
height: 100% !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 15px;
|
||||
z-index:20
|
||||
}
|
||||
|
||||
.CodeMirror-line > span {
|
||||
background-color: #2a323699;
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.darken::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
@@ -28,4 +17,4 @@ body {
|
||||
display: block;
|
||||
background: black;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
+129
-120
@@ -108,6 +108,8 @@ function App() {
|
||||
pattern,
|
||||
pushLog,
|
||||
pending,
|
||||
hideHeader,
|
||||
hideConsole,
|
||||
} = useRepl({
|
||||
tune: '// LOADING...',
|
||||
defaultSynth,
|
||||
@@ -167,142 +169,149 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100',
|
||||
isEmbedded ? 'h-8' : 'h-14',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" />
|
||||
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => {
|
||||
getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
togglePlay();
|
||||
}}
|
||||
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
|
||||
{cycle.started ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{cycle.started ? 'pause' : 'play'}
|
||||
</span>
|
||||
) : (
|
||||
<>loading...</>
|
||||
)}
|
||||
</button>
|
||||
{!isEmbedded && (
|
||||
{!hideHeader && (
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none w-full px-2 flex border-b border-gray-200 justify-between z-[10] bg-gray-100',
|
||||
isEmbedded ? 'h-8' : 'h-14',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={logo} className={cx('Tidal-logo', isEmbedded ? 'w-6 h-6' : 'w-10 h-10')} alt="logo" />
|
||||
<h1 className={isEmbedded ? 'text-l' : 'text-xl'}>Strudel {isEmbedded ? 'Mini ' : ''}REPL</h1>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
className="hover:bg-gray-300 p-2"
|
||||
onClick={async () => {
|
||||
const _code = getRandomTune();
|
||||
console.log('tune', _code); // uncomment this to debug when random code fails
|
||||
setCode(_code);
|
||||
cleanupDraw();
|
||||
cleanupUi();
|
||||
resetLoadedSamples();
|
||||
prebake();
|
||||
const parsed = await evaluate(_code);
|
||||
setPattern(parsed.pattern);
|
||||
setActiveCode(_code);
|
||||
onClick={() => {
|
||||
getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
togglePlay();
|
||||
}}
|
||||
className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
🎲 random
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
|
||||
{cycle.started ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{cycle.started ? 'pause' : 'play'}
|
||||
</span>
|
||||
) : (
|
||||
<>loading...</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<button className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
|
||||
<a href="./tutorial">📚 tutorial</a>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
className={cx('cursor-pointer hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
onClick={async () => {
|
||||
const codeToShare = activeCode || code;
|
||||
if (lastShared === codeToShare) {
|
||||
// alert('Link already generated!');
|
||||
pushLog(`Link already generated!`);
|
||||
return;
|
||||
}
|
||||
// generate uuid in the browser
|
||||
const hash = nanoid(12);
|
||||
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
|
||||
if (!error) {
|
||||
setLastShared(activeCode || code);
|
||||
const shareUrl = window.location.origin + '?' + hash;
|
||||
// copy shareUrl to clipboard
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
// alert(message);
|
||||
pushLog(message);
|
||||
} else {
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
pushLog(message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
📣 share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:bg-gray-300 px-2')}>
|
||||
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
||||
🚀 open
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:bg-gray-300 px-2')}>
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.href = initialUrl;
|
||||
window.location.reload();
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
className="hover:bg-gray-300 p-2"
|
||||
onClick={async () => {
|
||||
const _code = getRandomTune();
|
||||
// console.log('tune', _code); // uncomment this to debug when random code fails
|
||||
setCode(_code);
|
||||
cleanupDraw();
|
||||
cleanupUi();
|
||||
resetLoadedSamples();
|
||||
await prebake(); // declare default samples
|
||||
const parsed = await evaluate(_code);
|
||||
setPattern(parsed.pattern);
|
||||
setActiveCode(_code);
|
||||
}}
|
||||
title="Reset"
|
||||
>
|
||||
💔 reset
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
🎲 random
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<button className={cx('hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}>
|
||||
<a href="./tutorial">📚 tutorial</a>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
className={cx('cursor-pointer hover:bg-gray-300', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
onClick={async () => {
|
||||
const codeToShare = activeCode || code;
|
||||
if (lastShared === codeToShare) {
|
||||
// alert('Link already generated!');
|
||||
pushLog(`Link already generated!`);
|
||||
return;
|
||||
}
|
||||
// generate uuid in the browser
|
||||
const hash = nanoid(12);
|
||||
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
|
||||
if (!error) {
|
||||
setLastShared(activeCode || code);
|
||||
const shareUrl = window.location.origin + '?' + hash;
|
||||
// copy shareUrl to clipboard
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
// alert(message);
|
||||
pushLog(message);
|
||||
} else {
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
pushLog(message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
📣 share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:bg-gray-300 px-2')}>
|
||||
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
||||
🚀 open
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:bg-gray-300 px-2')}>
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.href = initialUrl;
|
||||
window.location.reload();
|
||||
}}
|
||||
title="Reset"
|
||||
>
|
||||
💔 reset
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
<section className="grow flex flex-col text-gray-100">
|
||||
<div className="grow relative flex overflow-auto" id="code">
|
||||
<div className="grow relative flex overflow-auto pb-8 cursor-text" id="code">
|
||||
{/* onCursor={markParens} */}
|
||||
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
|
||||
<span className="z-[20] py-1 px-2 absolute top-0 right-0 text-xs whitespace-pre text-right pointer-events-none">
|
||||
<span className="z-[20] bg-black rounded-t-md py-1 px-2 fixed bottom-0 right-1 text-xs whitespace-pre text-right pointer-events-none">
|
||||
{!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'}
|
||||
</span>
|
||||
{error && (
|
||||
<div className={cx('absolute right-2 bottom-2 px-2', 'text-red-500')}>
|
||||
<div
|
||||
className={cx(
|
||||
'rounded-md fixed pointer-events-none left-2 bottom-1 text-xs bg-black px-2 z-[20]',
|
||||
'text-red-500',
|
||||
)}
|
||||
>
|
||||
{error?.message || 'unknown error'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isEmbedded && (
|
||||
{!isEmbedded && !hideConsole && (
|
||||
<textarea
|
||||
className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none"
|
||||
value={log}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Pattern, toMidi } from '@strudel.cycles/core';
|
||||
import { samples } from '@strudel.cycles/webaudio';
|
||||
|
||||
export function prebake() {
|
||||
export async function prebake(isMock = false) {
|
||||
samples(
|
||||
{
|
||||
piano: {
|
||||
@@ -41,9 +41,11 @@ export function prebake() {
|
||||
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
|
||||
'./piano/',
|
||||
);
|
||||
fetch('EmuSP12.json')
|
||||
.then(res => res.json())
|
||||
.then(json => samples(json, './EmuSP12/'));
|
||||
if (!isMock) {
|
||||
await fetch('EmuSP12.json')
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, './EmuSP12/'));
|
||||
}
|
||||
}
|
||||
|
||||
const maxPan = toMidi('C8');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// import * as tunes from './tunes.mjs';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { extend } from '@strudel.cycles/eval';
|
||||
import { evalScope } from '@strudel.cycles/eval';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import * as webaudio from '@strudel.cycles/webaudio';
|
||||
import controls from '@strudel.cycles/core/controls.mjs';
|
||||
@@ -123,10 +123,10 @@ const uiHelpersMocked = {
|
||||
backgroundImage: id,
|
||||
};
|
||||
|
||||
prebake();
|
||||
prebake(true);
|
||||
|
||||
// TODO: refactor to evalScope
|
||||
extend(
|
||||
evalScope(
|
||||
// Tone,
|
||||
strudel,
|
||||
strudel.Pattern.prototype.bootstrap(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,14 @@
|
||||
import { queryCode, testCycles } from '../runtime.mjs';
|
||||
import * as snaps from '../tunes.snapshot.mjs';
|
||||
import * as tunes from '../tunes.mjs';
|
||||
import { strict as assert } from 'assert';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
async function testTune(key) {
|
||||
// console.log('test tune', key);
|
||||
const haps = await queryCode(tunes[key], testCycles[key] || 1);
|
||||
assert.deepStrictEqual(haps, snaps[key]);
|
||||
}
|
||||
const tuneKeys = Object.keys(tunes);
|
||||
|
||||
describe('renders tunes', () => {
|
||||
Object.keys(tunes).forEach((key) => {
|
||||
it(`tune: ${key}`, async () => {
|
||||
await testTune(key);
|
||||
tuneKeys.forEach((key) => {
|
||||
it(`tune: ${key}`, async ({ expect }) => {
|
||||
const haps = await queryCode(tunes[key], testCycles[key] || 1);
|
||||
expect(haps).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+13
-2
@@ -306,7 +306,7 @@ const drums = stack(
|
||||
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
|
||||
const synths = stack(
|
||||
"<eb4 d4 c4 b3>/2".scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).struct("[~ x]*2")
|
||||
.edit(
|
||||
.layer(
|
||||
scaleTranspose(0).early(0),
|
||||
scaleTranspose(2).early(1/8),
|
||||
scaleTranspose(7).early(1/4),
|
||||
@@ -517,7 +517,7 @@ const snare = noise({type:'white',...adsr(0,0.2,0)}).chain(lowpass(5000),vol(1.8
|
||||
const s = polysynth().set({...osc('sawtooth4'),...adsr(0.01,.2,.6,0.2)}).chain(vol(.23).connect(delay),out());
|
||||
stack(
|
||||
stack(
|
||||
"0 1 4 [3!2 5]".edit(
|
||||
"0 1 4 [3!2 5]".layer(
|
||||
// chords
|
||||
x=>x.add("0,3").duration("0.05!3 0.02"),
|
||||
// bass
|
||||
@@ -1019,3 +1019,14 @@ x=>x.add(7).color('steelblue')
|
||||
.stack(s("bd:1*2,~ sd:0,[~ hh:0]*2"))
|
||||
.out()
|
||||
.pianoroll({vertical:1})`;
|
||||
|
||||
export const bossaRandom = `const chords = "<Am7 Am7 Dm7 E7>"
|
||||
const roots = chords.rootNotes(2)
|
||||
|
||||
stack(
|
||||
chords.voicings(['F4', 'A5']).struct(
|
||||
\` x@2 ~ x ~ ~ ~ x |
|
||||
x? ~ ~ x@3 ~ x |
|
||||
x? ~ ~ x ~ x@3\`),
|
||||
roots.struct("x [~ x?0.2] x [~ x?] | x!4 | x@2 ~ ~ ~ x x x").transpose("0 7")
|
||||
).slow(2).pianoroll().note().piano().out();`;
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
@import '@strudel.cycles/react/dist/style.css';
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
+17
-15
@@ -32,14 +32,14 @@ const drums = stack(
|
||||
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
|
||||
const synths = stack(
|
||||
"<eb4 d4 c4 b3>/2".scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).struct("[~ x]\*2")
|
||||
.edit(
|
||||
.layer(
|
||||
scaleTranspose(0).early(0),
|
||||
scaleTranspose(2).early(1/8),
|
||||
scaleTranspose(7).early(1/4),
|
||||
scaleTranspose(8).early(3/8)
|
||||
).edit(thru).tone(keys).bypass("<1 0>/16"),
|
||||
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).edit(thru).tone(bass),
|
||||
"<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.1 ~]".fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4))
|
||||
).layer(thru).tone(keys).bypass("<1 0>/16"),
|
||||
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).layer(thru).tone(bass),
|
||||
"<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.1 ~]".fast(2)).voicings().layer(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4))
|
||||
)
|
||||
stack(
|
||||
drums.fast(2),
|
||||
@@ -175,15 +175,23 @@ Using "!" we can repeat without speeding up:
|
||||
|
||||
In essence, the `x!n` is like a shortcut for `[x*n]@n`.
|
||||
|
||||
## Euclidian
|
||||
|
||||
Using round brackets, we can create rhythmical sub-divisions based on three parameters: beats, segments and offset.
|
||||
The first parameter controls how may beats will be played.
|
||||
The second parameter controls the total amount of segments the beats will be distributed over.
|
||||
The third (optional) parameter controls the starting position for distributing the beats.
|
||||
One popular Euclidian rhythm (going by various names, such as "Pop Clave") is "(3,8,1)" or simply "(3,8)",
|
||||
resulting in a rhythmical structure of "x ~ ~ x ~ ~ x ~" (3 beats over 8 segments, starting on position 1).
|
||||
|
||||
<MiniRepl tune={`"e5(2,8) b4(3,8) d5(2,8) c5(3,8)".slow(4)`} />
|
||||
|
||||
## Mini Notation TODO
|
||||
|
||||
Compared to [tidal mini notation](https://tidalcycles.org/docs/patternlib/tutorials/mini_notation/), the following mini notation features are missing from Strudel:
|
||||
|
||||
- [x] Euclidean algorithm "c3(3,2,1)" TODO: document
|
||||
- [ ] Tie symbols "\_"
|
||||
- [ ] feet marking "."
|
||||
- [ ] random choice "|"
|
||||
- [ ] Random removal "?"
|
||||
- [ ] Polymetric sequences "{ ... }"
|
||||
- [ ] Fixed steps using "%"
|
||||
|
||||
@@ -436,12 +444,6 @@ Applies the given function by the given time offset:
|
||||
|
||||
<MiniRepl tune={`"c3 eb3 g3".off(1/8, add(7))`} />
|
||||
|
||||
### append(pat)
|
||||
|
||||
Appends the given pattern after the current pattern:
|
||||
|
||||
<MiniRepl tune={`"c4,eb4,g4".append("c4,f4,ab4")`} />
|
||||
|
||||
### stack(pat)
|
||||
|
||||
Stacks the given pattern to the current pattern:
|
||||
@@ -643,10 +645,10 @@ Turns chord symbols into root notes of chords in given octave.
|
||||
|
||||
<MiniRepl tune={`"<C^7 A7b13 Dm7 G7>".rootNotes(3)`} />
|
||||
|
||||
Together with edit, struct and voicings, this can be used to create a basic backing track:
|
||||
Together with layer, struct and voicings, this can be used to create a basic backing track:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"<C^7 A7b13 Dm7 G7>".edit(
|
||||
tune={`"<C^7 A7b13 Dm7 G7>".layer(
|
||||
x => x.voicings(['d3','g4']).struct("~ x"),
|
||||
x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out()))
|
||||
)`}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { configDefaults, defineConfig } from 'vitest/config';
|
||||
|
||||
/// <reference types="vitest" />
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// ...
|
||||
deps: {
|
||||
registerNodeLoader: true,
|
||||
},
|
||||
threads: false,
|
||||
reporters: 'verbose',
|
||||
isolate: false,
|
||||
silent: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user