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 {compose} from "../pkg/ramda.js";
import {isNote, toMidi} from "./util.js";
const removeUndefineds = (xs) => xs.filter((x) => x != void 0);
const flatten = (arr) => [].concat(...arr);
const id = (a) => a;
@ -327,17 +328,39 @@ class Pattern {
_opleft(other, func) {
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) {
return this._opleft(other, (a) => (b) => a + b);
return this._asNumber()._opleft(other, (a) => (b) => a + b);
}
sub(other) {
return this._opleft(other, (a) => (b) => a - b);
return this._asNumber()._opleft(other, (a) => (b) => a - b);
}
mul(other) {
return this._opleft(other, (a) => (b) => a * b);
return this._asNumber()._opleft(other, (a) => (b) => a * b);
}
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) {
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 "./voicings.js";
import "./tonal.js";
import "./xen.js";
import "./tune.js";
import "./tonal.js";
import gist from "./gist.js";
import shapeshifter from "./shapeshifter.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 {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
import {mod, tokenizeNote} from "../_snowpack/link/util.js";
const Pattern = _Pattern;
const mod = (n, m) => n < 0 ? mod(n + m, m) : n % m;
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) {
export function scaleTranspose(scale, offset, note) {
let [tonic, scaleName] = Scale.tokenize(scale);
let {notes} = Scale.get(`${tonic} ${scaleName}`);
notes = notes.map((note2) => Note.get(note2).pc);

7
docs/dist/tone.js vendored
View File

@ -19,24 +19,28 @@ import {
Players
} from "../_snowpack/pkg/tone.js";
import {Piano} from "../_snowpack/pkg/@tonejs/piano.js";
import {getPlayableNoteValue} from "../_snowpack/link/util.js";
const Pattern = _Pattern;
Pattern.prototype.tone = function(instrument) {
return this._withEvent((event) => {
const onTrigger = (time, event2) => {
let note = event2.value;
let note;
let velocity = event2.context?.velocity ?? 0.75;
switch (instrument.constructor.name) {
case "PluckSynth":
note = getPlayableNoteValue(event2);
instrument.triggerAttack(note, time);
break;
case "NoiseSynth":
instrument.triggerAttackRelease(event2.duration, time);
break;
case "Piano":
note = getPlayableNoteValue(event2);
instrument.keyDown({note, time, velocity: 0.5});
instrument.keyUp({note, time: time + event2.duration, velocity});
break;
case "Sampler":
note = getPlayableNoteValue(event2);
instrument.triggerAttackRelease(note, event2.duration, time, velocity);
break;
case "Players":
@ -48,6 +52,7 @@ Pattern.prototype.tone = function(instrument) {
player.stop(time + event2.duration);
break;
default:
note = getPlayableNoteValue(event2);
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")
.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 {isNote} from "../_snowpack/pkg/tone.js";
import {getPlayableNoteValue} from "../_snowpack/link/util.js";
import {evaluate} from "./evaluate.js";
import useCycle from "./useCycle.js";
import usePostMessage from "./usePostMessage.js";
@ -54,11 +54,8 @@ function useRepl({tune, defaultSynth, autolink = true, onEvent, onDraw}) {
onEvent?.(event);
const {onTrigger, velocity} = event.context;
if (!onTrigger) {
const note = event.value;
if (!isNote(note)) {
throw new Error("not a note: " + note);
}
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration, time, velocity);
} else {
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;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-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-hidden {
overflow: hidden;
}.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}.whitespace-pre {
white-space: pre;
}.rounded-md {
@ -1346,4 +1350,4 @@ span.CodeMirror-selectedtext { background: none; }
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>
<meta charset="utf-8">
<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="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.d9bcaff1.js" defer=""></script>
<script src="/tutorial/index.7a60a07f.js" defer=""></script>
</body>
</html>

View File

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