mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 06:19:33 -04:00
Merge branch 'main' into glossing/ducking-click
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -54,11 +54,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)}
|
||||
@@ -74,15 +75,43 @@ const isValidDoc = (doc) => {
|
||||
const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
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;
|
||||
})();
|
||||
|
||||
export const strudelAutocomplete = (context) => {
|
||||
const word = context.matchBefore(/\w*/);
|
||||
|
||||
@@ -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,6 +553,7 @@ 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.
|
||||
@@ -558,6 +561,7 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
|
||||
*
|
||||
* @name duckorbit
|
||||
* @synonyms duck
|
||||
* @param {number | Pattern} orbit target orbit
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
@@ -637,8 +641,6 @@ export const { duckonset } = registerControl('duckonset', 'duckons');
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
*
|
||||
@@ -761,7 +763,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)
|
||||
@@ -1256,6 +1258,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`);
|
||||
|
||||
+165
-25
@@ -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;
|
||||
|
||||
@@ -7471,6 +7471,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 ]",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user