This commit is contained in:
Felix Roos 2022-03-06 21:39:10 +01:00
parent ad42e5eb61
commit a2ab2b9da5
19 changed files with 72400 additions and 62 deletions

View File

@ -1,5 +1,6 @@
import Fraction from "../pkg/fractionjs.js"; import Fraction from "../pkg/fractionjs.js";
import {compose} from "../pkg/ramda.js"; import {compose} from "../pkg/ramda.js";
import {isNote, toMidi} from "./util.js";
const removeUndefineds = (xs) => xs.filter((x) => x != void 0); const removeUndefineds = (xs) => xs.filter((x) => x != void 0);
const flatten = (arr) => [].concat(...arr); const flatten = (arr) => [].concat(...arr);
const id = (a) => a; const id = (a) => a;
@ -327,17 +328,39 @@ class Pattern {
_opleft(other, func) { _opleft(other, func) {
return this.fmap(func).appLeft(reify(other)); return this.fmap(func).appLeft(reify(other));
} }
_asNumber() {
return this._withEvent((event) => {
const asNumber = Number(event.value);
if (!isNaN(asNumber)) {
return event.withValue(() => asNumber);
}
const specialValue = {
e: Math.E,
pi: Math.PI
}[event.value];
if (typeof specialValue !== "undefined") {
return event.withValue(() => specialValue);
}
if (isNote(event.value)) {
return new Hap(event.whole, event.part, toMidi(event.value), {...event.context, type: "midi"});
}
throw new Error('cannot parse as number: "' + event.value + '"');
});
}
add(other) { add(other) {
return this._opleft(other, (a) => (b) => a + b); return this._asNumber()._opleft(other, (a) => (b) => a + b);
} }
sub(other) { sub(other) {
return this._opleft(other, (a) => (b) => a - b); return this._asNumber()._opleft(other, (a) => (b) => a - b);
} }
mul(other) { mul(other) {
return this._opleft(other, (a) => (b) => a * b); return this._asNumber()._opleft(other, (a) => (b) => a * b);
} }
div(other) { div(other) {
return this._opleft(other, (a) => (b) => a / b); return this._asNumber()._opleft(other, (a) => (b) => a / b);
}
round() {
return this._asNumber().fmap((v) => Math.round(v));
} }
union(other) { union(other) {
return this._opleft(other, (a) => (b) => Object.assign({}, a, b)); return this._opleft(other, (a) => (b) => Object.assign({}, a, b));

View File

@ -0,0 +1,33 @@
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) : void 0];
};
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;
};
export const mod = (n, m) => n < 0 ? mod(n + m, m) : n % m;
export const getPlayableNoteValue = (event) => {
let {value: note, context} = event;
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;
};

View File

@ -3,6 +3,9 @@ import "./tone.js";
import "./midi.js"; import "./midi.js";
import "./voicings.js"; import "./voicings.js";
import "./tonal.js"; import "./tonal.js";
import "./xen.js";
import "./tune.js";
import "./tonal.js";
import gist from "./gist.js"; import gist from "./gist.js";
import shapeshifter from "./shapeshifter.js"; import shapeshifter from "./shapeshifter.js";
import {minify} from "./parse.js"; import {minify} from "./parse.js";

9
docs/dist/tonal.js vendored
View File

@ -1,13 +1,8 @@
import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js"; import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js"; import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
import {mod, tokenizeNote} from "../_snowpack/link/util.js";
const Pattern = _Pattern; const Pattern = _Pattern;
const mod = (n, m) => n < 0 ? mod(n + m, m) : n % m; export function scaleTranspose(scale, offset, note) {
export function intervalDirection(from, to, direction = 1) {
const sign = Math.sign(direction);
const interval = sign < 0 ? Interval.distance(to, from) : Interval.distance(from, to);
return (sign < 0 ? "-" : "") + interval;
}
function scaleTranspose(scale, offset, note) {
let [tonic, scaleName] = Scale.tokenize(scale); let [tonic, scaleName] = Scale.tokenize(scale);
let {notes} = Scale.get(`${tonic} ${scaleName}`); let {notes} = Scale.get(`${tonic} ${scaleName}`);
notes = notes.map((note2) => Note.get(note2).pc); notes = notes.map((note2) => Note.get(note2).pc);

7
docs/dist/tone.js vendored
View File

@ -19,24 +19,28 @@ import {
Players Players
} from "../_snowpack/pkg/tone.js"; } from "../_snowpack/pkg/tone.js";
import {Piano} from "../_snowpack/pkg/@tonejs/piano.js"; import {Piano} from "../_snowpack/pkg/@tonejs/piano.js";
import {getPlayableNoteValue} from "../_snowpack/link/util.js";
const Pattern = _Pattern; const Pattern = _Pattern;
Pattern.prototype.tone = function(instrument) { Pattern.prototype.tone = function(instrument) {
return this._withEvent((event) => { return this._withEvent((event) => {
const onTrigger = (time, event2) => { const onTrigger = (time, event2) => {
let note = event2.value; let note;
let velocity = event2.context?.velocity ?? 0.75; let velocity = event2.context?.velocity ?? 0.75;
switch (instrument.constructor.name) { switch (instrument.constructor.name) {
case "PluckSynth": case "PluckSynth":
note = getPlayableNoteValue(event2);
instrument.triggerAttack(note, time); instrument.triggerAttack(note, time);
break; break;
case "NoiseSynth": case "NoiseSynth":
instrument.triggerAttackRelease(event2.duration, time); instrument.triggerAttackRelease(event2.duration, time);
break; break;
case "Piano": case "Piano":
note = getPlayableNoteValue(event2);
instrument.keyDown({note, time, velocity: 0.5}); instrument.keyDown({note, time, velocity: 0.5});
instrument.keyUp({note, time: time + event2.duration, velocity}); instrument.keyUp({note, time: time + event2.duration, velocity});
break; break;
case "Sampler": case "Sampler":
note = getPlayableNoteValue(event2);
instrument.triggerAttackRelease(note, event2.duration, time, velocity); instrument.triggerAttackRelease(note, event2.duration, time, velocity);
break; break;
case "Players": case "Players":
@ -48,6 +52,7 @@ Pattern.prototype.tone = function(instrument) {
player.stop(time + event2.duration); player.stop(time + event2.duration);
break; break;
default: default:
note = getPlayableNoteValue(event2);
instrument.triggerAttackRelease(note, event2.duration, time, velocity); instrument.triggerAttackRelease(note, event2.duration, time, velocity);
} }
}; };

14
docs/dist/tune.js vendored Normal file
View File

@ -0,0 +1,14 @@
import Tune from "./tunejs.js";
import {Pattern} from "../_snowpack/link/strudel.js";
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
docs/dist/tunejs.js vendored Normal file

File diff suppressed because one or more lines are too long

28
docs/dist/tunes.js vendored
View File

@ -489,3 +489,31 @@ export const wavyKalimba = `sampler({
.legato("<.4 .8 1 1.2 1.4 1.6 1.8 2>/8") .legato("<.4 .8 1 1.2 1.4 1.6 1.8 2>/8")
.fast(1) .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());
return 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)
}`;

View File

@ -1,5 +1,5 @@
import {useCallback, useState, useMemo} from "../_snowpack/pkg/react.js"; import {useCallback, useState, useMemo} from "../_snowpack/pkg/react.js";
import {isNote} from "../_snowpack/pkg/tone.js"; import {getPlayableNoteValue} from "../_snowpack/link/util.js";
import {evaluate} from "./evaluate.js"; import {evaluate} from "./evaluate.js";
import useCycle from "./useCycle.js"; import useCycle from "./useCycle.js";
import usePostMessage from "./usePostMessage.js"; import usePostMessage from "./usePostMessage.js";
@ -54,11 +54,8 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
onEvent?.(event); onEvent?.(event);
const {onTrigger, velocity} = event.context; const {onTrigger, velocity} = event.context;
if (!onTrigger) { if (!onTrigger) {
const note = event.value;
if (!isNote(note)) {
throw new Error("not a note: " + note);
}
if (defaultSynth) { if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration, time, velocity); defaultSynth.triggerAttackRelease(note, event.duration, time, velocity);
} else { } else {
throw new Error("no defaultSynth passed to useRepl."); throw new Error("no defaultSynth passed to useRepl.");

52
docs/dist/xen.js vendored Normal file
View File

@ -0,0 +1,52 @@
import {Pattern} from "../_snowpack/link/strudel.js";
import {mod} from "../_snowpack/link/util.js";
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}, (_2, 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);
}
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});

View File

@ -1052,6 +1052,11 @@ select {
.overflow-hidden { .overflow-hidden {
overflow: hidden; overflow: hidden;
} }
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-pre { .whitespace-pre {
white-space: pre; white-space: pre;
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -762,6 +762,10 @@ Ensure the default browser behavior of the `hidden` attribute.
overflow: auto; overflow: auto;
}.overflow-hidden { }.overflow-hidden {
overflow: hidden; overflow: hidden;
}.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}.whitespace-pre { }.whitespace-pre {
white-space: pre; white-space: pre;
}.rounded-md { }.rounded-md {
@ -1346,4 +1350,4 @@ span.CodeMirror-selectedtext { background: none; }
color: white !important; color: white !important;
} }
/*# sourceMappingURL=index.0ea4d9ed.css.map */ /*# sourceMappingURL=index.edd7bd0d.css.map */

File diff suppressed because one or more lines are too long

View File

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

View File

@ -5,7 +5,7 @@ import './voicings';
import './tonal.mjs'; import './tonal.mjs';
import './xen.mjs'; import './xen.mjs';
import './tune.mjs'; import './tune.mjs';
import './tonal'; import './tonal.mjs';
import gist from './gist.js'; import gist from './gist.js';
import shapeshifter from './shapeshifter'; import shapeshifter from './shapeshifter';
import { minify } from './parse'; import { minify } from './parse';