Merge commit '1375932c05cea422500a9328fec3207ac4e1a64c' into stateful-events

This commit is contained in:
Felix Roos 2022-02-27 15:05:17 +01:00
commit 634d4f1099
22 changed files with 627 additions and 519 deletions

19
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Strudel tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 16, 17]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
- run: npm test

View File

@ -1,4 +1,12 @@
# strudel
# strudel
[![Strudel test status](https://github.com/tidalcycles/strudel/actions/workflows/test.yml/badge.svg)](https://github.com/tidalcycles/strudel/actions)
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully.
Try it here: https://strudel.tidalcycles.org/
Tutorial: https://strudel.tidalcycles.org/tutorial/
## Local development
@ -8,4 +16,4 @@ Run the REPL locally:
cd repl
npm install
npm run start
```
```

View File

@ -140,7 +140,7 @@ class Hap {
return this.spanEquals(other) && this.part.equals(other.part) && this.value === other.value;
}
show() {
return "(" + (this.whole == void 0 ? "~" : this.whole.show()) + ", " + this.part.show() + ", " + this.value + ")";
return "(" + (this.whole == void 0 ? "~" : this.whole.show()) + ", " + this.part.show() + ", " + JSON.stringify(this.value?.value ?? this.value) + ")";
}
}
class Pattern {
@ -434,7 +434,7 @@ class Pattern {
};
const left = this.withValue((val) => Object.assign({}, val, {pan: elem_or(val, "pan", 0.5) - by}));
const right = this.withValue((val) => Object.assign({}, val, {pan: elem_or(val, "pan", 0.5) + by}));
return stack([left, func(right)]);
return stack(left, func(right));
}
stack(...pats) {
return stack(this, ...pats);

8
docs/dist/App.js vendored
View File

@ -56,13 +56,13 @@ function App() {
}, [pattern, code]);
useWebMidi({
ready: useCallback(({outputs}) => {
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `"${o.name}"`).join(" | ")}) to the pattern. `);
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(", ")}`);
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(", ")}`);
pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(", ")}`);
}, [])
});
return /* @__PURE__ */ React.createElement("div", {
@ -111,7 +111,7 @@ function App() {
}, !cycle.started ? `press ctrl+enter to play
` : dirty ? `ctrl+enter to update
` : "no changes\n")), error && /* @__PURE__ */ React.createElement("div", {
className: cx("absolute right-2 bottom-2", "text-red-500")
className: cx("absolute right-2 bottom-2 px-2", "text-red-500")
}, error?.message || "unknown error")), /* @__PURE__ */ React.createElement("button", {
className: "flex-none w-full border border-gray-700 p-2 bg-slate-700 hover:bg-slate-500",
onClick: () => togglePlay()

5
docs/dist/midi.js vendored
View File

@ -21,6 +21,9 @@ export default function enableWebMidi() {
}
const outputByName = (name) => WebMidi.getOutputByName(name);
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.fmap((value) => ({
...value,
onTrigger: (time, event) => {
@ -36,7 +39,7 @@ Pattern.prototype.midi = function(output, channel = 1) {
}
const device = output ? outputByName(output) : WebMidi.outputs[0];
if (!device) {
throw new Error(`🔌 MIDI device ${output ? output : ""} not found. Use one of ${WebMidi.outputs.map((o) => `"${o.name}"`).join(" | ")}`);
throw new Error(`🔌 MIDI device '${output ? output : ""}' not found. Use one of ${WebMidi.outputs.map((o) => `'${o.name}'`).join(" | ")}`);
}
const timingOffset = WebMidi.time - Tone.context.currentTime * 1e3;
time = time * 1e3 + timingOffset;

View File

@ -91,7 +91,9 @@ export default (code) => {
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
// add to location to pure(x) calls
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
return reifyWithLocation(node.arguments[0].name, node.arguments[0], ast.locations, artificialNodes);
const literal = node.arguments[0];
const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
return reifyWithLocation(value + '', node.arguments[0], ast.locations, artificialNodes);
}
// replace pseudo note variables
if (node.type === 'IdentifierExpression') {
@ -339,4 +341,3 @@ function getLocationObject(node, locations) {
],
};
}

185
docs/dist/tunes.js vendored
View File

@ -54,49 +54,27 @@ export const tetrisWithFunctions = `stack(sequence(
)
).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",
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 tetrisRev = `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 ~",
).rev(),
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",
).rev()
).slow(16)`;
export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
[a4 [a4 c5] e5 [d5 c5]]
[b4 [~ c5] d5 e5]
@ -114,12 +92,6 @@ export const tetrisMini = `\`[[e5 [b4 c5] d5 [c5 b4]]
[[b1 b2]*2 [e2 e3]*2]
[[a1 a2]*4]\`.slow(16)
`;
export const spanish = `slowcat(
stack(c4,eb4,g4),
stack(bb3,d4,f4),
stack(ab3,c4,eb4),
stack(g3,b3,d4)
)`;
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
.every(4, fast(2))
.every(3, slow(1.5))
@ -255,13 +227,6 @@ export const magicSofa = `stack(
.voicings(),
"<c2 f2 g2> <d2 g2 a2 e2>"
).slow(1).transpose.slowcat(0, 2, 3, 4)`;
export const confusedPhoneDynamic = `stack("[g2 ~@1.3] [c3 ~@1.3]".slow(2))
.superimpose(
...[-12,7,10,12,24].slice(0,5).map((t,i,{length}) => x => transpose(t,x).late(i/length))
)
.scale(sequence('C dorian', 'C mixolydian').slow(4))
.scaleTranspose(slowcat(0,1,2,1).slow(2))
.synth('triangle').gain(0.5).filter(1500)`;
export const confusedPhone = `"[g2 ~@1.3] [c3 ~@1.3]"
.superimpose(
transpose(-12).late(0),
@ -309,21 +274,21 @@ export const loungerave = `() => {
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"),
"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).bypass("<0@7 1>/8".early(1/4))
"<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))
)
return stack(
drums,
synths
)
//.bypass("<0 1>*4")
//.mask("<x ~>*4")
//.early("0.25 0");
}`;
export const caverave = `() => {
@ -335,8 +300,8 @@ export const caverave = `() => {
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"),
"c1*2".tone(kick).mask("<x@7 ~>/8"),
"~ <x!7 [x@3 x]>".tone(snare).mask("<x@7 ~>/4"),
"[~ c4]*2".tone(hihat)
);
@ -348,9 +313,9 @@ export const caverave = `() => {
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))
).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))
)
return stack(
drums.fast(2),
@ -363,9 +328,9 @@ export const callcenterhero = `()=>{
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));
return stack(
"0 2".struct("<x ~> [x ~]").edit(s).scaleTranspose(stack(0,2)).tone(lead),
"<6 7 9 7>".struct("[~ [x ~]*2]*2").edit(s).scaleTranspose("[0,2] [2,4]".fast(2).every(4,rev)).tone(lead),
"-14".struct("[~ x@0.8]*2".early(0.01)).edit(s).tone(bass),
"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))
@ -373,3 +338,91 @@ export const callcenterhero = `()=>{
.slow(120 / bpm)
}
`;
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)
}`;
export const drums = `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 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();
return 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
return 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)
}`;

View File

@ -41,8 +41,6 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent}) {
const pushLog = (message) => setLog((log2) => log2 + `${log2 ? "\n\n" : ""}${message}`);
const logCycle = (_events, cycle2) => {
if (_events.length) {
pushLog(`# cycle ${cycle2}
` + _events.map((e) => e.show()).join("\n"));
}
};
const cycle = useCycle({

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1346,4 +1346,4 @@ span.CodeMirror-selectedtext { background: none; }
color: white !important;
}
/*# sourceMappingURL=index.a25424f4.css.map */
/*# sourceMappingURL=index.fd7d9b66.css.map */

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico">
<link rel="stylesheet" type="text/css" href="/tutorial/index.a25424f4.css">
<link rel="stylesheet" type="text/css" href="/tutorial/index.fd7d9b66.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Strudel REPL">
<title>Strudel Tutorial</title>
@ -11,6 +11,6 @@
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script src="/tutorial/index.c743025b.js" defer=""></script>
<script src="/tutorial/index.8834957f.js" defer=""></script>
</body>
</html>

17
package-lock.json generated
View File

@ -18,6 +18,7 @@
},
"devDependencies": {
"mocha": "^9.1.4",
"ramda": "^0.28.0",
"snowpack": "^3.8.8"
}
},
@ -3888,6 +3889,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ramda": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz",
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ramda"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -7981,6 +7992,12 @@
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true
},
"ramda": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz",
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==",
"dev": true
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",

View File

@ -4,7 +4,7 @@
"description": "Experimental port of tidalcycles to javascript",
"main": "strudel.mjs",
"scripts": {
"test": "mocha"
"test": "mocha --colors"
},
"repository": {
"type": "git",
@ -25,6 +25,7 @@
"homepage": "https://github.com/yaxu/strudel#readme",
"devDependencies": {
"mocha": "^9.1.4",
"ramda": "^0.28.0",
"snowpack": "^3.8.8"
},
"dependencies": {

View File

@ -17,8 +17,9 @@ try {
} catch (err) {
console.warn('failed to decode', err);
}
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination);
// "balanced" | "interactive" | "playback";
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.getDestination());
defaultSynth.set({
oscillator: { type: 'triangle' },
envelope: {
@ -39,7 +40,7 @@ function App() {
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({
tune: decoded || randomTune,
defaultSynth,
onEvent: useCallback(markEvent(editor), [editor]),
onDraw: useCallback(markEvent(editor), [editor]),
});
const logBox = useRef<any>();
// scroll log box to bottom when log changes
@ -123,7 +124,9 @@ function App() {
</span>
</div>
{error && (
<div className={cx('absolute right-2 bottom-2 px-2', 'text-red-500')}>{error?.message || 'unknown error'}</div>
<div className={cx('absolute right-2 bottom-2 px-2', 'text-red-500')}>
{error?.message || 'unknown error'}
</div>
)}
</div>
<button

View File

@ -17,7 +17,7 @@ export default function CodeMirror({ value, onChange, options, editorDidMount }:
return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} editorDidMount={editorDidMount} />;
}
export const markEvent = (editor) => (event) => {
export const markEvent = (editor) => (time, event) => {
const locs = event.context.locations;
if (!locs || !editor) {
return;

View File

@ -16,6 +16,7 @@ import {
NoiseSynth,
PluckSynth,
Sampler,
getDestination
} from 'tone';
// what about
@ -61,7 +62,7 @@ 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 = Destination;
export const out = () => getDestination();
/*
@ -77,7 +78,7 @@ const chainable = function (instr) {
instr.chain = (...args) => {
chained = chained.concat(args);
instr.disconnect(); // disconnect from destination / previous chain
return _chain(...chained, Destination);
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') =>
@ -175,7 +176,7 @@ Pattern.prototype.chain = function (...effectGetters: any) {
const chain = (event.context.chain || []).concat(effectGetters);
const getChain = () => {
const effects = chain.map((getEffect: any) => getEffect());
return event.context.getInstrument().chain(...effects, Destination);
return event.context.getInstrument().chain(...effects, getDestination());
};
const onTrigger = getTrigger(getChain, event.value);
return event.setContext({ ...event.context, getChain, onTrigger, chain });

View File

@ -273,22 +273,22 @@ export const zeldasRescue = `stack(
new Gain(0.3),
new Chorus(2, 2.5, 0.5).start(),
new Freeverb(),
Destination)
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),Destination))
"[~ 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 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"),
@ -311,12 +311,12 @@ export const loungerave = `() => {
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 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"),
@ -345,16 +345,16 @@ export const caverave = `() => {
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 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));
return 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))
"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)
}
@ -364,22 +364,22 @@ 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))
"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 drums = `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))
"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 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 delay = new FeedbackDelay(1/8, .6).chain(vol(0.1), out());
const chorus = new Chorus(1,2.5,0.5).start();
return stack(
// melody
@ -389,28 +389,28 @@ export const xylophoneCalling = `()=>{
.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)),
}).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"),
}).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)),
}).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)),
}).chain(vol(0.4),out())),
// kick
"<c1!3 [c1 ~]*2>*2".tone(membrane().chain(vol(0.8),out)),
"<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)),
"~ <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))
"c3*4".transpose("[-24 0]*2").tone(metal(adsr(0,.02)).chain(vol(0.5).connect(delay),out()))
).slow(1)
}`;
@ -424,18 +424,18 @@ export const sowhatelse = `()=> {
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 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)
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");

View File

@ -8,15 +8,16 @@ 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 } = props;
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
const [started, setStarted] = useState<boolean>(false);
const cycleDuration = 1;
const activeCycle = () => Math.floor(Tone.Transport.seconds / cycleDuration);
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
// pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => {
@ -27,13 +28,13 @@ function useCycle(props: UseCycleProps) {
// console.log('schedule', cycle);
// query next cycle in the middle of the current
const cancelFrom = timespan.begin.valueOf();
Tone.Transport.cancel(cancelFrom);
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.Transport.seconds, queryNextTime) + 0.1;
Tone.Transport.schedule(() => {
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
Tone.getTransport().schedule(() => {
query(cycle + 1);
}, t);
@ -41,7 +42,7 @@ function useCycle(props: UseCycleProps) {
events
?.filter((event) => event.part.begin.valueOf() === event.whole.begin.valueOf())
.forEach((event) => {
Tone.Transport.schedule((time) => {
Tone.getTransport().schedule((time) => {
const toneEvent = {
time: event.part.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(),
@ -49,6 +50,10 @@ function useCycle(props: UseCycleProps) {
context: event.context,
};
onEvent(time, toneEvent);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, toneEvent);
}, time);
}, event.part.begin.valueOf());
});
};
@ -60,12 +65,12 @@ function useCycle(props: UseCycleProps) {
const start = async () => {
setStarted(true);
await Tone.start();
Tone.Transport.start('+0.1');
Tone.getTransport().start('+0.1');
};
const stop = () => {
console.log('stop');
setStarted(false);
Tone.Transport.pause();
Tone.getTransport().pause();
};
const toggle = () => (started ? stop() : start());
return { start, stop, setStarted, onEvent, started, toggle, query, activeCycle };

View File

@ -11,7 +11,7 @@ let s4 = () => {
.substring(1);
};
function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }: any) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState<string>(tune);
const [activeCode, setActiveCode] = useState<string>();
@ -52,6 +52,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
};
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event) => {
try {

View File

@ -709,6 +709,9 @@ class Pattern {
edit(...funcs) {
return stack(...funcs.map(func => func(this)));
}
pipe(func) {
return func(this);
}
_bypass(on) {
on = Boolean(parseInt(on));