start packaging

This commit is contained in:
Felix Roos 2022-03-25 14:39:25 +01:00
parent a19d789145
commit 6f60a3a1d5
98 changed files with 11162 additions and 11122 deletions

29
packages/core/euclid.mjs Normal file
View file

@ -0,0 +1,29 @@
import { Pattern, timeCat } from './strudel.mjs';
import bjork from 'bjork';
import { rotate } from '../../packages/core/util.mjs';
import Fraction from './fraction.mjs';
const euclid = (pulses, steps, rotation = 0) => {
const b = bjork(steps, pulses);
if (rotation) {
return rotate(b, -rotation);
}
return b;
};
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
return this.struct(euclid(pulses, steps, rotation));
};
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
const bin_pat = euclid(pulses, steps, rotation);
const firstOne = bin_pat.indexOf(1);
const gapless = rotate(bin_pat, firstOne)
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
};
export default euclid;

View file

@ -0,0 +1,84 @@
import Fraction from 'fraction.js';
import { TimeSpan } from './strudel.mjs';
// Returns the start of the cycle.
Fraction.prototype.sam = function () {
return this.floor();
};
// Returns the start of the next cycle.
Fraction.prototype.nextSam = function () {
return this.sam().add(1);
};
// Returns a TimeSpan representing the begin and end of the Time value's cycle
Fraction.prototype.wholeCycle = function () {
return new TimeSpan(this.sam(), this.nextSam());
};
Fraction.prototype.lt = function (other) {
return this.compare(other) < 0;
};
Fraction.prototype.gt = function (other) {
return this.compare(other) > 0;
};
Fraction.prototype.lte = function (other) {
return this.compare(other) <= 0;
};
Fraction.prototype.gte = function (other) {
return this.compare(other) >= 0;
};
Fraction.prototype.eq = function (other) {
return this.compare(other) == 0;
};
Fraction.prototype.max = function (other) {
return this.gt(other) ? this : other;
};
Fraction.prototype.min = function (other) {
return this.lt(other) ? this : other;
};
Fraction.prototype.show = function () {
return this.s * this.n + '/' + this.d;
};
Fraction.prototype.or = function (other) {
return this.eq(0) ? other : this;
};
const fraction = (n) => {
if (typeof n === 'number') {
/*
https://github.com/infusion/Fraction.js/#doubles
If you pass a double as it is, Fraction.js will perform a number analysis based on Farey Sequences."
If you want to keep the number as it is, convert it to a string, as the string parser will not perform any further observations
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
*/
n = String(n);
}
return Fraction(n);
};
export default fraction;
// "If you concern performance, cache Fraction.js objects and pass arrays/objects.“
// -> tested memoized version, but it's slower than unmemoized, even with repeated evaluation
/* const memo = {};
const memoizedFraction = (n) => {
if (typeof n === 'number') {
n = String(n);
}
if (memo[n] !== undefined) {
return memo[n];
}
memo[n] = Fraction(n);
return memo[n];
}; */

6
packages/core/gist.js Normal file
View file

@ -0,0 +1,6 @@
// this is a shortcut to eval code from a gist
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
export default (route) =>
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${Date.now()}`)
.then((res) => res.text())
.then((code) => eval(code));

0
packages/core/index.mjs Normal file
View file

1654
packages/core/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
{
"name": "@strudel/core",
"version": "0.0.1",
"description": "Port of Tidal Cycles to JavaScript",
"main": "strudel.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0",
"ramda": "^0.28.0"
},
"devDependencies": {
"mocha": "^9.2.2"
}
}

1077
packages/core/strudel.mjs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,475 @@
import Fraction from 'fraction.js'
import { strict as assert } from 'assert';
import {
TimeSpan,
Hap,
State,
Pattern,
pure,
stack,
fastcat,
slowcat,
cat,
sequence,
polyrhythm,
silence,
fast,
timeCat,
add,
sub,
mul,
div,
saw,
saw2,
isaw,
isaw2,
sine,
sine2,
square,
square2,
tri,
tri2,
} from '../strudel.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)
const third = Fraction(1,3)
const twothirds = Fraction(2,3)
describe('TimeSpan', function() {
describe('equals()', function() {
it('Should be equal to the same value', function() {
assert.equal((new TimeSpan(0,4)).equals(new TimeSpan(0,4)), true);
});
});
describe('splitCycles', function() {
it('Should split two cycles into two', function() {
assert.equal(new TimeSpan(Fraction(0),Fraction(2)).spanCycles.length, 2)
})
})
describe('intersection_e', function () {
var a = new TimeSpan(Fraction(0), Fraction(2))
var b = new TimeSpan(Fraction(1), Fraction(3))
var c = new TimeSpan(Fraction(1), Fraction(2))
var d = new TimeSpan(Fraction(1), Fraction(2))
it('Should create an intersection', function () {
assert.equal(a.intersection_e(b).equals(c), true)
})
})
});
describe('Hap', function() {
describe('hasOnset()', function() {
it('True if part includes onset from whole', function() {
assert.equal(new Hap(new TimeSpan(0,1), new TimeSpan(0,1), "thing").hasOnset(), true);
});
});
var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), "a")
var b = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), "b")
var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), "c")
var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), "d")
var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), "e")
describe('spanEquals', function() {
it('True if two haps have the same whole and part', function() {
assert.equal(a.spanEquals(b), true)
})
it('False if two haps don\'t the same whole and part', function() {
assert.equal(a.spanEquals(c), false)
})
it('True if two haps have the same part and undefined wholes', function() {
assert.equal(d.spanEquals(e), true)
})
})
describe('resolveState()', () => {
it('Can increment some state', function() {
const stateful_value = state => {
const newValue = state["incrementme"]
// TODO Does the state *need* duplicating here?
const newState = {...state}
newState["incrementme"]++
return [newState, newValue]
}
const state = {incrementme: 10}
const ev1 = new Hap(ts(0,1), ts(0,1), stateful_value, {}, true)
const [state2, ev2] = ev1.resolveState(state)
const [state3, ev3] = ev1.resolveState(state2)
assert.deepStrictEqual(
ev3, new Hap(ts(0,1),ts(0,1),11,{},false)
)
assert.deepStrictEqual(
state3, {incrementme: 12}
)
})
})
});
describe('Pattern', function() {
describe('pure', function () {
it('Can make a pattern', function() {
assert.equal(pure("hello").query(st(0.5, 2.5)).length, 3)
})
})
describe('fmap()', function () {
it('Can add things', function () {
assert.equal(pure(3).fmap(x => x + 4).firstCycle()[0].value, 7)
})
})
describe('add()', function () {
it('Can add things', function() {
assert.equal(pure(3).add(pure(4)).query(st(0,1))[0].value, 7)
})
})
describe('sub()', function () {
it('Can subtract things', function() {
assert.equal(pure(3).sub(pure(4)).query(st(0,1))[0].value, -1)
})
})
describe('mul()', function () {
it('Can multiply things', function() {
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6)
})
})
describe('div()', function () {
it('Can divide things', function() {
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5)
})
})
describe('union()', function () {
it('Can union things', function () {
assert.deepStrictEqual(pure({a: 4, b: 6}).union(pure({c: 7})).firstCycle()[0].value, {a: 4, b: 6, c: 7})
})
})
describe('stack()', function () {
it('Can stack things', function () {
assert.deepStrictEqual(stack(pure("a"), pure("b"), pure("c")).firstCycle().map(h => h.value), ["a", "b", "c"])
})
})
describe('_fast()', function () {
it('Makes things faster', function () {
assert.equal(pure("a")._fast(2).firstCycle().length, 2)
})
})
describe('_fastGap()', function () {
it('Makes things faster, with a gap', function () {
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).firstCycle(),
sequence(["a","b","c"], silence).firstCycle()
)
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(3).firstCycle(),
sequence(["a","b","c"], silence, silence).firstCycle()
)
})
it('Makes things faster, with a gap, when speeded up further', function () {
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).fast(2).firstCycle(),
sequence(["a","b","c"], silence, ["a","b","c"], silence).firstCycle()
)
})
})
describe('_compressSpan()', function () {
it('Can squash cycles of a pattern into a given timespan', function () {
assert.deepStrictEqual(
pure("a")._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
sequence(silence, "a", silence, silence).firstCycle()
)
})
})
describe('fast()', function () {
it('Makes things faster', function () {
assert.equal(pure("a").fast(2).firstCycle().length, 2)
})
it('Makes things faster, with a pattern of factors', function () {
assert.equal(pure("a").fast(sequence(1,4)).firstCycle().length, 3)
// .fast(sequence(1,silence) is a quick hack to cut an event in two..
assert.deepStrictEqual(pure("a").fast(sequence(1,4)).firstCycle(), stack(pure("a").fast(sequence(1,silence)), sequence(silence, ["a","a"])).firstCycle())
})
it('defaults to accepting sequences', function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(sequence(1.5,2)).firstCycle(),
sequence(1,2,3).fast(1.5,2).firstCycle()
)
})
it("works as a static function", function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2), sequence(1,2,3)).firstCycle()
)
})
it("works as a curried static function", function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2))(sequence(1,2,3)).firstCycle()
)
})
})
describe('_slow()', function () {
it('Makes things slower', function () {
assert.deepStrictEqual(pure("a")._slow(2).firstCycle()[0], new Hap(new TimeSpan(Fraction(0),Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), "a"))
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")
)
// the following test fails
/* assert.deepStrictEqual(
pat.query(ts(1,2))[1], undefined
) */
// expecting [c3 eb3] [c3 ~]
// what happens [c3 eb3] [c3 eb3]
// notable examples:
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
})
})
describe('_filterValues()', function () {
it('Filters true', function () {
assert.equal(pure(true)._filterValues(x => x).firstCycle().length, 1)
})
})
describe('when()', function () {
it('Always faster', function () {
assert.equal(pure("a").when(pure(true), x => x._fast(2)).firstCycle().length, 2)
})
it('Never faster', function () {
assert.equal(pure("a").when(pure(false), x => x._fast(2)).firstCycle().length, 1)
})
it('Can alternate', function () {
assert.deepStrictEqual(
pure(10).when(slowcat(true,false),add(3)).fast(4)._sortEventsByPart().firstCycle(),
fastcat(13,10,13,10).firstCycle()
)
})
})
describe('fastcat()', function () {
it('Can concatenate two things', function () {
assert.deepStrictEqual(fastcat(pure("a"), pure("b")).firstCycle().map(x => x.value), ["a", "b"])
})
})
describe('slowcat()', function () {
it('Can concatenate things slowly', function () {
assert.deepStrictEqual(slowcat("a", "b").firstCycle().map(x => x.value), ["a"])
assert.deepStrictEqual(slowcat("a", "b")._early(1).firstCycle().map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(1).firstCycle().map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(3).firstCycle().map(x => x.value), ["c"])
})
})
describe('rev()', function () {
it('Can reverse things', function () {
assert.deepStrictEqual(fastcat("a","b","c").rev().firstCycle().sort((a,b) => a.part.begin.sub(b.part.begin)).map(a => a.value), ["c", "b","a"])
})
})
describe('sequence()', () => {
it('Can work like fastcat', () => {
assert.deepStrictEqual(sequence(1,2,3).firstCycle(), fastcat(1,2,3).firstCycle())
})
})
describe('polyrhythm()', () => {
it('Can layer up cycles', () => {
assert.deepStrictEqual(
polyrhythm(["a","b"],["c"]).firstCycle(),
stack(fastcat(pure("a"),pure("b")),pure("c")).firstCycle()
)
})
})
describe('every()', () => {
it('Can apply a function every 3rd time', () => {
assert.deepStrictEqual(
pure("a").every(3, x => x._fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle()
)
})
it("works with currying", () => {
assert.deepStrictEqual(
pure("a").every(3, fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle()
)
assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).fast(5).firstCycle(),
sequence(6,7,8,3,4,5,3,4,5,6,7,8,3,4,5).firstCycle()
)
assert.deepStrictEqual(
sequence(3,4,5).every(2, sub(1)).fast(5).firstCycle(),
sequence(2,3,4,3,4,5,2,3,4,3,4,5,2,3,4).firstCycle()
)
assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
sequence(5,6,7,3,4,5).firstCycle()
)
})
})
describe('timeCat()', function() {
it('Can concatenate patterns with different relative durations', function() {
assert.deepStrictEqual(
sequence("a", ["a", "a"]).firstCycle(),
timeCat([1,"a"], [0.5, "a"], [0.5, "a"]).firstCycle()
)
})
})
describe('struct()', function() {
it('Can restructure a pattern', function() {
assert.deepStrictEqual(
sequence("a", "b").struct(sequence(true, true, true)).firstCycle(),
[hap(ts(0,third), ts(0,third), "a"),
hap(ts(third, twothirds), ts(third, 0.5), "a"),
hap(ts(third, twothirds), ts(0.5, twothirds), "b"),
hap(ts(twothirds, 1), ts(twothirds, 1), "b")
]
)
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle(),
)
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true).invert()).firstCycle(),
sequence(silence, [silence, "a"], silence).firstCycle(),
)
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,silence], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle(),
)
})
})
describe('mask()', function() {
it('Can fragment a pattern', function() {
assert.deepStrictEqual(
sequence("a", "b").mask(sequence(true, true, true)).firstCycle(),
[hap(ts(0, 0.5), ts(0,third), "a"),
hap(ts(0, 0.5), ts(third, 0.5), "a"),
hap(ts(0.5, 1), ts(0.5, twothirds), "b"),
hap(ts(0.5, 1), ts(twothirds, 1), "b")
]
)
})
it('Can mask off parts of a pattern', function() {
assert.deepStrictEqual(
sequence(["a", "b"], "c").mask(sequence(true, false)).firstCycle(),
sequence(["a","b"], silence).firstCycle()
)
assert.deepStrictEqual(
sequence("a").mask(sequence(true, false)).firstCycle(),
[hap(ts(0,1),ts(0,0.5), "a")]
)
})
})
describe('invert()', function() {
it('Can invert a binary pattern', function() {
assert.deepStrictEqual(
sequence(true, false, [true, false]).invert().firstCycle(),
sequence(false, true, [false, true]).firstCycle()
)
})
})
describe('signal()', function() {
it('Can make saw/saw2', function() {
assert.deepStrictEqual(
saw.struct(true,true,true,true).firstCycle(),
sequence(1/8,3/8,5/8,7/8).firstCycle()
)
assert.deepStrictEqual(
saw2.struct(true,true,true,true).firstCycle(),
sequence(-3/4,-1/4,1/4,3/4).firstCycle()
)
})
it('Can make isaw/isaw2', function() {
assert.deepStrictEqual(
isaw.struct(true,true,true,true).firstCycle(),
sequence(7/8,5/8,3/8,1/8).firstCycle()
)
assert.deepStrictEqual(
isaw2.struct(true,true,true,true).firstCycle(),
sequence(3/4,1/4,-1/4,-3/4).firstCycle()
)
})
})
describe('_setContext()', () => {
it('Can set the event context', () => {
assert.deepStrictEqual(
pure("a")._setContext([[[0,1],[1,2]]]).firstCycle(true),
[hap(ts(0,1),
ts(0,1),
"a",
[[[0,1],[1,2]]]
)
]
)
})
})
describe('_withContext()', () => {
it('Can update the event context', () => {
assert.deepStrictEqual(
pure("a")._setContext([[[0,1],[1,2]]])._withContext(c => [...c,[[3,4],[3,4]]]).firstCycle(true),
[hap(ts(0,1),
ts(0,1),
"a",
[[[0,1],[1,2]],[[3,4],[3,4]]]
)
]
)
})
})
describe("apply", () => {
it('Can apply a function', () => {
assert.deepStrictEqual(
sequence("a", "b")._apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle()
)
}),
it('Can apply a pattern of functions', () => {
assert.deepStrictEqual(
sequence("a", "b").apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle()
)
assert.deepStrictEqual(
sequence("a", "b").apply(fast(2),fast(3)).firstCycle(),
sequence("a", "b").fast(2,3).firstCycle()
)
})
})
describe("layer", () => {
it('Can layer up multiple functions', () => {
assert.deepStrictEqual(
sequence(1,2,3).layer(fast(2), pat => pat.add(3,4)).firstCycle(),
stack(sequence(1,2,3).fast(2), sequence(1,2,3).add(3,4)).firstCycle()
)
})
})
describe("early", () => {
it("Can shift an event earlier", () => {
assert.deepStrictEqual(
pure(30)._late(0.25).query(st(1,2)),
[hap(ts(1/4,5/4), ts(1,5/4), 30), hap(ts(5/4,9/4), ts(5/4,2), 30)]
)
})
it("Can shift an event earlier, into negative time", () => {
assert.deepStrictEqual(
pure(30)._late(0.25).query(st(0,1)),
[hap(ts(-3/4,1/4), ts(0,1/4), 30), hap(ts(1/4,5/4), ts(1/4,1), 30)]
)
})
})
describe("off", () => {
it("Can offset a transformed pattern from the original", () => {
assert.deepStrictEqual(
pure(30).off(0.25, add(2)).firstCycle(),
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle()
)
})
})
describe("jux", () => {
it("Can juxtapose", () => {
assert.deepStrictEqual(
pure({a: 1}).jux(fast(2))._sortEventsByPart().firstCycle(),
stack(pure({a:1, pan: 0}), pure({a:1, pan: 1}).fast(2))._sortEventsByPart().firstCycle()
)
})
})
})

View file

@ -0,0 +1,85 @@
import { strict as assert } from 'assert';
import { isNote, tokenizeNote, toMidi, mod } from '../util.mjs';
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);
});
});
it('should recognize notes with accidentals', () => {
'C#3 D##3 Eb3 Fbb3 Bb5'.split(' ').forEach((note) => {
assert.equal(isNote(note), 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);
});
});
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]);
});
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]);
});
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]);
});
it('should not tokenize invalid notes', () => {
assert.deepStrictEqual(tokenizeNote('X'), []);
assert.deepStrictEqual(tokenizeNote('asfasf'), []);
assert.deepStrictEqual(tokenizeNote(123), []);
});
});
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);
});
});
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);
});
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);
});
});

View file

@ -0,0 +1,18 @@
import { strict as assert } from 'assert';
import { map, valued, mul } from '../value.mjs';
describe('Value', () => {
it('unionWith', () => {
const { value } = valued({ freq: 2000, distortion: 1.2 }).unionWith({ distortion: 2 }, mul);
assert.deepStrictEqual(value, { 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);
});
});

44
packages/core/util.mjs Normal file
View file

@ -0,0 +1,44 @@
// returns true if the given string is a note
export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name);
export const tokenizeNote = (note) => {
if (typeof note !== 'string') {
return [];
}
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#b]*)([0-9])?$/)?.slice(1) || [];
if (!pc) {
return [];
}
return [pc, acc, oct ? Number(oct) : undefined];
};
// turns the given note into its midi number representation
export const toMidi = (note) => {
const [pc, acc, oct] = tokenizeNote(note);
if (!pc) {
throw new Error('not a note: "' + note + '"');
}
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset;
};
export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
export const mod = (n, m) => (n < 0 ? mod(n + m, m) : n % m);
export const getPlayableNoteValue = (event) => {
let { value: note, context } = event;
// 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(event.value);
} else if (typeof note === 'string' && !isNote(note)) {
throw new Error('not a note: ' + note);
}
return note;
};
// rotate array by n steps (to the left)
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));

52
packages/core/value.mjs Normal file
View file

@ -0,0 +1,52 @@
import { curry } from 'ramda';
function unionWithObj(a, b, func) {
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
}
export const mul = curry((a, b) => a * b);
export const valued = (value) => {
if (value?.constructor?.name === 'Value') {
return value;
}
return Value.of(value);
};
export class Value {
constructor(value) {
this.value = value;
}
static of(x) {
return new Value(x);
}
get isNothing() {
return this.value === null || this.value === undefined;
}
map(f) {
if (this.isNothing) {
return this;
}
return Value.of(f(this.value));
}
mul(n) {
return this.map(mul).ap(n);
}
ap(other) {
return valued(other).map(this.value);
}
unionWith(other, func) {
const type = typeof this.value;
other = valued(other);
if (type !== typeof other.value) {
throw new Error('unionWith: both Values must have same type!');
}
if (Array.isArray(type) || type !== 'object') {
throw new Error('unionWith: expected objects');
}
return this.map((v) => unionWithObj(v, other.value, func));
}
}
export const map = curry((f, anyFunctor) => anyFunctor.map(f));

3
packages/eval/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
shift-parser
shift-reducer
shift-traverser

View file

@ -0,0 +1,56 @@
import * as strudel from '../core/strudel.mjs';
import '../tone/tone.mjs';
import '../midi/midi.mjs';
import '../tonal/voicings.mjs';
import '../../packages/tonal/tonal.mjs';
import '../../packages/xen/xen.mjs';
import '../../packages/xen/tune.mjs';
import '../core/euclid.mjs';
import euclid from '../core/euclid.mjs';
import '../repl-react/pianoroll.mjs';
import '../repl-react/draw.mjs';
import * as uiHelpers from '../repl-react/ui.mjs';
import * as drawHelpers from '../repl-react/draw.mjs';
import gist from '../core/gist.js';
import shapeshifter from './shapeshifter.mjs';
import { mini } from '../mini/mini.mjs';
import * as Tone from 'tone';
import * as toneHelpers from '../tone/tone.mjs';
import * as voicingHelpers from '../tonal/voicings.mjs';
// this will add all methods from definedMethod to strudel + connect all the partial application stuff
const bootstrapped = { ...strudel, ...strudel.Pattern.prototype.bootstrap() };
// console.log('bootstrapped',bootstrapped.transpose(2).transpose);
function hackLiteral(literal, names, func) {
names.forEach((name) => {
Object.defineProperty(literal.prototype, name, {
get: function () {
return func(String(this));
},
});
});
}
// with this, you can do 'c2 [eb2 g2]'.mini.fast(2) or 'c2 [eb2 g2]'.m.fast(2),
hackLiteral(String, ['mini', 'm'], bootstrapped.mini); // comment out this line if you panic
hackLiteral(String, ['pure', 'p'], bootstrapped.pure); // comment out this line if you panic
// this will add everything to global scope, which is accessed by eval
Object.assign(globalThis, Tone, bootstrapped, toneHelpers, voicingHelpers, drawHelpers, uiHelpers, {
gist,
euclid,
mini,
});
export const evaluate = async (code) => {
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
drawHelpers.cleanup();
uiHelpers.cleanup();
let evaluated = await eval(shapeshifted);
if (evaluated?.constructor?.name !== 'Pattern') {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
};

304
packages/eval/package-lock.json generated Normal file
View file

@ -0,0 +1,304 @@
{
"name": "@strudel/eval",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel/eval",
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"devDependencies": {
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
"shift-parser": "^7.0.3",
"shift-traverser": "^1.0.0"
}
},
"node_modules/estraverse": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/multimap": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz",
"integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==",
"dev": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/shift-ast": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.1.0.tgz",
"integrity": "sha512-Vj4XUIJIFPIh6VcBGJ1hjH/kM88XGer94Pr7Rvxa+idEylDsrwtLw268HoxGo5xReL6T3DdRl/9/Pr1XihZ/8Q==",
"dev": true
},
"node_modules/shift-codegen": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/shift-codegen/-/shift-codegen-7.0.3.tgz",
"integrity": "sha512-dfCVVdBF0qZ6pkajQ3bjxRdNEltyxEITVe7tBJkQt2eCI3znUkSxq0VSe/tTWq1LKHeAS4HuOiqYEuHMFkSq9w==",
"dev": true,
"dependencies": {
"esutils": "^2.0.2",
"object-assign": "^4.1.0",
"shift-reducer": "6.0.0"
}
},
"node_modules/shift-parser": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
"dev": true,
"dependencies": {
"multimap": "^1.0.2",
"shift-ast": "6.0.0",
"shift-reducer": "6.0.0",
"shift-regexp-acceptor": "2.0.3"
}
},
"node_modules/shift-parser/node_modules/shift-ast": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw==",
"dev": true
},
"node_modules/shift-reducer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
"integrity": "sha512-2rJraRP8drIOjvaE/sALa+0tGJmMVUzlmS3wIJerJbaYuCjpFAiF0WjkTOFVtz1144Nm/ECmqeG+7yRhuMVsMg==",
"dev": true,
"dependencies": {
"shift-ast": "6.0.0"
}
},
"node_modules/shift-reducer/node_modules/shift-ast": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw==",
"dev": true
},
"node_modules/shift-regexp-acceptor": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/shift-regexp-acceptor/-/shift-regexp-acceptor-2.0.3.tgz",
"integrity": "sha512-sxL7e5JNUFxm+gutFRXktX2D6KVgDAHNuDsk5XHB9Z+N5yXooZG6pdZ1GEbo3Jz6lF7ETYLBC4WAjIFm2RKTmA==",
"dev": true,
"dependencies": {
"unicode-match-property-ecmascript": "1.0.4",
"unicode-match-property-value-ecmascript": "1.0.2",
"unicode-property-aliases-ecmascript": "1.0.4"
}
},
"node_modules/shift-spec": {
"version": "2018.0.0",
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ==",
"dev": true
},
"node_modules/shift-traverser": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
"dev": true,
"dependencies": {
"estraverse": "4.2.0",
"shift-spec": "2018.0.0"
}
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
"integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/unicode-match-property-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
"integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
"dev": true,
"dependencies": {
"unicode-canonical-property-names-ecmascript": "^1.0.4",
"unicode-property-aliases-ecmascript": "^1.0.4"
},
"engines": {
"node": ">=4"
}
},
"node_modules/unicode-match-property-value-ecmascript": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
"integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/unicode-property-aliases-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
"integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==",
"dev": true,
"engines": {
"node": ">=4"
}
}
},
"dependencies": {
"estraverse": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
"dev": true
},
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
"multimap": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz",
"integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"shift-ast": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.1.0.tgz",
"integrity": "sha512-Vj4XUIJIFPIh6VcBGJ1hjH/kM88XGer94Pr7Rvxa+idEylDsrwtLw268HoxGo5xReL6T3DdRl/9/Pr1XihZ/8Q==",
"dev": true
},
"shift-codegen": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/shift-codegen/-/shift-codegen-7.0.3.tgz",
"integrity": "sha512-dfCVVdBF0qZ6pkajQ3bjxRdNEltyxEITVe7tBJkQt2eCI3znUkSxq0VSe/tTWq1LKHeAS4HuOiqYEuHMFkSq9w==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"object-assign": "^4.1.0",
"shift-reducer": "6.0.0"
}
},
"shift-parser": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
"dev": true,
"requires": {
"multimap": "^1.0.2",
"shift-ast": "6.0.0",
"shift-reducer": "6.0.0",
"shift-regexp-acceptor": "2.0.3"
},
"dependencies": {
"shift-ast": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw==",
"dev": true
}
}
},
"shift-reducer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
"integrity": "sha512-2rJraRP8drIOjvaE/sALa+0tGJmMVUzlmS3wIJerJbaYuCjpFAiF0WjkTOFVtz1144Nm/ECmqeG+7yRhuMVsMg==",
"dev": true,
"requires": {
"shift-ast": "6.0.0"
},
"dependencies": {
"shift-ast": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw==",
"dev": true
}
}
},
"shift-regexp-acceptor": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/shift-regexp-acceptor/-/shift-regexp-acceptor-2.0.3.tgz",
"integrity": "sha512-sxL7e5JNUFxm+gutFRXktX2D6KVgDAHNuDsk5XHB9Z+N5yXooZG6pdZ1GEbo3Jz6lF7ETYLBC4WAjIFm2RKTmA==",
"dev": true,
"requires": {
"unicode-match-property-ecmascript": "1.0.4",
"unicode-match-property-value-ecmascript": "1.0.2",
"unicode-property-aliases-ecmascript": "1.0.4"
}
},
"shift-spec": {
"version": "2018.0.0",
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ==",
"dev": true
},
"shift-traverser": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
"dev": true,
"requires": {
"estraverse": "4.2.0",
"shift-spec": "2018.0.0"
}
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
"integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
"dev": true
},
"unicode-match-property-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
"integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
"dev": true,
"requires": {
"unicode-canonical-property-names-ecmascript": "^1.0.4",
"unicode-property-aliases-ecmascript": "^1.0.4"
}
},
"unicode-match-property-value-ecmascript": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
"integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==",
"dev": true
},
"unicode-property-aliases-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
"integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==",
"dev": true
}
}
}

View file

@ -0,0 +1,36 @@
{
"name": "@strudel/eval",
"version": "0.0.1",
"description": "Code evaluator for strudel",
"main": "evaluate.mjs",
"directories": {
"test": "test"
},
"scripts": {
"test": "mocha --colors"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"type": "module",
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"devDependencies": {
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
"shift-parser": "^7.0.3",
"shift-traverser": "^1.0.0"
}
}

View file

@ -0,0 +1,265 @@
/* import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */
import { parseScriptWithLocation } from 'shift-parser';
import traverser from 'shift-traverser';
const { replace } = traverser;
import {
LiteralStringExpression,
IdentifierExpression,
CallExpression,
StaticMemberExpression,
ReturnStatement,
ArrayExpression,
LiteralNumericExpression,
} from 'shift-ast';
import shiftCodegen from 'shift-codegen';
const codegen = shiftCodegen.default;
console.log('codegen', codegen);
import * as strudel from '../core/strudel.mjs';
const { Pattern } = strudel;
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export default (_code) => {
const { code, addReturn } = wrapAsync(_code);
const ast = parseScriptWithLocation(code);
const artificialNodes = [];
const parents = [];
const shifted = replace(ast.tree, {
enter(node, parent) {
parents.push(parent);
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
if (isSynthetic) {
return node;
}
// replace template string `xxx` with mini(`xxx`)
if (isBackTickString(node)) {
return minifyWithLocation(node, node, ast.locations, artificialNodes);
}
// allows to use top level strings, which are normally directives... but we don't need directives
if (node.directives?.length === 1 && !node.statements?.length) {
const str = new LiteralStringExpression({ value: node.directives[0].rawValue });
const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes);
return { ...node, directives: [], statements: [wrapped] };
}
// replace double quote string "xxx" with mini('xxx')
if (isStringWithDoubleQuotes(node, ast.locations, code)) {
return minifyWithLocation(node, node, ast.locations, artificialNodes);
}
// operator overloading => still not done
const operators = {
'*': 'fast',
'/': 'slow',
'&': 'stack',
'&&': 'append',
};
if (
node.type === 'BinaryExpression' &&
operators[node.operator] &&
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
canBeOverloaded(node.left)
) {
let arg = node.left;
if (node.left.type === 'IdentifierExpression') {
arg = wrapFunction('reify', node.left);
}
return new CallExpression({
callee: new StaticMemberExpression({
property: operators[node.operator],
object: wrapFunction('reify', arg),
}),
arguments: [node.right],
});
}
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
// add to location to pure(x) calls
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
const literal = node.arguments[0];
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
}
// replace pseudo note variables
if (node.type === 'IdentifierExpression') {
if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
if (addLocations && isMarkable) {
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
}
return new LiteralStringExpression({ value });
}
if (node.name === 'r') {
return new IdentifierExpression({ name: 'silence' });
}
}
if (
addLocations &&
['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) &&
isMarkable
) {
// TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat...
return reifyWithLocation(node, node, ast.locations, artificialNodes);
}
if (addMiniLocations) {
return addMiniNotationLocations(node, ast.locations, artificialNodes);
}
return node;
},
leave() {
parents.pop();
},
});
// add return to last statement (because it's wrapped in an async function artificially)
addReturn(shifted);
const generated = codegen(shifted);
return generated;
};
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
code = `(async () => {
${code}
})()`;
const addReturn = (ast) => {
const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body
body.statements = body.statements
.slice(0, -1)
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
};
return {
code,
addReturn,
};
}
function addMiniNotationLocations(node, locations, artificialNodes) {
const miniFunctions = ['mini', 'm'];
// const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) {
// mini('c3')
if (node.arguments.length > 1) {
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
console.warn('multi arg mini locations not supported yet...');
return node;
}
const str = node.arguments[0];
return minifyWithLocation(str, str, locations, artificialNodes);
}
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) {
// 'c3'.mini or 'c3'.m
return minifyWithLocation(node.object, node, locations, artificialNodes);
}
return node;
}
function wrapFunction(name, ...args) {
return new CallExpression({
callee: new IdentifierExpression({ name }),
arguments: args,
});
}
function isBackTickString(node) {
return node.type === 'TemplateExpression' && node.elements.length === 1;
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'LiteralStringExpression') {
return false;
}
const loc = locations.get(node);
const snippet = code.slice(loc.start.offset, loc.end.offset);
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
}
// returns true if the given parents belong to a pattern argument node
// this is used to check if a node should receive a location for highlighting
function isPatternArg(parents) {
if (!parents.length) {
return false;
}
const ancestors = parents.slice(0, -1);
const parent = parents[parents.length - 1];
if (isPatternFactory(parent)) {
return true;
}
if (parent?.type === 'ArrayExpression') {
return isPatternArg(ancestors);
}
return false;
}
function hasModifierCall(parent) {
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
// need all prototypes of Pattern
return (
parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property)
);
}
function isPatternFactory(node) {
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
}
function canBeOverloaded(node) {
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
// TODO: support sequence(c3).transpose(3).x.y.z
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
const args = getLocationArguments(node, locations);
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: wrapFunction('reify', literalNode),
property: 'withLocation',
}),
arguments: args,
});
artificialNodes.push(withLocation);
return withLocation;
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function minifyWithLocation(literalNode, node, locations, artificialNodes) {
const args = getLocationArguments(node, locations);
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: wrapFunction('mini', literalNode),
property: 'withMiniLocation',
}),
arguments: args,
});
artificialNodes.push(withLocation);
return withLocation;
}
function getLocationArguments(node, locations) {
const loc = locations.get(node);
return [
new ArrayExpression({
elements: [
new LiteralNumericExpression({ value: loc.start.line - 1 }), // the minus 1 assumes the code has been wrapped in async iife
new LiteralNumericExpression({ value: loc.start.column }),
new LiteralNumericExpression({ value: loc.start.offset }),
],
}),
new ArrayExpression({
elements: [
new LiteralNumericExpression({ value: loc.end.line - 1 }), // the minus 1 assumes the code has been wrapped in async iife
new LiteralNumericExpression({ value: loc.end.column }),
new LiteralNumericExpression({ value: loc.end.offset }),
],
}),
];
}

View file

@ -0,0 +1,9 @@
/* import { strict as assert } from 'assert';
import evaluate from '../evaluate.mjs';
describe('evaluate', () => {
it('Should evaluate simple double quote string', () => {
assert.deepStrictEqual(evaluate('"c3"'), pure('c3'));
});
});
*/

View file

@ -0,0 +1,8 @@
import { strict as assert } from 'assert';
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])})()');
});
});

78
packages/midi/midi.mjs Normal file
View file

@ -0,0 +1,78 @@
import { isNote } from 'tone';
import _WebMidi from 'webmidi';
import { Pattern as _Pattern } from '../core/strudel.mjs';
import * as Tone from 'tone';
const WebMidi = _WebMidi;
const Pattern = _Pattern;
export default function enableWebMidi() {
return new Promise((resolve, reject) => {
if (WebMidi.enabled) {
// if already enabled, just resolve WebMidi
resolve(WebMidi);
return;
}
WebMidi.enable((err) => {
if (err) {
reject(err);
}
resolve(WebMidi);
});
});
}
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
Pattern.prototype.midi = function (output, channel = 1) {
if (output?.constructor?.name === 'Pattern') {
throw new Error(
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`
);
}
return this._withEvent((event) => {
// const onTrigger = (time: number, event: any) => {
const onTrigger = (time, event) => {
let note = event.value;
const velocity = event.context?.velocity ?? 0.9;
if (!isNote(note)) {
throw new Error('not a note: ' + note);
}
if (!WebMidi.enabled) {
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
}
if (!WebMidi.outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
let device;
if (typeof output === 'number') {
device = WebMidi.outputs[output];
} else if (typeof output === 'string') {
device = outputByName(output);
} else {
device = WebMidi.outputs[0];
}
if (!device) {
throw new Error(
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
.map((o) => `'${o.name}'`)
.join(' | ')}`
);
}
// console.log('midi', value, output);
const timingOffset = WebMidi.time - Tone.context.currentTime * 1000;
time = time * 1000 + timingOffset;
// const inMs = '+' + (time - Tone.context.currentTime) * 1000;
// await enableWebMidi()
device.playNote(note, channel, {
time,
duration: event.duration * 1000 - 5,
velocity,
});
};
return event.setContext({ ...event.context, onTrigger });
});
};

File diff suppressed because it is too large Load diff

211
packages/mini/krill.pegjs Normal file
View file

@ -0,0 +1,211 @@
// Some terminology:
// 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
{
var PatternStub = function(source, alignment)
{
this.type_ = "pattern";
this.arguments_ = { alignment : alignment};
this.source_ = source;
}
var OperatorStub = function(name, args, source)
{
this.type_ = name;
this.arguments_ = args;
this.source_ = source;
}
var ElementStub = function(source, options)
{
this.type_ = "element";
this.source_ = source;
this.options_ = options;
this.location_ = location();
}
var CommandStub = function(name, options)
{
this.type_ = "command";
this.name_ = name;
this.options_ = options;
}
}
start = statement
// ----- Numbers -----
number "number"
= minus? int frac? exp? { return parseFloat(text()); }
decimal_point
= "."
digit1_9
= [1-9]
e
= [eE]
exp
= e (minus / plus)? DIGIT+
frac
= decimal_point DIGIT+
int
= zero / (digit1_9 DIGIT*)
minus
= "-"
plus
= "+"
zero
= "0"
DIGIT = [0-9]
// ------------------ delimiters ---------------------------
ws "whitespace" = [ \n\r\t]*
comma = ws "," ws;
quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
// single step definition (e.g bd)
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}
// 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
{ sc.arguments_.alignment = "t"; return sc;}
// a slice is either a single step or a sub cycle
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_weight = "@" a:number
{ return { weight: a} }
slice_replicate = "!"a:number
{ return { replicate: a } }
slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws ")"
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }
slice_slow = "/"a:number
{ return { operator : { type_: "stretch", arguments_ :{ amount:a } } } }
slice_fast = "*"a:number
{ return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } }
slice_fixed_step = "%"a:number
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
// 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);}
// a single cycle is a combination of one or more successive slices (as an array). If we
// have only one element, we skip the array and return the element itself
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
// 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");} }
// a sequence is a quoted stack
sequence = ws quote s:stack quote
{ return s; }
// ------------------ operators ---------------------------
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
struct = "struct" ws s:sequence_or_operator
{ return { name: "struct", args: { sequence:s }}}
target = "target" ws quote s:step quote
{ return { name: "target", args : { name:s}}}
bjorklund = "euclid" ws p:int ws s:int ws r:int?
{ return { name: "bjorklund", args :{ pulse: parseInt(p), step:parseInt(s) }}}
slow = "slow" ws a:number
{ return { name: "stretch", args :{ amount: a}}}
rotL = "rotL" ws a:number
{ return { name: "shift", args :{ amount: "-"+a}}}
rotR = "rotR" ws a:number
{ return { name: "shift", args :{ amount: a}}}
fast = "fast" ws a:number
{ return { name: "stretch", args :{ amount: "1/"+a}}}
scale = "scale" ws quote s:(step_char)+ quote
{ return { name: "scale", args :{ scale: s.join("")}}}
comment = '//' p:([^\n]*)
// ---------------- grouping --------------------------------
group_operator = cat
// cat is another form of timeline
cat = "cat" ws "[" ws s:sequence_or_operator ss:(comma v:sequence_or_operator { return v})* ws "]"
{ ss.unshift(s); return new PatternStub(ss,"t"); }
// ------------------ high level sequence ---------------------------
sequence_or_group =
group_operator /
sequence
sequence_or_operator =
sg:sequence_or_group ws (comment)*
{return sg}
/ o:operator ws "$" ws soc:sequence_or_operator
{ return new OperatorStub(o.name,o.args,soc)}
sequ_or_operator_or_comment =
sc: sequence_or_operator
{ return sc }
/ comment
sequence_definition = s:sequ_or_operator_or_comment
// ---------------------- statements ----------------------------
command = ws c:(setcps / setbpm / hush) ws
{ return c }
setcps = "setcps" ws v:number
{ return new CommandStub("setcps", { value: v})}
setbpm = "setbpm" ws v:number
{ return new CommandStub("setcps", { value: (v/120/2)})}
hush = "hush"
{ return new CommandStub("hush")}
// ---------------------- statements ----------------------------
statement = sequence_definition / command

175
packages/mini/mini.mjs Normal file
View file

@ -0,0 +1,175 @@
import * as krill from './krill-parser.js';
import * as strudel from '../core/strudel.mjs';
import { addMiniLocations } from '../eval/shapeshifter.mjs';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence } = strudel;
const applyOptions = (parent) => (pat, i) => {
const ast = parent.source_[i];
const options = ast.options_;
const operator = options?.operator;
if (operator) {
switch (operator.type_) {
case 'stretch':
const speed = Fraction(operator.arguments_.amount).inverse();
return reify(pat).fast(speed);
case 'bjorklund':
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
// TODO: case 'fixed-step': "%"
}
console.warn(`operator "${operator.type_}" not implemented`);
}
if (options?.weight) {
// weight is handled by parent
return pat;
}
// TODO: bjorklund e.g. "c3(5,8)"
const unimplemented = Object.keys(options || {}).filter((key) => key !== 'operator');
if (unimplemented.length) {
console.warn(
`option${unimplemented.length > 1 ? 's' : ''} ${unimplemented.map((o) => `"${o}"`).join(', ')} not implemented`,
);
}
return pat;
};
function resolveReplications(ast) {
// the general idea here: x!3 = [x*3]@3
// could this be made easier?!
ast.source_ = ast.source_.map((child) => {
const { replicate, ...options } = child.options_ || {};
if (replicate) {
return {
...child,
options_: { ...options, weight: replicate },
source_: {
type_: 'pattern',
arguments_: {
alignment: 'h',
},
source_: [
{
type_: 'element',
source_: child.source_,
location_: child.location_,
options_: {
operator: {
type_: 'stretch',
arguments_: { amount: Fraction(replicate).inverse().toString() },
},
},
},
],
},
};
}
return child;
});
}
export function patternifyAST(ast) {
switch (ast.type_) {
case 'pattern':
resolveReplications(ast);
const children = ast.source_.map(patternifyAST).map(applyOptions(ast));
const alignment = ast.arguments_.alignment;
if (alignment === 'v') {
return stack(...children);
}
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
if (!weightedChildren && alignment === 't') {
return slowcat(...children);
}
if (weightedChildren) {
const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === 't') {
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
return pat._slow(weightSum); // timecat + slow
}
return pat;
}
return sequence(...children);
case 'element':
if (ast.source_ === '~') {
return silence;
}
if (typeof ast.source_ !== 'object') {
if (!addMiniLocations) {
return ast.source_;
}
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const { start, end } = ast.location_;
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
// the following line expects the shapeshifter append .withMiniLocation
// because location_ is only relative to the mini string, but we need it relative to whole code
return pure(value).withLocation([start.line, start.column, start.offset], [end.line, end.column, end.offset]);
}
return patternifyAST(ast.source_);
case 'stretch':
return patternifyAST(ast.source_).slow(ast.arguments_.amount);
/* case 'scale':
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
const intervals = Scale.get(scale).intervals;
const pattern = patternifyAST(ast.source_);
tonic = tonic || 'C4';
// console.log('scale', ast, pattern, tonic, scale);
console.log('tonic', tonic);
return pattern.fmap((step: any) => {
step = Number(step);
if (isNaN(step)) {
console.warn(`scale step "${step}" not a number`);
return step;
}
const octaves = Math.floor(step / intervals.length);
const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2
const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12));
return Note.transpose(tonic, interval || '1P');
}); */
/* case 'struct':
// TODO:
return silence; */
default:
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
return silence;
}
}
// mini notation only (wraps in "")
export const mini = (...strings) => {
const pats = strings.map((str) => {
const ast = krill.parse(`"${str}"`);
return patternifyAST(ast);
});
return sequence(...pats);
};
// includes haskell style (raw krill parsing)
export const h = (string) => {
const ast = krill.parse(string);
// console.log('ast', ast);
return patternifyAST(ast);
};
// shorthand for mini
Pattern.prototype.define('mini', mini, { composable: true });
Pattern.prototype.define('m', mini, { composable: true });
Pattern.prototype.define('h', h, { composable: true });
// TODO: move this to strudel?
export function reify(thing) {
if (thing?.constructor?.name === 'Pattern') {
return thing;
}
return pure(thing);
}
export function minify(thing) {
if (typeof thing === 'string') {
return mini(thing);
}
return reify(thing);
}

View file

@ -0,0 +1,27 @@
{
"name": "@strudel/mini",
"version": "0.0.1",
"description": "Mini notation for strudel",
"main": "mini.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}

View file

@ -0,0 +1,12 @@
import { strict as assert } from 'assert';
import { mini } from '../mini.mjs';
describe('mini', () => {
it('Should parse mini notation', () => {
const min = (v) => mini(v)._firstCycleValues;
assert.deepStrictEqual(min('c3'), ['c3']);
assert.deepStrictEqual(min('c3 d3'), ['c3', 'd3']);
assert.deepStrictEqual(min('<c3 d3>'), ['c3']);
});
});

6
packages/repl-react/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.snowpack
build
node_modules
.DS_Store
.parcel-cache
oldtunes.ts

179
packages/repl-react/App.jsx Normal file
View file

@ -0,0 +1,179 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import CodeMirror, { markEvent, markParens } from './CodeMirror';
import cx from './cx';
import { evaluate } from '../eval/evaluate.mjs';
// import logo from './logo.svg';
import playStatic from './static.mjs';
import { defaultSynth } from '../tone/tone.mjs';
import * as tunes from '../tunes/tunes';
import useRepl from '../../repl/src/useRepl';
import { useWebMidi } from './useWebMidi';
// TODO: use https://www.npmjs.com/package/@monaco-editor/react
const [_, codeParam] = window.location.href.split('#');
let decoded;
try {
decoded = atob(decodeURIComponent(codeParam || ''));
} catch (err) {
console.warn('failed to decode', err);
}
function getRandomTune() {
const allTunes = Object.values(tunes);
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
return randomItem(allTunes);
}
const randomTune = getRandomTune();
function App() {
const [editor, setEditor] = useState();
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog, pending } =
useRepl({
tune: decoded || randomTune,
defaultSynth,
onDraw: useCallback(markEvent(editor), [editor]),
});
const [uiHidden, setUiHidden] = useState(false);
const logBox = useRef();
// scroll log box to bottom when log changes
useLayoutEffect(() => {
logBox.current.scrollTop = logBox.current?.scrollHeight;
}, [log]);
// set active pattern on ctrl+enter
useLayoutEffect(() => {
// TODO: make sure this is only fired when editor has focus
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
switch (e.code) {
case 'Enter':
await activateCode();
break;
case 'Period':
cycle.stop();
}
}
};
window.addEventListener('keydown', handleKeyPress);
return () => window.removeEventListener('keydown', handleKeyPress);
}, [pattern, code]);
useWebMidi({
ready: useCallback(({ outputs }) => {
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(' | ')}) to the pattern. `);
}, []),
connected: useCallback(({ outputs }) => {
pushLog(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
}, []),
disconnected: useCallback(({ outputs }) => {
pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
}, []),
});
return (
<div className="min-h-screen flex flex-col">
<header
id="header"
className={cx(
'flex-none w-full h-14 px-2 flex border-b border-gray-200 justify-between z-[10]',
uiHidden ? 'bg-transparent text-white' : 'bg-white',
)}
>
<div className="flex items-center space-x-2">
{/* <img src={logo} className="Tidal-logo w-12 h-12" alt="logo" /> */}
<h1 className="text-2xl">Strudel REPL</h1>
</div>
<div className="flex space-x-4">
<button onClick={() => togglePlay()}>
{!pending ? (
<span className="flex items-center 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>
<button
onClick={async () => {
const _code = getRandomTune();
console.log('tune', _code); // uncomment this to debug when random code fails
setCode(_code);
const parsed = await evaluate(_code);
setPattern(parsed.pattern);
}}
>
🎲 random
</button>
<button>
<a href="./tutorial">📚 tutorial</a>
</button>
<button onClick={() => setUiHidden((c) => !c)}>👀 {uiHidden ? 'show ui' : 'hide ui'}</button>
</div>
</header>
<section className="grow flex flex-col text-gray-100">
<div className="grow relative" id="code">
<div
className={cx(
'h-full transition-opacity',
error ? 'focus:ring-red-500' : 'focus:ring-slate-800',
uiHidden ? 'opacity-0' : 'opacity-100',
)}
>
<CodeMirror
value={code}
editorDidMount={setEditor}
options={{
mode: 'javascript',
theme: 'material',
lineNumbers: false,
styleSelectedText: true,
cursorBlinkRate: 0,
}}
onCursor={markParens}
onChange={(_, __, value) => setCode(value)}
/>
<span className="p-4 absolute top-0 right-0 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>
</div>
{error && (
<div className={cx('absolute right-2 bottom-2 px-2', 'text-red-500')}>
{error?.message || 'unknown error'}
</div>
)}
</div>
<textarea
className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none"
value={log}
readOnly
ref={logBox}
style={{ fontFamily: 'monospace' }}
/>
</section>
<button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}>
static
</button>
</div>
);
}
export default App;

View file

@ -0,0 +1,121 @@
import React from 'react';
import { Controlled as CodeMirror2 } from 'react-codemirror2';
import 'codemirror/mode/javascript/javascript.js';
import 'codemirror/mode/pegjs/pegjs.js';
// import 'codemirror/theme/material.css';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
export default function CodeMirror({ value, onChange, onCursor, options, editorDidMount }) {
options = options || {
mode: 'javascript',
theme: 'material',
lineNumbers: true,
styleSelectedText: true,
cursorBlinkRate: 500,
};
return (
<CodeMirror2
value={value}
options={options}
onBeforeChange={onChange}
editorDidMount={editorDidMount}
onCursor={(editor, data) => onCursor?.(editor, data)}
/>
);
}
export const markEvent = (editor) => (time, event) => {
const locs = event.context.locations;
if (!locs || !editor) {
return;
}
const col = event.context?.color || '#FFCA28';
// mark active event
const marks = locs.map(({ start, end }) =>
editor.getDoc().markText(
{ line: start.line - 1, ch: start.column },
{ line: end.line - 1, ch: end.column },
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
//{ css: `background-color: ${col};border-radius:5px` },
),
);
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
setTimeout(() => {
marks.forEach((mark) => mark.clear());
// }, '+' + event.duration * 0.5);
}, event.duration /* * 0.9 */ * 1000);
};
let parenMark;
export const markParens = (editor, data) => {
const v = editor.getDoc().getValue();
const marked = getCurrentParenArea(v, data);
parenMark?.clear();
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
};
// returns { line, ch } from absolute character offset
export function offsetToPosition(offset, code) {
const lines = code.split('\n');
let line = 0;
let ch = 0;
for (let i = 0; i < offset; i++) {
if (ch === lines[line].length) {
line++;
ch = 0;
} else {
ch++;
}
}
return { line, ch };
}
// returns absolute character offset from { line, ch }
export function positionToOffset(position, code) {
const lines = code.split('\n');
let offset = 0;
for (let i = 0; i < position.line; i++) {
offset += lines[i].length + 1;
}
offset += position.ch;
return offset;
}
// given code and caret position, the functions returns the indices of the parens we are in
export function getCurrentParenArea(code, caretPosition) {
const caret = positionToOffset(caretPosition, code);
let open, i, begin, end;
// walk left
i = caret;
open = 0;
while (i > 0) {
if (code[i - 1] === '(') {
open--;
} else if (code[i - 1] === ')') {
open++;
}
if (open === -1) {
break;
}
i--;
}
begin = i;
// walk right
i = caret;
open = 0;
while (i < code.length) {
if (code[i] === '(') {
open--;
} else if (code[i] === ')') {
open++;
}
if (open === 1) {
break;
}
i++;
}
end = i;
return [begin, end].map((o) => offsetToPosition(o, code));
}

View file

@ -0,0 +1,22 @@
# Strudel REPL
## Default Snowpack Readme
> ✨ Bootstrapped with Create Snowpack App (CSA).
## Available Scripts
### npm start
Runs the app in the development mode.
Open http://localhost:8080 to view it in the browser.
The page will reload if you make edits.
You will also see any lint errors in the console.
### npm run build
Builds a static copy of your site to the `docs/` folder.
Your app is ready to be deployed!
**For the best production performance:** Add a build bundler plugin like "@snowpack/plugin-webpack" to your `snowpack.config.mjs` config file.

View file

@ -0,0 +1,3 @@
export default function cx(...classes) { // : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}

View file

@ -0,0 +1,53 @@
import * as Tone from 'tone';
import { Pattern } from '../core/strudel.mjs';
export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id);
if (!canvas) {
canvas = document.createElement('canvas');
canvas.id = id;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
document.body.prepend(canvas);
}
return canvas.getContext('2d');
};
Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
const ctx = getDrawContext();
let cycle,
events = [];
const animate = (time) => {
const t = Tone.getTransport().seconds;
if (cycleSpan) {
const currentCycle = Math.floor(t / cycleSpan);
if (cycle !== currentCycle) {
cycle = currentCycle;
const begin = currentCycle * cycleSpan;
const end = (currentCycle + lookaheadCycles) * cycleSpan;
events = this._asNumber(true) // true = silent error
.query(new State(new TimeSpan(begin, end)))
.filter((event) => event.part.begin.equals(event.whole.begin));
}
}
callback(ctx, events, t, cycleSpan, time);
window.strudelAnimation = requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
return this;
};
export const cleanup = () => {
const ctx = getDrawContext();
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
if (window.strudelScheduler) {
clearInterval(window.strudelScheduler);
}
};

View file

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://snowpack.dev/concepts/hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept();
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

21647
packages/repl-react/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
{
"scripts": {
"start": "snowpack dev --polyfill-node",
"build": "snowpack build --polyfill-node && cp ./public/.nojekyll ../docs && npm run build-tutorial",
"static": "npx serve ../docs",
"test": "web-test-runner \"src/**/*.test.tsx\"",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
"peggy": "peggy -o krill-parser.js --format es ./krill.pegjs",
"tutorial": "parcel src/tutorial/index.html --no-cache",
"build-tutorial": "parcel build src/tutorial/index.html --dist-dir ../docs/tutorial --public-url /tutorial --no-optimize --no-scope-hoist --no-cache"
},
"dependencies": {
"@tonaljs/tonal": "^4.6.5",
"@tonejs/piano": "^0.2.1",
"bjork": "^0.0.1",
"chord-voicings": "^0.0.1",
"codemirror": "^5.65.1",
"estraverse": "^5.3.0",
"events": "^3.3.0",
"multimap": "^1.1.0",
"ramda": "^0.28.0",
"react": "^17.0.2",
"react-codemirror2": "^7.2.1",
"react-dom": "^17.0.2",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
"shift-regexp-acceptor": "^2.0.3",
"shift-spec": "^2018.0.2",
"tone": "^14.7.77",
"tune.js": "^1.0.1",
"webmidi": "^2.5.2"
},
"devDependencies": {
"@mdx-js/react": "^1.6.22",
"@parcel/transformer-mdx": "^2.3.1",
"@snowpack/plugin-dotenv": "^2.1.0",
"@snowpack/plugin-postcss": "^1.4.3",
"@snowpack/plugin-react-refresh": "^2.5.0",
"@snowpack/plugin-typescript": "^1.2.1",
"@snowpack/web-test-runner-plugin": "^0.2.2",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.2",
"@testing-library/react": "^11.2.6",
"@types/chai": "^4.2.17",
"@types/mocha": "^8.2.2",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/snowpack-env": "^2.3.3",
"@web/test-runner": "^0.13.3",
"autoprefixer": "^10.4.2",
"chai": "^4.3.4",
"parcel": "^2.3.1",
"peggy": "^1.2.0",
"postcss": "^8.4.6",
"prettier": "^2.2.1",
"snowpack": "^3.3.7",
"tailwindcss": "^3.0.18",
"typescript": "^4.2.4"
}
}

View file

@ -0,0 +1,38 @@
import { Pattern } from '../core/strudel.mjs';
Pattern.prototype.pianoroll = function ({
timeframe = 10,
inactive = '#C9E597',
active = '#FFCA28',
background = '#2A3236',
maxMidi = 90,
minMidi = 0,
} = {}) {
const w = window.innerWidth;
const h = window.innerHeight;
const midiRange = maxMidi - minMidi + 1;
const height = h / midiRange;
this.draw(
(ctx, events, t) => {
ctx.fillStyle = background;
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
events.forEach((event) => {
const isActive = event.whole.begin <= t && event.whole.end >= t;
ctx.fillStyle = event.context?.color || inactive;
ctx.strokeStyle = event.context?.color || active;
ctx.globalAlpha = event.context.velocity ?? 1;
const x = Math.round((event.whole.begin / timeframe) * w);
const width = Math.round(((event.whole.end - event.whole.begin) / timeframe) * w);
const y = Math.round(h - ((Number(event.value) - minMidi) / midiRange) * h);
const offset = (t / timeframe) * w;
const margin = 0;
const coords = [x - offset + margin + 1, y + 1, width - 2, height - 2];
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
});
},
timeframe,
2, // lookaheadCycles
);
return this;
};

View file

@ -0,0 +1,8 @@
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
// other plugins can go here, such as autoprefixer
},
};

View file

View file

@ -0,0 +1 @@
strudel.tidalcycles.org

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,31 @@
@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;
}
.darken::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: block;
background: black;
opacity: 0.5;
}

View file

@ -0,0 +1,45 @@
// this file can be used to livecode from the comfort of your editor.
// just export a pattern from export default
// enable hot mode by pressing "toggle hot mode" on the top right of the repl
import { mini, h } from '../../mini/mini.mjs';
import { sequence, pure, reify, slowcat, fastcat, cat, stack, silence } from '../../packages/core/strudel.mjs';
import { gain, filter } from '../../tone/tone.mjs';
export default stack(
sequence(
mini(
'e5 [b4 c5] d5 [c5 b4]',
'a4 [a4 c5] e5 [d5 c5]',
'b4 [~ c5] d5 e5',
'c5 a4 a4 ~',
'[~ d5] [~ f5] a5 [g5 f5]',
'e5 [~ c5] e5 [d5 c5]',
'b4 [b4 c5] d5 e5',
'c5 a4 a4 ~'
)
.synth({
oscillator: { type: 'sine' },
envelope: { attack: 0.1 },
})
.rev()
),
sequence(
mini(
'e2 e3 e2 e3 e2 e3 e2 e3',
'a2 a3 a2 a3 a2 a3 a2 a3',
'g#2 g#3 g#2 g#3 e2 e3 e2 e3',
'a2 a3 a2 a3 a2 a3 b1 c2',
'd2 d3 d2 d3 d2 d3 d2 d3',
'c2 c3 c2 c3 c2 c3 c2 c3',
'b1 b2 b1 b2 e2 e3 e2 e3',
'a1 a2 a1 a2 a1 a2 a1 a2'
)
.synth({
oscillator: { type: 'sawtooth' },
envelope: { attack: 0.1 },
})
.chain(gain(0.7), filter(2000))
.rev()
)
).slow(16);

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/global.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Strudel REPL" />
<title>Strudel REPL</title>
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="%PUBLIC_URL%/dist/index.js"></script>
</body>
</html>

View file

@ -0,0 +1,41 @@
/** @type {import("snowpack").SnowpackUserConfig } */
export default {
workspaceRoot: '..',
mount: {
public: { url: '/', static: false },
src: { url: '/dist' },
},
plugins: [
'@snowpack/plugin-postcss',
'@snowpack/plugin-react-refresh',
'@snowpack/plugin-dotenv',
[
'@snowpack/plugin-typescript',
{
/* Yarn PnP workaround: see https://www.npmjs.com/package/@snowpack/plugin-typescript */
...(process.versions.pnp ? { tsc: 'yarn pnpify tsc' } : {}),
},
],
],
routes: [
/* Enable an SPA Fallback in development: */
// {"match": "routes", "src": ".*", "dest": "/index.html"},
],
exclude: ['**/node_modules/**/*', '**/tutorial/**/*'],
optimize: {
/* Example: Bundle your final build: */
// "bundle": true,
},
packageOptions: {
/* ... */
knownEntrypoints: ['fraction.js', 'codemirror', 'shift-ast', 'ramda'],
},
devOptions: {
tailwindConfig: './tailwind.config.js',
},
buildOptions: {
/* ... */
out: '../docs',
baseUrl: '/',
},
};

View file

@ -0,0 +1,66 @@
import * as Tone from 'tone';
import { State, TimeSpan } from '../core/strudel.mjs';
import { getPlayableNoteValue } from '../../packages/core/util.mjs';
import { evaluate } from '../eval/evaluate.mjs';
// this is a test to play back events with as less runtime code as possible..
// the code asks for the number of seconds to prequery
// after the querying is done, the events are scheduled
// after the scheduling is done, the transport is started
// nothing happens while tone.js runs except the schedule callback, which is a thin wrapper around triggerAttackRelease calls
// so all glitches that appear here should have nothing to do with strudel and or the repl
async function playStatic(code) {
Tone.getTransport().cancel();
Tone.getTransport().stop();
let start, took;
const seconds = Number(prompt('How many seconds to run?')) || 60;
start = performance.now();
console.log('evaluating..');
const { pattern: pat } = await evaluate(code);
took = performance.now() - start;
console.log('evaluate took', took, 'ms');
console.log('querying..');
start = performance.now();
const events = pat
?.query(new State(new TimeSpan(0, seconds)))
?.filter((event) => event.part.begin.equals(event.whole.begin))
?.map((event) => ({
time: event.whole.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(),
value: event.value,
context: event.context,
}));
took = performance.now() - start;
console.log('query took', took, 'ms');
console.log('scheduling..');
start = performance.now();
events.forEach((event) => {
Tone.getTransport().schedule((time) => {
try {
const { onTrigger, velocity } = event.context;
if (!onTrigger) {
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration, time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
} else {
onTrigger(time, event);
}
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
console.error(err);
}
}, event.time);
});
took = performance.now() - start;
console.log('scheduling took', took, 'ms');
console.log('now starting!');
Tone.getTransport().start('+0.5');
}
export default playStatic;

View file

@ -0,0 +1,8 @@
module.exports = {
content: ['./public/**/*.html', './src/**/*.{js,jsx,ts,tsx,mdx}'],
// specify other options here
theme: {
extend: {},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
};

View file

@ -0,0 +1,50 @@
import * as Tone from 'tone';
export const hideHeader = () => {
document.getElementById('header').style = 'display:none';
};
function frame(callback) {
if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation);
}
const animate = (animationTime) => {
const toneTime = Tone.getTransport().seconds;
callback(animationTime, toneTime);
window.strudelAnimation = requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
export const backgroundImage = function (src, animateOptions = {}) {
const container = document.getElementById('code');
const bg = 'background-image:url(' + src + ');background-size:contain;';
container.style = bg;
const { className: initialClassName } = container;
const handleOption = (option, value) => {
({
style: () => (container.style = bg + ';' + value),
className: () => (container.className = value + ' ' + initialClassName),
}[option]());
};
const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function');
const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string');
stringOptions.forEach(([option, value]) => handleOption(option, value));
if (funcOptions.length === 0) {
return;
}
frame((_, t) =>
funcOptions.forEach(([option, value]) => {
handleOption(option, value(t));
}),
);
};
export const cleanup = () => {
const container = document.getElementById('code');
if (container) {
container.style = '';
container.className = 'grow relative'; // has to match App.tsx
}
};

View file

@ -0,0 +1,86 @@
import { useEffect, useState } from 'react';
import type { ToneEventCallback } from 'tone';
import * as Tone from 'tone';
import { Hap, State, TimeSpan } from '../core/strudel.mjs';
export declare interface UseCycleProps {
onEvent: ToneEventCallback<any>;
onQuery?: (state: State) => Hap[];
onSchedule?: (events: Hap[], cycle: number) => void;
onDraw?: ToneEventCallback<any>;
ready?: boolean; // if false, query will not be called on change props
}
function useCycle(props: UseCycleProps) {
// onX must use useCallback!
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
const [started, setStarted] = useState(false);
const cycleDuration = 1;
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
// pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle);
// cancel events after current query. makes sure no old events are player for rescheduled cycles
// console.log('schedule', cycle);
// query next cycle in the middle of the current
const cancelFrom = timespan.begin.valueOf();
Tone.getTransport().cancel(cancelFrom);
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
Tone.getTransport().schedule(() => {
query(cycle + 1);
}, t);
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
const toneEvent = {
time: event.whole.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(),
value: event.value,
context: event.context,
};
onEvent(time, toneEvent);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, toneEvent);
}, time);
}, event.part.begin.valueOf());
});
};
useEffect(() => {
ready && query();
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
const start = async () => {
setStarted(true);
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
return {
start,
stop,
onEvent,
started,
setStarted,
toggle,
query,
activeCycle,
};
}
export default useCycle;

View file

@ -0,0 +1,11 @@
import { useEffect } from 'react';
function usePostMessage(listener) {
useEffect(() => {
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}, [listener]);
return (data) => window.postMessage(data, '*');
}
export default usePostMessage;

View file

@ -0,0 +1,174 @@
import { useCallback, useState, useMemo } from 'react';
import { evaluate } from '../../packages/eval/evaluate.mjs';
import { getPlayableNoteValue } from '../core/util.mjs';
import useCycle from './useCycle';
import usePostMessage from './usePostMessage';
let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
const [log, setLog] = useState('');
const [error, setError] = useState();
const [pending, setPending] = useState(false);
const [hash, setHash] = useState('');
const [pattern, setPattern] = useState();
const dirty = code !== activeCode || error;
const generateHash = () => encodeURIComponent(btoa(code));
const activateCode = async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
setHash(generateHash());
setError(undefined);
setActiveCode(_code);
setPending(false);
} catch (err: any) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err);
}
};
const pushLog = (message: string) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`);
// logs events of cycle
const logCycle = (_events: any, cycle: any) => {
if (_events.length) {
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
}
};
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return onDraw;
}
}, [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event) => {
try {
onEvent?.(event);
const { onTrigger, velocity } = event.context;
if (!onTrigger) {
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration, time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event);
}
} catch (err: any) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
},
[onEvent]
),
onQuery: useCallback(
(state) => {
try {
return pattern?.query(state) || [];
} catch (err: any) {
console.warn(err);
err.message = 'query error: ' + err.message;
setError(err);
return [];
}
},
[pattern]
),
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), [pattern]),
ready: !!pattern && !!activeCode,
});
const broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
cycle.setStarted(false);
setActiveCode(undefined);
}
});
// set active pattern on ctrl+enter
/* useLayoutEffect(() => {
// TODO: make sure this is only fired when editor has focus
const handleKeyPress = (e: any) => {
if (e.ctrlKey || e.altKey) {
switch (e.code) {
case 'Enter':
activateCode();
!cycle.started && cycle.start();
break;
case 'Period':
cycle.stop();
}
}
};
document.addEventListener('keypress', handleKeyPress);
return () => document.removeEventListener('keypress', handleKeyPress);
}, [pattern, code]); */
/* useWebMidi({
ready: useCallback(({ outputs }) => {
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(' | ')}) to the pattern. `);
}, []),
connected: useCallback(({ outputs }) => {
pushLog(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
}, []),
disconnected: useCallback(({ outputs }) => {
pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`);
}, []),
}); */
const togglePlay = () => {
if (!cycle.started) {
activateCode();
} else {
cycle.stop();
}
};
return {
pending,
code,
setCode,
pattern,
error,
cycle,
setPattern,
dirty,
log,
togglePlay,
activateCode,
activeCode,
pushLog,
hash,
};
}
export default useRepl;

View file

@ -0,0 +1,33 @@
import { useEffect, useState } from 'react';
export function useWebMidi(props) {
const { ready, connected, disconnected } = props;
const [loading, setLoading] = useState(true);
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
useEffect(() => {
enableWebMidi()
.then(() => {
// Reacting when a new device becomes available
WebMidi.addListener('connected', (e) => {
setOutputs([...WebMidi.outputs]);
connected?.(WebMidi, e);
});
// Reacting when a device becomes unavailable
WebMidi.addListener('disconnected', (e) => {
setOutputs([...WebMidi.outputs]);
disconnected?.(WebMidi, e);
});
ready?.(WebMidi);
setLoading(false);
})
.catch((err) => {
if (err) {
//throw new Error("Web Midi could not be enabled...");
console.warn('Web Midi could not be enabled..');
return;
}
});
}, [ready, connected, disconnected, outputs]);
const outputByName = (name) => WebMidi.getOutputByName(name);
return { loading, outputs, outputByName };
}

View file

@ -0,0 +1,27 @@
{
"name": "@strudel/tonal",
"version": "0.0.1",
"description": "Tonal functions for strudel",
"main": "tonal.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme"
}

View file

@ -0,0 +1,9 @@
import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure } from '../../core/strudel.mjs';
describe('tonal', () => {
it('Should run tonal functions ', () => {
assert.deepStrictEqual(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues, ['D3']);
});
});

89
packages/tonal/tonal.mjs Normal file
View file

@ -0,0 +1,89 @@
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { Pattern as _Pattern } from '../core/strudel.mjs';
import { mod, tokenizeNote } from '../../packages/core/util.mjs';
const Pattern = _Pattern; // as any;
// transpose note inside scale by offset steps
// function scaleTranspose(scale: string, offset: number, note: string) {
export function scaleTranspose(scale, offset, note) {
let [tonic, scaleName] = Scale.tokenize(scale);
let { notes } = Scale.get(`${tonic} ${scaleName}`);
notes = notes.map((note) => Note.get(note).pc); // use only pc!
offset = Number(offset);
if (isNaN(offset)) {
throw new Error(`scale offset "${offset}" not a number`);
}
const { pc: fromPc, oct = 3 } = Note.get(note);
const noteIndex = notes.indexOf(fromPc);
if (noteIndex === -1) {
throw new Error(`note "${note}" is not in scale "${scale}"`);
}
let i = noteIndex,
o = oct,
n = fromPc;
const direction = Math.sign(offset);
// TODO: find way to do this smarter
while (Math.abs(i - noteIndex) < Math.abs(offset)) {
i += direction;
const index = mod(i, notes.length);
if (direction < 0 && n[0] === 'C') {
o += direction;
}
n = notes[index];
if (direction > 0 && n[0] === 'C') {
o += direction;
}
}
return n + o;
}
// Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
Pattern.prototype._transpose = function (intervalOrSemitones) {
return this._withEvent((event) => {
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones /* as number */)
: String(intervalOrSemitones);
if (typeof event.value === 'number') {
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
return event.withValue(() => event.value + semitones);
}
// TODO: move simplify to player to preserve enharmonics
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
return event.withValue(() => Note.simplify(Note.transpose(event.value, interval)));
});
};
// example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2))
// TODO: add Pattern.define(name, function, options) that handles all the meta programming stuff
// TODO: find out how to patternify this function when it's standalone
// e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
return this._withEvent((event) => {
if (!event.context.scale) {
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof event.value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return event.withValue(() => scaleTranspose(event.context.scale, Number(offset), event.value));
});
};
Pattern.prototype._scale = function (scale /* : string */) {
return this._withEvent((event) => {
let note = event.value;
const asNumber = Number(note);
if (!isNaN(asNumber)) {
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct);
}
return event.withValue(() => note).setContext({ ...event.context, scale });
});
};
Pattern.prototype.define('transpose', (a, pat) => pat.transpose(a), { composable: true, patternified: true });
Pattern.prototype.define('scale', (a, pat) => pat.scale(a), { composable: true, patternified: true });
Pattern.prototype.define('scaleTranspose', (a, pat) => pat.scaleTranspose(a), { composable: true, patternified: true });

View file

@ -0,0 +1,50 @@
import { Pattern as _Pattern, stack, Hap, reify } from '../core/strudel.mjs';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings;
const getVoicing = (chord, lastVoicing, range = ['F3', 'A4']) =>
dictionaryVoicing({
chord,
dictionary: lefthand,
range,
picker: minTopNoteDiff,
lastVoicing,
});
const Pattern = _Pattern;
Pattern.prototype.fmapNested = function (func) {
return new Pattern((span) =>
this.query(span)
.map((event) =>
reify(func(event))
.query(span)
.map((hap) => new Hap(event.whole, event.part, hap.value, hap.context)),
)
.flat(),
);
};
Pattern.prototype.voicings = function (range) {
let lastVoicing;
if (!range?.length) {
// allows to pass empty array, if too lazy to specify range
range = ['F3', 'A4'];
}
return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value, lastVoicing, range);
return stack(...lastVoicing)._withContext(() => ({
locations: event.context.locations || [],
}));
});
};
Pattern.prototype._rootNotes = function (octave = 2) {
return this.fmap((value) => {
const [_, root] = value.match(/^([a-gA-G][b#]?).*$/);
return root + octave;
});
};
Pattern.prototype.define('voicings', (range, pat) => pat.voicings(range), { composable: true });
Pattern.prototype.define('rootNotes', (oct, pat) => pat.rootNotes(oct), { composable: true, patternified: true });

603
packages/tone/package-lock.json generated Normal file
View file

@ -0,0 +1,603 @@
{
"name": "@strudel/tone",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel/tone",
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
}
},
"node_modules/@babel/runtime": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@tonaljs/abc-notation": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/abc-notation/-/abc-notation-4.6.5.tgz",
"integrity": "sha512-1S0Jnx0NfDLgyhkQOMEHqOacELL6RUdPcWWUP+nAnsOsb9owvB9RKYLSzp5odd16FVUR7U8c/JLc2yxIRvSeJw==",
"dependencies": {
"@tonaljs/core": "^4.6.5"
}
},
"node_modules/@tonaljs/array": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/array/-/array-4.6.5.tgz",
"integrity": "sha512-7A3DbBQ+qIQ134FqE518b4tJ8V2a15Sn303JjHzgnqZqKrNh/s3wqwkL60F7LKcd09tcp+vIKQP/MYt4xMcRAA==",
"dependencies": {
"@tonaljs/core": "^4.6.5"
}
},
"node_modules/@tonaljs/chord": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/chord/-/chord-4.6.5.tgz",
"integrity": "sha512-Pjdel4aDVv4kcx9PW6Qozt5BB9nAt13AOExfzKztpgPmlBSy0SKHse7Jp1cA4MGAuLHU8dzVssTFYpCskEFw3w==",
"dependencies": {
"@tonaljs/chord-detect": "^4.6.5",
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5"
}
},
"node_modules/@tonaljs/chord-detect": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/chord-detect/-/chord-detect-4.6.5.tgz",
"integrity": "sha512-4xu53UP4kNTfdTNpAAVijhXcQ+ypJqmeMnsST08ZXSjoYfJUhmf5rWDWfz36KOTtNdCA6AbYgdtTYV/Xw0nd/w==",
"dependencies": {
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5"
}
},
"node_modules/@tonaljs/chord-type": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/chord-type/-/chord-type-4.6.5.tgz",
"integrity": "sha512-Ol4DDopqpZCF9odosO2i8I+plud3Ul7VWJGNvL+PPCf4Qnwuz87q3aJQDLNoRUz4VyW0u66mq3LyVh6A8kb6Ug==",
"dependencies": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5"
}
},
"node_modules/@tonaljs/collection": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@tonaljs/collection/-/collection-4.6.2.tgz",
"integrity": "sha512-bfPCotLJNB/tG1NrdbsQPLDKZB5jlMs7uPQ6RYKiNkaena3345ZKkbCGl5pj6YTXeDm/oblXiSbFAn7SlLRZdQ=="
},
"node_modules/@tonaljs/core": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/core/-/core-4.6.5.tgz",
"integrity": "sha512-t7Vx0+L3j/ubQj2AhI1H45D/K745np4DwJjJjXNi5FlGD+TL2wyw50dCwkHKGHsrLDqup1qqP6yN7LBpC6UwNg=="
},
"node_modules/@tonaljs/duration-value": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@tonaljs/duration-value/-/duration-value-4.6.2.tgz",
"integrity": "sha512-zrXT0L/qsDQ6251Mlqz54vcUbYUB9xb6uJhlxUzc6VauXOt8UOfrdTULubRTXTaBwWt1h8J5n9pXTQmNGzNI9A=="
},
"node_modules/@tonaljs/interval": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/interval/-/interval-4.6.5.tgz",
"integrity": "sha512-7EDWhqZ7Nnh9oD4ahRYJHLc799ACGxYL4hDHwMKD16B2MgXqPvDeDvwQ31qUuO0ruGz8tMb3FDlgg0Hplowcbw==",
"dependencies": {
"@tonaljs/core": "^4.6.5"
}
},
"node_modules/@tonaljs/key": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/key/-/key-4.6.5.tgz",
"integrity": "sha512-ZdZWb5IStx6CLRmdEjawR66CqNpoW3EVUua2nVZBMdgnNebWxt4nvgH/ZNvGlCQGFZkUZzRhCfTwqsS6e3OmSA==",
"dependencies": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/note": "^4.6.5",
"@tonaljs/roman-numeral": "^4.6.5"
}
},
"node_modules/@tonaljs/midi": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/midi/-/midi-4.6.5.tgz",
"integrity": "sha512-fJEZtNvV3M6yW1w+Tep60Rbv5PvuKszQcQzaJS1Loq5mHOKAzdmRfuJSpEpZBiaKEZ1WAMh1QKXYyOd+imyGQg==",
"dependencies": {
"@tonaljs/core": "^4.6.5"
}
},
"node_modules/@tonaljs/mode": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/mode/-/mode-4.6.5.tgz",
"integrity": "sha512-54iaON1rJ6q8fV5iuei8RGDxYhKBGGxZz3rjAxGSqdTUwBRVOdPqtzOkofThf9gRGYOMhzPp1BMbxbV+UCAPsA==",
"dependencies": {
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/interval": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5"
}
},
"node_modules/@tonaljs/note": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/note/-/note-4.6.5.tgz",
"integrity": "sha512-Y0/eTzcReXzfcSLLG4k/dLLayqbvh/XYIkybG/QMDyR0BREuJq0Sw+NavbzhTtO0dadIQb/qfe0GFq4k2xS+NQ==",
"dependencies": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/midi": "^4.6.5"
}
},
"node_modules/@tonaljs/pcset": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/pcset/-/pcset-4.6.5.tgz",
"integrity": "sha512-oWAKflP3cREnUfScqsBzg2LLKNevxSnpDtrq8CPtwOAsrAa8PjQG07NQfhqIiFMjPUdgkDiER3qVA1n8dDwAJA==",
"dependencies": {
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5"
}
},
"node_modules/@tonaljs/progression": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/progression/-/progression-4.6.5.tgz",
"integrity": "sha512-ijYEgMFQG4izHYUw5cRtBRNBuoYzmpGvb/tRiykhJNI6XIjekZEMiMsOMfb1u5q+EGvnVNXRmrluMRDIz2rmRw==",
"dependencies": {
"@tonaljs/chord": "^4.6.5",
"@tonaljs/core": "^4.6.5",
"@tonaljs/roman-numeral": "^4.6.5"
}
},
"node_modules/@tonaljs/range": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/range/-/range-4.6.5.tgz",
"integrity": "sha512-99cOvVJ3l4X0UJuTSa6qE87JriREnnWIsi3xo1/n7RoqFxnfi8YPh4SfJJyysvHcT18X4EfcTNde9ancMBVu6A==",
"dependencies": {
"@tonaljs/collection": "^4.6.2",
"@tonaljs/midi": "^4.6.5"
}
},
"node_modules/@tonaljs/roman-numeral": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/roman-numeral/-/roman-numeral-4.6.5.tgz",
"integrity": "sha512-bWYQNZWKmYDDcmbQQNwcWAHfTWanpzmvI0wplrMnGd4x0op5etwUEv+Yzjg0B1ef+E+zcU02Sl0WwRJhaDK3hg==",
"dependencies": {
"@tonaljs/core": "^4.6.5"
}
},
"node_modules/@tonaljs/scale": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/scale/-/scale-4.6.5.tgz",
"integrity": "sha512-isYDamelOBtcd5bEnJ8QV0Js7jKRwZ0FlFVE/+bUN3wsyo9u6KLL5gMyfH9RKdx74m8lE13JXYTXgKqe+AOa4A==",
"dependencies": {
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/note": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5"
}
},
"node_modules/@tonaljs/scale-type": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/scale-type/-/scale-type-4.6.5.tgz",
"integrity": "sha512-rwcDOYf2UifjLJhmuQ8f8bJSeOCMDQJ1lB7lzlqdFxes03OeQhdOEfrT0nPtW8BhBEvq4GMM2NA6CLxX8MTwOQ==",
"dependencies": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5"
}
},
"node_modules/@tonaljs/time-signature": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@tonaljs/time-signature/-/time-signature-4.6.2.tgz",
"integrity": "sha512-OlZY4gdLd21WpMeAI1nS9E9zWcYU6oAzh6ptAUndqmVnFIrIWIWKCkWapdFx8dWdqrX8jqya3m4T33wmeo7w5Q=="
},
"node_modules/@tonaljs/tonal": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/tonal/-/tonal-4.6.5.tgz",
"integrity": "sha512-lmsWinI9dy7nQyzCEgDVeVAwJtsk4ey05cJZd6oa4QVuSFD+CR8ebaEiwT4/Na+W0kHrKicT3h0uYc2PJIvx5Q==",
"dependencies": {
"@tonaljs/abc-notation": "^4.6.5",
"@tonaljs/array": "^4.6.5",
"@tonaljs/chord": "^4.6.5",
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/duration-value": "^4.6.2",
"@tonaljs/interval": "^4.6.5",
"@tonaljs/key": "^4.6.5",
"@tonaljs/midi": "^4.6.5",
"@tonaljs/mode": "^4.6.5",
"@tonaljs/note": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/progression": "^4.6.5",
"@tonaljs/range": "^4.6.5",
"@tonaljs/roman-numeral": "^4.6.5",
"@tonaljs/scale": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5",
"@tonaljs/time-signature": "^4.6.2"
}
},
"node_modules/@tonejs/piano": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@tonejs/piano/-/piano-0.2.1.tgz",
"integrity": "sha512-JIwZ91RSFR7Rt16o7cA7O7G30wenFl0lY5yhTsuwZmn48MO9KV+X7kyXE98Bqvs/dCBVg9PoAJ1GKMabPOW4yQ==",
"dependencies": {
"tslib": "^1.11.1"
},
"peerDependencies": {
"tone": "^14.6.1",
"webmidi": "^2.5.1"
}
},
"node_modules/automation-events": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.14.tgz",
"integrity": "sha512-CB2Me0yW8sz7gSGwMiSfgfs1Oqlgs53k+eVESN6axvRyMAD3zlSp2nqndD2TQAtW3yOtSEJWNGsw0r48+f1wtw==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"tslib": "^2.3.1"
},
"engines": {
"node": ">=12.20.1"
}
},
"node_modules/automation-events/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/chord-voicings": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/chord-voicings/-/chord-voicings-0.0.1.tgz",
"integrity": "sha512-SutgB/4ynkkuiK6qdQ/k3QvCFcH0Vj8Ch4t6LbRyRQbVzP/TOztiCk3kvXd516UZ6fqk7ijDRELEFcKN+6V8sA==",
"dependencies": {
"@tonaljs/tonal": "^4.6.5"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"node_modules/standardized-audio-context": {
"version": "25.3.21",
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.21.tgz",
"integrity": "sha512-CZEnayJJjNefeU+z1QGDhaid1LAAYxWYa2ipNk75ropwec9rq6fmclyhXb1wGtDsZ402irX3HLt1U/PwP9+1fA==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"automation-events": "^4.0.14",
"tslib": "^2.3.1"
}
},
"node_modules/standardized-audio-context/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/tone": {
"version": "14.7.77",
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
"dependencies": {
"standardized-audio-context": "^25.1.8",
"tslib": "^2.0.1"
}
},
"node_modules/tone/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/webmidi": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg==",
"peer": true,
"engines": {
"node": ">0.6.x"
}
}
},
"dependencies": {
"@babel/runtime": {
"version": "7.17.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@tonaljs/abc-notation": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/abc-notation/-/abc-notation-4.6.5.tgz",
"integrity": "sha512-1S0Jnx0NfDLgyhkQOMEHqOacELL6RUdPcWWUP+nAnsOsb9owvB9RKYLSzp5odd16FVUR7U8c/JLc2yxIRvSeJw==",
"requires": {
"@tonaljs/core": "^4.6.5"
}
},
"@tonaljs/array": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/array/-/array-4.6.5.tgz",
"integrity": "sha512-7A3DbBQ+qIQ134FqE518b4tJ8V2a15Sn303JjHzgnqZqKrNh/s3wqwkL60F7LKcd09tcp+vIKQP/MYt4xMcRAA==",
"requires": {
"@tonaljs/core": "^4.6.5"
}
},
"@tonaljs/chord": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/chord/-/chord-4.6.5.tgz",
"integrity": "sha512-Pjdel4aDVv4kcx9PW6Qozt5BB9nAt13AOExfzKztpgPmlBSy0SKHse7Jp1cA4MGAuLHU8dzVssTFYpCskEFw3w==",
"requires": {
"@tonaljs/chord-detect": "^4.6.5",
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5"
}
},
"@tonaljs/chord-detect": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/chord-detect/-/chord-detect-4.6.5.tgz",
"integrity": "sha512-4xu53UP4kNTfdTNpAAVijhXcQ+ypJqmeMnsST08ZXSjoYfJUhmf5rWDWfz36KOTtNdCA6AbYgdtTYV/Xw0nd/w==",
"requires": {
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5"
}
},
"@tonaljs/chord-type": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/chord-type/-/chord-type-4.6.5.tgz",
"integrity": "sha512-Ol4DDopqpZCF9odosO2i8I+plud3Ul7VWJGNvL+PPCf4Qnwuz87q3aJQDLNoRUz4VyW0u66mq3LyVh6A8kb6Ug==",
"requires": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5"
}
},
"@tonaljs/collection": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@tonaljs/collection/-/collection-4.6.2.tgz",
"integrity": "sha512-bfPCotLJNB/tG1NrdbsQPLDKZB5jlMs7uPQ6RYKiNkaena3345ZKkbCGl5pj6YTXeDm/oblXiSbFAn7SlLRZdQ=="
},
"@tonaljs/core": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/core/-/core-4.6.5.tgz",
"integrity": "sha512-t7Vx0+L3j/ubQj2AhI1H45D/K745np4DwJjJjXNi5FlGD+TL2wyw50dCwkHKGHsrLDqup1qqP6yN7LBpC6UwNg=="
},
"@tonaljs/duration-value": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@tonaljs/duration-value/-/duration-value-4.6.2.tgz",
"integrity": "sha512-zrXT0L/qsDQ6251Mlqz54vcUbYUB9xb6uJhlxUzc6VauXOt8UOfrdTULubRTXTaBwWt1h8J5n9pXTQmNGzNI9A=="
},
"@tonaljs/interval": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/interval/-/interval-4.6.5.tgz",
"integrity": "sha512-7EDWhqZ7Nnh9oD4ahRYJHLc799ACGxYL4hDHwMKD16B2MgXqPvDeDvwQ31qUuO0ruGz8tMb3FDlgg0Hplowcbw==",
"requires": {
"@tonaljs/core": "^4.6.5"
}
},
"@tonaljs/key": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/key/-/key-4.6.5.tgz",
"integrity": "sha512-ZdZWb5IStx6CLRmdEjawR66CqNpoW3EVUua2nVZBMdgnNebWxt4nvgH/ZNvGlCQGFZkUZzRhCfTwqsS6e3OmSA==",
"requires": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/note": "^4.6.5",
"@tonaljs/roman-numeral": "^4.6.5"
}
},
"@tonaljs/midi": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/midi/-/midi-4.6.5.tgz",
"integrity": "sha512-fJEZtNvV3M6yW1w+Tep60Rbv5PvuKszQcQzaJS1Loq5mHOKAzdmRfuJSpEpZBiaKEZ1WAMh1QKXYyOd+imyGQg==",
"requires": {
"@tonaljs/core": "^4.6.5"
}
},
"@tonaljs/mode": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/mode/-/mode-4.6.5.tgz",
"integrity": "sha512-54iaON1rJ6q8fV5iuei8RGDxYhKBGGxZz3rjAxGSqdTUwBRVOdPqtzOkofThf9gRGYOMhzPp1BMbxbV+UCAPsA==",
"requires": {
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/interval": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5"
}
},
"@tonaljs/note": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/note/-/note-4.6.5.tgz",
"integrity": "sha512-Y0/eTzcReXzfcSLLG4k/dLLayqbvh/XYIkybG/QMDyR0BREuJq0Sw+NavbzhTtO0dadIQb/qfe0GFq4k2xS+NQ==",
"requires": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/midi": "^4.6.5"
}
},
"@tonaljs/pcset": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/pcset/-/pcset-4.6.5.tgz",
"integrity": "sha512-oWAKflP3cREnUfScqsBzg2LLKNevxSnpDtrq8CPtwOAsrAa8PjQG07NQfhqIiFMjPUdgkDiER3qVA1n8dDwAJA==",
"requires": {
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5"
}
},
"@tonaljs/progression": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/progression/-/progression-4.6.5.tgz",
"integrity": "sha512-ijYEgMFQG4izHYUw5cRtBRNBuoYzmpGvb/tRiykhJNI6XIjekZEMiMsOMfb1u5q+EGvnVNXRmrluMRDIz2rmRw==",
"requires": {
"@tonaljs/chord": "^4.6.5",
"@tonaljs/core": "^4.6.5",
"@tonaljs/roman-numeral": "^4.6.5"
}
},
"@tonaljs/range": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/range/-/range-4.6.5.tgz",
"integrity": "sha512-99cOvVJ3l4X0UJuTSa6qE87JriREnnWIsi3xo1/n7RoqFxnfi8YPh4SfJJyysvHcT18X4EfcTNde9ancMBVu6A==",
"requires": {
"@tonaljs/collection": "^4.6.2",
"@tonaljs/midi": "^4.6.5"
}
},
"@tonaljs/roman-numeral": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/roman-numeral/-/roman-numeral-4.6.5.tgz",
"integrity": "sha512-bWYQNZWKmYDDcmbQQNwcWAHfTWanpzmvI0wplrMnGd4x0op5etwUEv+Yzjg0B1ef+E+zcU02Sl0WwRJhaDK3hg==",
"requires": {
"@tonaljs/core": "^4.6.5"
}
},
"@tonaljs/scale": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/scale/-/scale-4.6.5.tgz",
"integrity": "sha512-isYDamelOBtcd5bEnJ8QV0Js7jKRwZ0FlFVE/+bUN3wsyo9u6KLL5gMyfH9RKdx74m8lE13JXYTXgKqe+AOa4A==",
"requires": {
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/note": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5"
}
},
"@tonaljs/scale-type": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/scale-type/-/scale-type-4.6.5.tgz",
"integrity": "sha512-rwcDOYf2UifjLJhmuQ8f8bJSeOCMDQJ1lB7lzlqdFxes03OeQhdOEfrT0nPtW8BhBEvq4GMM2NA6CLxX8MTwOQ==",
"requires": {
"@tonaljs/core": "^4.6.5",
"@tonaljs/pcset": "^4.6.5"
}
},
"@tonaljs/time-signature": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@tonaljs/time-signature/-/time-signature-4.6.2.tgz",
"integrity": "sha512-OlZY4gdLd21WpMeAI1nS9E9zWcYU6oAzh6ptAUndqmVnFIrIWIWKCkWapdFx8dWdqrX8jqya3m4T33wmeo7w5Q=="
},
"@tonaljs/tonal": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/@tonaljs/tonal/-/tonal-4.6.5.tgz",
"integrity": "sha512-lmsWinI9dy7nQyzCEgDVeVAwJtsk4ey05cJZd6oa4QVuSFD+CR8ebaEiwT4/Na+W0kHrKicT3h0uYc2PJIvx5Q==",
"requires": {
"@tonaljs/abc-notation": "^4.6.5",
"@tonaljs/array": "^4.6.5",
"@tonaljs/chord": "^4.6.5",
"@tonaljs/chord-type": "^4.6.5",
"@tonaljs/collection": "^4.6.2",
"@tonaljs/core": "^4.6.5",
"@tonaljs/duration-value": "^4.6.2",
"@tonaljs/interval": "^4.6.5",
"@tonaljs/key": "^4.6.5",
"@tonaljs/midi": "^4.6.5",
"@tonaljs/mode": "^4.6.5",
"@tonaljs/note": "^4.6.5",
"@tonaljs/pcset": "^4.6.5",
"@tonaljs/progression": "^4.6.5",
"@tonaljs/range": "^4.6.5",
"@tonaljs/roman-numeral": "^4.6.5",
"@tonaljs/scale": "^4.6.5",
"@tonaljs/scale-type": "^4.6.5",
"@tonaljs/time-signature": "^4.6.2"
}
},
"@tonejs/piano": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@tonejs/piano/-/piano-0.2.1.tgz",
"integrity": "sha512-JIwZ91RSFR7Rt16o7cA7O7G30wenFl0lY5yhTsuwZmn48MO9KV+X7kyXE98Bqvs/dCBVg9PoAJ1GKMabPOW4yQ==",
"requires": {
"tslib": "^1.11.1"
}
},
"automation-events": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.14.tgz",
"integrity": "sha512-CB2Me0yW8sz7gSGwMiSfgfs1Oqlgs53k+eVESN6axvRyMAD3zlSp2nqndD2TQAtW3yOtSEJWNGsw0r48+f1wtw==",
"requires": {
"@babel/runtime": "^7.17.2",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"chord-voicings": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/chord-voicings/-/chord-voicings-0.0.1.tgz",
"integrity": "sha512-SutgB/4ynkkuiK6qdQ/k3QvCFcH0Vj8Ch4t6LbRyRQbVzP/TOztiCk3kvXd516UZ6fqk7ijDRELEFcKN+6V8sA==",
"requires": {
"@tonaljs/tonal": "^4.6.5"
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"standardized-audio-context": {
"version": "25.3.21",
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.21.tgz",
"integrity": "sha512-CZEnayJJjNefeU+z1QGDhaid1LAAYxWYa2ipNk75ropwec9rq6fmclyhXb1wGtDsZ402irX3HLt1U/PwP9+1fA==",
"requires": {
"@babel/runtime": "^7.17.2",
"automation-events": "^4.0.14",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"tone": {
"version": "14.7.77",
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
"requires": {
"standardized-audio-context": "^25.1.8",
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"webmidi": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg==",
"peer": true
}
}
}

View file

@ -0,0 +1,32 @@
{
"name": "@strudel/tone",
"version": "0.0.1",
"description": "Tone.js API for strudel",
"main": "tone.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
}
}

View file

@ -0,0 +1,12 @@
import { strict as assert } from 'assert';
import '../tone.mjs';
import { pure } from '../../core/strudel.mjs';
import Tone from 'tone';
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']);
});
});

275
packages/tone/tone.mjs Normal file
View file

@ -0,0 +1,275 @@
import { Pattern as _Pattern } from '../core/strudel.mjs';
import Tone from 'tone';
const {
AutoFilter,
Destination,
Filter,
Gain,
isNote,
Synth,
PolySynth,
MembraneSynth,
MetalSynth,
MonoSynth,
AMSynth,
DuoSynth,
FMSynth,
NoiseSynth,
PluckSynth,
Sampler,
getDestination,
Players,
} = Tone;
/* import tonePiano from '@tonejs/piano';
const { Piano } = tonePiano; */
import { getPlayableNoteValue } from '../../packages/core/util.mjs';
// "balanced" | "interactive" | "playback";
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
export const defaultSynth = new PolySynth().chain(new Gain(0.5), getDestination());
defaultSynth.set({
oscillator: { type: 'triangle' },
envelope: {
release: 0.01,
},
});
// what about
// https://www.charlie-roberts.com/gibberish/playground/
const Pattern = _Pattern;
// with this function, you can play the pattern with any tone synth
Pattern.prototype.tone = function (instrument) {
// instrument.toDestination();
return this._withEvent((event) => {
const onTrigger = (time, event) => {
let note;
let velocity = event.context?.velocity ?? 0.75;
switch (instrument.constructor.name) {
case 'PluckSynth':
note = getPlayableNoteValue(event);
instrument.triggerAttack(note, time);
break;
case 'NoiseSynth':
instrument.triggerAttackRelease(event.duration, time); // noise has no value
break;
case 'Piano':
note = getPlayableNoteValue(event);
instrument.keyDown({ note, time, velocity });
instrument.keyUp({ note, time: time + event.duration, velocity });
break;
case 'Sampler':
note = getPlayableNoteValue(event);
instrument.triggerAttackRelease(note, event.duration, time, velocity);
break;
case 'Players':
if (!instrument.has(event.value)) {
throw new Error(`name "${event.value}" not defined for players`);
}
const player = instrument.player(event.value);
// velocity ?
player.start(time);
player.stop(time + event.duration);
break;
default:
note = getPlayableNoteValue(event);
instrument.triggerAttackRelease(note, event.duration, time, velocity);
}
};
return event.setContext({ ...event.context, instrument, onTrigger });
});
};
Pattern.prototype.define('tone', (type, pat) => pat.tone(type), { composable: true, patternified: false });
// synth helpers
export const amsynth = (options) => new AMSynth(options);
export const duosynth = (options) => new DuoSynth(options);
export const fmsynth = (options) => new FMSynth(options);
export const membrane = (options) => new MembraneSynth(options);
export const metal = (options) => new MetalSynth(options);
export const monosynth = (options) => new MonoSynth(options);
export const noise = (options) => new NoiseSynth(options);
export const pluck = (options) => new PluckSynth(options);
export const polysynth = (options) => new PolySynth(options);
export const sampler = (options, baseUrl) =>
new Promise((resolve) => {
const s = new Sampler(options, () => resolve(s), baseUrl);
});
export const players = (options, baseUrl = '') => {
options = !baseUrl
? options
: Object.fromEntries(Object.entries(options).map(([key, value]) => [key, baseUrl + value]));
return new Promise((resolve) => {
const s = new Players(options, () => resolve(s));
});
};
export const synth = (options) => new Synth(options);
/* export const piano = async (options = { velocities: 1 }) => {
const p = new Piano(options);
await p.load();
return p;
}; */
// effect helpers
export const vol = (v) => new Gain(v);
export const lowpass = (v) => new Filter(v, 'lowpass');
export const highpass = (v) => new Filter(v, 'highpass');
export const adsr = (a, d = 0.1, s = 0.4, r = 0.01) => ({ envelope: { attack: a, decay: d, sustain: s, release: r } });
export const osc = (type) => ({ oscillator: { type } });
export const out = () => getDestination();
/*
You are entering experimental zone
*/
// the following code is an attempt to minimize tonejs code.. it is still an experiment
const chainable = function (instr) {
const _chain = instr.chain.bind(instr);
let chained = [];
instr.chain = (...args) => {
chained = chained.concat(args);
instr.disconnect(); // disconnect from destination / previous chain
return _chain(...chained, getDestination());
};
// shortcuts: chaining multiple won't work forn now.. like filter(1000).gain(0.5). use chain + native Tone calls instead
// instr.filter = (freq = 1000, type: BiquadFilterType = 'lowpass') =>
instr.filter = (freq = 1000, type = 'lowpass') =>
instr.chain(
new Filter(freq, type), // .Q.setValueAtTime(q, time);
);
instr.gain = (gain = 0.9) => instr.chain(new Gain(gain));
return instr;
};
// helpers
export const poly = (type) => {
const s = new PolySynth(Synth, { oscillator: { type } }).toDestination();
return chainable(s);
};
Pattern.prototype._poly = function (type = 'triangle') {
const instrumentConfig = {
oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
};
if (!this.instrument) {
// create only once to keep the js heap happy
// this.instrument = new PolySynth(Synth, instrumentConfig).toDestination();
this.instrument = poly(type);
}
return this._withEvent((event) => {
const onTrigger = (time, event) => {
this.instrument.set(instrumentConfig);
this.instrument.triggerAttackRelease(event.value, event.duration, time);
};
return event.setContext({ ...event.context, instrumentConfig, onTrigger });
});
};
Pattern.prototype.define('poly', (type, pat) => pat.poly(type), { composable: true, patternified: true });
/*
You are entering danger zone
*/
// everything below is nice in theory, but not healthy for the JS heap, as nodes get recreated on every call
const getTrigger = (getChain, value) => (time, event) => {
const chain = getChain(); // make sure this returns a node that is connected toDestination // time
if (!isNote(value)) {
throw new Error('not a note: ' + value);
}
chain.triggerAttackRelease(value, event.duration, time);
setTimeout(() => {
// setTimeout is a little bit better compared to Transport.scheduleOnce
chain.dispose(); // mark for garbage collection
}, event.duration * 2000);
};
Pattern.prototype._synth = function (type = 'triangle') {
return this._withEvent((event) => {
const instrumentConfig = {
oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
};
const getInstrument = () => {
const instrument = new Synth();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
});
};
Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
return this._withEvent((event) => {
if (!event.context.getInstrument) {
throw new Error('cannot chain adsr: need instrument first (like synth)');
}
const instrumentConfig = { ...event.context.instrumentConfig, envelope: { attack, decay, sustain, release } };
const getInstrument = () => {
const instrument = event.context.getInstrument();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return event.setContext({ ...event.context, getInstrument, instrumentConfig, onTrigger });
});
};
Pattern.prototype.chain = function (...effectGetters) {
return this._withEvent((event) => {
if (!event.context?.getInstrument) {
throw new Error('cannot chain: need instrument first (like synth)');
}
const chain = (event.context.chain || []).concat(effectGetters);
const getChain = () => {
const effects = chain.map((getEffect) => getEffect());
return event.context.getInstrument().chain(...effects, getDestination());
};
const onTrigger = getTrigger(getChain, event.value);
return event.setContext({ ...event.context, getChain, onTrigger, chain });
});
};
export const autofilter =
(freq = 1) =>
() =>
new AutoFilter(freq).start();
export const filter =
// (freq = 1, q = 1, type: BiquadFilterType = 'lowpass') =>
(freq = 1, q = 1, type = 'lowpass') =>
() =>
new Filter(freq, type); // .Q.setValueAtTime(q, time);
export const gain =
(gain = 0.9) =>
() =>
new Gain(gain);
Pattern.prototype._gain = function (g) {
return this.chain(gain(g));
};
// Pattern.prototype._filter = function (freq: number, q: number, type: BiquadFilterType = 'lowpass') {
Pattern.prototype._filter = function (freq, q, type = 'lowpass') {
return this.chain(filter(freq, q, type));
};
Pattern.prototype._autofilter = function (g) {
return this.chain(autofilter(g));
};
Pattern.prototype.define('synth', (type, pat) => pat.synth(type), { composable: true, patternified: true });
Pattern.prototype.define('gain', (gain, pat) => pat.synth(gain), { composable: true, patternified: true });
Pattern.prototype.define('filter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });
Pattern.prototype.define('autofilter', (cutoff, pat) => pat.filter(cutoff), { composable: true, patternified: true });

164
packages/tunes/oldtunes.ts Normal file
View file

@ -0,0 +1,164 @@
export const timeCatMini = `stack(
"c3@3 [eb3, g3, [c4 d4]/2]",
"c2 g2",
"[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2".slow(8)
)`;
export const timeCat = `stack(
timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
cat(c2, g2),
sequence(
timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
cat(eb4, c4).slow(2)
).slow(4)
)`;
export const shapeShifted = `stack(
sequence(
e5, [b4, c5], d5, [c5, b4],
a4, [a4, c5], e5, [d5, c5],
b4, [r, c5], d5, e5,
c5, a4, a4, r,
[r, d5], [r, f5], a5, [g5, f5],
e5, [r, c5], e5, [d5, c5],
b4, [b4, c5], d5, e5,
c5, a4, a4, r,
).rev(),
sequence(
e2, e3, e2, e3, e2, e3, e2, e3,
a2, a3, a2, a3, a2, a3, a2, a3,
gs2, gs3, gs2, gs3, e2, e3, e2, e3,
a2, a3, a2, a3, a2, a3, b1, c2,
d2, d3, d2, d3, d2, d3, d2, d3,
c2, c3, c2, c3, c2, c3, c2, c3,
b1, b2, b1, b2, e2, e3, e2, e3,
a1, a2, a1, a2, a1, a2, a1, a2,
).rev()
).slow(16)`;
export const tetrisWithFunctions = `stack(sequence(
'e5', sequence('b4', 'c5'), 'd5', sequence('c5', 'b4'),
'a4', sequence('a4', 'c5'), 'e5', sequence('d5', 'c5'),
'b4', sequence(silence, 'c5'), 'd5', 'e5',
'c5', 'a4', 'a4', silence,
sequence(silence, 'd5'), sequence(silence, 'f5'), 'a5', sequence('g5', 'f5'),
'e5', sequence(silence, 'c5'), 'e5', sequence('d5', 'c5'),
'b4', sequence('b4', 'c5'), 'd5', 'e5',
'c5', 'a4', 'a4', silence),
sequence(
'e2', 'e3', 'e2', 'e3', 'e2', 'e3', 'e2', 'e3',
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'a2', 'a3',
'g#2', 'g#3', 'g#2', 'g#3', 'e2', 'e3', 'e2', 'e3',
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'b1', 'c2',
'd2', 'd3', 'd2', 'd3', 'd2', 'd3', 'd2', 'd3',
'c2', 'c3', 'c2', 'c3', 'c2', 'c3', 'c2', 'c3',
'b1', 'b2', 'b1', 'b2', 'e2', 'e3', 'e2', 'e3',
'a1', 'a2', 'a1', 'a2', 'a1', 'a2', 'a1', 'a2',
)
).slow(16)`;
export const tetris = `stack(
cat(
"e5 [b4 c5] d5 [c5 b4]",
"a4 [a4 c5] e5 [d5 c5]",
"b4 [~ c5] d5 e5",
"c5 a4 a4 ~",
"[~ d5] [~ f5] a5 [g5 f5]",
"e5 [~ c5] e5 [d5 c5]",
"b4 [b4 c5] d5 e5",
"c5 a4 a4 ~"
),
cat(
"e2 e3 e2 e3 e2 e3 e2 e3",
"a2 a3 a2 a3 a2 a3 a2 a3",
"g#2 g#3 g#2 g#3 e2 e3 e2 e3",
"a2 a3 a2 a3 a2 a3 b1 c2",
"d2 d3 d2 d3 d2 d3 d2 d3",
"c2 c3 c2 c3 c2 c3 c2 c3",
"b1 b2 b1 b2 e2 e3 e2 e3",
"a1 a2 a1 a2 a1 a2 a1 a2",
)
).slow(16)`;
export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]\`.slow(16)
`;
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
.every(4, fast(2))
.every(3, slow(1.5))
.fast(slowcat(1.25, 1, 1.5))
.every(2, _ => sequence(e4, r, e3, d4, r))`;
export const giantSteps = `stack(
// melody
cat(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 F#5]",
),
// chords
cat(
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
).voicings(['E3', 'G4']),
// bass
cat(
"[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]",
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]"
)
).slow(20);`;
export const transposedChordsHacked = `stack(
"c2 eb2 g2",
"Cm7".voicings(['g2','c4']).slow(2)
).transpose(
slowcat(1, 2, 3, 2).slow(2)
).transpose(5)`;
export const scaleTranspose = `stack(f2, f3, c4, ab4)
.scale(sequence('F minor', 'F harmonic minor').slow(4))
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
.transpose(sequence(0, 1).slow(16))`;
export const struct = `stack(
"c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]",
"[C^7 A7] [Dm7 G7]".struct("[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2")
.voicings(['G3','A4'])
).slow(4)`;
export const magicSofa = `stack(
"<C^7 F^7 ~> <Dm7 G7 A7 ~>"
.every(2, fast(2))
.voicings(),
"<c2 f2 g2> <d2 g2 a2 e2>"
).slow(1).transpose.slowcat(0, 2, 3, 4)`;
export const primalEnemy = `()=>{
const f = fast("<1 <2 [4 8]>>");
return stack(
"c3,g3,c4".struct("[x ~]*2").apply(f).transpose("<0 <3 [5 [7 [9 [11 13]]]]>>"),
"c2 [c2 ~]*2".tone(synth(osc('sawtooth8')).chain(vol(0.8),out())),
"c1*2".tone(membrane().chain(vol(0.8),out()))
).slow(1)
}`;

670
packages/tunes/tunes.ts Normal file
View file

@ -0,0 +1,670 @@
export const timeCatMini = `stack(
"c3@3 [eb3, g3, [c4 d4]/2]",
"c2 g2",
"[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2".slow(8)
)`;
export const timeCat = `stack(
timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
cat(c2, g2),
sequence(
timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
cat(eb4, c4).slow(2)
).slow(4)
)`;
export const shapeShifted = `stack(
sequence(
e5, [b4, c5], d5, [c5, b4],
a4, [a4, c5], e5, [d5, c5],
b4, [r, c5], d5, e5,
c5, a4, a4, r,
[r, d5], [r, f5], a5, [g5, f5],
e5, [r, c5], e5, [d5, c5],
b4, [b4, c5], d5, e5,
c5, a4, a4, r,
).rev(),
sequence(
e2, e3, e2, e3, e2, e3, e2, e3,
a2, a3, a2, a3, a2, a3, a2, a3,
gs2, gs3, gs2, gs3, e2, e3, e2, e3,
a2, a3, a2, a3, a2, a3, b1, c2,
d2, d3, d2, d3, d2, d3, d2, d3,
c2, c3, c2, c3, c2, c3, c2, c3,
b1, b2, b1, b2, e2, e3, e2, e3,
a1, a2, a1, a2, a1, a2, a1, a2,
).rev()
).slow(16)`;
export const tetrisWithFunctions = `stack(sequence(
'e5', sequence('b4', 'c5'), 'd5', sequence('c5', 'b4'),
'a4', sequence('a4', 'c5'), 'e5', sequence('d5', 'c5'),
'b4', sequence(silence, 'c5'), 'd5', 'e5',
'c5', 'a4', 'a4', silence,
sequence(silence, 'd5'), sequence(silence, 'f5'), 'a5', sequence('g5', 'f5'),
'e5', sequence(silence, 'c5'), 'e5', sequence('d5', 'c5'),
'b4', sequence('b4', 'c5'), 'd5', 'e5',
'c5', 'a4', 'a4', silence),
sequence(
'e2', 'e3', 'e2', 'e3', 'e2', 'e3', 'e2', 'e3',
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'a2', 'a3',
'g#2', 'g#3', 'g#2', 'g#3', 'e2', 'e3', 'e2', 'e3',
'a2', 'a3', 'a2', 'a3', 'a2', 'a3', 'b1', 'c2',
'd2', 'd3', 'd2', 'd3', 'd2', 'd3', 'd2', 'd3',
'c2', 'c3', 'c2', 'c3', 'c2', 'c3', 'c2', 'c3',
'b1', 'b2', 'b1', 'b2', 'e2', 'e3', 'e2', 'e3',
'a1', 'a2', 'a1', 'a2', 'a1', 'a2', 'a1', 'a2',
)
).slow(16)`;
export const tetris = `stack(
cat(
"e5 [b4 c5] d5 [c5 b4]",
"a4 [a4 c5] e5 [d5 c5]",
"b4 [~ c5] d5 e5",
"c5 a4 a4 ~",
"[~ d5] [~ f5] a5 [g5 f5]",
"e5 [~ c5] e5 [d5 c5]",
"b4 [b4 c5] d5 e5",
"c5 a4 a4 ~"
),
cat(
"e2 e3 e2 e3 e2 e3 e2 e3",
"a2 a3 a2 a3 a2 a3 a2 a3",
"g#2 g#3 g#2 g#3 e2 e3 e2 e3",
"a2 a3 a2 a3 a2 a3 b1 c2",
"d2 d3 d2 d3 d2 d3 d2 d3",
"c2 c3 c2 c3 c2 c3 c2 c3",
"b1 b2 b1 b2 e2 e3 e2 e3",
"a1 a2 a1 a2 a1 a2 a1 a2",
)
).slow(16)`;
export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]],
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]\`.slow(16)
`;
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
.every(4, fast(2))
.every(3, slow(1.5))
.fast(slowcat(1.25, 1, 1.5))
.every(2, _ => sequence(e4, r, e3, d4, r))`;
export const swimming = `stack(
cat(
"~",
"~",
"~",
"A5 [F5@2 C5] [D5@2 F5] F5",
"[C5@2 F5] [F5@2 C6] A5 G5",
"A5 [F5@2 C5] [D5@2 F5] F5",
"[C5@2 F5] [Bb5 A5 G5] F5@2",
"A5 [F5@2 C5] [D5@2 F5] F5",
"[C5@2 F5] [F5@2 C6] A5 G5",
"A5 [F5@2 C5] [D5@2 F5] F5",
"[C5@2 F5] [Bb5 A5 G5] F5@2",
"A5 [F5@2 C5] A5 F5",
"Ab5 [F5@2 Ab5] G5@2",
"A5 [F5@2 C5] A5 F5",
"Ab5 [F5@2 C5] C6@2",
"A5 [F5@2 C5] [D5@2 F5] F5",
"[C5@2 F5] [Bb5 A5 G5] F5@2"
),
cat(
"[F4,Bb4,D5] [[D4,G4,Bb4]@2 [Bb3,D4,F4]] [[G3,C4,E4]@2 [[Ab3,F4] [A3,Gb4]]] [Bb3,E4,G4]",
"[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, Bb3, Db3] [F3, Bb3, Db3]]",
"[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]",
"[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]",
"[~ [A3, C4, E4] [A3, C4, E4]] [~ [Ab3, C4, Eb4] [Ab3, C4, Eb4]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [G3, C4, E4] [G3, C4, E4]]",
"[~ [F3, A3, C4] [F3, A3, C4]] [~ [F3, A3, C4] [F3, A3, C4]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]",
"[~ [F3, Bb3, D4] [F3, Bb3, D4]] [~ [F3, Bb3, C4] [F3, Bb3, C4]] [~ [F3, A3, C4] [F3, A3, C4]] [~ [F3, A3, C4] [F3, A3, C4]]",
"[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]",
"[~ [A3, C4, E4] [A3, C4, E4]] [~ [Ab3, C4, Eb4] [Ab3, C4, Eb4]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [G3, C4, E4] [G3, C4, E4]]",
"[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]",
"[~ [F3, Bb3, D4] [F3, Bb3, D4]] [~ [F3, Bb3, C4] [F3, Bb3, C4]] [~ [F3, A3, C4] [F3, A3, C4]] [~ [F3, A3, C4] [F3, A3, C4]]",
"[~ [Bb3, D3, F4] [Bb3, D3, F4]] [~ [Bb3, D3, F4] [Bb3, D3, F4]] [~ [A3, C4, F4] [A3, C4, F4]] [~ [A3, C4, F4] [A3, C4, F4]]",
"[~ [Ab3, B3, F4] [Ab3, B3, F4]] [~ [Ab3, B3, F4] [Ab3, B3, F4]] [~ [G3, Bb3, F4] [G3, Bb3, F4]] [~ [G3, Bb3, E4] [G3, Bb3, E4]]",
"[~ [Bb3, D3, F4] [Bb3, D3, F4]] [~ [Bb3, D3, F4] [Bb3, D3, F4]] [~ [A3, C4, F4] [A3, C4, F4]] [~ [A3, C4, F4] [A3, C4, F4]]",
"[~ [Ab3, B3, F4] [Ab3, B3, F4]] [~ [Ab3, B3, F4] [Ab3, B3, F4]] [~ [G3, Bb3, F4] [G3, Bb3, F4]] [~ [G3, Bb3, E4] [G3, Bb3, E4]]",
"[~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, A3, C3] [F3, A3, C3]] [~ [F3, Bb3, D3] [F3, Bb3, D3]] [~ [F3, B3, D3] [F3, B3, D3]]",
"[~ [F3, Bb3, D4] [F3, Bb3, D4]] [~ [F3, Bb3, C4] [F3, Bb3, C4]] [~ [F3, A3, C4] [F3, A3, C4]] [~ [F3, A3, C4] [F3, A3, C4]]"
),
cat(
"[G3 G3 C3 E3]",
"[F2 D2 G2 C2]",
"[F2 D2 G2 C2]",
"[F2 A2 Bb2 B2]",
"[A2 Ab2 G2 C2]",
"[F2 A2 Bb2 B2]",
"[G2 C2 F2 F2]",
"[F2 A2 Bb2 B2]",
"[A2 Ab2 G2 C2]",
"[F2 A2 Bb2 B2]",
"[G2 C2 F2 F2]",
"[Bb2 Bb2 A2 A2]",
"[Ab2 Ab2 G2 [C2 D2 E2]]",
"[Bb2 Bb2 A2 A2]",
"[Ab2 Ab2 G2 [C2 D2 E2]]",
"[F2 A2 Bb2 B2]",
"[G2 C2 F2 F2]"
)
).slow(51);
`;
export const giantSteps = `stack(
// melody
cat(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 F#5]",
),
// chords
cat(
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
).voicings(['E3', 'G4']),
// bass
cat(
"[B2 D2] [G2 Bb2] [Eb2 Bb3] [A2 D2]",
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb3] [C#2 F#2]"
)
).slow(20);`;
export const giantStepsReggae = `stack(
// melody
cat(
"[F#5 D5] [B4 G4] Bb4 [B4 A4]",
"[D5 Bb4] [G4 Eb4] F#4 [G4 F4]",
"Bb4 [B4 A4] D5 [D#5 C#5]",
"F#5 [G5 F5] Bb5 [F#5 [F#5 ~@3]]",
),
// chords
cat(
"[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]",
"[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]",
"Eb^7 [Am7 D7] G^7 [C#m7 F#7]",
"B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]"
)
.struct("~ [x ~]".fast(4*8))
.voicings(['E3', 'G4']),
// bass
cat(
"[B2 D2] [G2 D2] [Eb2 Bb2] [A2 D2]",
"[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]",
"[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]",
"[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]"
)
.struct("x ~".fast(4*8))
).slow(25)`;
export const transposedChordsHacked = `stack(
"c2 eb2 g2",
"Cm7".voicings(['g2','c4']).slow(2)
).transpose(
slowcat(1, 2, 3, 2).slow(2)
).transpose(5)`;
export const scaleTranspose = `stack(f2, f3, c4, ab4)
.scale(sequence('F minor', 'F harmonic minor').slow(4))
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
.transpose(sequence(0, 1).slow(16))`;
export const struct = `stack(
"c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]",
"[C^7 A7] [Dm7 G7]".struct("[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2")
.voicings(['G3','A4'])
).slow(4)`;
export const magicSofa = `stack(
"<C^7 F^7 ~> <Dm7 G7 A7 ~>"
.every(2, fast(2))
.voicings(),
"<c2 f2 g2> <d2 g2 a2 e2>"
).slow(1).transpose(slowcat(0, 2, 3, 4))`;
// below doesn't work anymore due to constructor cleanup
// ).slow(1).transpose.slowcat(0, 2, 3, 4)`;
export const confusedPhone = `"[g2 ~@1.3] [c3 ~@1.3]"
.superimpose(
transpose(-12).late(0),
transpose(7).late(0.1),
transpose(10).late(0.2),
transpose(12).late(0.3),
transpose(24).late(0.4)
)
.scale(slowcat('C dorian', 'C mixolydian'))
.scaleTranspose(slowcat(0,1,2,1))
.slow(2)`;
export const zeldasRescue = `stack(
// melody
\`[B3@2 D4] [A3@2 [G3 A3]] [B3@2 D4] [A3]
[B3@2 D4] [A4@2 G4] [D4@2 [C4 B3]] [A3]
[B3@2 D4] [A3@2 [G3 A3]] [B3@2 D4] [A3]
[B3@2 D4] [A4@2 G4] D5@2
[D5@2 [C5 B4]] [[C5 B4] G4@2] [C5@2 [B4 A4]] [[B4 A4] E4@2]
[D5@2 [C5 B4]] [[C5 B4] G4 C5] [G5] [~ ~ B3]\`,
// bass
\`[[C2 G2] E3@2] [[C2 G2] F#3@2] [[C2 G2] E3@2] [[C2 G2] F#3@2]
[[B1 D3] G3@2] [[Bb1 Db3] G3@2] [[A1 C3] G3@2] [[D2 C3] F#3@2]
[[C2 G2] E3@2] [[C2 G2] F#3@2] [[C2 G2] E3@2] [[C2 G2] F#3@2]
[[B1 D3] G3@2] [[Bb1 Db3] G3@2] [[A1 C3] G3@2] [[D2 C3] F#3@2]
[[F2 C3] E3@2] [[E2 B2] D3@2] [[D2 A2] C3@2] [[C2 G2] B2@2]
[[F2 C3] E3@2] [[E2 B2] D3@2] [[Eb2 Bb2] Db3@2] [[D2 A2] C3 [F3,G2]]\`
).transpose(12).slow(48).tone(
new PolySynth().chain(
new Gain(0.3),
new Chorus(2, 2.5, 0.5).start(),
new Freeverb(),
getDestination())
)`;
export const technoDrums = `stack(
"c1*2".tone(new Tone.MembraneSynth().toDestination()),
"~ x".tone(new Tone.NoiseSynth().toDestination()),
"[~ c4]*2".tone(new Tone.MetalSynth().set({envelope:{decay:0.06,sustain:0}}).chain(new Gain(0.5),getDestination()))
)`;
export const loungerave = `const delay = new FeedbackDelay(1/8, .2).chain(vol(0.5), out());
const kick = new MembraneSynth().chain(vol(.8), out());
const snare = new NoiseSynth().chain(vol(.8), out());
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out());
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out());
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out());
const drums = stack(
"c1*2".tone(kick).mask("<x@7 ~>/8"),
"~ <x!7 [x@3 x]>".tone(snare).mask("<x@7 ~>/4"),
"[~ c4]*2".tone(hihat)
);
const thru = (x) => x.transpose("<0 1>/8").transpose(1);
const synths = stack(
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2").edit(thru).tone(bass),
"<Cm7 Bb7 Fm7 G7b9>/2".struct("~ [x@0.1 ~]").voicings().edit(thru).every(2, early(1/4)).tone(keys).mask("<x@7 ~>/8".early(1/4))
)
stack(
drums,
synths
)`;
export const caverave = `const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out());
const kick = new MembraneSynth().chain(vol(.8), out());
const snare = new NoiseSynth().chain(vol(.8), out());
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out());
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out());
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out());
const drums = stack(
"c1*2".tone(kick).mask("<x@7 ~>/8"),
"~ <x!7 [x@3 x]>".tone(snare).mask("<x@7 ~>/4"),
"[~ c4]*2".tone(hihat)
);
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(
scaleTranspose(0).early(0),
scaleTranspose(2).early(1/8),
scaleTranspose(7).early(1/4),
scaleTranspose(8).early(3/8)
).apply(thru).tone(keys).mask("<~ x>/16"),
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).apply(thru).tone(bass),
"<Cm7 Bb7 Fm7 G7b13>/2".struct("~ [x@0.1 ~]".fast(2)).voicings().apply(thru).every(2, early(1/8)).tone(keys).mask("<x@7 ~>/8".early(1/4))
)
stack(
drums.fast(2),
synths
).slow(2)`;
export const callcenterhero = `const bpm = 90;
const lead = polysynth().set({...osc('sine4'),...adsr(.004)}).chain(vol(0.15),out())
const bass = fmsynth({...osc('sawtooth6'),...adsr(0.05,.6,0.8,0.1)}).chain(vol(0.6), out());
const s = scale(slowcat('F3 minor', 'Ab3 major', 'Bb3 dorian', 'C4 phrygian dominant').slow(4));
stack(
"0 2".struct("<x ~> [x ~]").apply(s).scaleTranspose(stack(0,2)).tone(lead),
"<6 7 9 7>".struct("[~ [x ~]*2]*2").apply(s).scaleTranspose("[0,2] [2,4]".fast(2).every(4,rev)).tone(lead),
"-14".struct("[~ x@0.8]*2".early(0.01)).apply(s).tone(bass),
"c2*2".tone(membrane().chain(vol(0.6), out())),
"~ c2".tone(noise().chain(vol(0.2), out())),
"c4*4".tone(metal(adsr(0,.05,0)).chain(vol(0.03), out()))
)
.slow(120 / bpm)`;
export const primalEnemy = `const f = fast("<1 <2 [4 8]>>");
stack(
"c3,g3,c4".struct("[x ~]*2").apply(f).transpose("<0 <3 [5 [7 [9 [11 13]]]]>>"),
"c2 [c2 ~]*2".tone(synth(osc('sawtooth8')).chain(vol(0.8),out())),
"c1*2".tone(membrane().chain(vol(0.8),out()))
).slow(1)`;
export const synthDrums = `stack(
"c1*2".tone(membrane().chain(vol(0.8),out())),
"~ c3".tone(noise().chain(vol(0.8),out())),
"c3*4".transpose("[-24 0]*2").tone(metal(adsr(0,.015)).chain(vol(0.8),out()))
)
`;
export const sampleDrums = `const drums = await players({
bd: 'bd/BT0A0D0.wav',
sn: 'sn/ST0T0S3.wav',
hh: 'hh/000_hh3closedhh.wav'
}, 'https://loophole-letters.vercel.app/samples/tidal/')
stack(
"<bd!3 bd(3,4,2)>",
"hh*4",
"~ <sn!3 sn(3,4,1)>"
).tone(drums.chain(out()))
`;
export const xylophoneCalling = `const t = x => x.scaleTranspose("<0 2 4 3>/4").transpose(-2)
const s = x => x.scale(slowcat('C3 minor pentatonic','G3 minor pentatonic').slow(4))
const delay = new FeedbackDelay(1/8, .6).chain(vol(0.1), out());
const chorus = new Chorus(1,2.5,0.5).start();
stack(
// melody
"<<10 7> <8 3>>/4".struct("x*3").apply(s)
.scaleTranspose("<0 3 2> <1 4 3>")
.superimpose(scaleTranspose(2).early(1/8))
.apply(t).tone(polysynth().set({
...osc('triangle4'),
...adsr(0,.08,0)
}).chain(vol(0.2).connect(delay),chorus,out())).mask("<~@3 x>/16".early(1/8)),
// pad
"[1,3]/4".scale('G3 minor pentatonic').apply(t).tone(polysynth().set({
...osc('square2'),
...adsr(0.1,.4,0.8)
}).chain(vol(0.2),chorus,out())).mask("<~ x>/32"),
// xylophone
"c3,g3,c4".struct("<x*2 x>").fast("<1 <2!3 [4 8]>>").apply(s).scaleTranspose("<0 <1 [2 [3 <4 5>]]>>").apply(t).tone(polysynth().set({
...osc('sawtooth4'),
...adsr(0,.1,0)
}).chain(vol(0.4).connect(delay),out())).mask("<x@3 ~>/16".early(1/8)),
// bass
"c2 [c2 ~]*2".scale('C hirajoshi').apply(t).tone(synth({
...osc('sawtooth6'),
...adsr(0,.03,.4,.1)
}).chain(vol(0.4),out())),
// kick
"<c1!3 [c1 ~]*2>*2".tone(membrane().chain(vol(0.8),out())),
// snare
"~ <c3!7 [c3 c3*2]>".tone(noise().chain(vol(0.8),out())),
// hihat
"c3*4".transpose("[-24 0]*2").tone(metal(adsr(0,.02)).chain(vol(0.5).connect(delay),out()))
).slow(1)`;
export const sowhatelse = `// mixer
const mix = (key) => vol({
chords: .2,
lead: 0.8,
bass: .4,
snare: .95,
kick: .9,
hihat: .35,
}[key]||0);
const delay = new FeedbackDelay(1/6, .3).chain(vol(.7), out());
const delay2 = new FeedbackDelay(1/6, .2).chain(vol(.15), out());
const chorus = new Chorus(1,2.5,0.5).start();
// instruments
const instr = (instrument) => ({
organ: polysynth().set({...osc('sawtooth4'), ...adsr(.01,.2,0)}).chain(mix('chords').connect(delay),out()),
lead: polysynth().set({...osc('triangle4'),...adsr(0.01,.05,0)}).chain(mix('lead').connect(delay2), out()),
bass: polysynth().set({...osc('sawtooth8'),...adsr(.02,.05,.3,.2)}).chain(mix('bass'),lowpass(3000), out()),
pad: polysynth().set({...osc('square2'),...adsr(0.1,.4,0.8)}).chain(vol(0.15),chorus,out()),
hihat: metal(adsr(0, .02, 0)).chain(mix('hihat'), out()),
snare: noise(adsr(0, .15, 0.01)).chain(mix('snare'), lowpass(5000), out()),
kick: membrane().chain(mix('kick'), out())
}[instrument]);
// harmony
const t = transpose("<0 0 1 0>/8");
const sowhat = scaleTranspose("0,3,6,9,11");
// track
stack(
"[<0 4 [3 [2 1]]>]/4".struct("[x]*3").mask("[~ x ~]").scale('D5 dorian').off(1/6, scaleTranspose(-7)).off(1/3, scaleTranspose(-5)).apply(t).tone(instr('lead')).mask("<~ ~ x x>/8"),
"<<e3 [~@2 a3]> <[d3 ~] [c3 f3] g3>>".scale('D dorian').apply(sowhat).apply(t).tone(instr('organ')).mask("<x x x ~>/8"),
"<[d2 [d2 ~]*3]!3 <a1*2 c2*3 [a1 e2]>>".apply(t).tone(instr('bass')),
"c1*6".tone(instr('hihat')),
"~ c3".tone(instr('snare')),
"<[c1@5 c1] <c1 [[c1@2 c1] ~] [c1 ~ c1] [c1!2 ~ c1!3]>>".tone(instr('kick')),
"[2,4]/4".scale('D dorian').apply(t).tone(instr('pad')).mask("<x x x ~>/8")
).fast(6/8)`;
export const barryHarris = `backgroundImage(
'https://media.npr.org/assets/img/2017/02/03/barryharris_600dpi_wide-7eb49998aa1af377d62bb098041624c0a0d1a454.jpg',
{style:'background-size:cover'})
"0,2,[7 6]"
.add("<0 1 2 3 4 5 7 8>")
.scale('C bebop major')
.transpose("<0 1 2 1>/8")
.slow(2)
.tone((await piano()).toDestination())
`;
export const blippyRhodes = `const delay = new FeedbackDelay(1/12, .4).chain(vol(0.3), out());
const drums = await players({
bd: 'samples/tidal/bd/BT0A0D0.wav',
sn: 'samples/tidal/sn/ST0T0S3.wav',
hh: 'samples/tidal/hh/000_hh3closedhh.wav'
}, 'https://loophole-letters.vercel.app/')
const rhodes = await sampler({
E1: 'samples/rhodes/MK2Md2000.mp3',
E2: 'samples/rhodes/MK2Md2012.mp3',
E3: 'samples/rhodes/MK2Md2024.mp3',
E4: 'samples/rhodes/MK2Md2036.mp3',
E5: 'samples/rhodes/MK2Md2048.mp3',
E6: 'samples/rhodes/MK2Md2060.mp3',
E7: 'samples/rhodes/MK2Md2072.mp3'
}, 'https://loophole-letters.vercel.app/')
const bass = synth(osc('sawtooth8')).chain(vol(.5),out())
const scales = sequence('C major', 'C mixolydian', 'F lydian', ['F minor', slowcat('Db major','Db mixolydian')]).slow(4)
stack(
"<bd sn> <hh hh*2 hh*3>"
.tone(drums.chain(out())),
"<g4 c5 a4 [ab4 <eb5 f5>]>"
.scale(scales)
.struct("x*8")
.scaleTranspose("0 [-5,-2] -7 [-9,-2]")
.legato(.3)
.slow(2)
.tone(rhodes.chain(vol(0.5).connect(delay), out())),
//"<C^7 C7 F^7 [Fm7 <Db^7 Db7>]>".slow(2).voicings().struct("~ x").legato(.25).tone(rhodes),
"<c2 c3 f2 [[F2 C2] db2]>"
.legato("<1@3 [.3 1]>")
.slow(2)
.tone(bass),
).fast(3/2)`;
export const wavyKalimba = `const delay = new FeedbackDelay(1/3, .5).chain(vol(.2), out());
let kalimba = await sampler({
C5: 'https://freesound.org/data/previews/536/536549_11935698-lq.mp3'
})
kalimba = kalimba.chain(vol(0.6).connect(delay),out());
const scales = sequence('C major', 'C mixolydian', 'F lydian', ['F minor', 'Db major']).slow(4);
stack(
"[0 2 4 6 9 2 0 -2]*3"
.add("<0 2>/4")
.scale(scales)
.struct("x*8")
.velocity("<.8 .3 .6>*8")
.slow(2)
.tone(kalimba),
"<c2 c2 f2 [[F2 C2] db2]>"
.scale(scales)
.scaleTranspose("[0 <2 4>]*2")
.struct("x*4")
.velocity("<.8 .5>*4")
.velocity(0.8)
.slow(2)
.tone(kalimba)
)
.legato("<.4 .8 1 1.2 1.4 1.6 1.8 2>/8")
.fast(1)`;
export const jemblung = `const delay = new FeedbackDelay(1/8, .6).chain(vol(0.15), out());
const snare = noise({type:'white',...adsr(0,0.2,0)}).chain(lowpass(5000),vol(1.8),out());
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(
// chords
x=>x.add("0,3").duration("0.05!3 0.02"),
// bass
x=>x.add("-8").struct("x*8").duration(0.1)
),
// melody
"12 11*3 12 ~".duration(0.005)
)
.add("<0 1>")
.tune("jemblung2")
//.mul(22/5).round().xen("22edo")
//.mul(12/5).round().xen("12edo")
.tone(s),
// kick
"[c2 ~]*2".duration(0.05).tone(membrane().chain(out())),
// snare
"[~ c1]*2".early(0.001).tone(snare),
// hihat
"c2*8".tone(noise().chain(highpass(6000),vol(0.5).connect(delay),out())),
).slow(3)`;
export const risingEnemy = `stack(
"2,6"
.scale('F3 dorian')
.transpose(sine2.struct("x*64").slow(4).mul(2).round())
.fast(2)
.struct("x x*3")
.legato(".9 .3"),
"0@3 -3*3".legato(".95@3 .4").scale('F2 dorian')
)
.transpose("<0 1 2 1>/2".early(0.5))
.transpose(5)
.fast(2 / 3)
.tone((await piano()).toDestination())`;
export const festivalOfFingers = `const chords = "<Cm7 Fm7 G7 F#7>";
stack(
chords.voicings().struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)),
chords.rootNotes(2).struct("x(4,8,-2)"),
chords.rootNotes(4)
.scale(slowcat('C minor','F dorian','G dorian','F# mixolydian'))
.struct("x(3,8,-2)".fast(2))
.scaleTranspose("0 4 0 6".early(".125 .5")).layer(scaleTranspose("0,<2 [4,6] [5,7]>/4"))
).slow(2)
.velocity(sine.struct("x*8").add(3/5).mul(2/5).fast(8))
.tone((await piano()).chain(out()))`;
export const festivalOfFingers2 = `const chords = "<Cm7 Fm7 G7 F#7 >";
const scales = slowcat('C minor','F dorian','G dorian','F# mixolydian')
stack(
chords.voicings().struct("x(3,8,-1)").velocity(.5).off(1/7,x=>x.transpose(12).velocity(.2)),
chords.rootNotes(2).struct("x(4,8)"),
chords.rootNotes(4)
.scale(scales)
.struct("x(3,8,-2)".fast(2))
.scaleTranspose("0 4 0 6".early(".125 .5")).layer(scaleTranspose("0,<2 [4,6] [5,7]>/3"))
).slow(2).transpose(-1)
.legato(cosine.struct("x*8").add(4/5).mul(4/5).fast(8))
.velocity(sine.struct("x*8").add(3/5).mul(2/5).fast(8))
.tone((await piano()).chain(out())).fast(3/4)`;
// iter, stut, stutWith
export const undergroundPlumber = `backgroundImage('https://images.nintendolife.com/news/2016/08/video_exploring_the_funky_inspiration_for_the_super_mario_bros_underground_theme/large.jpg',{ className:'darken' })
const drums = await players({
bd: 'bd/BT0A0D0.wav',
sn: 'sn/ST0T0S3.wav',
hh: 'hh/000_hh3closedhh.wav',
cp: 'cp/HANDCLP0.wav',
}, 'https://loophole-letters.vercel.app/samples/tidal/')
stack(
"<<bd*2 bd> sn> hh".fast(4).slow(2).tone(drums.chain(vol(.5),out())),
stack(
"[c2 a1 bb1 ~] ~"
.stut(2, .6, 1/16)
.legato(.4)
.slow(2)
.tone(synth({...osc('sawtooth7'),...adsr(0,.3,0)}).chain(out())),
"[g2,[c3 eb3]]".iter(4)
.stutWith(4, 1/8, (x,n)=>x.transpose(n*12).velocity(Math.pow(.4,n)))
.legato(.1)
)
.transpose("<0@2 5 0 7 5 0 -5>/2")
)
.fast(2/3)
.pianoroll({minMidi:21,maxMidi:180, background:'transparent',inactive:'#3F8F90',active:'#DE3123'})`;
export const bridgeIsOver = `const breaks = (await players({mad:'https://freesound.org/data/previews/22/22274_109943-lq.mp3'})).chain(out())
stack(
stack(
"c3*2 [[c3@1.4 bb2] ab2] gb2*2 <[[gb2@1.4 ab2] bb2] gb2>".legato(".5 1".fast(2)).velocity(.8),
"0 ~".scale('c4 whole tone')
.euclidLegato(3,8).slow(2).mask("x ~")
.stutWith(8, 1/16, (x,n)=>x.scaleTranspose(n).velocity(Math.pow(.7,n)))
.scaleTranspose("<0 1 2 3 4 3 2 1>")
.fast(2)
.velocity(.7)
.legato(.5)
.stut(3, .5, 1/8)
).transpose(-1).tone((await piano()).chain(out())),
"mad".slow(2).tone(breaks)
).cpm(78).slow(4).pianoroll()
`;
export const goodTimes = `const scale = slowcat('C3 dorian','Bb2 major').slow(4);
stack(
"2*4".add(12).scale(scale)
.off(1/8,x=>x.scaleTranspose("2")).fast(2)
.scaleTranspose("<0 1 2 1>").hush(),
"<0 1 2 3>(3,8,2)"
.scale(scale)
.off(1/4,x=>x.scaleTranspose("2,4")),
"<0 4>(5,8)".scale(scale).transpose(-12)
)
.velocity(".6 .7".fast(4))
.legato("2")
.scale(scale)
.scaleTranspose("<0>".slow(4))
.tone((await piano()).chain(out()))
//.midi()
.velocity(.8)
.transpose(5)
.slow(2)
.pianoroll({maxMidi:100,minMidi:20})`;
export const echoPiano = `"<0 2 [4 6](3,4,1) 3*2>"
.scale('D minor')
.color('salmon')
.off(1/4, x=>x.scaleTranspose(2).color('green'))
.off(1/2, x=>x.scaleTranspose(6).color('steelblue'))
.legato(.5)
.echo(4, 1/8, .5)
.tone((await piano()).chain(out()))
.pianoroll()`;

View file

@ -0,0 +1,101 @@
import React, { useCallback, useMemo, useState } from 'react';
import * as Tone from 'tone';
import useRepl from '../useRepl';
import CodeMirror, { markEvent } from '../repl-react/CodeMirror';
import cx from '../repl-react/cx';
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination).set({
oscillator: { type: 'triangle' },
envelope: {
release: 0.01,
},
});
// "balanced" | "interactive" | "playback";
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
function MiniRepl({ tune, maxHeight = 500 }) {
const [editor, setEditor] = useState<any>();
const { code, setCode, activateCode, activeCode, setPattern, error, cycle, dirty, log, togglePlay, hash } = useRepl({
tune,
defaultSynth,
autolink: false,
onDraw: useCallback(markEvent(editor), [editor]),
});
const lines = code.split('\n').length;
const height = Math.min(lines * 30 + 30, maxHeight);
return (
<div className="rounded-md overflow-hidden">
<div className="flex justify-between bg-slate-700 border-t border-slate-500">
<div className="flex">
<button
className={cx(
'w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600',
cycle.started ? 'animate-pulse' : ''
)}
onClick={() => togglePlay()}
>
{!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="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>
) : (
<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>
)}
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 border-slate-500 hover:bg-slate-600',
dirty
? 'bg-slate-700 border-r border-slate-500 text-white'
: 'bg-slate-600 text-slate-400 cursor-not-allowed'
)}
onClick={() => activateCode()}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<div className="text-right p-1 text-sm">{error && <span className="text-red-200">{error.message}</span>}</div>{' '}
</div>
<div className="flex space-y-0 overflow-auto" style={{ height }}>
<CodeMirror
className="w-full"
value={code}
editorDidMount={setEditor}
options={{
mode: 'javascript',
theme: 'material',
lineNumbers: true,
}}
onChange={(_: any, __: any, value: any) => setCode(value)}
/>
</div>
{/* <div className="bg-slate-700 border-t border-slate-500 content-right pr-2 text-right">
<a href={`https://strudel.tidalcycles.org/#${hash}`} className="text-white items-center inline-flex">
<span>open in REPL</span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</a>
</div> */}
</div>
);
}
export default MiniRepl;

View file

@ -0,0 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Tutorial from './tutorial.mdx';
// import logo from '../logo.svg';
ReactDOM.render(
<React.StrictMode>
<div className="min-h-screen">
<header className="flex-none flex justify-center w-full h-16 px-2 items-center border-b border-gray-200 bg-white">
<div className="p-4 w-full max-w-3xl flex justify-between">
<div className="flex items-center space-x-2">
<img src={'https://tidalcycles.org/img/logo.svg'} className="Tidal-logo w-16 h-16" alt="logo" />
<h1 className="text-2xl">Strudel Tutorial</h1>
</div>
{!window.location.href.includes('localhost') && (
<div className="flex space-x-4">
<a href="../">go to REPL</a>
</div>
)}
</div>
</header>
<main className="p-4 max-w-3xl prose">
<Tutorial />
</main>
</div>
</React.StrictMode>,
document.getElementById('root')
);

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="../../public/favicon.ico" />
<link rel="stylesheet" type="text/css" href="./style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Strudel REPL" />
<title>Strudel Tutorial</title>
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="Tutorial.js"></script>
</body>
</html>

View file

@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.react-codemirror2,
.CodeMirror {
width: 100% !important;
height: inherit !important;
}
main {
margin: 0 auto;
}

View file

@ -0,0 +1,748 @@
import MiniRepl from './MiniRepl';
# What is Strudel?
With Strudel, you can expressively write dynamic music pieces.
It aims to be [Tidal Cycles](https://tidalcycles.org/) for JavaScript (started by the same author).
You don't need to know JavaScript or Tidal Cycles to make music with Strudel.
This interactive tutorial will guide you through the basics of Strudel.
The best place to actually make music with Strudel is the [Strudel REPL](https://strudel.tidalcycles.org/).
## Show me a Demo
To get a taste of what Strudel can do, check out this track:
<MiniRepl tune={`const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out());
const kick = new MembraneSynth().chain(vol(.8), out());
const snare = new NoiseSynth().chain(vol(.8), out());
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out());
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out());
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out());
const drums = stack(
"c1*2".tone(kick).bypass("<0@7 1>/8"),
"~ <x!7 [x@3 x]>".tone(snare).bypass("<0@7 1>/4"),
"[~ c4]*2".tone(hihat)
);
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(
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))
)
stack(
drums.fast(2),
synths
).slow(2);
`}
/>
[Open this track in the REPL](https://strudel.tidalcycles.org/#KCkgPT4gewogIGNvbnN0IGRlbGF5ID0gbmV3IEZlZWRiYWNrRGVsYXkoMS84LCAuNCkuY2hhaW4odm9sKDAuNSksIG91dCk7CiAgY29uc3Qga2ljayA9IG5ldyBNZW1icmFuZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBzbmFyZSA9IG5ldyBOb2lzZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBoaWhhdCA9IG5ldyBNZXRhbFN5bnRoKCkuc2V0KGFkc3IoMCwgLjA4LCAwLCAuMSkpLmNoYWluKHZvbCguMykuY29ubmVjdChkZWxheSksb3V0KTsKICBjb25zdCBiYXNzID0gbmV3IFN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC4xLCAuNCkgfSkuY2hhaW4obG93cGFzcyg5MDApLCB2b2woLjUpLCBvdXQpOwogIGNvbnN0IGtleXMgPSBuZXcgUG9seVN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC41LCAuMiwgLjcpIH0pLmNoYWluKGxvd3Bhc3MoMTIwMCksIHZvbCguNSksIG91dCk7CiAgCiAgY29uc3QgZHJ1bXMgPSBzdGFjaygKICAgICdjMSoyJy5tLnRvbmUoa2ljaykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0pLAogICAgJ34gPHghNyBbeEAzIHhdPicubS50b25lKHNuYXJlKS5ieXBhc3MoJzwwQDcgMT4vNCcubSksCiAgICAnW34gYzRdKjInLm0udG9uZShoaWhhdCkKICApOwogIAogIGNvbnN0IHRocnUgPSAoeCkgPT4geC50cmFuc3Bvc2UoJzwwIDE%2BLzgnLm0pLnRyYW5zcG9zZSgtMSk7CiAgY29uc3Qgc3ludGhzID0gc3RhY2soCiAgICAnPGViNCBkNCBjNCBiMz4vMicubS5zY2FsZSh0aW1lQ2F0KFszLCdDIG1pbm9yJ10sWzEsJ0MgbWVsb2RpYyBtaW5vciddKS5zbG93KDgpKS5ncm9vdmUoJ1t%2BIHhdKjInLm0pCiAgICAuZWRpdCgKICAgICAgc2NhbGVUcmFuc3Bvc2UoMCkuZWFybHkoMCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDIpLmVhcmx5KDEvOCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDcpLmVhcmx5KDEvNCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDgpLmVhcmx5KDMvOCkKICAgICkuZWRpdCh0aHJ1KS50b25lKGtleXMpLmJ5cGFzcygnPDEgMD4vMTYnLm0pLAogICAgJzxDMiBCYjEgQWIxIFtHMSBbRzIgRzFdXT4vMicubS5ncm9vdmUoJ1t4IFt%2BIHhdIDxbfiBbfiB4XV0hMyBbeCB4XT5AMl0vMicubS5mYXN0KDIpKS5lZGl0KHRocnUpLnRvbmUoYmFzcyksCiAgICAnPENtNyBCYjcgRm03IEc3YjEzPi8yJy5tLmdyb292ZSgnfiBbeEAwLjEgfl0nLm0uZmFzdCgyKSkudm9pY2luZ3MoKS5lZGl0KHRocnUpLmV2ZXJ5KDIsIGVhcmx5KDEvOCkpLnRvbmUoa2V5cykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0uZWFybHkoMS80KSkKICApCiAgcmV0dXJuIHN0YWNrKAogICAgZHJ1bXMuZmFzdCgyKSwgCiAgICBzeW50aHMKICApLnNsb3coMik7Cn0%3D)
## Disclaimer
- This project is still in its experimental state. In the future, parts of it might change significantly.
- This tutorial is far from complete.
<br />
# Mini Notation
Similar to Tidal Cycles, Strudel has an embedded mini language that is designed to write rhythmic patterns in a short manner.
Before diving deeper into the details, here is a flavor of how the mini language looks like:
<MiniRepl
tune={`\`[
[
[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
[c5 a4 a4 ~]
[[~ d5] [~ f5] a5 [g5 f5]]
[e5 [~ c5] e5 [d5 c5]]
[b4 [b4 c5] d5 e5]
[c5 a4 a4 ~]
],[
[[e2 e3]*4]
[[a2 a3]*4]
[[g#2 g#3]*2 [e2 e3]*2]
[a2 a3 a2 a3 a2 a3 b1 c2]
[[d2 d3]*4]
[[c2 c3]*4]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]
]
]/16\``}
/>
The snippet above is enclosed in backticks (`), which allows you to write multi-line strings.
You can also use double quotes (") for single line mini notation.
## Notes
Notes are notated with the note letter, followed by the octave number. You can notate flats with `b` and sharps with `#`.
<MiniRepl tune={`"e5"`} />
Here, the same note is played over and over again, once a second. This one second is the default length of one so called "cycle".
By the way, you can edit the contents of the player, and press "update" to hear your change!
You can also press "play" on the next player without needing to stop the last one.
## Sequences
We can play more notes by seperating them with spaces:
<MiniRepl tune={`"e5 b4 d5 c5"`} />
Here, those four notes are squashed into one cycle, so each note is a quarter second long.
## Division
We can slow the sequence down by enclosing it in brackets and dividing it by a number:
<MiniRepl tune={`"[e5 b4 d5 c5]/2"`} />
The division by two means that the sequence will be played over the course of two cycles.
You can also use decimal numbers for any tempo you like.
## Angle Brackets
Using angle brackets, we can define the sequence length based on the number of children:
<MiniRepl tune={`"<e5 b4 d5 c5>"`} />
The above snippet is the same as:
<MiniRepl tune={`"[e5 b4 d5 c5]/4"`} />
The advantage of the angle brackets, is that we can add more children without needing to change the number at the end.
## Multiplication
Contrary to division, a sequence can be sped up by multiplying it by a number:
<MiniRepl tune={`"[e5 b4 d5 c5]*2"`} />
The multiplication by 2 here means that the sequence will play twice a cycle.
## Bracket Nesting
To create more interesting rhythms, you can nest sequences with brackets, like this:
<MiniRepl tune={`"e5 [b4 c5] d5 [c5 b4]"`} />
## Rests
The "~" represents a rest:
<MiniRepl tune={`"[b4 [~ c5] d5 e5]"`} />
## Parallel
Using commas, we can play chords:
<MiniRepl tune={`"g3,b3,e4"`} />
To play multiple chords in a sequence, we have to wrap them in brackets:
<MiniRepl tune={`"<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>"`} />
## Elongation
With the "@" symbol, we can specify temporal "weight" of a sequence child:
<MiniRepl tune={`"<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>"`} />
Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1.
## Replication
Using "!" we can repeat without speeding up:
<MiniRepl tune={`"<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>"`} />
In essence, the `x!n` is like a shortcut for `[x*n]@n`.
## 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 "%"
<br />
# Core API
While the mini notation is powerful on its own, there is much more to discover.
Internally, the mini notation will expand to use the actual functional JavaScript API.
## Notes
Notes are automatically available as variables:
<MiniRepl tune={`e4`} />
An important difference to the mini notation:
For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name.
The above is the same as:
<MiniRepl tune={`"e4"`} />
Using strings, you can also use "#".
## Functions that create Patterns
The following functions will return a pattern. We will see later what that means.
## pure(value)
To create a pattern from a value, you can wrap the value in pure:
<MiniRepl tune={`pure('e4')`} />
Most of the time, you won't need that function as input values of pattern creating functions are purified by default.
### cat(...values)
The given items are con**cat**enated spread evenly over one cycle:
<MiniRepl tune={`cat(e5, b4, d5, c5)`} />
The function **fastcat** does the same as **cat**.
### sequence(...values)
Like **cat**, but allows nesting with arrays:
<MiniRepl tune={`sequence(e5, [b4, c5], d5, [c5, b4])`} />
### stack(...values)
The given items are played at the same time at the same length:
<MiniRepl tune={`stack(g3,b3,e4)`} />
### slowcat(...values)
Like cat, but each item has the length of one cycle:
<MiniRepl tune={`slowcat(e5, b4, d5, c5)`} />
<!-- ## slowcatPrime ? -->
### Nesting functions
You can nest functions inside one another:
<MiniRepl
tune={`slowcat(
stack(g3,b3,e4),
stack(a3,c3,e4),
stack(b3,d3,fs4),
stack(b3,e4,g4)
)`}
/>
The above is equivalent to
<MiniRepl tune={`"<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>"`} />
### timeCat(...[weight,value])
Like with "@" in mini notation, we can specify weights to the items in a sequence:
<MiniRepl tune={`timeCat([3,e3],[1, g3])`} />
<!-- ## polymeter
how to use?
<MiniRepl tune={`polymeter(3, e3, g3, b3)`} /> -->
### polyrhythm(...[...values])
Plays the given items at the same time, within the same length:
<MiniRepl tune={`polyrhythm([e3, g3], [e4, g4, b4])`} />
We can write the same with **stack** and **cat**:
<MiniRepl tune={`stack(cat(e3, g3), cat(e4, g4, b4))`} />
You can also use the shorthand **pr** instead of **polyrhythm**.
## Pattern modifier functions
The following functions modify a pattern.
### slow(factor)
Like "/" in mini notation, **slow** will slow down a pattern over the given number of cycles:
<MiniRepl tune={`cat(e5, b4, d5, c5).slow(2)`} />
The same in mini notation:
<MiniRepl tune={`"[e5 b4 d5 c5]/2"`} />
### fast(factor)
Like "\*" in mini notation, **fast** will play a pattern times the given number in one cycle:
<MiniRepl tune={`cat(e5, b4, d5, c5).fast(2)`} />
### early(cycles)
With early, you can nudge a pattern to start earlier in time:
<MiniRepl tune={`cat(e5, b4.early(0.5))`} />
### late(cycles)
Like early, but in the other direction:
<MiniRepl tune={`cat(e5, b4.late(0.5))`} />
<!-- TODO: shouldn't it sound different? -->
### rev()
Will reverse the pattern:
<MiniRepl tune={`cat(c3,d3,e3,f3).rev()`} />
### every(n, func)
Will apply the given function every n cycles:
<MiniRepl tune={`cat(e5, pure(b4).every(4, late(0.5)))`} />
<!-- TODO: should be able to do b4.every => like already possible with fast slow etc.. -->
Note that late is called directly. This is a shortcut for:
<MiniRepl tune={`cat(e5, pure(b4).every(4, x => x.late(0.5)))`} />
<!-- TODO: should the function really run the first cycle? -->
### add(n)
Adds the given number to each item in the pattern:
<MiniRepl tune={`"0 2 4".add("<0 3 4 0>").scale('C major')`} />
Here, the triad `0, 2, 4` is shifted by different amounts. Without add, the equivalent would be:
<MiniRepl tune={`"<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major')`} />
You can also use add with notes:
<MiniRepl tune={`"c3 e3 g3".add("<0 5 7 0>")`} />
Behind the scenes, the notes are converted to midi numbers as soon before add is applied, which is equivalent to:
<MiniRepl tune={`"48 52 55".add("<0 5 7 0>")`} />
### sub(n)
Like add, but the given numbers are subtracted:
<MiniRepl tune={`"0 2 4".sub("<0 1 2 3>").scale('C4 minor')`} />
See add for more information.
### mul(n)
Multiplies each number by the given factor:
<MiniRepl tune={`"0,1,2".mul("<2 3 4 3>").scale('C4 minor')`} />
... is equivalent to:
<MiniRepl tune={`"<[0,2,4] [0,3,6] [0,4,8] [0,3,6]>".scale('C4 minor')`} />
This function is really useful in combination with signals:
<MiniRepl tune={`sine.struct("x*16").mul(7).round().scale('C major')`} />
Here, we sample a sine wave 16 times, and multiply each sample by 7. This way, we let values oscillate between 0 and 7.
### div(n)
Like mul, but dividing by the given number.
### round()
Rounds all values to the nearest integer:
<MiniRepl tune={`"0.5 1.5 2.5".round().scale('C major')`} />
### struct(binary_pat)
Applies the given structure to the pattern:
<MiniRepl tune={`"c3,eb3,g3".struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~").slow(4)`} />
This is also useful to sample signals:
<MiniRepl
tune={`sine.struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~").mul(7).round()
.scale('C minor').slow(4)`}
/>
### when(binary_pat, func)
Applies the given function whenever the given pattern is in a true state.
<MiniRepl tune={`"c3 eb3 g3".when("<0 1>/2", sub(5))`} />
### superimpose(...func)
Superimposes the result of the given function(s) on top of the original pattern:
<MiniRepl tune={`"<c3 eb3 g3>".scale('C minor').superimpose(scaleTranspose("2,4"))`} />
### layer(...func)
Layers the result of the given function(s) on top of each other. Like superimpose, but the original pattern is not part of the result.
<MiniRepl tune={`"<c3 eb3 g3>".scale('C minor').layer(scaleTranspose("0,2,4"))`} />
### apply(func)
Like layer, but with a single function:
<MiniRepl tune={`"<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4"))`} />
### off(time, func)
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:
<MiniRepl tune={`"c4,eb4,g4".stack("bb4,d5")`} />
## Tone API
To make the sounds more interesting, we can use Tone.js instruments ands effects.
[Show Source on Github](https://github.com/tidalcycles/strudel/blob/main/repl/src/tone.ts)
<MiniRepl
tune={`stack(
"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~"
.tone(synth(adsr(0,.1,0,0)).chain(out())),
"[c2 c3]*8"
.tone(synth({
...osc('sawtooth'),
...adsr(0,.1,0.4,0)
}).chain(lowpass(300), out()))
).slow(4)`}
/>
### tone(instrument)
To change the instrument of a pattern, you can pass any [Tone.js Source](https://tonejs.github.io/docs/14.7.77/index.html) to .tone:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(new FMSynth().toDestination())`}
/>
While this works, it is a little bit verbose. To simplify things, all Tone Synths have a shortcut:
```js
const amsynth = (options) => new AMSynth(options);
const duosynth = (options) => new DuoSynth(options);
const fmsynth = (options) => new FMSynth(options);
const membrane = (options) => new MembraneSynth(options);
const metal = (options) => new MetalSynth(options);
const monosynth = (options) => new MonoSynth(options);
const noise = (options) => new NoiseSynth(options);
const pluck = (options) => new PluckSynth(options);
const polysynth = (options) => new PolySynth(options);
const synth = (options) => new Synth(options);
const sampler = (options, baseUrl?) => new Sampler(options); // promisified, see below
const players = (options, baseUrl?) => new Sampler(options); // promisified, see below
```
### sampler
With sampler, you can create tonal instruments from samples:
<MiniRepl
tune={`sampler({
C5: 'https://freesound.org/data/previews/536/536549_11935698-lq.mp3'
}).then(kalimba =>
saw.struct("x*8").mul(16).round()
.legato(4).scale('D dorian').slow(2)
.tone(kalimba.toDestination())
)`}
/>
The sampler function promisifies [Tone.js Sampler](https://tonejs.github.io/docs/14.7.77/Sampler).
Note that this function currently only works with this promise notation, but in the future,
it will be possible to use async instruments in a synchronous fashion.
### players
With players, you can create sound banks:
<MiniRepl
tune={`players({
bd: 'samples/tidal/bd/BT0A0D0.wav',
sn: 'samples/tidal/sn/ST0T0S3.wav',
hh: 'samples/tidal/hh/000_hh3closedhh.wav'
}, 'https://loophole-letters.vercel.app/')
.then(drums=>
"bd hh sn hh".tone(drums.toDestination())
)
`}
/>
The sampler function promisifies [Tone.js Players](https://tonejs.github.io/docs/14.7.77/Players).
Note that this function currently only works with this promise notation, but in the future,
it will be possible to use async instruments in a synchronous fashion.
### out
Shortcut for Tone.Destination. Intended to be used with Tone's .chain:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(membrane().chain(out()))`}
/>
This alone is not really useful, so read on..
### vol(volume)
Helper that returns a Gain Node with the given volume. Intended to be used with Tone's .chain:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(noise().chain(vol(0.5), out()))`}
/>
### osc(type)
Helper to set the waveform of a synth, monosynth or polysynth:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(osc('sawtooth4')).chain(out()))`}
/>
The base types are `sine`, `square`, `sawtooth`, `triangle`. You can also append a number between 1 and 32 to reduce the harmonic partials.
### lowpass(cutoff)
Helper that returns a Filter Node of type lowpass with the given cutoff. Intended to be used with Tone's .chain:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(osc('sawtooth')).chain(lowpass(800), out()))`}
/>
### highpass(cutoff)
Helper that returns a Filter Node of type highpass with the given cutoff. Intended to be used with Tone's .chain:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(osc('sawtooth')).chain(highpass(2000), out()))`}
/>
### adsr(attack, decay?, sustain?, release?)
Helper to set the envelope of a Tone.js instrument. Intended to be used with Tone's .set:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(adsr(0,.1,0,0)).chain(out()))`}
/>
### Experimental: Patternification
While the above methods work for static sounds, there is also the option to patternify tone methods.
This is currently experimental, because the performance is not stable, and audio glitches will appear after some time.
It would be great to get this to work without glitches though, because it is fun!
#### synth(type)
With .synth, you can create a synth with a variable wave type:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
.synth("<sawtooth8 square8>").slow(4)`}
/>
#### adsr(attack, decay?, sustain?, release?)
Chainable Envelope helper:
<MiniRepl
tune={`"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~".slow(4)
.synth('sawtooth16').adsr(0,.1,0,0)`}
/>
Due to having more than one argument, this method is not patternified.
#### filter(cuttoff)
Patternified filter:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
.synth('sawtooth16').filter("[500 2000]*8").slow(4)`}
/>
#### gain(value)
Patternified gain:
<MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
.synth('sawtooth16').gain("[.2 .8]*8").slow(4)`}
/>
#### autofilter(value)
Patternified autofilter:
<MiniRepl
tune={`"c2 c3"
.synth('sawtooth16').autofilter("<1 4 8>"")`}
/>
## Tonal API
The Tonal API, uses [tonaljs](https://github.com/tonaljs/tonal) to provide helpers for musical operations.
### transpose(semitones)
Transposes all notes to the given number of semitones:
<MiniRepl tune={`"c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).transpose(0)`} />
This method gets really exciting when we use it with a pattern as above.
Instead of numbers, scientific interval notation can be used as well:
<MiniRepl tune={`"c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).transpose(1)`} />
### scale(name)
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranpose.
<MiniRepl
tune={`"0 2 4 6 4 2"
.scale(slowcat('C2 major', 'C2 minor').slow(2))`}
/>
Note that the scale root is octaved here. You can also omit the octave, then index zero will default to octave 3.
All the available scale names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
### scaleTranspose(steps)
Transposes notes inside the scale by the number of steps:
<MiniRepl
tune={`"-8 [2,4,6]"
.scale('C4 bebop major')
.scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")`}
/>
### voicings(range?)
Turns chord symbols into voicings, using the smoothest voice leading possible:
<MiniRepl tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")`} />
<!-- TODO: use voicing collection as first param + patternify. -->
### rootNotes(octave = 2)
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:
<MiniRepl
tune={`"<C^7 A7b13 Dm7 G7>".edit(
x => x.voicings(['d3','g4']).struct("~ x"),
x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out()))
)`}
/>
<!-- TODO: use range instead of octave. -->
<!-- TODO: find out why composition does not work -->
## Microtonal API
TODO
## MIDI API
Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
### midi(outputName?)
Make sure to have a midi device connected or to use an IAC Driver.
If no outputName is given, it uses the first midi output it finds.
Midi is currently not supported by the mini repl used here, but you can [open the midi example in the repl](https://strudel.tidalcycles.org/#c3RhY2soIjxDXjcgQTcgRG03IEc3PiIubS52b2ljaW5ncygpLCAnPEMzIEEyIEQzIEcyPicubSkKICAubWlkaSgp).
In the REPL, you will se a log of the available MIDI devices.
<!--<MiniRepl
tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")
.midi()`}
/>-->
# Contributing
Contributions of any sort are very welcome! You can contribute by editing [this file](https://github.com/tidalcycles/strudel/blob/main/repl/src/tutorial/tutorial.mdx).
All you need is a github account.
If you want to run the tutorial locally, you can clone the and run:
```sh
cd repl && npm i && npm run tutorial
```
If you want to contribute in another way, either
- [fork strudel repo on GitHub](https://github.com/tidalcycles/strudel)
- [Join the Discord Channel](https://discord.gg/remJ6gQA)
- [play with the Strudel REPL](https://strudel.tidalcycles.org/)

16
packages/xen/tune.mjs Normal file
View file

@ -0,0 +1,16 @@
import Tune from './tunejs.js';
import { Pattern } from '../core/strudel.mjs';
Pattern.prototype._tune = function (scale, tonic = 220) {
const tune = new Tune();
if (!tune.isValidScale(scale)) {
throw new Error('not a valid tune.js scale name: "' + scale + '". See http://abbernie.github.io/tune/scales.html');
}
tune.loadScale(scale);
tune.tonicize(tonic);
return this._asNumber()._withEvent((event) => {
return event.withValue(() => tune.note(event.value)).setContext({ ...event.context, type: 'frequency' });
});
};
Pattern.prototype.define('tune', (scale, pat) => pat.tune(scale), { composable: true, patternified: true });

233
packages/xen/tunejs.js Normal file

File diff suppressed because one or more lines are too long

62
packages/xen/xen.mjs Normal file
View file

@ -0,0 +1,62 @@
import { Pattern } from '../core/strudel.mjs';
import { mod } from '../../packages/core/util.mjs';
function edo(name) {
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
throw new Error('not an edo scale: "' + name + '"');
}
const [_, divisions] = name.match(/^([1-9]+[0-9]*)edo$/);
return Array.from({ length: divisions }, (_, i) => Math.pow(2, i / divisions));
}
const presets = {
'12ji': [1 / 1, 16 / 15, 9 / 8, 6 / 5, 5 / 4, 4 / 3, 45 / 32, 3 / 2, 8 / 5, 5 / 3, 16 / 9, 15 / 8],
};
function withBase(freq, scale) {
return scale.map((r) => r * freq);
}
const defaultBase = 220;
function getXenScale(scale, indices) {
if (typeof scale === 'string') {
if (/^[1-9]+[0-9]*edo$/.test(scale)) {
scale = edo(scale);
} else if (presets[scale]) {
scale = presets[scale];
} else {
throw new Error('unknown scale name: "' + scale + '"');
}
}
scale = withBase(defaultBase, scale);
if (!indices) {
return scale;
}
return scale.filter((_, i) => indices.includes(i));
}
function xenOffset(xenScale, offset, index = 0) {
const i = mod(index + offset, xenScale.length);
const oct = Math.floor(offset / xenScale.length);
return xenScale[i] * Math.pow(2, oct);
}
// scaleNameOrRatios: string || number[], steps?: number
Pattern.prototype._xen = function (scaleNameOrRatios, steps) {
return this._asNumber()._withEvent((event) => {
const scale = getXenScale(scaleNameOrRatios);
steps = steps || scale.length;
const frequency = xenOffset(scale, event.value);
return event.withValue(() => frequency).setContext({ ...event.context, type: 'frequency' });
});
};
Pattern.prototype.tuning = function (steps) {
return this._asNumber()._withEvent((event) => {
const frequency = xenOffset(steps, event.value);
return event.withValue(() => frequency).setContext({ ...event.context, type: 'frequency' });
});
};
Pattern.prototype.define('xen', (scale, pat) => pat.xen(scale), { composable: true, patternified: true });
// Pattern.prototype.define('tuning', (scale, pat) => pat.xen(scale), { composable: true, patternified: false });