This commit is contained in:
Felix Roos 2022-02-16 20:13:40 +01:00
parent b8d3fe6f1a
commit f97837d15a
10 changed files with 526 additions and 295 deletions

67
docs/dist/App.js vendored
View file

@ -10,7 +10,7 @@ import hot from "../hot.js";
import {isNote} from "../_snowpack/pkg/tone.js";
import {useWebMidi} from "./midi.js";
const [_, codeParam] = window.location.href.split("#");
const decoded = atob(codeParam || "");
const decoded = atob(decodeURIComponent(codeParam || ""));
const getHotCode = async () => {
return fetch("/hot.js").then((res) => res.text()).then((src) => {
return src.split("export default").slice(-1)[0].trim();
@ -30,17 +30,37 @@ function getRandomTune() {
}
const randomTune = getRandomTune();
function App() {
const [mode, setMode] = useState("javascript");
const [code, setCode] = useState(decoded || randomTune);
const [activeCode, setActiveCode] = useState();
const [log, setLog] = useState("");
const logBox = useRef();
const [error, setError] = useState();
const [pattern, setPattern] = useState();
const [activePattern, setActivePattern] = useState();
const activatePattern = (_pattern = pattern) => {
setActivePattern(() => _pattern);
window.location.hash = "#" + btoa(code);
!cycle.started && cycle.start();
const dirty = code !== activeCode;
const activateCode = (_code = code) => {
if (activeCode && !dirty) {
setError(void 0);
return;
}
try {
const parsed = evaluate(_code);
setPattern(() => parsed.pattern);
activatePattern(parsed.pattern);
setError(void 0);
setActiveCode(_code);
} catch (err) {
setError(err);
}
};
const activatePattern = (_pattern) => {
try {
setActivePattern(() => _pattern);
window.location.hash = "#" + encodeURIComponent(btoa(code));
!cycle.started && cycle.start();
} catch (err) {
setError(err);
}
};
const [isHot, setIsHot] = useState(false);
const pushLog = (message) => setLog((log2) => log2 + `${log2 ? "\n\n" : ""}${message}`);
@ -85,7 +105,7 @@ function App() {
if (e.ctrlKey || e.altKey) {
switch (e.code) {
case "Enter":
activatePattern();
activateCode();
break;
case "Period":
cycle.stop();
@ -94,34 +114,19 @@ function App() {
};
document.addEventListener("keypress", handleKeyPress);
return () => document.removeEventListener("keypress", handleKeyPress);
}, [pattern]);
}, [pattern, code]);
useEffect(() => {
let _code = code;
if (isHot) {
if (typeof hot !== "string") {
getHotCode().then((_code2) => {
setCode(_code2);
setMode("javascript");
getHotCode().then((_code) => {
});
activatePattern(hot);
return;
} else {
_code = hot;
setCode(_code);
setCode(hot);
activateCode(hot);
}
}
try {
const parsed = evaluate(_code);
setPattern(() => parsed.pattern);
if (isHot) {
activatePattern(parsed.pattern);
}
setMode(parsed.mode);
setError(void 0);
} catch (err) {
console.warn(err);
setError(err);
}
}, [code, isHot]);
useLayoutEffect(() => {
logBox.current.scrollTop = logBox.current?.scrollHeight;
@ -175,7 +180,7 @@ function App() {
value: code,
readOnly: isHot,
options: {
mode,
mode: "javascript",
theme: "material",
lineNumbers: true
},
@ -187,14 +192,14 @@ function App() {
}), /* @__PURE__ */ React.createElement("span", {
className: "p-4 absolute top-0 right-0 text-xs whitespace-pre text-right"
}, !cycle.started ? `press ctrl+enter to play
` : !isHot && activePattern !== pattern ? `ctrl+enter to update
` : "no changes\n", !isHot && /* @__PURE__ */ React.createElement(React.Fragment, null, {pegjs: "mini"}[mode] || mode, " mode"), isHot && "🔥 hot mode: go to hot.js to edit pattern, then save")), error && /* @__PURE__ */ React.createElement("div", {
className: "absolute right-2 bottom-2 text-red-500"
` : !isHot && code !== activeCode ? `ctrl+enter to update
` : "no changes\n", isHot && "🔥 hot mode: go to hot.js to edit pattern, then save")), error && /* @__PURE__ */ React.createElement("div", {
className: cx("absolute right-2 bottom-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: () => {
if (!cycle.started) {
activatePattern();
activateCode();
} else {
cycle.stop();
}

10
docs/dist/evaluate.js vendored
View file

@ -6,6 +6,8 @@ import "./tonal.js";
import "./groove.js";
import shapeshifter from "./shapeshifter.js";
import {minify} from "./parse.js";
import * as Tone from "../_snowpack/pkg/tone.js";
import * as toneHelpers from "./tone.js";
const bootstrapped = {...strudel, ...strudel.Pattern.prototype.bootstrap()};
function hackLiteral(literal, names, func) {
names.forEach((name) => {
@ -18,10 +20,14 @@ function hackLiteral(literal, names, func) {
}
hackLiteral(String, ["mini", "m"], bootstrapped.mini);
hackLiteral(String, ["pure", "p"], bootstrapped.pure);
Object.assign(globalThis, bootstrapped);
Object.assign(globalThis, bootstrapped, Tone, toneHelpers);
export const evaluate = (code) => {
const shapeshifted = shapeshifter(code);
const pattern = minify(eval(shapeshifted));
let evaluated = eval(shapeshifted);
if (typeof evaluated === "function") {
evaluated = evaluated();
}
const pattern = minify(evaluated);
if (pattern?.constructor?.name !== "Pattern") {
const message = `got "${typeof pattern}" instead of pattern`;
throw new Error(message + (typeof pattern === "function" ? ", did you forget to call a function?" : "."));

34
docs/dist/parse.js vendored
View file

@ -23,9 +23,40 @@ const applyOptions = (parent) => (pat, i) => {
}
return pat;
};
function resolveReplications(ast) {
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_,
options_: {
operator: {
type_: "stretch",
arguments_: {amount: String(new Fraction(replicate).inverse().valueOf())}
}
}
}
]
}
};
}
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") {
@ -38,7 +69,8 @@ export function patternifyAST(ast) {
if (weightedChildren) {
const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
if (alignment === "t") {
return pat._slow(children.length);
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
return pat._slow(weightSum);
}
return pat;
}

2
docs/dist/tonal.js vendored
View file

@ -46,7 +46,7 @@ function scaleTranspose(scale, offset, note) {
Pattern.prototype._mapNotes = function(func) {
return this.fmap((event) => {
const noteEvent = toNoteEvent(event);
return func(noteEvent);
return {...noteEvent, ...func(noteEvent)};
});
};
Pattern.prototype._transpose = function(intervalOrSemitones) {

59
docs/dist/tone.js vendored
View file

@ -1,6 +1,62 @@
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
import {AutoFilter, Destination, Filter, Gain, isNote, Synth} from "../_snowpack/pkg/tone.js";
import {AutoFilter, Destination, Filter, Gain, isNote, Synth, PolySynth} from "../_snowpack/pkg/tone.js";
const Pattern = _Pattern;
Pattern.prototype.tone = function(instrument) {
return this.fmap((value) => {
value = typeof value !== "object" && !Array.isArray(value) ? {value} : value;
const onTrigger = (time, event) => {
if (instrument.constructor.name === "PluckSynth") {
instrument.triggerAttack(value.value, time);
} else if (instrument.constructor.name === "NoiseSynth") {
instrument.triggerAttackRelease(event.duration, time);
} else {
instrument.triggerAttackRelease(value.value, event.duration, time);
}
};
return {...value, instrument, onTrigger};
});
};
Pattern.prototype.define("tone", (type, pat) => pat.tone(type), {composable: true, patternified: false});
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 = Destination;
const chainable = function(instr) {
const _chain = instr.chain.bind(instr);
let chained = [];
instr.chain = (...args) => {
chained = chained.concat(args);
instr.disconnect();
return _chain(...chained, Destination);
};
instr.filter = (freq = 1e3, type = "lowpass") => instr.chain(new Filter(freq, type));
instr.gain = (gain2 = 0.9) => instr.chain(new Gain(gain2));
return instr;
};
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) {
this.instrument = poly(type);
}
return this.fmap((value) => {
value = typeof value !== "object" && !Array.isArray(value) ? {value} : value;
const onTrigger = (time, event) => {
this.instrument.set(instrumentConfig);
this.instrument.triggerAttackRelease(value.value, event.duration, time);
};
return {...value, instrumentConfig, onTrigger};
});
};
Pattern.prototype.define("poly", (type, pat) => pat.poly(type), {composable: true, patternified: true});
const getTrigger = (getChain, value) => (time, event) => {
const chain = getChain();
if (!isNote(value)) {
@ -68,7 +124,6 @@ Pattern.prototype._filter = function(freq, q, type = "lowpass") {
Pattern.prototype.autofilter = function(g) {
return this.chain(autofilter(g));
};
Pattern.prototype.patternified = Pattern.prototype.patternified.concat(["synth", "gain", "filter"]);
Pattern.prototype.define("synth", (type, pat) => pat.synth(type), {composable: true, patternified: true});
Pattern.prototype.define("gain", (gain2, pat) => pat.synth(gain2), {composable: true, patternified: true});
Pattern.prototype.define("filter", (cutoff, pat) => pat.filter(cutoff), {composable: true, patternified: true});

91
docs/dist/tunes.js vendored
View file

@ -250,12 +250,11 @@ export const groove = `stack(
.voicings(['G3','A4'])
).slow(4)`;
export const magicSofa = `stack(
'[C^7 F^7 ~]/3 [Dm7 G7 A7 ~]/4'.mini
'<C^7 F^7 ~> <Dm7 G7 A7 ~>'.m
.every(2, fast(2))
.voicings(),
'[c2 f2 g2]/3 [d2 g2 a2 e2]/4'.mini
).slow(1)
.transpose.slowcat(0, 2, 3, 4)`;
'<c2 f2 g2> <d2 g2 a2 e2>'.m
).slow(1).transpose.slowcat(0, 2, 3, 4)`;
export const confusedPhoneDynamic = `stack('[g2 ~@1.3] [c3 ~@1.3]'.mini.slow(2))
.superimpose(
...[-12,7,10,12,24].slice(0,5).map((t,i,{length}) => x => transpose(t,x).late(i/length))
@ -274,3 +273,87 @@ export const confusedPhone = `'[g2 ~@1.3] [c3 ~@1.3]'.mini
.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]\`.mini,
// 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]]\`.mini
).transpose(12).slow(48).tone(
new PolySynth().chain(
new Gain(0.3),
new Chorus(2, 2.5, 0.5).start(),
new Freeverb(),
Destination)
)`;
export const technoDrums = `stack(
'c1*2'.m.tone(new Tone.MembraneSynth().toDestination()),
'~ x'.m.tone(new Tone.NoiseSynth().toDestination()),
'[~ c4]*2'.m.tone(new Tone.MetalSynth().set({envelope:{decay:0.06,sustain:0}}).chain(new Gain(0.5),Destination))
)`;
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'.m.tone(kick).bypass('<0@7 1>/8'.m),
'~ <x!7 [x@3 x]>'.m.tone(snare).bypass('<0@7 1>/4'.m),
'[~ c4]*2'.m.tone(hihat)
);
const thru = (x) => x.transpose('<0 1>/8'.m).transpose(1);
const synths = stack(
'<C2 Bb1 Ab1 [G1 [G2 G1]]>/2'.m.groove('[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2'.m).edit(thru).tone(bass),
'<Cm7 Bb7 Fm7 G7b9>/2'.m.groove('~ [x@0.1 ~]'.m).voicings().edit(thru).every(2, early(1/4)).tone(keys).bypass('<0@7 1>/8'.m.early(1/4))
)
return stack(
drums,
synths
)
//.bypass('<0 1>*4'.m)
//.early('0.25 0'.m);
}`;
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'.m.tone(kick).bypass('<0@7 1>/8'.m),
'~ <x!7 [x@3 x]>'.m.tone(snare).bypass('<0@7 1>/4'.m),
'[~ c4]*2'.m.tone(hihat)
);
const thru = (x) => x.transpose('<0 1>/8'.m).transpose(-1);
const synths = stack(
'<eb4 d4 c4 b3>/2'.m.scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).groove('[~ x]*2'.m)
.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'.m),
'<C2 Bb1 Ab1 [G1 [G2 G1]]>/2'.m.groove('[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2'.m.fast(2)).edit(thru).tone(bass),
'<Cm7 Bb7 Fm7 G7b13>/2'.m.groove('~ [x@0.1 ~]'.m.fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass('<0@7 1>/8'.m.early(1/4))
)
return stack(
drums.fast(2),
synths
).slow(2);
}`;

12
docs/dist/useCycle.js vendored
View file

@ -8,12 +8,8 @@ function useCycle(props) {
const activeCycle = () => Math.floor(Tone.Transport.seconds / cycleDuration);
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const _events = onQuery?.(timespan) || [];
onSchedule?.(_events, cycle);
schedule(_events, cycle);
};
const schedule = (events, cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(timespan) || [];
onSchedule?.(events, cycle);
const cancelFrom = timespan.begin.valueOf();
Tone.Transport.cancel(cancelFrom);
const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
@ -29,7 +25,7 @@ function useCycle(props) {
Tone.Transport.schedule((time) => {
const toneEvent = {
time: event.part.begin.valueOf(),
duration: event.whole.end.valueOf() - event.whole.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(),
value: event.value
};
onEvent(time, toneEvent);
@ -51,6 +47,6 @@ function useCycle(props) {
Tone.Transport.pause();
};
const toggle = () => started ? stop() : start();
return {start, stop, onEvent, started, toggle, schedule, query, activeCycle};
return {start, stop, onEvent, started, toggle, query, activeCycle};
}
export default useCycle;