Merged autocomplete.mjs conflict.
This commit is contained in:
commit
978616833a
19 changed files with 528 additions and 72 deletions
|
|
@ -1,6 +1,6 @@
|
|||
name: Strudel tests
|
||||
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm run format-check
|
||||
- run: pnpm run lint
|
||||
|
|
|
|||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM node:24
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install pnpm --global
|
||||
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY packages/ ./packages/
|
||||
COPY examples/ ./examples/
|
||||
RUN mkdir -p website/public
|
||||
COPY website/package.json ./website/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
CMD ["pnpm", "dev"]
|
||||
|
|
@ -59,11 +59,12 @@ const buildExamples = (examples) =>
|
|||
`
|
||||
: '';
|
||||
|
||||
export const Autocomplete = ({ doc, label }) =>
|
||||
export const Autocomplete = (doc) =>
|
||||
h`
|
||||
<div class="autocomplete-info-container">
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
|
||||
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
|
||||
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
|
||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
|
|
@ -80,7 +81,6 @@ const hasExcludedTags = (doc) =>
|
|||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
|
||||
|
||||
export function bankCompletions() {
|
||||
const soundDict = soundMap.get();
|
||||
const banks = new Set();
|
||||
|
|
@ -91,10 +91,6 @@ export function bankCompletions() {
|
|||
return Array.from(banks).sort().map((name) => ({ label: name, type: 'bank' }));
|
||||
}
|
||||
|
||||
// Placeholder: Replace with actual logic to get live sound names
|
||||
const soundCompletions = [
|
||||
// Example: { label: 'clap', type: 'sound' }, ...
|
||||
];
|
||||
|
||||
// Attempt to get all scale names from Tonal
|
||||
let scaleCompletions = [];
|
||||
|
|
@ -104,15 +100,43 @@ try {
|
|||
console.warn('[autocomplete] Could not load scale names from Tonal:', e);
|
||||
}
|
||||
|
||||
const jsdocCompletions = jsdoc.docs
|
||||
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
.map((doc) => ({
|
||||
label: getDocLabel(doc),
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => Autocomplete({ doc }),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
export const getSynonymDoc = (doc, synonym) => {
|
||||
const synonyms = doc.synonyms || [];
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
|
||||
return {
|
||||
...doc,
|
||||
name: synonym,
|
||||
longname: synonym,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
};
|
||||
};
|
||||
|
||||
const jsdocCompletions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const completions = [];
|
||||
for (const doc of jsdoc.docs) {
|
||||
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Remove duplicates
|
||||
const synonyms = doc.synonyms || [];
|
||||
let labels = [docLabel, ...synonyms];
|
||||
for (const label of labels) {
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
if (label && !seen.has(label)) {
|
||||
seen.add(label);
|
||||
completions.push({
|
||||
label,
|
||||
info: () => Autocomplete(getSynonymDoc(doc, label)),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
})();
|
||||
|
||||
|
||||
// --- Handler functions for each context ---
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||
export let html = (string) => {
|
||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = string.trim();
|
||||
return template.content.childNodes;
|
||||
};
|
||||
let parseChunk = (chunk) => {
|
||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { hoverTooltip } from '@codemirror/view';
|
||||
import jsdoc from '../../doc.json';
|
||||
import { Autocomplete } from './autocomplete.mjs';
|
||||
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
|
|
@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
|
|||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||
if (!entry) {
|
||||
// Try for synonyms
|
||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!entry) {
|
||||
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
entry = getSynonymDoc(doc, word);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
|
|||
create(view) {
|
||||
let dom = document.createElement('div');
|
||||
dom.className = 'strudel-tooltip';
|
||||
const ac = Autocomplete({ doc: entry, label: word });
|
||||
const ac = Autocomplete(entry);
|
||||
dom.appendChild(ac);
|
||||
return { dom };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
|||
* Define a custom webaudio node to use as a sound source.
|
||||
*
|
||||
* @name source
|
||||
* @synonyms src
|
||||
* @param {function} getSource
|
||||
* @synonyms src
|
||||
*
|
||||
|
|
@ -536,6 +537,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
|||
* shape of amplitude modulation
|
||||
*
|
||||
* @name tremoloshape
|
||||
* @synonyms tremshape
|
||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||
* @example
|
||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||
|
|
@ -551,11 +553,13 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
|||
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
||||
*
|
||||
*/
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* modulate the amplitude of an orbit to create a "sidechain" like effect
|
||||
*
|
||||
* @name duckorbit
|
||||
* @synonyms duck
|
||||
* @param {number | Pattern} orbit target orbit
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
|
|
@ -580,6 +584,7 @@ export const { duckdepth } = registerControl('duckdepth');
|
|||
* the attack time of the duck effect
|
||||
*
|
||||
* @name duckattack
|
||||
* @synonyms duckatt
|
||||
* @param {number | Pattern} time
|
||||
* @example
|
||||
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
|
||||
|
|
@ -587,8 +592,6 @@ export const { duckdepth } = registerControl('duckdepth');
|
|||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
*
|
||||
|
|
@ -711,7 +714,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
|||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||
*
|
||||
* @name phaserdepth
|
||||
* @synonyms phd
|
||||
* @synonyms phd, phasdp
|
||||
* @param {number | Pattern} depth number between 0 and 1
|
||||
* @example
|
||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||
|
|
@ -1206,6 +1209,7 @@ export const { dry } = registerControl('dry');
|
|||
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||
*
|
||||
* @name fadeTime
|
||||
* @synonyms fadeOutTime
|
||||
* @param {number | Pattern} time between 0 and 1
|
||||
* @example
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
|
|
|
|||
|
|
@ -1246,7 +1246,8 @@ export const silence = gap(1);
|
|||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||
export const nothing = gap(0);
|
||||
|
||||
/** A discrete value that repeats once per cycle.
|
||||
/**
|
||||
* A discrete value that repeats once per cycle.
|
||||
*
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
|
|
@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
|
|||
return result;
|
||||
}
|
||||
|
||||
/** The given items are played at the same time at the same length.
|
||||
/**
|
||||
* The given items are played at the same time at the same length.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms polyrhythm, pr
|
||||
|
|
@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
|
|||
.setSteps(steps);
|
||||
}
|
||||
|
||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||
*
|
||||
* synonyms: `cat`
|
||||
/**
|
||||
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms cat
|
||||
* @example
|
||||
* slowcat("e5", "b4", ["d5", "c5"])
|
||||
*
|
||||
|
|
@ -2397,6 +2399,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
|
|||
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
|
||||
});
|
||||
|
||||
export const applyN = register('applyN', function (n, func, p) {
|
||||
let result = p;
|
||||
for (let i = 0; i < n; i++) {
|
||||
result = func(result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
|
||||
* @name plyWith
|
||||
* @synonyms plywith
|
||||
* @param {number} factor how many times to repeat
|
||||
* @param {function} func function to apply, given the pattern
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .plyWith(4, (p) => p.add(2))
|
||||
* .scale("C:minor").note()
|
||||
*/
|
||||
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
|
||||
const result = pat
|
||||
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
|
||||
.squeezeJoin();
|
||||
if (__steps) {
|
||||
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
|
||||
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
|
||||
* @name plyForEach
|
||||
* @synonyms plyforeach
|
||||
* @param {number} factor how many times to repeat
|
||||
* @param {function} func function to apply, given the pattern and the iteration index
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .plyForEach(4, (p,n) => p.add(n*2))
|
||||
* .scale("C:minor").note()
|
||||
*/
|
||||
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
|
||||
const result = pat
|
||||
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
|
||||
.squeezeJoin();
|
||||
if (__steps) {
|
||||
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
|
||||
* @name iter
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ let curves = ['linear', 'exponential'];
|
|||
export function getPitchEnvelope(param, value, t, holdEnd) {
|
||||
// envelope is active when any of these values is set
|
||||
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
||||
if (!hasEnvelope) {
|
||||
if (hasEnvelope === undefined) {
|
||||
return;
|
||||
}
|
||||
const penv = nanFallback(value.penv, 1, true);
|
||||
|
|
|
|||
|
|
@ -113,6 +113,19 @@ export async function aliasBank(...args) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an alias for a sound.
|
||||
* @param {string} original - The original sound name
|
||||
* @param {string} alias - The alias to use for the sound
|
||||
*/
|
||||
export function soundAlias(original, alias) {
|
||||
if (getSound(original) == null) {
|
||||
logger('soundAlias: original sound not found');
|
||||
return;
|
||||
}
|
||||
soundMap.setKey(alias, getSound(original));
|
||||
}
|
||||
|
||||
export function getSound(s) {
|
||||
if (typeof s !== 'string') {
|
||||
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// this is dough, the superdough without dependencies
|
||||
// @ts-check
|
||||
// @ts-ignore ignore next line because sampleRate is unknown
|
||||
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
|
||||
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
||||
const ISR = 1 / SAMPLE_RATE;
|
||||
|
|
@ -641,8 +643,8 @@ const note2midi = (note, defaultOctave = 3) => {
|
|||
}
|
||||
const chroma = chromas[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
oct = Number(oct || defaultOctave);
|
||||
return (oct + 1) * 12 + chroma + offset;
|
||||
const octave = Number(oct || defaultOctave);
|
||||
return (octave + 1) * 12 + chroma + offset;
|
||||
};
|
||||
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
|
||||
const note2freq = (note) => {
|
||||
|
|
@ -654,9 +656,152 @@ const note2freq = (note) => {
|
|||
};
|
||||
|
||||
export class DoughVoice {
|
||||
/** @type {number} */
|
||||
id = 0;
|
||||
/** @type {number[]} */
|
||||
out = [0, 0];
|
||||
|
||||
/** @type {number | undefined} */
|
||||
attack;
|
||||
/** @type {number | undefined} */
|
||||
decay;
|
||||
/** @type {number | undefined} */
|
||||
sustain;
|
||||
/** @type {number} */
|
||||
release;
|
||||
/** @type {number} */
|
||||
_begin;
|
||||
/** @type {number} */
|
||||
_duration;
|
||||
|
||||
/** @type {any} */
|
||||
_sound;
|
||||
/** @type {number} */
|
||||
_channels = 1;
|
||||
/** @type {BufferPlayer[] | undefined} */
|
||||
_buffers;
|
||||
/** @type {string | undefined} */
|
||||
unit;
|
||||
|
||||
/** @type {ADSR | undefined} */
|
||||
_penv;
|
||||
/** @type {number | undefined} */
|
||||
penv;
|
||||
/** @type {number | undefined} */
|
||||
pattack;
|
||||
/** @type {number | undefined} */
|
||||
pdecay;
|
||||
/** @type {number | undefined} */
|
||||
psustain;
|
||||
/** @type {number | undefined} */
|
||||
prelease;
|
||||
|
||||
/** @type {number | undefined} */
|
||||
vib;
|
||||
|
||||
_vib;
|
||||
/** @type {number | undefined} */
|
||||
vibmod;
|
||||
|
||||
/** @type {SineOsc | undefined} */
|
||||
_fm;
|
||||
/** @type {number | undefined} */
|
||||
fmh;
|
||||
/** @type {number | undefined} */
|
||||
fmi;
|
||||
|
||||
/** @type {ADSR | undefined} */
|
||||
_fmenv;
|
||||
/** @type {number | undefined} */
|
||||
fmattack;
|
||||
/** @type {number | undefined} */
|
||||
fmdecay;
|
||||
/** @type {number | undefined} */
|
||||
fmsustain;
|
||||
/** @type {number | undefined} */
|
||||
fmrelease;
|
||||
|
||||
/** @type {ADSR | undefined} */
|
||||
_lpenv;
|
||||
lpenv;
|
||||
/** @type {number | undefined} */
|
||||
lpattack;
|
||||
/** @type {number | undefined} */
|
||||
lpdecay;
|
||||
/** @type {number | undefined} */
|
||||
lpsustain;
|
||||
/** @type {number | undefined} */
|
||||
lprelease;
|
||||
|
||||
/** @type {ADSR | undefined} */
|
||||
_hpenv;
|
||||
/** @type {number | undefined} */
|
||||
hpenv;
|
||||
/** @type {number | undefined} */
|
||||
hpattack;
|
||||
/** @type {number | undefined} */
|
||||
hpdecay;
|
||||
/** @type {number | undefined} */
|
||||
hpsustain;
|
||||
/** @type {number | undefined} */
|
||||
hprelease;
|
||||
|
||||
/** @type {ADSR | undefined} */
|
||||
_bpenv;
|
||||
/** @type {number | undefined} */
|
||||
bpenv;
|
||||
/** @type {number | undefined} */
|
||||
bpattack;
|
||||
/** @type {number | undefined} */
|
||||
bpdecay;
|
||||
/** @type {number | undefined} */
|
||||
bpsustain;
|
||||
/** @type {number | undefined} */
|
||||
bprelease;
|
||||
|
||||
/** @type {number | undefined} */
|
||||
cutoff;
|
||||
/** @type {number | undefined} */
|
||||
hcutoff;
|
||||
/** @type {number | undefined} */
|
||||
bandf;
|
||||
/** @type {number | undefined} */
|
||||
coarse;
|
||||
/** @type {number | undefined} */
|
||||
crush;
|
||||
/** @type {number | undefined} */
|
||||
distort;
|
||||
|
||||
/** @type {number} */
|
||||
freq;
|
||||
/** @type {string | undefined} */
|
||||
note;
|
||||
|
||||
/** @type {TwoPoleFilter[] | null | undefined} */
|
||||
_lpf;
|
||||
/** @type {TwoPoleFilter[] | null | undefined} */
|
||||
_hpf;
|
||||
/** @type {TwoPoleFilter[] | null | undefined} */
|
||||
_bpf;
|
||||
/** @type {Chorus[] | null | undefined} */
|
||||
_chorus;
|
||||
/** @type {Coarse[] | null | undefined} */
|
||||
_coarse;
|
||||
/** @type {Crush[] | null | undefined} */
|
||||
_crush;
|
||||
/** @type {Distort[] | null | undefined} */
|
||||
_distort;
|
||||
|
||||
/**
|
||||
* @param {DoughVoice} value
|
||||
*/
|
||||
constructor(value) {
|
||||
value.freq ??= note2freq(value.note);
|
||||
// mandatory controls
|
||||
this.freq ??= note2freq(value.note);
|
||||
this._begin = value._begin;
|
||||
this._duration = value._duration;
|
||||
this.release = value.release ?? 0;
|
||||
// the rest.. we use $ for readability
|
||||
let $ = this;
|
||||
Object.assign($, value);
|
||||
$.s = $.s ?? getDefaultValue('s');
|
||||
|
|
@ -773,7 +918,7 @@ export class DoughVoice {
|
|||
let freq = this.freq * this.speed;
|
||||
|
||||
// frequency modulation
|
||||
if (this._fm) {
|
||||
if (this._fm && this.fmh !== undefined && this.fmi !== undefined) {
|
||||
let fmi = this.fmi;
|
||||
if (this._fmenv) {
|
||||
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
|
||||
|
|
@ -785,37 +930,31 @@ export class DoughVoice {
|
|||
}
|
||||
|
||||
// vibrato
|
||||
if (this._vib) {
|
||||
if (this._vib && this.vibmod !== undefined) {
|
||||
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
|
||||
}
|
||||
|
||||
// pitch envelope
|
||||
if (this._penv) {
|
||||
if (this._penv && this.penv !== undefined) {
|
||||
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
|
||||
freq = freq + env * this.penv;
|
||||
}
|
||||
|
||||
// filters
|
||||
let lpf = this.cutoff;
|
||||
if (this._lpf) {
|
||||
if (this._lpenv) {
|
||||
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
|
||||
lpf = this.lpenv * env * lpf + lpf;
|
||||
}
|
||||
if (lpf !== undefined && this._lpenv) {
|
||||
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
|
||||
lpf = this.lpenv * env * lpf + lpf;
|
||||
}
|
||||
let hpf = this.hcutoff;
|
||||
if (this._hpf) {
|
||||
if (this._hpenv) {
|
||||
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
|
||||
hpf = 2 ** this.hpenv * env * hpf + hpf;
|
||||
}
|
||||
if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) {
|
||||
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
|
||||
hpf = 2 ** this.hpenv * env * hpf + hpf;
|
||||
}
|
||||
let bpf = this.bandf;
|
||||
if (this._bpf) {
|
||||
if (this._bpenv) {
|
||||
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
|
||||
bpf = 2 ** this.bpenv * env * bpf + bpf;
|
||||
}
|
||||
if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) {
|
||||
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
|
||||
bpf = 2 ** this.bpenv * env * bpf + bpf;
|
||||
}
|
||||
// gain envelope
|
||||
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
|
||||
|
|
@ -891,8 +1030,8 @@ export class Dough {
|
|||
this.sampleRate = sampleRate;
|
||||
this.t = Math.floor(currentTime * sampleRate); // samples
|
||||
// console.log('init dough', this.sampleRate, this.t);
|
||||
this._delayL = new PitchDelay();
|
||||
this._delayR = new PitchDelay();
|
||||
this._delayL = new Delay();
|
||||
this._delayR = new Delay();
|
||||
}
|
||||
loadSample(name, channels, sampleRate) {
|
||||
BufferPlayer.samples.set(name, { channels, sampleRate });
|
||||
|
|
@ -965,8 +1104,9 @@ export class Dough {
|
|||
}
|
||||
}
|
||||
// todo: how to change delaytime / delayfeedback from a voice?
|
||||
const delayL = this._delayL.update(this.delaysend[0], this.delaytime, this.delayspeed);
|
||||
const delayR = this._delayR.update(this.delaysend[1], this.delaytime, this.delayspeed);
|
||||
const delayL = this._delayL.update(this.delaysend[0], this.delaytime);
|
||||
const delayR = this._delayR.update(this.delaysend[1], this.delaytime);
|
||||
|
||||
this.delaysend[0] = delayL * this.delayfeedback;
|
||||
this.delaysend[1] = delayR * this.delayfeedback;
|
||||
this.out[0] += delayL;
|
||||
|
|
|
|||
|
|
@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
|||
|
||||
/* Load a new scale */
|
||||
|
||||
Tune.prototype.loadScale = function(name){
|
||||
Tune.prototype.loadScale = function(scale){
|
||||
|
||||
/* load the scale */
|
||||
var freqs = TuningList[name].frequencies
|
||||
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||
this.scale = []
|
||||
for (var i=0;i<freqs.length-1;i++) {
|
||||
this.scale.push(freqs[i]/freqs[0])
|
||||
|
|
@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
|
|||
return possible
|
||||
}
|
||||
|
||||
Tune.prototype.isValidScale = function(name) {
|
||||
return !!TuningList[name];
|
||||
function isArrayOfNumbers(arg) {
|
||||
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
|
||||
}
|
||||
|
||||
/* allow an array of values too */
|
||||
Tune.prototype.isValidScale = function(scale) {
|
||||
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
|
||||
}
|
||||
|
||||
/* Return a collection of notes as an array */
|
||||
|
|
|
|||
|
|
@ -7365,6 +7365,64 @@ exports[`runs examples > example "ply" example index 0 1`] = `
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "plyForEach" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:C3 ]",
|
||||
"[ 1/4 → 1/2 | note:Eb3 ]",
|
||||
"[ 1/2 → 3/4 | note:G3 ]",
|
||||
"[ 3/4 → 1/1 | note:Bb3 ]",
|
||||
"[ 1/1 → 9/8 | note:Eb3 ]",
|
||||
"[ 9/8 → 5/4 | note:G3 ]",
|
||||
"[ 5/4 → 11/8 | note:Bb3 ]",
|
||||
"[ 11/8 → 3/2 | note:D4 ]",
|
||||
"[ 3/2 → 13/8 | note:G3 ]",
|
||||
"[ 13/8 → 7/4 | note:Bb3 ]",
|
||||
"[ 7/4 → 15/8 | note:D4 ]",
|
||||
"[ 15/8 → 2/1 | note:F4 ]",
|
||||
"[ 2/1 → 9/4 | note:C3 ]",
|
||||
"[ 9/4 → 5/2 | note:Eb3 ]",
|
||||
"[ 5/2 → 11/4 | note:G3 ]",
|
||||
"[ 11/4 → 3/1 | note:Bb3 ]",
|
||||
"[ 3/1 → 25/8 | note:Eb3 ]",
|
||||
"[ 25/8 → 13/4 | note:G3 ]",
|
||||
"[ 13/4 → 27/8 | note:Bb3 ]",
|
||||
"[ 27/8 → 7/2 | note:D4 ]",
|
||||
"[ 7/2 → 29/8 | note:G3 ]",
|
||||
"[ 29/8 → 15/4 | note:Bb3 ]",
|
||||
"[ 15/4 → 31/8 | note:D4 ]",
|
||||
"[ 31/8 → 4/1 | note:F4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "plyWith" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:C3 ]",
|
||||
"[ 1/4 → 1/2 | note:Eb3 ]",
|
||||
"[ 1/2 → 3/4 | note:G3 ]",
|
||||
"[ 3/4 → 1/1 | note:Bb3 ]",
|
||||
"[ 1/1 → 9/8 | note:Eb3 ]",
|
||||
"[ 9/8 → 5/4 | note:G3 ]",
|
||||
"[ 5/4 → 11/8 | note:Bb3 ]",
|
||||
"[ 11/8 → 3/2 | note:D4 ]",
|
||||
"[ 3/2 → 13/8 | note:G3 ]",
|
||||
"[ 13/8 → 7/4 | note:Bb3 ]",
|
||||
"[ 7/4 → 15/8 | note:D4 ]",
|
||||
"[ 15/8 → 2/1 | note:F4 ]",
|
||||
"[ 2/1 → 9/4 | note:C3 ]",
|
||||
"[ 9/4 → 5/2 | note:Eb3 ]",
|
||||
"[ 5/2 → 11/4 | note:G3 ]",
|
||||
"[ 11/4 → 3/1 | note:Bb3 ]",
|
||||
"[ 3/1 → 25/8 | note:Eb3 ]",
|
||||
"[ 25/8 → 13/4 | note:G3 ]",
|
||||
"[ 13/4 → 27/8 | note:Bb3 ]",
|
||||
"[ 27/8 → 7/2 | note:D4 ]",
|
||||
"[ 7/2 → 29/8 | note:G3 ]",
|
||||
"[ 29/8 → 15/4 | note:Bb3 ]",
|
||||
"[ 15/4 → 31/8 | note:D4 ]",
|
||||
"[ 31/8 → 4/1 | note:F4 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/6 | note:c ]",
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
|
|||
Understand: [
|
||||
{ text: 'Coding syntax', link: 'learn/code' },
|
||||
{ text: 'Pitch', link: 'understand/pitch' },
|
||||
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
||||
{ text: 'Cycles', link: 'understand/cycles' },
|
||||
{ text: 'Voicings', link: 'understand/voicings' },
|
||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
|
|||
|
||||
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
||||
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
|
||||
|
||||
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
||||
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
||||
|
|
|
|||
|
|
@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co
|
|||
|
||||
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
|
||||
|
||||
You can also create custom aliases for existing sounds using the `soundAlias` function:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`soundAlias("RolandTR808_bd", "kick")
|
||||
s("kick")`}
|
||||
/>
|
||||
|
||||
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
|
||||
This behaviour of loading things only when they are needed is also called `lazy loading`.
|
||||
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
|
||||
|
|
|
|||
95
website/src/pages/learn/xen.mdx
Normal file
95
website/src/pages/learn/xen.mdx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
title: Xen Harmonic Functions
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Xen Harmonic Functions
|
||||
|
||||
These functions allow the use of scales other than your typical chromatic 12 based ones.
|
||||
|
||||
### tune(scale)
|
||||
|
||||
<JsDoc client:idle name="tune" h={0} />
|
||||
|
||||
Here's an example of how to configure a basic hexany scale:
|
||||
|
||||
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
|
||||
|
||||
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
|
||||
|
||||
For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html
|
||||
|
||||
You can set your root to be a particular note with `getFreq`
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.5).room(1)`}
|
||||
/>
|
||||
|
||||
Some tunings become more pronounced with a longer reverb decay:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
|
||||
/>
|
||||
|
||||
Additionally, you can combo this with `fmap` so that the base note changes:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"9 11 12 10 - - -".tune("gunkali")
|
||||
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
|
||||
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
|
||||
/>
|
||||
|
||||
Combining this with various polyrhythm tricks can become very evocative:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
|
||||
.transpose(4)
|
||||
.tune("iraq")
|
||||
.mul("<c3 d3 c#3>".fmap(getFreq))
|
||||
.freq().clip(.5).room(1).rfade(9)`}
|
||||
/>
|
||||
|
||||
Another helpful trick when exploring new tunings is to strum them.
|
||||
Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed.
|
||||
|
||||
Take the `sanza` tuning:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 5 6 7 8 9".tune("sanza")
|
||||
.mul(getFreq('c3'))
|
||||
.freq()`}
|
||||
/>
|
||||
|
||||
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
|
||||
See how close they are on the pitch wheel?
|
||||
|
||||
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
|
||||
|
||||
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
|
||||
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"[0 1 2 3 4 5 6]@0.3 -"
|
||||
.transpose("<2 5 8 1>")
|
||||
.tune("sanza")
|
||||
.mul(getFreq('c3')).freq()
|
||||
.legato("3").room(1).rfade(5)`}
|
||||
/>
|
||||
|
||||
Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the
|
||||
tones sound even more alive, too.
|
||||
|
||||
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
|
||||
|
|
@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
|
|||
|
||||
**selecting sample numbers separately**
|
||||
|
||||
Instead of using ":", we can also use the `n` function to select sample numbers:
|
||||
|
||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
||||
|
||||
This is shorter and more readable than:
|
||||
Instead of selecting sample numbers one by one:
|
||||
|
||||
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
|
||||
|
||||
We can also use the `n` function to make it shorter and more readable:
|
||||
|
||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
||||
|
||||
## Recap
|
||||
|
||||
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@
|
|||
min-width: 300px !important;
|
||||
max-height: 400px !important;
|
||||
background-color: var(--lineHighlight) !important;
|
||||
overflow: auto;
|
||||
background: var(--background) !important;
|
||||
}
|
||||
|
||||
/* Main tooltip container */
|
||||
|
|
@ -91,7 +93,7 @@
|
|||
font-size: var(--font-size, 13px);
|
||||
line-height: 1.4;
|
||||
max-width: 600px;
|
||||
max-height: 400px;
|
||||
height: 100%;
|
||||
min-width: 400px;
|
||||
white-space: normal !important;
|
||||
overflow-y: auto !important;
|
||||
|
|
@ -112,6 +114,13 @@
|
|||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-synonyms {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
line-height: 1.5;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-description {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
|
|||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
|
||||
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
||||
|
||||
const availableFunctions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const functions = [];
|
||||
for (const doc of jsdocJson.docs) {
|
||||
if (!isValid(doc)) continue;
|
||||
functions.push(doc);
|
||||
const synonyms = doc.synonyms || [];
|
||||
for (const s of synonyms) {
|
||||
if (!s || seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
|
||||
functions.push({
|
||||
...doc,
|
||||
name: s, // update names for the synonym
|
||||
longname: s,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
});
|
||||
}
|
||||
}
|
||||
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
})();
|
||||
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue