mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-12 22:15:27 -04:00
build
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Vendored
+3
@@ -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";
|
||||
|
||||
Vendored
+2
-7
@@ -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);
|
||||
|
||||
Vendored
+6
-1
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
Vendored
+14
@@ -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});
|
||||
Vendored
+233
File diff suppressed because one or more lines are too long
Vendored
+28
@@ -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)
|
||||
}`;
|
||||
|
||||
Vendored
+2
-5
@@ -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.");
|
||||
|
||||
Vendored
+52
@@ -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});
|
||||
@@ -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 it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user