This commit is contained in:
Felix Roos 2022-02-27 22:11:34 +01:00
parent df0650e48d
commit 827c983175
24 changed files with 22150 additions and 20110 deletions

14
docs/dist/App.js vendored
View file

@ -14,7 +14,6 @@ try {
} catch (err) {
console.warn("failed to decode", err);
}
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"},
@ -30,7 +29,7 @@ function getRandomTune() {
const randomTune = getRandomTune();
function App() {
const [editor, setEditor] = useState();
const {setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog} = useRepl({
const {setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog, pending} = useRepl({
tune: decoded || randomTune,
defaultSynth,
onDraw: useCallback(markEvent(editor), [editor])
@ -40,12 +39,11 @@ function App() {
logBox.current.scrollTop = logBox.current?.scrollHeight;
}, [log]);
useLayoutEffect(() => {
const handleKeyPress = (e) => {
const handleKeyPress = async (e) => {
if (e.ctrlKey || e.altKey) {
switch (e.code) {
case "Enter":
activateCode();
!cycle.started && cycle.start();
await activateCode();
break;
case "Period":
cycle.stop();
@ -81,11 +79,11 @@ function App() {
}, "Strudel REPL")), /* @__PURE__ */ React.createElement("div", {
className: "flex space-x-4"
}, /* @__PURE__ */ React.createElement("button", {
onClick: () => {
onClick: async () => {
const _code = getRandomTune();
console.log("tune", _code);
setCode(_code);
const parsed = evaluate(_code);
const parsed = await evaluate(_code);
setPattern(parsed.pattern);
}
}, "🎲 random tune"), /* @__PURE__ */ React.createElement("button", null, /* @__PURE__ */ React.createElement("a", {
@ -116,7 +114,7 @@ function App() {
}, 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()
}, cycle.started ? "pause" : "play"), /* @__PURE__ */ React.createElement("textarea", {
}, !pending ? /* @__PURE__ */ React.createElement(React.Fragment, null, cycle.started ? "pause" : "play") : /* @__PURE__ */ React.createElement(React.Fragment, null, "loading...")), /* @__PURE__ */ React.createElement("textarea", {
className: "grow bg-[#283237] border-0 text-xs min-h-[200px]",
value: log,
readOnly: true,

View file

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

View file

@ -20,9 +20,9 @@ function hackLiteral(literal, names, func) {
hackLiteral(String, ["mini", "m"], bootstrapped.mini);
hackLiteral(String, ["pure", "p"], bootstrapped.pure);
Object.assign(globalThis, bootstrapped, Tone, toneHelpers);
export const evaluate = (code) => {
export const evaluate = async (code) => {
const shapeshifted = shapeshifter(code);
let evaluated = eval(shapeshifted);
let evaluated = await eval(shapeshifted);
if (typeof evaluated === "function") {
evaluated = evaluated();
}

3
docs/dist/parse.js vendored
View file

@ -90,7 +90,8 @@ export function patternifyAST(ast) {
return ast.source_;
}
const {start, end} = ast.location_;
return pure(ast.source_).withLocation({start, end});
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
return pure(value).withLocation({start, end});
}
return patternifyAST(ast.source_);
case "stretch":

View file

@ -92,15 +92,16 @@ export default (code) => {
// 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(value + '', node.arguments[0], ast.locations, artificialNodes);
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
// console.log('value',value);
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(value, node, ast.locations, artificialNodes);
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
}
return new LiteralStringExpression({ value });
}
@ -110,7 +111,7 @@ export default (code) => {
}
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
// console.log('add', node);
return reifyWithLocation(node.value, node, ast.locations, artificialNodes);
return reifyWithLocation(node, node, ast.locations, artificialNodes);
}
if (!addMiniLocations) {
return wrapFunction('reify', node);
@ -219,10 +220,10 @@ function wrapLocationOffset(node, stringNode, locations, artificialNodes) {
// 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(value, node, locations, artificialNodes) {
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: wrapFunction('reify', new LiteralStringExpression({ value })),
object: wrapFunction('reify', literalNode),
property: 'withLocation',
}),
arguments: [getLocationObject(node, locations)],

37
docs/dist/tonal.js vendored
View file

@ -1,15 +1,6 @@
import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
const Pattern = _Pattern;
function toNoteEvent(event) {
if (typeof event === "string" || typeof event === "number") {
return {value: event};
}
if (event.value) {
return event;
}
throw new Error("not a valid note event: " + JSON.stringify(event));
}
const mod = (n, m) => n < 0 ? mod(n + m, m) : n % m;
export function intervalDirection(from, to, direction = 1) {
const sign = Math.sign(direction);
@ -44,43 +35,37 @@ function scaleTranspose(scale, offset, note) {
}
return n + o;
}
Pattern.prototype._mapNotes = function(func) {
return this.fmap((event) => {
const noteEvent = toNoteEvent(event);
return {...noteEvent, ...func(noteEvent)};
});
};
Pattern.prototype._transpose = function(intervalOrSemitones) {
return this._mapNotes(({value, scale}) => {
return this._withEvent((event) => {
const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones);
if (typeof value === "number") {
if (typeof event.value === "number") {
const semitones = typeof interval === "string" ? Interval.semitones(interval) || 0 : interval;
return {value: value + semitones};
return event.withValue(() => event.value + semitones);
}
return {value: Note.transpose(value, interval), scale};
return event.withValue(() => Note.transpose(event.value, interval));
});
};
Pattern.prototype._scaleTranspose = function(offset) {
return this._mapNotes(({value, scale}) => {
if (!scale) {
return this._withEvent((event) => {
if (!event.context.scale) {
throw new Error("can only use scaleTranspose after .scale");
}
if (typeof value !== "string") {
if (typeof event.value !== "string") {
throw new Error("can only use scaleTranspose with notes");
}
return {value: scaleTranspose(scale, Number(offset), value), scale};
return event.withValue(() => scaleTranspose(event.context.scale, Number(offset), event.value));
});
};
Pattern.prototype._scale = function(scale) {
return this._mapNotes((value) => {
let note = value.value;
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 {...value, value: note, scale};
return event.withValue(() => note).setContext({...event.context, scale});
});
};
Pattern.prototype.define("transpose", (a, pat) => pat.transpose(a), {composable: true, patternified: true});

62
docs/dist/tone.js vendored
View file

@ -17,20 +17,23 @@ import {
Sampler,
getDestination
} from "../_snowpack/pkg/tone.js";
import {Piano} from "../_snowpack/pkg/@tonejs/piano.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) => {
return this._withEvent((event) => {
const onTrigger = (time, event2) => {
if (instrument.constructor.name === "PluckSynth") {
instrument.triggerAttack(value.value, time);
instrument.triggerAttack(event2.value, time);
} else if (instrument.constructor.name === "NoiseSynth") {
instrument.triggerAttackRelease(event.duration, time);
instrument.triggerAttackRelease(event2.duration, time);
} else if (instrument.constructor.name === "Piano") {
instrument.keyDown({note: event2.value, time, velocity: 0.5});
instrument.keyUp({note: event2.value, time: time + event2.duration});
} else {
instrument.triggerAttackRelease(value.value, event.duration, time);
instrument.triggerAttackRelease(event2.value, event2.duration, time);
}
};
return {...value, instrument, onTrigger};
return event.setContext({...event.context, instrument, onTrigger});
});
};
Pattern.prototype.define("tone", (type, pat) => pat.tone(type), {composable: true, patternified: false});
@ -45,6 +48,11 @@ export const pluck = (options) => new PluckSynth(options);
export const polysynth = (options) => new PolySynth(options);
export const sampler = (options) => new Sampler(options);
export const synth = (options) => new Synth(options);
export const piano = async (options = {velocities: 1}) => {
const p = new Piano(options);
await p.load();
return p;
};
export const vol = (v) => new Gain(v);
export const lowpass = (v) => new Filter(v, "lowpass");
export const highpass = (v) => new Filter(v, "highpass");
@ -75,13 +83,12 @@ Pattern.prototype._poly = function(type = "triangle") {
if (!this.instrument) {
this.instrument = poly(type);
}
return this.fmap((value) => {
value = typeof value !== "object" && !Array.isArray(value) ? {value} : value;
const onTrigger = (time, event) => {
return this._withEvent((event) => {
const onTrigger = (time, event2) => {
this.instrument.set(instrumentConfig);
this.instrument.triggerAttackRelease(value.value, event.duration, time);
this.instrument.triggerAttackRelease(event2.value, event2.duration, time);
};
return {...value, instrumentConfig, onTrigger};
return event.setContext({...event.context, instrumentConfig, onTrigger});
});
};
Pattern.prototype.define("poly", (type, pat) => pat.poly(type), {composable: true, patternified: true});
@ -96,8 +103,7 @@ const getTrigger = (getChain, value) => (time, event) => {
}, event.duration * 2e3);
};
Pattern.prototype._synth = function(type = "triangle") {
return this.fmap((value) => {
value = typeof value !== "object" && !Array.isArray(value) ? {value} : value;
return this._withEvent((event) => {
const instrumentConfig = {
oscillator: {type},
envelope: {attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01}
@ -107,37 +113,37 @@ Pattern.prototype._synth = function(type = "triangle") {
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
return {...value, getInstrument, instrumentConfig, onTrigger};
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.fmap((value) => {
if (!value?.getInstrument) {
return this._withEvent((event) => {
if (!event.context.getInstrument) {
throw new Error("cannot chain adsr: need instrument first (like synth)");
}
const instrumentConfig = {...value.instrumentConfig, envelope: {attack, decay, sustain, release}};
const instrumentConfig = {...event.context.instrumentConfig, envelope: {attack, decay, sustain, release}};
const getInstrument = () => {
const instrument = value.getInstrument();
const instrument = event.context.getInstrument();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
return {...value, getInstrument, instrumentConfig, onTrigger};
const onTrigger = getTrigger(() => getInstrument().toDestination(), event.value);
return event.setContext({...event.context, getInstrument, instrumentConfig, onTrigger});
});
};
Pattern.prototype.chain = function(...effectGetters) {
return this.fmap((value) => {
if (!value?.getInstrument) {
return this._withEvent((event) => {
if (!event.context?.getInstrument) {
throw new Error("cannot chain: need instrument first (like synth)");
}
const chain = (value.chain || []).concat(effectGetters);
const chain = (event.context.chain || []).concat(effectGetters);
const getChain = () => {
const effects = chain.map((getEffect) => getEffect());
return value.getInstrument().chain(...effects, getDestination());
return event.context.getInstrument().chain(...effects, getDestination());
};
const onTrigger = getTrigger(getChain, value.value);
return {...value, getChain, onTrigger, chain};
const onTrigger = getTrigger(getChain, event.value);
return event.setContext({...event.context, getChain, onTrigger, chain});
});
};
export const autofilter = (freq = 1) => () => new AutoFilter(freq).start();

8
docs/dist/tunes.js vendored
View file

@ -426,3 +426,11 @@ export const sowhatelse = `()=> {
"[2,4]/4".scale('D dorian').apply(t).tone(instr('pad')).mask("<x x x ~>/8")
).fast(6/8)
}`;
export const barryHarris = `piano()
.then(p => "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(p.toDestination()))
`;

View file

@ -1,6 +1,6 @@
import {useEffect, useState} from "../_snowpack/pkg/react.js";
import * as Tone from "../_snowpack/pkg/tone.js";
import {TimeSpan} from "../_snowpack/link/strudel.js";
import {TimeSpan, State} from "../_snowpack/link/strudel.js";
function useCycle(props) {
const {onEvent, onQuery, onSchedule, ready = true, onDraw} = props;
const [started, setStarted] = useState(false);
@ -8,7 +8,7 @@ function useCycle(props) {
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(timespan) || [];
const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle);
const cancelFrom = timespan.begin.valueOf();
Tone.getTransport().cancel(cancelFrom);
@ -22,7 +22,8 @@ function useCycle(props) {
const toneEvent = {
time: event.part.begin.valueOf(),
duration: event.whole.end.sub(event.whole.begin).valueOf(),
value: event.value
value: event.value,
context: event.context
};
onEvent(time, toneEvent);
Tone.Draw.schedule(() => {

23
docs/dist/useRepl.js vendored
View file

@ -12,19 +12,22 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
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 = (_code = code) => {
!cycle.started && cycle.start();
broadcast({type: "start", from: id});
const activateCode = async (_code = code) => {
if (activeCode && !dirty) {
setError(void 0);
!cycle.started && cycle.start();
return;
}
try {
const parsed = evaluate(_code);
setPending(true);
const parsed = await evaluate(_code);
!cycle.started && cycle.start();
broadcast({type: "start", from: id});
setPattern(() => parsed.pattern);
if (autolink) {
window.location.hash = "#" + encodeURIComponent(btoa(code));
@ -32,6 +35,7 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
setHash(generateHash());
setError(void 0);
setActiveCode(_code);
setPending(false);
} catch (err) {
err.message = "evaluation error: " + err.message;
console.warn(err);
@ -48,8 +52,9 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
onEvent: useCallback((time, event) => {
try {
onEvent?.(event);
if (!event.value?.onTrigger) {
const note = event.value?.value || event.value;
const {onTrigger} = event.context;
if (!onTrigger) {
const note = event.value;
if (!isNote(note)) {
throw new Error("not a note: " + note);
}
@ -59,7 +64,6 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
throw new Error("no defaultSynth passed to useRepl.");
}
} else {
const {onTrigger} = event.value;
onTrigger(time, event);
}
} catch (err) {
@ -68,9 +72,9 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
pushLog(err.message);
}
}, [onEvent]),
onQuery: useCallback((span) => {
onQuery: useCallback((state) => {
try {
return pattern?.query(span) || [];
return pattern?.query(state) || [];
} catch (err) {
console.warn(err);
err.message = "query error: " + err.message;
@ -95,6 +99,7 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
}
};
return {
pending,
code,
setCode,
pattern,

15
docs/dist/voicings.js vendored
View file

@ -10,7 +10,7 @@ const getVoicing = (chord, lastVoicing, range = ["F3", "A4"]) => dictionaryVoici
});
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))).flat());
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;
@ -18,15 +18,16 @@ Pattern.prototype.voicings = function(range) {
range = ["F3", "A4"];
}
return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
return stack(...lastVoicing);
lastVoicing = getVoicing(event.value, lastVoicing, range);
return stack(...lastVoicing)._withContext(() => ({
locations: event.context.locations || []
}));
});
};
Pattern.prototype.rootNotes = function(octave = 2) {
return this._mapNotes((value) => {
const [_, root] = value.value.match(/^([a-gA-G])[b#]?.*$/);
const bassNote = root + octave;
return {...value, value: bassNote};
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});