Merge branch 'main' into glossing/distortion-modes
This commit is contained in:
commit
07e3ddbe40
38 changed files with 2107 additions and 520 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
name: Strudel tests
|
name: Strudel tests
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: "pnpm"
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm run format-check
|
- run: pnpm run format-check
|
||||||
- run: pnpm run lint
|
- run: pnpm run lint
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,12 @@ const buildExamples = (examples) =>
|
||||||
`
|
`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
export const Autocomplete = ({ doc, label }) =>
|
export const Autocomplete = (doc) =>
|
||||||
h`
|
h`
|
||||||
<div class="autocomplete-info-container">
|
<div class="autocomplete-info-container">
|
||||||
<div class="autocomplete-info-tooltip">
|
<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>` : ''}
|
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||||
${buildParamsList(doc.params)}
|
${buildParamsList(doc.params)}
|
||||||
${buildExamples(doc.examples)}
|
${buildExamples(doc.examples)}
|
||||||
|
|
@ -74,15 +75,43 @@ const isValidDoc = (doc) => {
|
||||||
const hasExcludedTags = (doc) =>
|
const hasExcludedTags = (doc) =>
|
||||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||||
|
|
||||||
const jsdocCompletions = jsdoc.docs
|
export const getSynonymDoc = (doc, synonym) => {
|
||||||
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
|
const synonyms = doc.synonyms || [];
|
||||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
const docLabel = getDocLabel(doc);
|
||||||
.map((doc) => ({
|
// Swap `doc.name` in for `s` in the list of synonyms
|
||||||
label: getDocLabel(doc),
|
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
|
||||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
return {
|
||||||
info: () => Autocomplete({ doc }),
|
...doc,
|
||||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
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) => {
|
export const strudelAutocomplete = (context) => {
|
||||||
const word = context.matchBefore(/\w*/);
|
const word = context.matchBefore(/\w*/);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
|
||||||
export let html = (string) => {
|
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) => {
|
let parseChunk = (chunk) => {
|
||||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
import jsdoc from '../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
import { Autocomplete } from './autocomplete.mjs';
|
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
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];
|
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
// Try for synonyms
|
// Try for synonyms
|
||||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||||
if (!entry) {
|
if (!doc) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
entry = getSynonymDoc(doc, word);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
|
||||||
create(view) {
|
create(view) {
|
||||||
let dom = document.createElement('div');
|
let dom = document.createElement('div');
|
||||||
dom.className = 'strudel-tooltip';
|
dom.className = 'strudel-tooltip';
|
||||||
const ac = Autocomplete({ doc: entry, label: word });
|
const ac = Autocomplete(entry);
|
||||||
dom.appendChild(ac);
|
dom.appendChild(ac);
|
||||||
return { dom };
|
return { dom };
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,54 @@ export function registerControl(names, ...aliases) {
|
||||||
*/
|
*/
|
||||||
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position in the wavetable of the wavetable oscillator
|
||||||
|
*
|
||||||
|
* @name wtPos
|
||||||
|
* @param {number | Pattern} position Position in the wavetable from 0 to 1
|
||||||
|
* @synonyms wavetablePosition
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
|
||||||
|
*
|
||||||
|
* @name wtWarp
|
||||||
|
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
|
||||||
|
* @synonyms wavetableWarp
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator.
|
||||||
|
*
|
||||||
|
* The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit,
|
||||||
|
* spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip
|
||||||
|
*
|
||||||
|
* @name wtWarpMode
|
||||||
|
* @param {number | string | Pattern} mode Warp mode
|
||||||
|
* @synonyms wavetableWarpMode
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', 'wavetableWarpMode');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of randomness of the initial phase of the wavetable oscillator.
|
||||||
|
*
|
||||||
|
* @name wtPhaseRand
|
||||||
|
* @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random)
|
||||||
|
* @synonyms wavetablePhaseRand
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a custom webaudio node to use as a sound source.
|
* Define a custom webaudio node to use as a sound source.
|
||||||
*
|
*
|
||||||
* @name source
|
* @name source
|
||||||
|
* @synonyms src
|
||||||
* @param {function} getSource
|
* @param {function} getSource
|
||||||
* @synonyms src
|
* @synonyms src
|
||||||
*
|
*
|
||||||
|
|
@ -113,7 +157,7 @@ export const { n } = registerControl('n');
|
||||||
*
|
*
|
||||||
* - a letter (a-g or A-G)
|
* - a letter (a-g or A-G)
|
||||||
* - optional accidentals (b or #)
|
* - optional accidentals (b or #)
|
||||||
* - optional octave number (0-9). Defaults to 3
|
* - optional (possibly negative) octave number (0-9). Defaults to 3
|
||||||
*
|
*
|
||||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||||
*
|
*
|
||||||
|
|
@ -126,6 +170,8 @@ export const { n } = registerControl('n');
|
||||||
* note("c4 a4 f4 e4")
|
* note("c4 a4 f4 e4")
|
||||||
* @example
|
* @example
|
||||||
* note("60 69 65 64")
|
* note("60 69 65 64")
|
||||||
|
* @example
|
||||||
|
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
|
||||||
*/
|
*/
|
||||||
export const { note } = registerControl(['note', 'n']);
|
export const { note } = registerControl(['note', 'n']);
|
||||||
|
|
||||||
|
|
@ -141,8 +187,8 @@ export const { note } = registerControl(['note', 'n']);
|
||||||
*/
|
*/
|
||||||
export const { accelerate } = registerControl('accelerate');
|
export const { accelerate } = registerControl('accelerate');
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
||||||
|
*
|
||||||
* @name velocity
|
* @name velocity
|
||||||
* @example
|
* @example
|
||||||
* s("hh*8")
|
* s("hh*8")
|
||||||
|
|
@ -254,7 +300,7 @@ export const { fmenv } = registerControl('fmenv');
|
||||||
export const { fmattack } = registerControl('fmattack');
|
export const { fmattack } = registerControl('fmattack');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* waveform of the fm modulator
|
* Waveform of the fm modulator
|
||||||
*
|
*
|
||||||
* @name fmwave
|
* @name fmwave
|
||||||
* @param {number | Pattern} wave waveform
|
* @param {number | Pattern} wave waveform
|
||||||
|
|
@ -388,7 +434,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
|
||||||
// ['bpq'],
|
// ['bpq'],
|
||||||
export const { bandq, bpq } = registerControl('bandq', 'bpq');
|
export const { bandq, bpq } = registerControl('bandq', 'bpq');
|
||||||
/**
|
/**
|
||||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
* A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||||
*
|
*
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name begin
|
* @name begin
|
||||||
|
|
@ -449,7 +495,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
|
||||||
*/
|
*/
|
||||||
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||||
/**
|
/**
|
||||||
* bit crusher effect.
|
* Bit crusher effect.
|
||||||
*
|
*
|
||||||
* @name crush
|
* @name crush
|
||||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||||
|
|
@ -460,7 +506,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||||
// ['clhatdecay'],
|
// ['clhatdecay'],
|
||||||
export const { crush } = registerControl('crush');
|
export const { crush } = registerControl('crush');
|
||||||
/**
|
/**
|
||||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||||
*
|
*
|
||||||
* @name coarse
|
* @name coarse
|
||||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||||
|
|
@ -471,7 +517,7 @@ export const { crush } = registerControl('crush');
|
||||||
export const { coarse } = registerControl('coarse');
|
export const { coarse } = registerControl('coarse');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modulate the amplitude of a sound with a continuous waveform
|
* Modulate the amplitude of a sound with a continuous waveform
|
||||||
*
|
*
|
||||||
* @name tremolo
|
* @name tremolo
|
||||||
* @synonyms trem
|
* @synonyms trem
|
||||||
|
|
@ -483,7 +529,7 @@ export const { coarse } = registerControl('coarse');
|
||||||
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modulate the amplitude of a sound with a continuous waveform
|
* Modulate the amplitude of a sound with a continuous waveform
|
||||||
*
|
*
|
||||||
* @name tremolosync
|
* @name tremolosync
|
||||||
* @synonyms tremsync
|
* @synonyms tremsync
|
||||||
|
|
@ -498,7 +544,7 @@ export const { tremolosync } = registerControl(
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* depth of amplitude modulation
|
* Depth of amplitude modulation
|
||||||
*
|
*
|
||||||
* @name tremolodepth
|
* @name tremolodepth
|
||||||
* @synonyms tremdepth
|
* @synonyms tremdepth
|
||||||
|
|
@ -509,7 +555,7 @@ export const { tremolosync } = registerControl(
|
||||||
*/
|
*/
|
||||||
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
||||||
/**
|
/**
|
||||||
* alter the shape of the modulation waveform
|
* Alter the shape of the modulation waveform
|
||||||
*
|
*
|
||||||
* @name tremoloskew
|
* @name tremoloskew
|
||||||
* @synonyms tremskew
|
* @synonyms tremskew
|
||||||
|
|
@ -521,7 +567,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
||||||
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* alter the phase of the modulation waveform
|
* Alter the phase of the modulation waveform
|
||||||
*
|
*
|
||||||
* @name tremolophase
|
* @name tremolophase
|
||||||
* @synonyms tremphase
|
* @synonyms tremphase
|
||||||
|
|
@ -533,9 +579,10 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
||||||
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shape of amplitude modulation
|
* Shape of amplitude modulation
|
||||||
*
|
*
|
||||||
* @name tremoloshape
|
* @name tremoloshape
|
||||||
|
* @synonyms tremshape
|
||||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||||
* @example
|
* @example
|
||||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||||
|
|
@ -543,7 +590,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||||
*/
|
*/
|
||||||
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||||
/**
|
/**
|
||||||
* filter overdrive for supported filter types
|
* Filter overdrive for supported filter types
|
||||||
*
|
*
|
||||||
* @name drive
|
* @name drive
|
||||||
* @param {number | Pattern} amount
|
* @param {number | Pattern} amount
|
||||||
|
|
@ -551,44 +598,94 @@ 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>")
|
* 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
|
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
|
||||||
|
*
|
||||||
|
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
|
||||||
*
|
*
|
||||||
* @name duckorbit
|
* @name duckorbit
|
||||||
|
* @synonyms duck
|
||||||
* @param {number | Pattern} orbit target orbit
|
* @param {number | Pattern} orbit target orbit
|
||||||
* @example
|
* @example
|
||||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
|
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
|
||||||
|
* @example
|
||||||
|
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* $: s("hh*16").orbit(3)
|
||||||
|
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { duck } = registerControl('duckorbit', 'duck');
|
export const { duck } = registerControl('duckorbit', 'duck');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the amount of ducking applied to target orbit
|
* The amount of ducking applied to target orbit
|
||||||
|
*
|
||||||
|
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
|
||||||
|
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||||
*
|
*
|
||||||
* @name duckdepth
|
* @name duckdepth
|
||||||
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
||||||
* @example
|
* @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).duckdepth("<1 .9 .6 0>"))
|
* 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).duckdepth("<1 .9 .6 0>"))
|
||||||
|
* @example
|
||||||
|
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* $: s("hh*16").orbit(3)
|
||||||
|
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const { duckdepth } = registerControl('duckdepth');
|
export const { duckdepth } = registerControl('duckdepth');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the attack time of the duck effect
|
* The time required for the ducked signal(s) to reach their lowest volume.
|
||||||
|
* Can be used to prevent clicking or for creative rhythmic effects.
|
||||||
|
*
|
||||||
|
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||||
|
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||||
|
*
|
||||||
|
* @name duckonset
|
||||||
|
* @synonyms duckons
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} time The onset time in seconds
|
||||||
|
* @example
|
||||||
|
* // Clicks
|
||||||
|
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||||
|
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
|
||||||
|
* @example
|
||||||
|
* // No clicks
|
||||||
|
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||||
|
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
|
||||||
|
* @example
|
||||||
|
* // Rhythmic
|
||||||
|
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
|
||||||
|
* hhat: s("hh*16").orbit(7)
|
||||||
|
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const { duckonset } = registerControl('duckonset', 'duckons');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time required for the ducked signal(s) to return to their normal volume.
|
||||||
|
*
|
||||||
|
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||||
|
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||||
*
|
*
|
||||||
* @name duckattack
|
* @name duckattack
|
||||||
* @param {number | Pattern} time
|
* @synonyms duckatt
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} time The attack time in seconds
|
||||||
* @example
|
* @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))
|
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
|
||||||
|
* @example
|
||||||
|
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||||
|
* lessduck: s("hh*16").orbit(5)
|
||||||
|
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||||
|
|
||||||
export const { drive } = registerControl('drive');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create byte beats with custom expressions
|
* Create byte beats with custom expressions
|
||||||
*
|
*
|
||||||
|
|
@ -629,7 +726,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
|
||||||
export const { channels, ch } = registerControl('channels', 'ch');
|
export const { channels, ch } = registerControl('channels', 'ch');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls the pulsewidth of the pulse oscillator
|
* Controls the pulsewidth of the pulse oscillator
|
||||||
*
|
*
|
||||||
* @name pw
|
* @name pw
|
||||||
* @param {number | Pattern} pulsewidth
|
* @param {number | Pattern} pulsewidth
|
||||||
|
|
@ -641,7 +738,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
|
||||||
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls the lfo rate for the pulsewidth of the pulse oscillator
|
* Controls the lfo rate for the pulsewidth of the pulse oscillator
|
||||||
*
|
*
|
||||||
* @name pwrate
|
* @name pwrate
|
||||||
* @param {number | Pattern} rate
|
* @param {number | Pattern} rate
|
||||||
|
|
@ -653,7 +750,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||||
export const { pwrate } = registerControl('pwrate');
|
export const { pwrate } = registerControl('pwrate');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls the lfo sweep for the pulsewidth of the pulse oscillator
|
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
|
||||||
*
|
*
|
||||||
* @name pwsweep
|
* @name pwsweep
|
||||||
* @param {number | Pattern} sweep
|
* @param {number | Pattern} sweep
|
||||||
|
|
@ -694,7 +791,7 @@ export const { phaserrate, ph, phaser } = registerControl(
|
||||||
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
|
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The center frequency of the phaser in HZ. Defaults to 1000
|
* The center frequency of the phaser in HZ. Defaults to 1000
|
||||||
*
|
*
|
||||||
* @name phasercenter
|
* @name phasercenter
|
||||||
* @synonyms phc
|
* @synonyms phc
|
||||||
|
|
@ -711,7 +808,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||||
*
|
*
|
||||||
* @name phaserdepth
|
* @name phaserdepth
|
||||||
* @synonyms phd
|
* @synonyms phd, phasdp
|
||||||
* @param {number | Pattern} depth number between 0 and 1
|
* @param {number | Pattern} depth number between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||||
|
|
@ -722,7 +819,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||||
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
|
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* choose the channel the pattern is sent to in superdirt
|
* Choose the channel the pattern is sent to in superdirt
|
||||||
*
|
*
|
||||||
* @name channel
|
* @name channel
|
||||||
* @param {number | Pattern} channel channel number
|
* @param {number | Pattern} channel channel number
|
||||||
|
|
@ -1074,7 +1171,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
|
||||||
* @name djf
|
* @name djf
|
||||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
||||||
* @example
|
* @example
|
||||||
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
|
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { djf } = registerControl('djf');
|
export const { djf } = registerControl('djf');
|
||||||
|
|
@ -1206,6 +1303,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.
|
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||||
*
|
*
|
||||||
* @name fadeTime
|
* @name fadeTime
|
||||||
|
* @synonyms fadeOutTime
|
||||||
* @param {number | Pattern} time between 0 and 1
|
* @param {number | Pattern} time between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,8 @@ export const lcm = (...fractions) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFraction = (x) => x instanceof Fraction;
|
||||||
|
|
||||||
fraction._original = Fraction;
|
fraction._original = Fraction;
|
||||||
|
|
||||||
export default fraction;
|
export default fraction;
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ let debounce = 1000,
|
||||||
lastTime;
|
lastTime;
|
||||||
|
|
||||||
export function errorLogger(e, origin = 'cyclist') {
|
export function errorLogger(e, origin = 'cyclist') {
|
||||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
if (process.env.NODE_ENV === 'development') {
|
||||||
// console.error(e);
|
console.error(e);
|
||||||
|
}
|
||||||
logger(`[${origin}] error: ${e.message}`);
|
logger(`[${origin}] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import TimeSpan from './timespan.mjs';
|
import TimeSpan from './timespan.mjs';
|
||||||
import Fraction, { lcm } from './fraction.mjs';
|
import Fraction, { isFraction, lcm } from './fraction.mjs';
|
||||||
import Hap from './hap.mjs';
|
import Hap from './hap.mjs';
|
||||||
import State from './state.mjs';
|
import State from './state.mjs';
|
||||||
import { unionWithObj } from './value.mjs';
|
import { unionWithObj } from './value.mjs';
|
||||||
|
|
@ -999,7 +999,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
|
||||||
// compose matrix functions
|
// compose matrix functions
|
||||||
|
|
||||||
function _nonArrayObject(x) {
|
function _nonArrayObject(x) {
|
||||||
return !Array.isArray(x) && typeof x === 'object';
|
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
|
||||||
}
|
}
|
||||||
function _composeOp(a, b, func) {
|
function _composeOp(a, b, func) {
|
||||||
if (_nonArrayObject(a) || _nonArrayObject(b)) {
|
if (_nonArrayObject(a) || _nonArrayObject(b)) {
|
||||||
|
|
@ -1246,7 +1246,8 @@ export const silence = gap(1);
|
||||||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||||
export const nothing = gap(0);
|
export const nothing = gap(0);
|
||||||
|
|
||||||
/** A discrete value that repeats once per cycle.
|
/**
|
||||||
|
* A discrete value that repeats once per cycle.
|
||||||
*
|
*
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
|
|
@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
|
||||||
return result;
|
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}
|
* @return {Pattern}
|
||||||
* @synonyms polyrhythm, pr
|
* @synonyms polyrhythm, pr
|
||||||
|
|
@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
|
||||||
.setSteps(steps);
|
.setSteps(steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
/**
|
||||||
*
|
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
||||||
* synonyms: `cat`
|
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
|
* @synonyms cat
|
||||||
* @example
|
* @example
|
||||||
* slowcat("e5", "b4", ["d5", "c5"])
|
* 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)));
|
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.
|
* 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
|
* @name iter
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,14 @@ export function repl({
|
||||||
return silence;
|
return silence;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// helper to get a patternified pure value out
|
||||||
|
function unpure(pat) {
|
||||||
|
if (pat._Pattern) {
|
||||||
|
return pat.__pure;
|
||||||
|
}
|
||||||
|
return pat;
|
||||||
|
}
|
||||||
|
|
||||||
const setPattern = async (pattern, autostart = true) => {
|
const setPattern = async (pattern, autostart = true) => {
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
await scheduler.setPattern(pattern, autostart);
|
await scheduler.setPattern(pattern, autostart);
|
||||||
|
|
@ -85,7 +93,10 @@ export function repl({
|
||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
const toggle = () => scheduler.toggle();
|
const toggle = () => scheduler.toggle();
|
||||||
const setCps = (cps) => scheduler.setCps(cps);
|
const setCps = (cps) => {
|
||||||
|
scheduler.setCps(unpure(cps));
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the global tempo to the given cycles per minute
|
* Changes the global tempo to the given cycles per minute
|
||||||
|
|
@ -97,7 +108,10 @@ export function repl({
|
||||||
* setcpm(140/4) // =140 bpm in 4/4
|
* setcpm(140/4) // =140 bpm in 4/4
|
||||||
* $: s("bd*4,[- sd]*2").bank('tr707')
|
* $: s("bd*4,[- sd]*2").bank('tr707')
|
||||||
*/
|
*/
|
||||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
const setCpm = (cpm) => {
|
||||||
|
scheduler.setCps(unpure(cpm) / 60);
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
|
||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ function evaluator(node, scope) {
|
||||||
let pat;
|
let pat;
|
||||||
if (type === 'plain' && typeof variable !== 'undefined') {
|
if (type === 'plain' && typeof variable !== 'undefined') {
|
||||||
// some function names are not patternable, so we skip reification here
|
// some function names are not patternable, so we skip reification here
|
||||||
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
|
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
|
||||||
return variable;
|
return variable;
|
||||||
}
|
}
|
||||||
pat = reify(variable);
|
pat = reify(variable);
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,13 @@ OSC output for strudel patterns! Currently only tested with super collider / sup
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
OSC will only work if you run the REPL locally + the OSC server besides it:
|
Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via:
|
||||||
|
|
||||||
From the project root:
|
```sh
|
||||||
|
npx @strudel/osc
|
||||||
```js
|
|
||||||
npm run repl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
and in a seperate shell:
|
You should see something like:
|
||||||
|
|
||||||
```js
|
|
||||||
npm run osc
|
|
||||||
```
|
|
||||||
|
|
||||||
This should give you
|
|
||||||
|
|
||||||
```log
|
```log
|
||||||
osc client running on port 57120
|
osc client running on port 57120
|
||||||
|
|
@ -26,14 +18,32 @@ osc server running on port 57121
|
||||||
websocket server running on port 8080
|
websocket server running on port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Now open Supercollider (with the super dirt startup file)
|
### --port
|
||||||
|
|
||||||
Now open the REPL and type:
|
By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option:
|
||||||
|
|
||||||
```js
|
```sh
|
||||||
s("<bd sd> hh").osc()
|
npx @strudel/osc --port 7771 # classic dirt
|
||||||
```
|
```
|
||||||
|
|
||||||
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
### --debug
|
||||||
|
|
||||||
|
To log all incoming osc messages, add the `--debug` flag:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx @strudel/osc --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Strudel
|
||||||
|
|
||||||
|
To test it in strudel, you have can use `all(osc)` to send all events through osc:
|
||||||
|
|
||||||
|
```js
|
||||||
|
$: s("bd*4")
|
||||||
|
|
||||||
|
all(osc)
|
||||||
|
```
|
||||||
|
|
||||||
|
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
|
||||||
|
|
||||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
|
|
||||||
import OSC from 'osc-js';
|
import OSC from 'osc-js';
|
||||||
|
|
||||||
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
function connect() {
|
function connect() {
|
||||||
|
|
@ -81,6 +81,4 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = function () {
|
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
|
||||||
return this.onTrigger(oscTrigger);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@strudel/osc",
|
"name": "@strudel/osc",
|
||||||
"version": "1.2.4",
|
"version": "1.2.10",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
|
"bin": "./server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.mjs"
|
"main": "dist/index.mjs"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/*
|
/*
|
||||||
server.js - <short description TODO>
|
server.js - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
|
||||||
|
|
@ -6,6 +8,19 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
|
|
||||||
import OSC from 'osc-js';
|
import OSC from 'osc-js';
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
function getArgValue(flag) {
|
||||||
|
const i = args.indexOf(flag);
|
||||||
|
if (i !== -1) {
|
||||||
|
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
|
||||||
|
if (nextIsFlag) return true;
|
||||||
|
return args[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let udpClientPort = Number(getArgValue('--port')) || 57120;
|
||||||
|
let debug = Number(getArgValue('--debug')) || 0;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||||
udpServer: {
|
udpServer: {
|
||||||
|
|
@ -17,7 +32,7 @@ const config = {
|
||||||
},
|
},
|
||||||
udpClient: {
|
udpClient: {
|
||||||
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
||||||
port: 57120, // @param {number} Port of udp client for messaging
|
port: udpClientPort, // @param {number} Port of udp client for messaging
|
||||||
},
|
},
|
||||||
wsServer: {
|
wsServer: {
|
||||||
host: 'localhost', // @param {string} Hostname of WebSocket server
|
host: 'localhost', // @param {string} Hostname of WebSocket server
|
||||||
|
|
@ -27,8 +42,34 @@ const config = {
|
||||||
|
|
||||||
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
||||||
|
|
||||||
osc.open(); // start a WebSocket server on port 8080
|
if (debug) {
|
||||||
|
osc.on('*', (message) => {
|
||||||
|
const { address, args } = message;
|
||||||
|
let str = '';
|
||||||
|
for (let i = 0; i < args.length; i += 2) {
|
||||||
|
str += `${args[i]}: ${args[i + 1]} `;
|
||||||
|
}
|
||||||
|
console.log(`${address} ${str}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
osc.on('error', (message) => {
|
||||||
|
if (message.toString().includes('EADDRINUSE')) {
|
||||||
|
console.log(`------ ERROR -------
|
||||||
|
a server is already running on port 57121! to stop it:
|
||||||
|
1. run "lsof -ti :57121 | xargs kill -9" (macos / linux)
|
||||||
|
2. re-run the osc server
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
osc.open();
|
||||||
|
|
||||||
console.log('osc client running on port', config.udpClient.port);
|
console.log('osc client running on port', config.udpClient.port);
|
||||||
console.log('osc server running on port', config.udpServer.port);
|
console.log('osc server running on port', config.udpServer.port);
|
||||||
console.log('websocket server running on port', config.wsServer.port);
|
console.log('websocket server running on port', config.wsServer.port);
|
||||||
|
if (debug) {
|
||||||
|
console.log('debug logs enabled. incoming messages will appear below');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { clamp, nanFallback } from './util.mjs';
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
|
@ -11,6 +11,13 @@ export function gainNode(value) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function effectSend(input, effect, wet) {
|
||||||
|
const send = gainNode(wet);
|
||||||
|
input.connect(send);
|
||||||
|
send.connect(effect);
|
||||||
|
return send;
|
||||||
|
}
|
||||||
|
|
||||||
const getSlope = (y1, y2, x1, x2) => {
|
const getSlope = (y1, y2, x1, x2) => {
|
||||||
const denom = x2 - x1;
|
const denom = x2 - x1;
|
||||||
if (denom === 0) {
|
if (denom === 0) {
|
||||||
|
|
@ -22,7 +29,9 @@ const getSlope = (y1, y2, x1, x2) => {
|
||||||
export function getWorklet(ac, processor, params, config) {
|
export function getWorklet(ac, processor, params, config) {
|
||||||
const node = new AudioWorkletNode(ac, processor, config);
|
const node = new AudioWorkletNode(ac, processor, config);
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
node.parameters.get(key).value = value;
|
if (value !== undefined) {
|
||||||
|
node.parameters.get(key).value = value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
@ -175,7 +184,7 @@ let curves = ['linear', 'exponential'];
|
||||||
export function getPitchEnvelope(param, value, t, holdEnd) {
|
export function getPitchEnvelope(param, value, t, holdEnd) {
|
||||||
// envelope is active when any of these values is set
|
// envelope is active when any of these values is set
|
||||||
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
||||||
if (!hasEnvelope) {
|
if (hasEnvelope === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const penv = nanFallback(value.penv, 1, true);
|
const penv = nanFallback(value.penv, 1, true);
|
||||||
|
|
@ -404,3 +413,25 @@ export const getDistortionAlgorithm = (algo) => {
|
||||||
export const getDistortion = (distort, postgain, algorithm) => {
|
export const getDistortion = (distort, postgain, algorithm) => {
|
||||||
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||||
|
let { note, freq } = value;
|
||||||
|
note = note || defaultNote;
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
note = noteToMidi(note); // e.g. c3 => 48
|
||||||
|
}
|
||||||
|
// get frequency
|
||||||
|
if (!freq && typeof note === 'number') {
|
||||||
|
freq = midiToFreq(note); // + 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(freq);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const destroyAudioWorkletNode = (node) => {
|
||||||
|
if (node == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.disconnect();
|
||||||
|
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
export * from './audioContext.mjs';
|
export * from './audioContext.mjs';
|
||||||
|
export * from './wavetable.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
let log = (msg) => console.log(msg);
|
let log = (msg) => console.log(msg);
|
||||||
|
|
||||||
export function errorLogger(e, origin = 'cyclist') {
|
export function errorLogger(e, origin = 'superdough') {
|
||||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
if (process.env.NODE_ENV === 'development') {
|
||||||
// console.error(e);
|
console.error(e);
|
||||||
|
}
|
||||||
logger(`[${origin}] error: ${e.message}`);
|
logger(`[${origin}] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,14 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
|
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, webAudioTimeout } from './helpers.mjs';
|
import { createFilter, gainNode, getCompressor, getDistortion, getWorklet, effectSend } from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger, errorLogger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
||||||
|
|
@ -114,6 +115,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) {
|
export function getSound(s) {
|
||||||
if (typeof s !== 'string') {
|
if (typeof s !== 'string') {
|
||||||
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
||||||
|
|
@ -270,66 +284,16 @@ export async function initAudioOnFirstClick(options) {
|
||||||
return audioReady;
|
return audioReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxfeedback = 0.98;
|
let controller;
|
||||||
|
function getSuperdoughAudioController() {
|
||||||
let channelMerger, destinationGain;
|
if (controller == null) {
|
||||||
//update the output channel configuration to match user's audio device
|
controller = new SuperdoughAudioController(getAudioContext());
|
||||||
export function initializeAudioOutput() {
|
}
|
||||||
const audioContext = getAudioContext();
|
return controller;
|
||||||
const maxChannelCount = audioContext.destination.maxChannelCount;
|
|
||||||
audioContext.destination.channelCount = maxChannelCount;
|
|
||||||
channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
|
|
||||||
destinationGain = new GainNode(audioContext);
|
|
||||||
channelMerger.connect(destinationGain);
|
|
||||||
destinationGain.connect(audioContext.destination);
|
|
||||||
}
|
}
|
||||||
|
export function connectToDestination(input, channels) {
|
||||||
// input: AudioNode, channels: ?Array<int>
|
const controller = getSuperdoughAudioController();
|
||||||
export const connectToDestination = (input, channels = [0, 1]) => {
|
controller.output.connectToDestination(input, channels);
|
||||||
const ctx = getAudioContext();
|
|
||||||
if (channelMerger == null) {
|
|
||||||
initializeAudioOutput();
|
|
||||||
}
|
|
||||||
//This upmix can be removed if correct channel counts are set throughout the app,
|
|
||||||
// and then strudel could theoretically support surround sound audio files
|
|
||||||
const stereoMix = new StereoPannerNode(ctx);
|
|
||||||
input.connect(stereoMix);
|
|
||||||
|
|
||||||
const splitter = new ChannelSplitterNode(ctx, {
|
|
||||||
numberOfOutputs: stereoMix.channelCount,
|
|
||||||
});
|
|
||||||
stereoMix.connect(splitter);
|
|
||||||
channels.forEach((ch, i) => {
|
|
||||||
splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const panic = () => {
|
|
||||||
if (destinationGain == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
|
||||||
destinationGain = null;
|
|
||||||
channelMerger == null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
|
||||||
if (delayfeedback > maxfeedback) {
|
|
||||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
|
||||||
}
|
|
||||||
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
|
||||||
if (!orbits[orbit].delayNode) {
|
|
||||||
const ac = getAudioContext();
|
|
||||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
|
||||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
|
||||||
connectToOrbit(dly, orbit);
|
|
||||||
orbits[orbit].delayNode = dly;
|
|
||||||
}
|
|
||||||
orbits[orbit].delayNode.delayTime.value !== delaytime &&
|
|
||||||
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
|
|
||||||
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
|
|
||||||
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
|
|
||||||
return orbits[orbit].delayNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||||
|
|
@ -385,78 +349,6 @@ function getFilterType(ftype) {
|
||||||
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
|
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
|
||||||
}
|
}
|
||||||
|
|
||||||
//type orbit {
|
|
||||||
// gain: number,
|
|
||||||
// reverbNode: reverbNode
|
|
||||||
// delayNode: delayNode
|
|
||||||
//}
|
|
||||||
let orbits = {};
|
|
||||||
function connectToOrbit(node, orbit) {
|
|
||||||
if (orbits[orbit] == null) {
|
|
||||||
errorLogger(new Error('target orbit does not exist'), 'superdough');
|
|
||||||
}
|
|
||||||
node.connect(orbits[orbit].gain);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOrbit(audioContext, orbit, channels) {
|
|
||||||
if (orbits[orbit] == null) {
|
|
||||||
orbits[orbit] = {
|
|
||||||
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
|
|
||||||
};
|
|
||||||
connectToDestination(orbits[orbit].gain, channels);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
|
|
||||||
const targetArr = [targetOrbit].flat();
|
|
||||||
|
|
||||||
targetArr.forEach((target) => {
|
|
||||||
if (orbits[target] == null) {
|
|
||||||
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
webAudioTimeout(
|
|
||||||
audioContext,
|
|
||||||
() => {
|
|
||||||
orbits[target].gain.gain.cancelScheduledValues(t);
|
|
||||||
const currVal = orbits[target].gain.gain.value;
|
|
||||||
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
|
|
||||||
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
t - 0.01,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
|
||||||
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
|
|
||||||
// If no reverb has been created for a given orbit, create one
|
|
||||||
if (!orbits[orbit].reverbNode) {
|
|
||||||
const ac = getAudioContext();
|
|
||||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
|
||||||
connectToOrbit(reverb, orbit);
|
|
||||||
orbits[orbit].reverbNode = reverb;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
|
|
||||||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
|
|
||||||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
|
|
||||||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
|
|
||||||
hasChanged(irspeed, orbits[orbit].reverbNode.irspeed) ||
|
|
||||||
hasChanged(irbegin, orbits[orbit].reverbNode.irbegin) ||
|
|
||||||
orbits[orbit].reverbNode.ir !== ir
|
|
||||||
) {
|
|
||||||
// only regenerate when something has changed
|
|
||||||
// avoids endless regeneration on things like
|
|
||||||
// stack(s("a"), s("b").rsize(8)).room(.5)
|
|
||||||
// this only works when args may stay undefined until here
|
|
||||||
// setting default values breaks this
|
|
||||||
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
|
||||||
}
|
|
||||||
return orbits[orbit].reverbNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export let analysers = {},
|
export let analysers = {},
|
||||||
analysersData = {};
|
analysersData = {};
|
||||||
|
|
||||||
|
|
@ -489,15 +381,8 @@ export function getAnalyzerData(type = 'time', id = 1) {
|
||||||
return analysersData[id];
|
return analysersData[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectSend(input, effect, wet) {
|
|
||||||
const send = gainNode(wet);
|
|
||||||
input.connect(send);
|
|
||||||
send.connect(effect);
|
|
||||||
return send;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetGlobalEffects() {
|
export function resetGlobalEffects() {
|
||||||
orbits = {};
|
controller?.reset();
|
||||||
analysers = {};
|
analysers = {};
|
||||||
analysersData = {};
|
analysersData = {};
|
||||||
}
|
}
|
||||||
|
|
@ -512,6 +397,7 @@ function mapChannelNumbers(channels) {
|
||||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
||||||
// new: t is always expected to be the absolute target onset time
|
// new: t is always expected to be the absolute target onset time
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
|
const audioController = getSuperdoughAudioController();
|
||||||
|
|
||||||
let { stretch } = value;
|
let { stretch } = value;
|
||||||
if (stretch != null) {
|
if (stretch != null) {
|
||||||
|
|
@ -551,8 +437,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
postgain = getDefaultValue('postgain'),
|
postgain = getDefaultValue('postgain'),
|
||||||
density = getDefaultValue('density'),
|
density = getDefaultValue('density'),
|
||||||
duckorbit,
|
duckorbit,
|
||||||
|
duckonset,
|
||||||
duckattack,
|
duckattack,
|
||||||
duckdepth,
|
duckdepth,
|
||||||
|
djf,
|
||||||
// filters
|
// filters
|
||||||
fanchor = getDefaultValue('fanchor'),
|
fanchor = getDefaultValue('fanchor'),
|
||||||
drive = 0.69,
|
drive = 0.69,
|
||||||
|
|
@ -630,10 +518,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
);
|
);
|
||||||
|
|
||||||
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
|
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
|
||||||
setOrbit(ac, orbit, channels, t, cycle, cps);
|
const orbitBus = audioController.getOrbit(orbit, channels);
|
||||||
|
|
||||||
if (duckorbit != null) {
|
if (duckorbit != null) {
|
||||||
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
|
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
gain = applyGainCurve(nanFallback(gain, 1));
|
gain = applyGainCurve(nanFallback(gain, 1));
|
||||||
|
|
@ -822,14 +709,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
chain.push(post);
|
chain.push(post);
|
||||||
|
|
||||||
// delay
|
// delay
|
||||||
let delaySend;
|
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
|
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||||
delaySend = effectSend(post, delayNode, delay);
|
orbitBus.sendDelay(post, delay);
|
||||||
audioNodes.push(delaySend);
|
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
let reverbSend;
|
|
||||||
if (room > 0) {
|
if (room > 0) {
|
||||||
let roomIR;
|
let roomIR;
|
||||||
if (ir !== undefined) {
|
if (ir !== undefined) {
|
||||||
|
|
@ -842,25 +726,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||||
}
|
}
|
||||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||||
}
|
}
|
||||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||||
reverbSend = effectSend(post, reverbNode, room);
|
orbitBus.sendReverb(post, room);
|
||||||
audioNodes.push(reverbSend);
|
}
|
||||||
|
|
||||||
|
if (djf != null) {
|
||||||
|
orbitBus.getDjf(djf, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// analyser
|
// analyser
|
||||||
let analyserSend;
|
|
||||||
if (analyze) {
|
if (analyze) {
|
||||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||||
analyserSend = effectSend(post, analyserNode, 1);
|
const analyserSend = effectSend(post, analyserNode, 1);
|
||||||
audioNodes.push(analyserSend);
|
audioNodes.push(analyserSend);
|
||||||
}
|
}
|
||||||
if (dry != null) {
|
if (dry != null) {
|
||||||
dry = applyGainCurve(dry);
|
dry = applyGainCurve(dry);
|
||||||
const dryGain = new GainNode(ac, { gain: dry });
|
const dryGain = new GainNode(ac, { gain: dry });
|
||||||
chain.push(dryGain);
|
chain.push(dryGain);
|
||||||
connectToOrbit(dryGain, orbit);
|
orbitBus.connectToOutput(dryGain);
|
||||||
} else {
|
} else {
|
||||||
connectToOrbit(post, orbit);
|
orbitBus.connectToOutput(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect chain elements together
|
// connect chain elements together
|
||||||
|
|
|
||||||
206
packages/superdough/superdoughoutput.mjs
Normal file
206
packages/superdough/superdoughoutput.mjs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||||
|
import { errorLogger } from './logger.mjs';
|
||||||
|
import { clamp } from './util.mjs';
|
||||||
|
|
||||||
|
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||||
|
|
||||||
|
export class Orbit {
|
||||||
|
reverbNode;
|
||||||
|
delayNode;
|
||||||
|
output;
|
||||||
|
summingNode;
|
||||||
|
djfNode;
|
||||||
|
audioContext;
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
|
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
|
this.summingNode.connect(this.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.output.disconnect();
|
||||||
|
this.summingNode.disconnect();
|
||||||
|
this.delayNode?.disconnect();
|
||||||
|
this.reverbNode?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDjf(value, t = 0) {
|
||||||
|
if (this.djfNode == null) {
|
||||||
|
this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value });
|
||||||
|
this.summingNode.disconnect();
|
||||||
|
this.summingNode.connect(this.djfNode);
|
||||||
|
this.djfNode.connect(this.output);
|
||||||
|
}
|
||||||
|
const val = this.djfNode.parameters.get('value');
|
||||||
|
val.setValueAtTime(value, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDelay(delaytime = 0, feedback = 0.5, t) {
|
||||||
|
const maxfeedback = 0.98;
|
||||||
|
if (feedback > maxfeedback) {
|
||||||
|
//logger(`feedback was clamped to ${maxfeedback} to save your ears`);
|
||||||
|
}
|
||||||
|
feedback = clamp(feedback, 0, 0.98);
|
||||||
|
if (this.delayNode == null) {
|
||||||
|
this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback);
|
||||||
|
this.delayNode.connect(this.summingNode);
|
||||||
|
this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||||
|
}
|
||||||
|
this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t);
|
||||||
|
this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t);
|
||||||
|
return this.delayNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||||
|
// If no reverb has been created for a given orbit, create one
|
||||||
|
if (this.reverbNode == null) {
|
||||||
|
this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
|
this.reverbNode.connect(this.summingNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasChanged(duration, this.reverbNode.duration) ||
|
||||||
|
hasChanged(fade, this.reverbNode.fade) ||
|
||||||
|
hasChanged(lp, this.reverbNode.lp) ||
|
||||||
|
hasChanged(dim, this.reverbNode.dim) ||
|
||||||
|
hasChanged(irspeed, this.reverbNode.irspeed) ||
|
||||||
|
hasChanged(irbegin, this.reverbNode.irbegin) ||
|
||||||
|
this.reverbNode.ir !== ir
|
||||||
|
) {
|
||||||
|
// only regenerate when something has changed
|
||||||
|
// avoids endless regeneration on things like
|
||||||
|
// stack(s("a"), s("b").rsize(8)).room(.5)
|
||||||
|
// this only works when args may stay undefined until here
|
||||||
|
// setting default values breaks this
|
||||||
|
this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
|
}
|
||||||
|
return this.reverbNode;
|
||||||
|
}
|
||||||
|
sendReverb(node, amount) {
|
||||||
|
effectSend(node, this.reverbNode, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDelay(node, amount) {
|
||||||
|
effectSend(node, this.delayNode, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||||
|
const onset = onsettime;
|
||||||
|
const attack = Math.max(attacktime, 0.002);
|
||||||
|
const gainParam = this.output.gain;
|
||||||
|
webAudioTimeout(
|
||||||
|
this.audioContext,
|
||||||
|
() => {
|
||||||
|
const now = this.audioContext.currentTime;
|
||||||
|
|
||||||
|
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
|
||||||
|
// on browsers which lack that method
|
||||||
|
const currVal = gainParam.value;
|
||||||
|
gainParam.cancelScheduledValues(now);
|
||||||
|
gainParam.setValueAtTime(currVal, now);
|
||||||
|
|
||||||
|
const t0 = Math.max(t, now); // guard against now > t
|
||||||
|
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
|
||||||
|
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
|
||||||
|
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
t - 0.01,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToOutput(node) {
|
||||||
|
node.connect(this.summingNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuperdoughOutput {
|
||||||
|
channelMerger;
|
||||||
|
destinationGain;
|
||||||
|
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
this.initializeAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeAudio() {
|
||||||
|
const audioContext = this.audioContext;
|
||||||
|
const maxChannelCount = audioContext.destination.maxChannelCount;
|
||||||
|
this.audioContext.destination.channelCount = maxChannelCount;
|
||||||
|
this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount });
|
||||||
|
this.destinationGain = new GainNode(audioContext);
|
||||||
|
this.channelMerger.connect(this.destinationGain);
|
||||||
|
this.destinationGain.connect(audioContext.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.channelMerger.disconnect();
|
||||||
|
this.destinationGain.disconnect();
|
||||||
|
this.destinationGain = null;
|
||||||
|
this.channelMerger = null;
|
||||||
|
this.nodes = {};
|
||||||
|
this.initializeAudio();
|
||||||
|
}
|
||||||
|
connectToDestination = (input, channels = [0, 1]) => {
|
||||||
|
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||||
|
// and then strudel could theoretically support surround sound audio files
|
||||||
|
const stereoMix = new StereoPannerNode(this.audioContext);
|
||||||
|
input.connect(stereoMix);
|
||||||
|
|
||||||
|
const splitter = new ChannelSplitterNode(this.audioContext, {
|
||||||
|
numberOfOutputs: stereoMix.channelCount,
|
||||||
|
});
|
||||||
|
stereoMix.connect(splitter);
|
||||||
|
channels.forEach((ch, i) => {
|
||||||
|
splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuperdoughAudioController {
|
||||||
|
audioContext;
|
||||||
|
output;
|
||||||
|
nodes = {};
|
||||||
|
|
||||||
|
constructor(audioContext) {
|
||||||
|
this.audioContext = audioContext;
|
||||||
|
this.output = new SuperdoughOutput(audioContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
Array.from(this.nodes).forEach((node) => {
|
||||||
|
node.disconnect();
|
||||||
|
});
|
||||||
|
this.output.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||||
|
const targetArr = [targetOrbits].flat();
|
||||||
|
const onsetArr = [onsettime].flat();
|
||||||
|
const attackArr = [attacktime].flat();
|
||||||
|
const depthArr = [depth].flat();
|
||||||
|
|
||||||
|
targetArr.forEach((target, idx) => {
|
||||||
|
const orbit = this.nodes[target];
|
||||||
|
|
||||||
|
if (orbit == null) {
|
||||||
|
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onset = onsetArr[idx] ?? onsetArr[0];
|
||||||
|
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
|
||||||
|
const depth = depthArr[idx] ?? depthArr[0];
|
||||||
|
|
||||||
|
orbit.duck(t, onset, attack, depth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrbit(orbitNum, channels) {
|
||||||
|
if (this.nodes[orbitNum] == null) {
|
||||||
|
this.nodes[orbitNum] = new Orbit(this.audioContext);
|
||||||
|
this.output.connectToDestination(this.nodes[orbitNum].output, channels);
|
||||||
|
}
|
||||||
|
return this.nodes[orbitNum];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,40 +1,21 @@
|
||||||
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
import { registerSound, soundMap, getLfo } from './superdough.mjs';
|
import { registerSound, soundMap, getLfo } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
|
destroyAudioWorkletNode,
|
||||||
gainNode,
|
gainNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
|
getFrequencyFromValue,
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
webAudioTimeout,
|
|
||||||
getWorklet,
|
getWorklet,
|
||||||
noises,
|
noises,
|
||||||
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
|
||||||
const getFrequencyFromValue = (value, defaultNote = 36) => {
|
|
||||||
let { note, freq } = value;
|
|
||||||
note = note || defaultNote;
|
|
||||||
if (typeof note === 'string') {
|
|
||||||
note = noteToMidi(note); // e.g. c3 => 48
|
|
||||||
}
|
|
||||||
// get frequency
|
|
||||||
if (!freq && typeof note === 'number') {
|
|
||||||
freq = midiToFreq(note); // + 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Number(freq);
|
|
||||||
};
|
|
||||||
function destroyAudioWorkletNode(node) {
|
|
||||||
if (node == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.disconnect();
|
|
||||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
['tri', 'triangle'],
|
['tri', 'triangle'],
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
239
packages/superdough/wavetable.mjs
Normal file
239
packages/superdough/wavetable.mjs
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
|
import { getSoundIndex, valueToMidi } from './util.mjs';
|
||||||
|
import {
|
||||||
|
destroyAudioWorkletNode,
|
||||||
|
getADSRValues,
|
||||||
|
getFrequencyFromValue,
|
||||||
|
getParamADSR,
|
||||||
|
getPitchEnvelope,
|
||||||
|
getVibratoOscillator,
|
||||||
|
getWorklet,
|
||||||
|
webAudioTimeout,
|
||||||
|
} from './helpers.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
|
const WT_MAX_MIP_LEVELS = 6;
|
||||||
|
export const WarpMode = Object.freeze({
|
||||||
|
NONE: 0,
|
||||||
|
ASYM: 1,
|
||||||
|
MIRROR: 2,
|
||||||
|
BENDP: 3,
|
||||||
|
BENDM: 4,
|
||||||
|
BENDMP: 5,
|
||||||
|
SYNC: 6,
|
||||||
|
QUANT: 7,
|
||||||
|
FOLD: 8,
|
||||||
|
PWM: 9,
|
||||||
|
ORBIT: 10,
|
||||||
|
SPIN: 11,
|
||||||
|
CHAOS: 12,
|
||||||
|
PRIMES: 13,
|
||||||
|
BINARY: 14,
|
||||||
|
BROWNIAN: 15,
|
||||||
|
RECIPROCAL: 16,
|
||||||
|
WORMHOLE: 17,
|
||||||
|
LOGISTIC: 18,
|
||||||
|
SIGMOID: 19,
|
||||||
|
FRACTAL: 20,
|
||||||
|
FLIP: 21,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadWavetableFrames(url, label, frameLen = 256) {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
const buf = await loadBuffer(url, ac, label);
|
||||||
|
const ch0 = buf.getChannelData(0);
|
||||||
|
const total = ch0.length;
|
||||||
|
const numFrames = Math.floor(total / frameLen);
|
||||||
|
const frames = new Array(numFrames);
|
||||||
|
for (let i = 0; i < numFrames; i++) {
|
||||||
|
const start = i * frameLen;
|
||||||
|
frames[i] = ch0.subarray(start, start + frameLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// build mipmaps
|
||||||
|
const mipmaps = [frames];
|
||||||
|
let levelFrames = frames;
|
||||||
|
for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) {
|
||||||
|
const prevLen = levelFrames[0].length;
|
||||||
|
if (prevLen <= 32) break;
|
||||||
|
const nextLen = prevLen >> 1;
|
||||||
|
const next = levelFrames.map((src) => {
|
||||||
|
const out = new Float32Array(nextLen);
|
||||||
|
for (let j = 0; j < nextLen; j++) {
|
||||||
|
out[j] = (src[2 * j] + src[2 * j + 1]) / 2;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
mipmaps.push(next);
|
||||||
|
levelFrames = next;
|
||||||
|
}
|
||||||
|
return { frames, mipmaps, frameLen, numFrames };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCache = {};
|
||||||
|
|
||||||
|
function humanFileSize(bytes, si) {
|
||||||
|
var thresh = si ? 1000 : 1024;
|
||||||
|
if (bytes < thresh) return bytes + ' B';
|
||||||
|
var units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
var u = -1;
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (bytes >= thresh);
|
||||||
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTableInfo(hapValue, tableUrls) {
|
||||||
|
const { s, n = 0 } = hapValue;
|
||||||
|
let midi = valueToMidi(hapValue, 36);
|
||||||
|
let transpose = midi - 36; // C3 is middle C;
|
||||||
|
const index = getSoundIndex(n, tableUrls.length);
|
||||||
|
const tableUrl = tableUrls[index];
|
||||||
|
const label = `${s}:${index}`;
|
||||||
|
return { transpose, tableUrl, index, midi, label };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadBuffer = (url, ac, label) => {
|
||||||
|
url = url.replace('#', '%23');
|
||||||
|
if (!loadCache[url]) {
|
||||||
|
logger(`[wavetable] load table ${label}..`, 'load-table', { url });
|
||||||
|
const timestamp = Date.now();
|
||||||
|
loadCache[url] = fetch(url)
|
||||||
|
.then((res) => res.arrayBuffer())
|
||||||
|
.then(async (res) => {
|
||||||
|
const took = Date.now() - timestamp;
|
||||||
|
const size = humanFileSize(res.byteLength);
|
||||||
|
logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url });
|
||||||
|
const decoded = await ac.decodeAudioData(res);
|
||||||
|
return decoded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return loadCache[url];
|
||||||
|
};
|
||||||
|
|
||||||
|
function githubPath(base, subpath = '') {
|
||||||
|
if (!base.startsWith('github:')) {
|
||||||
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
|
}
|
||||||
|
let [_, path] = base.split('github:');
|
||||||
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
if (path.split('/').length === 2) {
|
||||||
|
// assume main as default branch if none set
|
||||||
|
path += '/main';
|
||||||
|
}
|
||||||
|
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _processTables = (json, baseUrl, frameLen) => {
|
||||||
|
return Object.entries(json).forEach(([key, value]) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = [value];
|
||||||
|
}
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
throw new Error('wrong json format for ' + key);
|
||||||
|
}
|
||||||
|
baseUrl = value._base || baseUrl;
|
||||||
|
if (baseUrl.startsWith('github:')) {
|
||||||
|
baseUrl = githubPath(baseUrl, '');
|
||||||
|
}
|
||||||
|
value = value.map((v) => baseUrl + v);
|
||||||
|
registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, value, frameLen), {
|
||||||
|
type: 'wavetable',
|
||||||
|
tables: value,
|
||||||
|
baseUrl,
|
||||||
|
frameLen,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a collection of wavetables to use with `s`
|
||||||
|
*
|
||||||
|
* @name tables
|
||||||
|
*/
|
||||||
|
export const tables = async (url, frameLen, json) => {
|
||||||
|
if (json !== undefined) return _processTables(json, url, frameLen);
|
||||||
|
if (url.startsWith('github:')) {
|
||||||
|
url = githubPath(url, 'strudel.json');
|
||||||
|
}
|
||||||
|
if (url.startsWith('local:')) {
|
||||||
|
url = `http://localhost:5432`;
|
||||||
|
}
|
||||||
|
if (typeof fetch !== 'function') {
|
||||||
|
// not a browser
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
// skip fetch when in node / testing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return fetch(url)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((json) => _processTables(json, url, frameLen))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`error loading "${url}"`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onTriggerSynth(t, value, onended, bank, frameLen) {
|
||||||
|
const { s, n = 0, duration } = value;
|
||||||
|
const ac = getAudioContext();
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||||
|
let { wtWarpMode } = value;
|
||||||
|
if (typeof wtWarpMode === 'string') {
|
||||||
|
wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE;
|
||||||
|
}
|
||||||
|
const frequency = getFrequencyFromValue(value);
|
||||||
|
const { tableUrl, label } = getTableInfo(value, bank);
|
||||||
|
const payload = await loadWavetableFrames(tableUrl, label, frameLen);
|
||||||
|
const holdEnd = t + duration;
|
||||||
|
const envEnd = holdEnd + release + 0.01;
|
||||||
|
const source = getWorklet(
|
||||||
|
ac,
|
||||||
|
'wavetable-oscillator-processor',
|
||||||
|
{
|
||||||
|
begin: t,
|
||||||
|
end: envEnd,
|
||||||
|
frequency,
|
||||||
|
detune: value.detune,
|
||||||
|
position: value.wtPos,
|
||||||
|
warp: value.wtWarp,
|
||||||
|
warpMode: wtWarpMode,
|
||||||
|
voices: value.unison,
|
||||||
|
spread: value.spread,
|
||||||
|
phaserand: value.wtPhaseRand,
|
||||||
|
},
|
||||||
|
{ outputChannelCount: [2] },
|
||||||
|
);
|
||||||
|
source.port.postMessage({ type: 'tables', payload });
|
||||||
|
if (ac.currentTime > t) {
|
||||||
|
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const vibratoOscillator = getVibratoOscillator(source.detune, value, t);
|
||||||
|
const envGain = ac.createGain();
|
||||||
|
const node = source.connect(envGain);
|
||||||
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||||
|
getPitchEnvelope(source.detune, value, t, holdEnd);
|
||||||
|
const handle = { node, source };
|
||||||
|
const timeoutNode = webAudioTimeout(
|
||||||
|
ac,
|
||||||
|
() => {
|
||||||
|
source.disconnect();
|
||||||
|
destroyAudioWorkletNode(source);
|
||||||
|
vibratoOscillator?.stop();
|
||||||
|
node.disconnect();
|
||||||
|
onended();
|
||||||
|
},
|
||||||
|
t,
|
||||||
|
envEnd,
|
||||||
|
);
|
||||||
|
handle.stop = (time) => {
|
||||||
|
timeoutNode.stop(time);
|
||||||
|
};
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,21 @@ import FFT from './fft.js';
|
||||||
import { getDistortionAlgorithm } from './helpers.mjs';
|
import { getDistortionAlgorithm } from './helpers.mjs';
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
const _mod = (n, m) => ((n % m) + m) % m;
|
const mod = (n, m) => ((n % m) + m) % m;
|
||||||
|
const lerp = (a, b, n) => n * (b - a) + a;
|
||||||
const pv = (arr, n) => arr[n] ?? arr[0];
|
const pv = (arr, n) => arr[n] ?? arr[0];
|
||||||
|
const frac = (x) => x - Math.floor(x);
|
||||||
|
const ffloor = (x) => x | 0; // fast floor for non-negative
|
||||||
|
|
||||||
|
const getUnisonDetune = (unison, detune, voiceIndex) => {
|
||||||
|
if (unison < 2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
||||||
|
};
|
||||||
|
const applySemitoneDetuneToFrequency = (frequency, detune) => {
|
||||||
|
return frequency * Math.pow(2, detune / 12);
|
||||||
|
};
|
||||||
|
|
||||||
// Restrict phase to the range [0, maxPhase) via wrapping
|
// Restrict phase to the range [0, maxPhase) via wrapping
|
||||||
function wrapPhase(phase, maxPhase = 1) {
|
function wrapPhase(phase, maxPhase = 1) {
|
||||||
|
|
@ -152,7 +165,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||||
const blockSize = output[0].length ?? 0;
|
const blockSize = output[0].length ?? 0;
|
||||||
|
|
||||||
if (this.phase == null) {
|
if (this.phase == null) {
|
||||||
this.phase = _mod(time * frequency + phaseoffset, 1);
|
this.phase = mod(time * frequency + phaseoffset, 1);
|
||||||
}
|
}
|
||||||
const dt = frequency / sampleRate;
|
const dt = frequency / sampleRate;
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
|
|
@ -273,6 +286,73 @@ class ShapeProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
registerProcessor('shape-processor', ShapeProcessor);
|
registerProcessor('shape-processor', ShapeProcessor);
|
||||||
|
|
||||||
|
class TwoPoleFilter {
|
||||||
|
s0 = 0;
|
||||||
|
s1 = 0;
|
||||||
|
update(s, cutoff, resonance = 0) {
|
||||||
|
// Out of bound values can produce NaNs
|
||||||
|
resonance = clamp(resonance, 0, 1);
|
||||||
|
cutoff = clamp(cutoff, 0, sampleRate / 2 - 1);
|
||||||
|
const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14);
|
||||||
|
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
||||||
|
const mrc = 1 - r * c;
|
||||||
|
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
|
||||||
|
this.s1 = mrc * this.s1 + c * this.s0; // lpf
|
||||||
|
return this.s1; // return lpf by default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DJFProcessor extends AudioWorkletProcessor {
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [{ name: 'value', defaultValue: 0.5 }];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.filters = [new TwoPoleFilter(), new TwoPoleFilter()];
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs, parameters) {
|
||||||
|
const input = inputs[0];
|
||||||
|
const output = outputs[0];
|
||||||
|
|
||||||
|
const hasInput = !(input[0] === undefined);
|
||||||
|
this.started = hasInput;
|
||||||
|
|
||||||
|
const value = clamp(parameters.value[0], 0, 1);
|
||||||
|
let filterType = 'none';
|
||||||
|
let cutoff;
|
||||||
|
let v = 1;
|
||||||
|
if (value > 0.51) {
|
||||||
|
filterType = 'hipass';
|
||||||
|
v = (value - 0.5) * 2;
|
||||||
|
} else if (value < 0.49) {
|
||||||
|
filterType = 'lopass';
|
||||||
|
v = value * 2;
|
||||||
|
}
|
||||||
|
cutoff = Math.pow(v * 11, 4);
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
for (let n = 0; n < blockSize; n++) {
|
||||||
|
if (filterType == 'none') {
|
||||||
|
output[i][n] = input[i][n];
|
||||||
|
} else {
|
||||||
|
this.filters[i].update(input[i][n], cutoff, 0.1);
|
||||||
|
if (filterType === 'lopass') {
|
||||||
|
output[i][n] = this.filters[i].s1;
|
||||||
|
} else if (filterType === 'hipass') {
|
||||||
|
output[i][n] = input[i][n] - this.filters[i].s1;
|
||||||
|
} else {
|
||||||
|
output[i][n] = input[i][n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProcessor('djf-processor', DJFProcessor);
|
||||||
|
|
||||||
function fast_tanh(x) {
|
function fast_tanh(x) {
|
||||||
const x2 = x * x;
|
const x2 = x * x;
|
||||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
||||||
|
|
@ -380,21 +460,6 @@ class DistortProcessor extends AudioWorkletProcessor {
|
||||||
registerProcessor('distort-processor', DistortProcessor);
|
registerProcessor('distort-processor', DistortProcessor);
|
||||||
|
|
||||||
// SUPERSAW
|
// SUPERSAW
|
||||||
function lerp(a, b, n) {
|
|
||||||
return n * (b - a) + a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUnisonDetune(unison, detune, voiceIndex) {
|
|
||||||
if (unison < 2) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySemitoneDetuneToFrequency(frequency, detune) {
|
|
||||||
return frequency * Math.pow(2, detune / 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
@ -456,29 +521,31 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const voices = params.voices[0];
|
|
||||||
const freqspread = params.freqspread[0];
|
|
||||||
const panspread = params.panspread[0] * 0.5 + 0.5;
|
|
||||||
const gain1 = Math.sqrt(1 - panspread);
|
|
||||||
const gain2 = Math.sqrt(panspread);
|
|
||||||
|
|
||||||
for (let n = 0; n < voices; n++) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
const isOdd = (n & 1) == 1;
|
const detune = pv(params.detune, i);
|
||||||
let gainL = gain1;
|
const voices = pv(params.voices, i);
|
||||||
let gainR = gain2;
|
const freqspread = pv(params.freqspread, i);
|
||||||
// invert right and left gain
|
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
||||||
if (isOdd) {
|
const gain1 = Math.sqrt(1 - panspread);
|
||||||
gainL = gain2;
|
const gain2 = Math.sqrt(panspread);
|
||||||
gainR = gain1;
|
let freq = pv(params.frequency, i);
|
||||||
}
|
// Main detuning
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
||||||
// Main detuning
|
for (let n = 0; n < voices; n++) {
|
||||||
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
|
const isOdd = (n & 1) == 1;
|
||||||
|
let gainL = gain1;
|
||||||
|
let gainR = gain2;
|
||||||
|
// invert right and left gain
|
||||||
|
if (isOdd) {
|
||||||
|
gainL = gain2;
|
||||||
|
gainR = gain1;
|
||||||
|
}
|
||||||
// Individual voice detuning
|
// Individual voice detuning
|
||||||
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
|
const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
|
||||||
// We must wrap this here because it is passed into sawblep below which
|
// We must wrap this here because it is passed into sawblep below which
|
||||||
// has domain [0, 1]
|
// has domain [0, 1]
|
||||||
const dt = _mod(freq / sampleRate, 1);
|
const dt = mod(freqVoice / sampleRate, 1);
|
||||||
this.phase[n] = this.phase[n] ?? Math.random();
|
this.phase[n] = this.phase[n] ?? Math.random();
|
||||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||||
|
|
||||||
|
|
@ -909,3 +976,322 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||||
|
|
||||||
|
export const WarpMode = Object.freeze({
|
||||||
|
NONE: 0,
|
||||||
|
ASYM: 1,
|
||||||
|
MIRROR: 2,
|
||||||
|
BENDP: 3,
|
||||||
|
BENDM: 4,
|
||||||
|
BENDMP: 5,
|
||||||
|
SYNC: 6,
|
||||||
|
QUANT: 7,
|
||||||
|
FOLD: 8,
|
||||||
|
PWM: 9,
|
||||||
|
ORBIT: 10,
|
||||||
|
SPIN: 11,
|
||||||
|
CHAOS: 12,
|
||||||
|
PRIMES: 13,
|
||||||
|
BINARY: 14,
|
||||||
|
BROWNIAN: 15,
|
||||||
|
RECIPROCAL: 16,
|
||||||
|
WORMHOLE: 17,
|
||||||
|
LOGISTIC: 18,
|
||||||
|
SIGMOID: 19,
|
||||||
|
FRACTAL: 20,
|
||||||
|
FLIP: 21,
|
||||||
|
});
|
||||||
|
|
||||||
|
function hash32(u) {
|
||||||
|
u = u + 0x7ed55d16 + (u << 12);
|
||||||
|
u = u ^ 0xc761c23c ^ (u >>> 19);
|
||||||
|
u = u + 0x165667b1 + (u << 5);
|
||||||
|
u = (u + 0xd3a2646c) ^ (u << 9);
|
||||||
|
u = u + 0xfd7046c5 + (u << 3);
|
||||||
|
u = u ^ 0xb55a4f09 ^ (u >>> 16);
|
||||||
|
return u >>> 0;
|
||||||
|
}
|
||||||
|
const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000;
|
||||||
|
|
||||||
|
function bitReverse(i, n) {
|
||||||
|
let r = 0;
|
||||||
|
for (let b = 0; b < n; b++) {
|
||||||
|
r = (r << 1) | (i & 1);
|
||||||
|
i >>>= 1;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function noise(x) {
|
||||||
|
const i = Math.floor(x),
|
||||||
|
f = x - i;
|
||||||
|
const a = hash01(i),
|
||||||
|
b = hash01(i + 1);
|
||||||
|
return a + (b - a) * f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function brownian(x, oct = 4) {
|
||||||
|
let amp = 0.5,
|
||||||
|
sum = 0,
|
||||||
|
norm = 0,
|
||||||
|
freq = 1;
|
||||||
|
for (let o = 0; o < oct; o++) {
|
||||||
|
sum += amp * noise(x * freq);
|
||||||
|
norm += amp;
|
||||||
|
amp *= 0.5;
|
||||||
|
freq *= 2;
|
||||||
|
}
|
||||||
|
return (sum / norm) * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
|
static get parameterDescriptors() {
|
||||||
|
return [
|
||||||
|
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
|
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||||
|
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
|
||||||
|
{ name: 'detune', defaultValue: 0 },
|
||||||
|
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
|
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
|
{ name: 'warpMode', defaultValue: 0 },
|
||||||
|
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
|
||||||
|
{ name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||||
|
{ name: 'phaserand', defaultValue: 1, minValue: 0, maxValue: 1 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
this.tables = null;
|
||||||
|
this.frameLen = 0;
|
||||||
|
this.numFrames = 0;
|
||||||
|
this.phase = [];
|
||||||
|
this.syncRatio = 1;
|
||||||
|
|
||||||
|
this.port.onmessage = (e) => {
|
||||||
|
const { type, payload } = e.data || {};
|
||||||
|
if (type === 'tables') {
|
||||||
|
this.tables = payload.mipmaps;
|
||||||
|
this.frameLen = payload.frameLen;
|
||||||
|
this.numFrames = this.tables[0].length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_chooseMip(dphi) {
|
||||||
|
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
||||||
|
let level = 0;
|
||||||
|
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
||||||
|
level++;
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mirror(x) {
|
||||||
|
return 1 - Math.abs(2 * x - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_toBits(amt, min = 2, max = 12) {
|
||||||
|
const b = max + (min - max) * amt;
|
||||||
|
return { b, n: Math.round(Math.pow(2, b)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
_warpPhase(phase, amt, mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case WarpMode.NONE: {
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
case WarpMode.ASYM: {
|
||||||
|
const a = 0.01 + 0.99 * amt;
|
||||||
|
return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a);
|
||||||
|
}
|
||||||
|
case WarpMode.MIRROR: {
|
||||||
|
// Asym, then mirror
|
||||||
|
return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM));
|
||||||
|
}
|
||||||
|
case WarpMode.BENDP: {
|
||||||
|
return Math.pow(phase, 1 + 3 * amt);
|
||||||
|
}
|
||||||
|
case WarpMode.BENDM: {
|
||||||
|
return Math.pow(phase, 1 / (1 + 3 * amt));
|
||||||
|
}
|
||||||
|
case WarpMode.BENDMP: {
|
||||||
|
return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2);
|
||||||
|
}
|
||||||
|
case WarpMode.SYNC: {
|
||||||
|
const syncRatio = Math.pow(16, amt * amt);
|
||||||
|
return (phase * syncRatio) % 1;
|
||||||
|
}
|
||||||
|
case WarpMode.QUANT: {
|
||||||
|
const { n } = this._toBits(amt);
|
||||||
|
return ffloor(phase * n) / n;
|
||||||
|
}
|
||||||
|
case WarpMode.FOLD: {
|
||||||
|
const K = 7;
|
||||||
|
const k = 1 + Math.max(1, Math.round(K * amt));
|
||||||
|
return Math.abs(frac(k * phase) - 0.5) * 2;
|
||||||
|
}
|
||||||
|
case WarpMode.PWM: {
|
||||||
|
const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1);
|
||||||
|
if (phase < w) return (phase / w) * 0.5;
|
||||||
|
return 0.5 + ((phase - w) / (1 - w)) * 0.5;
|
||||||
|
}
|
||||||
|
case WarpMode.ORBIT: {
|
||||||
|
const depth = 0.5 * amt;
|
||||||
|
const n = 3;
|
||||||
|
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
|
||||||
|
}
|
||||||
|
case WarpMode.SPIN: {
|
||||||
|
const depth = 0.5 * amt;
|
||||||
|
const { n } = this._toBits(amt, 1, 6);
|
||||||
|
return frac(phase + depth * Math.sin(2 * Math.PI * n * phase));
|
||||||
|
}
|
||||||
|
case WarpMode.CHAOS: {
|
||||||
|
const r = 3.7 + 0.3 * amt;
|
||||||
|
const logistic = r * phase * (1 - phase);
|
||||||
|
return clamp((1 - amt) * phase + amt * logistic, 0, 1);
|
||||||
|
}
|
||||||
|
case WarpMode.PRIMES: {
|
||||||
|
const isPrime = (n) => {
|
||||||
|
if (n < 2) return false;
|
||||||
|
if (n % 2 === 0) return n === 2;
|
||||||
|
for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let { n } = this._toBits(amt, 3);
|
||||||
|
while (!isPrime(n)) n++;
|
||||||
|
return ffloor(phase * n) / n;
|
||||||
|
}
|
||||||
|
case WarpMode.BINARY: {
|
||||||
|
let { b } = this._toBits(amt, 3);
|
||||||
|
b = Math.round(b);
|
||||||
|
const n = 1 << b;
|
||||||
|
const idx = ffloor(phase * n);
|
||||||
|
const ridx = bitReverse(idx, b);
|
||||||
|
return ridx / n;
|
||||||
|
}
|
||||||
|
case WarpMode.MODULAR: {
|
||||||
|
const { n } = this._toBits(amt);
|
||||||
|
const depth = 0.5 * amt;
|
||||||
|
const jump = frac(phase * n) / n;
|
||||||
|
return frac(phase + depth * jump);
|
||||||
|
}
|
||||||
|
case WarpMode.BROWNIAN: {
|
||||||
|
const disp = 0.25 * amt * brownian(64 * phase, 4);
|
||||||
|
return frac(phase + disp);
|
||||||
|
}
|
||||||
|
case WarpMode.RECIPROCAL: {
|
||||||
|
const g = 2 + 4 * amt;
|
||||||
|
const num = phase * g;
|
||||||
|
const den = phase + (1 - phase) * g;
|
||||||
|
const y = den > 1e-12 ? num / den : 0;
|
||||||
|
return clamp(y, 0, 1);
|
||||||
|
}
|
||||||
|
case WarpMode.WORMHOLE: {
|
||||||
|
const gap = clamp(0.8 * amt, 0, 1);
|
||||||
|
const a = 0.5 * (1 - gap);
|
||||||
|
const b = 0.5 * (1 + gap);
|
||||||
|
if (phase < a) return (phase / a) * 0.5;
|
||||||
|
if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b));
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
case WarpMode.LOGISTIC: {
|
||||||
|
let x = phase;
|
||||||
|
const r = 3.6 + 0.4 * amt;
|
||||||
|
const iters = 1 + Math.round(2 * amt);
|
||||||
|
for (let i = 0; i < iters; i++) x = r * x * (1 - x);
|
||||||
|
return clamp(x, 0, 1);
|
||||||
|
}
|
||||||
|
case WarpMode.SIGMOID: {
|
||||||
|
const k = 1 + 10 * amt;
|
||||||
|
const x = phase - 0.5;
|
||||||
|
const y = 1 / (1 + Math.exp(-k * x));
|
||||||
|
const y0 = 1 / (1 + Math.exp(0.5 * k));
|
||||||
|
const y1 = 1 / (1 + Math.exp(-0.5 * k));
|
||||||
|
return (y - y0) / (y1 - y0);
|
||||||
|
}
|
||||||
|
case WarpMode.FRACTAL: {
|
||||||
|
const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt;
|
||||||
|
return frac(phase + d);
|
||||||
|
}
|
||||||
|
case WarpMode.FLIP: {
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sampleFrame(frame, phase) {
|
||||||
|
const pos = phase * (frame.length - 1);
|
||||||
|
const i = pos | 0;
|
||||||
|
const frac = pos - i;
|
||||||
|
const a = frame[i];
|
||||||
|
const b = frame[(i + 1) % frame.length];
|
||||||
|
return a + (b - a) * frac;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(_inputs, outputs, parameters) {
|
||||||
|
if (currentTime >= parameters.end[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentTime <= parameters.begin[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const outL = outputs[0][0];
|
||||||
|
const outR = outputs[0][1] || outputs[0][0];
|
||||||
|
|
||||||
|
if (!this.tables) {
|
||||||
|
outL.fill(0);
|
||||||
|
if (outR !== outL) outR.set(outL);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < outL.length; i++) {
|
||||||
|
const detune = pv(parameters.detune, i);
|
||||||
|
const spread = pv(parameters.spread, i) * 0.5 + 0.5;
|
||||||
|
const tablePos = pv(parameters.position, i);
|
||||||
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
|
const fIdx = idx | 0;
|
||||||
|
const frac = idx - fIdx;
|
||||||
|
const warpAmount = pv(parameters.warp, i);
|
||||||
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
|
const voices = pv(parameters.voices, i);
|
||||||
|
const phaseRand = pv(parameters.phaserand, i);
|
||||||
|
const gain1 = Math.sqrt(1 - spread);
|
||||||
|
const gain2 = Math.sqrt(spread);
|
||||||
|
let f = pv(parameters.frequency, i);
|
||||||
|
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||||
|
for (let n = 0; n < voices; n++) {
|
||||||
|
const isOdd = (n & 1) == 1;
|
||||||
|
let gainL = gain1;
|
||||||
|
let gainR = gain2;
|
||||||
|
// invert right and left gain
|
||||||
|
if (isOdd) {
|
||||||
|
gainL = gain2;
|
||||||
|
gainR = gain1;
|
||||||
|
}
|
||||||
|
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
|
||||||
|
const dPhase = fVoice / sampleRate;
|
||||||
|
const level = this._chooseMip(dPhase);
|
||||||
|
const bank = this.tables[level];
|
||||||
|
|
||||||
|
// warp phase then sample
|
||||||
|
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
||||||
|
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||||
|
const s0 = this._sampleFrame(bank[fIdx], ph);
|
||||||
|
const s1 = this._sampleFrame(bank[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||||
|
let s = s0 + (s1 - s0) * frac;
|
||||||
|
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||||
|
s = -s;
|
||||||
|
}
|
||||||
|
outL[i] += (s * gainL) / Math.sqrt(voices);
|
||||||
|
outR[i] += (s * gainR) / Math.sqrt(voices);
|
||||||
|
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
// this is dough, the superdough without dependencies
|
// 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 SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
|
||||||
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
||||||
const ISR = 1 / SAMPLE_RATE;
|
const ISR = 1 / SAMPLE_RATE;
|
||||||
|
|
@ -641,8 +643,8 @@ const note2midi = (note, defaultOctave = 3) => {
|
||||||
}
|
}
|
||||||
const chroma = chromas[pc.toLowerCase()];
|
const chroma = chromas[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
oct = Number(oct || defaultOctave);
|
const octave = Number(oct || defaultOctave);
|
||||||
return (oct + 1) * 12 + chroma + offset;
|
return (octave + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
|
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
|
||||||
const note2freq = (note) => {
|
const note2freq = (note) => {
|
||||||
|
|
@ -654,9 +656,152 @@ const note2freq = (note) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DoughVoice {
|
export class DoughVoice {
|
||||||
|
/** @type {number} */
|
||||||
|
id = 0;
|
||||||
|
/** @type {number[]} */
|
||||||
out = [0, 0];
|
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) {
|
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;
|
let $ = this;
|
||||||
Object.assign($, value);
|
Object.assign($, value);
|
||||||
$.s = $.s ?? getDefaultValue('s');
|
$.s = $.s ?? getDefaultValue('s');
|
||||||
|
|
@ -773,7 +918,7 @@ export class DoughVoice {
|
||||||
let freq = this.freq * this.speed;
|
let freq = this.freq * this.speed;
|
||||||
|
|
||||||
// frequency modulation
|
// frequency modulation
|
||||||
if (this._fm) {
|
if (this._fm && this.fmh !== undefined && this.fmi !== undefined) {
|
||||||
let fmi = this.fmi;
|
let fmi = this.fmi;
|
||||||
if (this._fmenv) {
|
if (this._fmenv) {
|
||||||
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
|
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
|
||||||
|
|
@ -785,37 +930,31 @@ export class DoughVoice {
|
||||||
}
|
}
|
||||||
|
|
||||||
// vibrato
|
// vibrato
|
||||||
if (this._vib) {
|
if (this._vib && this.vibmod !== undefined) {
|
||||||
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
|
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pitch envelope
|
// 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);
|
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
|
||||||
freq = freq + env * this.penv;
|
freq = freq + env * this.penv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
let lpf = this.cutoff;
|
let lpf = this.cutoff;
|
||||||
if (this._lpf) {
|
if (lpf !== undefined && this._lpenv) {
|
||||||
if (this._lpenv) {
|
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
|
||||||
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
|
lpf = this.lpenv * env * lpf + lpf;
|
||||||
lpf = this.lpenv * env * lpf + lpf;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let hpf = this.hcutoff;
|
let hpf = this.hcutoff;
|
||||||
if (this._hpf) {
|
if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) {
|
||||||
if (this._hpenv) {
|
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
|
||||||
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
|
hpf = 2 ** this.hpenv * env * hpf + hpf;
|
||||||
hpf = 2 ** this.hpenv * env * hpf + hpf;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let bpf = this.bandf;
|
let bpf = this.bandf;
|
||||||
if (this._bpf) {
|
if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) {
|
||||||
if (this._bpenv) {
|
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
|
||||||
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
|
bpf = 2 ** this.bpenv * env * bpf + bpf;
|
||||||
bpf = 2 ** this.bpenv * env * bpf + bpf;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// gain envelope
|
// gain envelope
|
||||||
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
|
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.sampleRate = sampleRate;
|
||||||
this.t = Math.floor(currentTime * sampleRate); // samples
|
this.t = Math.floor(currentTime * sampleRate); // samples
|
||||||
// console.log('init dough', this.sampleRate, this.t);
|
// console.log('init dough', this.sampleRate, this.t);
|
||||||
this._delayL = new PitchDelay();
|
this._delayL = new Delay();
|
||||||
this._delayR = new PitchDelay();
|
this._delayR = new Delay();
|
||||||
}
|
}
|
||||||
loadSample(name, channels, sampleRate) {
|
loadSample(name, channels, sampleRate) {
|
||||||
BufferPlayer.samples.set(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?
|
// todo: how to change delaytime / delayfeedback from a voice?
|
||||||
const delayL = this._delayL.update(this.delaysend[0], 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.delayspeed);
|
const delayR = this._delayR.update(this.delaysend[1], this.delaytime);
|
||||||
|
|
||||||
this.delaysend[0] = delayL * this.delayfeedback;
|
this.delaysend[0] = delayL * this.delayfeedback;
|
||||||
this.delaysend[1] = delayR * this.delayfeedback;
|
this.delaysend[1] = delayR * this.delayfeedback;
|
||||||
this.out[0] += delayL;
|
this.out[0] += delayL;
|
||||||
|
|
|
||||||
|
|
@ -7,80 +7,130 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
// import { strict as assert } from 'assert';
|
// import { strict as assert } from 'assert';
|
||||||
|
|
||||||
import '../tonal.mjs'; // need to import this to add prototypes
|
import '../tonal.mjs'; // need to import this to add prototypes
|
||||||
import { pure, n, seq, note } from '@strudel/core';
|
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { mini } from '../../mini/mini.mjs';
|
import { mini } from '../../mini/mini.mjs';
|
||||||
|
|
||||||
describe('tonal', () => {
|
describe('tonal', () => {
|
||||||
it('Should run tonal functions ', () => {
|
describe('scaleTranspose', () => {
|
||||||
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
it('transposes notes by scale degrees', () => {
|
||||||
|
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('scale with plain values', () => {
|
describe('scale', () => {
|
||||||
expect(
|
it('converts plain values', () => {
|
||||||
seq(0, 1, 2)
|
expect(
|
||||||
.scale('C major')
|
seq(0, 1, 2)
|
||||||
.note()
|
.scale('C major')
|
||||||
.firstCycleValues.map((h) => h.note),
|
.note()
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values (mini notation)', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale('C:major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values (no tonic)', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale('major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts n values (explicit mini notation)', () => {
|
||||||
|
expect(
|
||||||
|
n(seq(0, 1, 2))
|
||||||
|
.scale(mini('C:major'))
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('converts decorated n values', () => {
|
||||||
|
expect(
|
||||||
|
n(seq('0b', '1#', '-2', '3##', '4bb'))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
|
||||||
|
});
|
||||||
|
it('produces silence for mixed sharps and flats', () => {
|
||||||
|
expect(
|
||||||
|
n(seq('0b#', '1#b', '2#b#'))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
it('snaps notes (upwards) to scale', () => {
|
||||||
|
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
|
||||||
|
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
note(seq(inputNotes))
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(expectedNotes);
|
||||||
|
});
|
||||||
|
it('snaps notes to the correct octave', () => {
|
||||||
|
const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
|
||||||
|
const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
note(seq(inputNotes))
|
||||||
|
.scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(expectedNotes);
|
||||||
|
});
|
||||||
|
it('handles scale names provided with colons', () => {
|
||||||
|
const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
|
||||||
|
const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
note(seq(inputNotes))
|
||||||
|
.scale('F#:pentatonic') // F#, G#, A#, C#, and D#
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(expectedNotes);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it('scale with n values', () => {
|
describe('transpose', () => {
|
||||||
expect(
|
it('transposes note numbers with interval numbers', () => {
|
||||||
n(seq(0, 1, 2))
|
expect(
|
||||||
.scale('C major')
|
note(seq(40, 40, 40))
|
||||||
.firstCycleValues.map((h) => h.note),
|
.transpose(0, 1, 2)
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
.firstCycleValues.map((h) => h.note),
|
||||||
});
|
).toEqual([40, 41, 42]);
|
||||||
it('scale with colon', () => {
|
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
||||||
expect(
|
});
|
||||||
n(seq(0, 1, 2))
|
it('transposes note numbers with interval strings', () => {
|
||||||
.scale('C:major')
|
expect(
|
||||||
.firstCycleValues.map((h) => h.note),
|
note(seq(40, 40, 40))
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
.transpose('1P', '2M', '3m')
|
||||||
});
|
.firstCycleValues.map((h) => h.note),
|
||||||
it('scale without tonic', () => {
|
).toEqual([40, 42, 43]);
|
||||||
expect(
|
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
||||||
n(seq(0, 1, 2))
|
});
|
||||||
.scale('major')
|
it('transposes note strings with interval numbers', () => {
|
||||||
.firstCycleValues.map((h) => h.note),
|
expect(
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
note(seq('c', 'c', 'c'))
|
||||||
});
|
.transpose(0, 1, 2)
|
||||||
it('scale with mininotation colon', () => {
|
.firstCycleValues.map((h) => h.note),
|
||||||
expect(
|
).toEqual(['C', 'Db', 'D']);
|
||||||
n(seq(0, 1, 2))
|
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
||||||
.scale(mini('C:major'))
|
});
|
||||||
.firstCycleValues.map((h) => h.note),
|
it('transposes note strings with interval strings', () => {
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
expect(
|
||||||
});
|
note(seq('c', 'c', 'c'))
|
||||||
it('transposes note numbers with interval numbers', () => {
|
.transpose('1P', '2M', '3m')
|
||||||
expect(
|
.firstCycleValues.map((h) => h.note),
|
||||||
note(seq(40, 40, 40))
|
).toEqual(['C', 'D', 'Eb']);
|
||||||
.transpose(0, 1, 2)
|
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
||||||
.firstCycleValues.map((h) => h.note),
|
});
|
||||||
).toEqual([40, 41, 42]);
|
|
||||||
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
|
||||||
});
|
|
||||||
it('transposes note numbers with interval strings', () => {
|
|
||||||
expect(
|
|
||||||
note(seq(40, 40, 40))
|
|
||||||
.transpose('1P', '2M', '3m')
|
|
||||||
.firstCycleValues.map((h) => h.note),
|
|
||||||
).toEqual([40, 42, 43]);
|
|
||||||
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
|
||||||
});
|
|
||||||
it('transposes note strings with interval numbers', () => {
|
|
||||||
expect(
|
|
||||||
note(seq('c', 'c', 'c'))
|
|
||||||
.transpose(0, 1, 2)
|
|
||||||
.firstCycleValues.map((h) => h.note),
|
|
||||||
).toEqual(['C', 'Db', 'D']);
|
|
||||||
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
|
||||||
});
|
|
||||||
it('transposes note strings with interval strings', () => {
|
|
||||||
expect(
|
|
||||||
note(seq('c', 'c', 'c'))
|
|
||||||
.transpose('1P', '2M', '3m')
|
|
||||||
.firstCycleValues.map((h) => h.note),
|
|
||||||
).toEqual(['C', 'D', 'Eb']);
|
|
||||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,28 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||||
|
|
||||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||||
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
|
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
|
||||||
import { stepInNamedScale } from './tonleiter.mjs';
|
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||||
|
import { noteToMidi } from '../core/util.mjs';
|
||||||
|
|
||||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||||
|
|
||||||
function scaleStep(step, scale) {
|
function getScale(scaleName) {
|
||||||
scale = scale.replaceAll(':', ' ');
|
scaleName = scaleName.replaceAll(':', ' ');
|
||||||
step = Math.ceil(step);
|
const scale = Scale.get(scaleName);
|
||||||
let { intervals, tonic, empty } = Scale.get(scale);
|
const { tonic, empty } = scale;
|
||||||
if ((empty && isNote(scale)) || (empty && !tonic)) {
|
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
|
||||||
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
|
throw new Error(
|
||||||
|
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
|
||||||
|
);
|
||||||
} else if (empty) {
|
} else if (empty) {
|
||||||
throw new Error(`invalid scale "${scale}"`);
|
throw new Error(`Invalid scale name "${scaleName}"`);
|
||||||
}
|
}
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaleStep(step, scale) {
|
||||||
|
step = Math.ceil(step);
|
||||||
|
let { intervals, tonic } = getScale(scale);
|
||||||
tonic = tonic || 'C';
|
tonic = tonic || 'C';
|
||||||
const { pc, oct = 3 } = Note.get(tonic);
|
const { pc, oct = 3 } = Note.get(tonic);
|
||||||
const octaveOffset = Math.floor(step / intervals.length);
|
const octaveOffset = Math.floor(step / intervals.length);
|
||||||
|
|
@ -30,8 +39,7 @@ function scaleStep(step, scale) {
|
||||||
// transpose note inside scale by offset steps
|
// transpose note inside scale by offset steps
|
||||||
// function scaleOffset(scale: string, offset: number, note: string) {
|
// function scaleOffset(scale: string, offset: number, note: string) {
|
||||||
function scaleOffset(scale, offset, note) {
|
function scaleOffset(scale, offset, note) {
|
||||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
let { notes } = getScale(scale);
|
||||||
let { notes } = Scale.get(`${tonic} ${scaleName}`);
|
|
||||||
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
||||||
offset = Number(offset);
|
offset = Number(offset);
|
||||||
if (isNaN(offset)) {
|
if (isNaN(offset)) {
|
||||||
|
|
@ -120,10 +128,7 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
||||||
const interval = !isNaN(Number(intervalOrSemitones))
|
const interval = !isNaN(Number(intervalOrSemitones))
|
||||||
? Interval.fromSemitones(intervalOrSemitones)
|
? Interval.fromSemitones(intervalOrSemitones)
|
||||||
: String(intervalOrSemitones);
|
: String(intervalOrSemitones);
|
||||||
// TODO: move simplify to player to preserve enharmonics
|
const targetNote = Note.transpose(note, interval);
|
||||||
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
|
|
||||||
// TODO: check if this is still relevant..
|
|
||||||
const targetNote = Note.simplify(Note.transpose(note, interval));
|
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
||||||
}
|
}
|
||||||
|
|
@ -171,8 +176,59 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Converts a step value, which is a number optionally decorated with sharps and flats,
|
||||||
|
// to a number and an `offset` number of semitones
|
||||||
|
function _convertStepToNumberAndOffset(step) {
|
||||||
|
let asNumber = Number(step);
|
||||||
|
let offset = 0;
|
||||||
|
if (isNaN(asNumber)) {
|
||||||
|
step = String(step);
|
||||||
|
// Check to see if the step matches the expected format:
|
||||||
|
// - A number (possibly negative)
|
||||||
|
// - Some number of sharps or flats (but not both)
|
||||||
|
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
|
||||||
|
}
|
||||||
|
asNumber = Number(match[1]);
|
||||||
|
// These decorations will determine the semitone offset based on the number of
|
||||||
|
// sharps or flats
|
||||||
|
const decorations = match[2] || '';
|
||||||
|
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
|
||||||
|
}
|
||||||
|
return [asNumber, offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
let scaleToMidisAndNotes = {};
|
||||||
|
// Finds the nearest scale note to `note`
|
||||||
|
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
||||||
|
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
|
||||||
|
if (scaleToMidisAndNotes[scaleName] === undefined) {
|
||||||
|
const { intervals, tonic } = getScale(scaleName);
|
||||||
|
const { pc } = Note.get(tonic);
|
||||||
|
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
|
||||||
|
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
|
||||||
|
const sMidi = sNotes.map(noteToMidi);
|
||||||
|
// Cache
|
||||||
|
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
|
||||||
|
}
|
||||||
|
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
|
||||||
|
const rootMidi = scaleMidis[0];
|
||||||
|
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
|
||||||
|
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
|
||||||
|
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
|
||||||
|
const noteMatch = scaleNotes[noteIdx];
|
||||||
|
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
|
||||||
|
*
|
||||||
|
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
|
||||||
|
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
|
||||||
|
*
|
||||||
|
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||||
*
|
*
|
||||||
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||||
*
|
*
|
||||||
|
|
@ -191,6 +247,12 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
||||||
* n(rand.range(0,12).segment(8))
|
* n(rand.range(0,12).segment(8))
|
||||||
* .scale("C:ritusen")
|
* .scale("C:ritusen")
|
||||||
* .s("piano")
|
* .s("piano")
|
||||||
|
* @example
|
||||||
|
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
|
||||||
|
* .scale("C:<major minor>/2")
|
||||||
|
* .s("piano")
|
||||||
|
* @example
|
||||||
|
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const scale = register(
|
export const scale = register(
|
||||||
|
|
@ -204,49 +266,35 @@ export const scale = register(
|
||||||
pat
|
pat
|
||||||
.fmap((value) => {
|
.fmap((value) => {
|
||||||
const isObject = typeof value === 'object';
|
const isObject = typeof value === 'object';
|
||||||
let step = isObject ? value.n : value;
|
// The case where the note has been defined via `n` or `pure`
|
||||||
if (isObject) {
|
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
|
||||||
|
const step = isObject ? (value.n ?? value.value) : value;
|
||||||
delete value.n; // remove n so it won't cause trouble
|
delete value.n; // remove n so it won't cause trouble
|
||||||
}
|
if (isNote(step)) {
|
||||||
if (isNote(step)) {
|
// legacy..
|
||||||
// legacy..
|
return pure(step);
|
||||||
return pure(step);
|
}
|
||||||
}
|
try {
|
||||||
let asNumber = Number(step);
|
const [number, offset] = _convertStepToNumberAndOffset(step);
|
||||||
let semitones = 0;
|
let note;
|
||||||
if (isNaN(asNumber)) {
|
if (isObject && value.anchor) {
|
||||||
step = String(step);
|
note = stepInNamedScale(number, scale, value.anchor);
|
||||||
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
} else {
|
||||||
logger(
|
note = scaleStep(number, scale);
|
||||||
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
}
|
||||||
'error',
|
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
|
||||||
);
|
value = pure(isObject ? { ...value, note } : note);
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[tonal] ${err.message}`, 'error');
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
const isharp = step.indexOf('#');
|
return value;
|
||||||
if (isharp >= 0) {
|
|
||||||
asNumber = Number(step.substring(0, isharp));
|
|
||||||
semitones = step.length - isharp;
|
|
||||||
} else {
|
|
||||||
const iflat = step.indexOf('b');
|
|
||||||
asNumber = Number(step.substring(0, iflat));
|
|
||||||
semitones = iflat - step.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try {
|
// The case where the note has been defined via `note`
|
||||||
let note;
|
else {
|
||||||
if (isObject && value.anchor) {
|
const note = _getNearestScaleNote(scale, value.note);
|
||||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
return pure(isObject ? { ...value, note } : note);
|
||||||
} else {
|
|
||||||
note = scaleStep(asNumber, scale);
|
|
||||||
}
|
|
||||||
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
|
|
||||||
value = pure(isObject ? { ...value, note } : note);
|
|
||||||
} catch (err) {
|
|
||||||
logger(`[tonal] ${err.message}`, 'error');
|
|
||||||
value = silence;
|
|
||||||
}
|
}
|
||||||
return value;
|
|
||||||
})
|
})
|
||||||
.outerJoin()
|
.outerJoin()
|
||||||
// legacy:
|
// legacy:
|
||||||
|
|
|
||||||
|
|
@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
|
||||||
let scaleSteps = {}; // [scaleName]: semitones[]
|
let scaleSteps = {}; // [scaleName]: semitones[]
|
||||||
|
|
||||||
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||||
let [root, scaleName] = Scale.tokenize(scale);
|
const [root, scaleName] = Scale.tokenize(scale);
|
||||||
const rootMidi = x2midi(root);
|
const rootMidi = x2midi(root);
|
||||||
const rootChroma = midi2chroma(rootMidi);
|
const rootChroma = midi2chroma(rootMidi);
|
||||||
if (!scaleSteps[scaleName]) {
|
if (!scaleSteps[scaleName]) {
|
||||||
let { intervals } = Scale.get(`C ${scaleName}`);
|
const { intervals } = Scale.get(`C ${scaleName}`);
|
||||||
// cache result
|
// cache result
|
||||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||||
}
|
}
|
||||||
|
|
@ -222,6 +222,7 @@ export const Note = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: support octave numbers
|
// TODO: support octave numbers
|
||||||
|
// Example: Note("Bb3").transpose("c3")
|
||||||
export function transpose(note, step) {
|
export function transpose(note, step) {
|
||||||
// example: E, 3
|
// example: E, 3
|
||||||
const stepNumber = Step.tokenize(step)[1]; // 3
|
const stepNumber = Step.tokenize(step)[1]; // 3
|
||||||
|
|
@ -235,5 +236,3 @@ export function transpose(note, step) {
|
||||||
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
|
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
|
||||||
return [targetNote, offsetAccidentals].join('');
|
return [targetNote, offsetAccidentals].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Note("Bb3").transpose("c3")
|
|
||||||
|
|
|
||||||
|
|
@ -2907,26 +2907,38 @@ exports[`runs examples > example "distortvol" example index 0 1`] = `
|
||||||
|
|
||||||
exports[`runs examples > example "djf" example index 0 1`] = `
|
exports[`runs examples > example "djf" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]",
|
"[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]",
|
||||||
"[ 1/4 → 1/2 | n:3 s:superzow octave:3 djf:0.5 ]",
|
"[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]",
|
||||||
"[ 1/2 → 3/4 | n:7 s:superzow octave:3 djf:0.5 ]",
|
"[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]",
|
||||||
"[ 3/4 → 1/1 | n:10 s:superzow octave:3 djf:0.5 ]",
|
"[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]",
|
||||||
"[ 3/4 → 1/1 | n:24 s:superzow octave:3 djf:0.5 ]",
|
"[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]",
|
||||||
"[ 1/1 → 5/4 | n:0 s:superzow octave:3 djf:0.25 ]",
|
"[ 5/8 → 3/4 | note:F3 s:supersaw djf:0.5 ]",
|
||||||
"[ 5/4 → 3/2 | n:3 s:superzow octave:3 djf:0.25 ]",
|
"[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]",
|
||||||
"[ 3/2 → 7/4 | n:7 s:superzow octave:3 djf:0.25 ]",
|
"[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]",
|
||||||
"[ 7/4 → 2/1 | n:10 s:superzow octave:3 djf:0.25 ]",
|
"[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]",
|
||||||
"[ 7/4 → 2/1 | n:24 s:superzow octave:3 djf:0.25 ]",
|
"[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]",
|
||||||
"[ 2/1 → 9/4 | n:0 s:superzow octave:3 djf:0.5 ]",
|
"[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]",
|
||||||
"[ 9/4 → 5/2 | n:3 s:superzow octave:3 djf:0.5 ]",
|
"[ 11/8 → 3/2 | note:F3 s:supersaw djf:0.3 ]",
|
||||||
"[ 5/2 → 11/4 | n:7 s:superzow octave:3 djf:0.5 ]",
|
"[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]",
|
||||||
"[ 11/4 → 3/1 | n:10 s:superzow octave:3 djf:0.5 ]",
|
"[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]",
|
||||||
"[ 11/4 → 3/1 | n:24 s:superzow octave:3 djf:0.5 ]",
|
"[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]",
|
||||||
"[ 3/1 → 13/4 | n:0 s:superzow octave:3 djf:0.75 ]",
|
"[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]",
|
||||||
"[ 13/4 → 7/2 | n:3 s:superzow octave:3 djf:0.75 ]",
|
"[ 2/1 → 17/8 | note:Eb5 s:supersaw djf:0.2 ]",
|
||||||
"[ 7/2 → 15/4 | n:7 s:superzow octave:3 djf:0.75 ]",
|
"[ 17/8 → 9/4 | note:D5 s:supersaw djf:0.2 ]",
|
||||||
"[ 15/4 → 4/1 | n:10 s:superzow octave:3 djf:0.75 ]",
|
"[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]",
|
||||||
"[ 15/4 → 4/1 | n:24 s:superzow octave:3 djf:0.75 ]",
|
"[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]",
|
||||||
|
"[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]",
|
||||||
|
"[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -3169,57 +3181,50 @@ exports[`runs examples > example "dry" example index 0 1`] = `
|
||||||
exports[`runs examples > example "duckattack" example index 0 1`] = `
|
exports[`runs examples > example "duckattack" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckattack" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -3280,6 +3285,94 @@ exports[`runs examples > example "duckdepth" example index 0 1`] = `
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckdepth" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckonset" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckonset" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckonset" example index 2 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "duckorbit" example index 0 1`] = `
|
exports[`runs examples > example "duckorbit" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
|
|
@ -3305,6 +3398,31 @@ exports[`runs examples > example "duckorbit" example index 0 1`] = `
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckorbit" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "duration" example index 0 1`] = `
|
exports[`runs examples > example "duration" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
|
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
|
||||||
|
|
@ -3720,6 +3838,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
|
||||||
|
|
||||||
exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
|
"[ 0/1 → 1/4 | color:red note:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | color:red note:1 ]",
|
||||||
"[ 1/2 → 3/4 | note:E2 ]",
|
"[ 1/2 → 3/4 | note:E2 ]",
|
||||||
"[ 3/4 → 1/1 | note:F2 ]",
|
"[ 3/4 → 1/1 | note:F2 ]",
|
||||||
"[ 1/1 → 5/4 | note:G2 ]",
|
"[ 1/1 → 5/4 | note:G2 ]",
|
||||||
|
|
@ -3728,6 +3848,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
||||||
"[ 7/4 → 2/1 | note:C3 ]",
|
"[ 7/4 → 2/1 | note:C3 ]",
|
||||||
"[ 2/1 → 9/4 | note:D3 ]",
|
"[ 2/1 → 9/4 | note:D3 ]",
|
||||||
"[ 9/4 → 5/2 | note:D2 ]",
|
"[ 9/4 → 5/2 | note:D2 ]",
|
||||||
|
"[ 5/2 → 11/4 | color:red note:2 ]",
|
||||||
|
"[ 11/4 → 3/1 | color:red note:3 ]",
|
||||||
"[ 3/1 → 13/4 | note:G2 ]",
|
"[ 3/1 → 13/4 | note:G2 ]",
|
||||||
"[ 13/4 → 7/2 | note:A2 ]",
|
"[ 13/4 → 7/2 | note:A2 ]",
|
||||||
"[ 7/2 → 15/4 | note:B2 ]",
|
"[ 7/2 → 15/4 | note:B2 ]",
|
||||||
|
|
@ -6636,6 +6758,27 @@ exports[`runs examples > example "note" example index 2 1`] = `
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "note" example index 3 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 1/4 → 1/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 1/2 → 3/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 3/4 → 1/1 | note:e##-2 s:saw ]",
|
||||||
|
"[ 1/1 → 5/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 5/4 → 3/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 3/2 → 7/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 7/4 → 2/1 | note:e##-2 s:saw ]",
|
||||||
|
"[ 2/1 → 9/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 9/4 → 5/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 5/2 → 11/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 11/4 → 3/1 | note:e##-2 s:saw ]",
|
||||||
|
"[ 3/1 → 13/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 13/4 → 7/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 7/2 → 15/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 15/4 → 4/1 | note:e##-2 s:saw ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "nrpnn" example index 0 1`] = `
|
exports[`runs examples > example "nrpnn" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
|
|
@ -7441,6 +7584,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`] = `
|
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/6 | note:c ]",
|
"[ 0/1 → 1/6 | note:c ]",
|
||||||
|
|
@ -8866,6 +9067,108 @@ exports[`runs examples > example "scale" example index 2 1`] = `
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "scale" example index 3 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:C3 s:piano ]",
|
||||||
|
"[ 0/1 → 1/4 | note:B3 s:piano ]",
|
||||||
|
"[ 1/4 → 3/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 3/8 → 1/2 | note:F2 s:piano ]",
|
||||||
|
"[ 1/2 → 3/4 | note:A2 s:piano ]",
|
||||||
|
"[ 1/2 → 3/4 | note:D4 s:piano ]",
|
||||||
|
"[ 3/4 → 1/1 | note:G3 s:piano ]",
|
||||||
|
"[ 1/1 → 5/4 | note:C3 s:piano ]",
|
||||||
|
"[ 1/1 → 5/4 | note:C4 s:piano ]",
|
||||||
|
"[ 5/4 → 11/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 11/8 → 3/2 | note:E2 s:piano ]",
|
||||||
|
"[ 3/2 → 7/4 | note:A2 s:piano ]",
|
||||||
|
"[ 3/2 → 7/4 | note:Eb4 s:piano ]",
|
||||||
|
"[ 7/4 → 2/1 | note:F#3 s:piano ]",
|
||||||
|
"[ 2/1 → 9/4 | note:C3 s:piano ]",
|
||||||
|
"[ 2/1 → 9/4 | note:B3 s:piano ]",
|
||||||
|
"[ 9/4 → 19/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 19/8 → 5/2 | note:F2 s:piano ]",
|
||||||
|
"[ 5/2 → 11/4 | note:Ab2 s:piano ]",
|
||||||
|
"[ 5/2 → 11/4 | note:D4 s:piano ]",
|
||||||
|
"[ 11/4 → 3/1 | note:G3 s:piano ]",
|
||||||
|
"[ 3/1 → 13/4 | note:C3 s:piano ]",
|
||||||
|
"[ 3/1 → 13/4 | note:C4 s:piano ]",
|
||||||
|
"[ 13/4 → 27/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 27/8 → 7/2 | note:E2 s:piano ]",
|
||||||
|
"[ 7/2 → 15/4 | note:Ab2 s:piano ]",
|
||||||
|
"[ 7/2 → 15/4 | note:Eb4 s:piano ]",
|
||||||
|
"[ 15/4 → 4/1 | note:F#3 s:piano ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "scale" example index 4 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | note:Gb1 ]",
|
||||||
|
"[ 1/16 → 1/8 | note:Gb1 ]",
|
||||||
|
"[ 1/8 → 3/16 | note:Gb1 ]",
|
||||||
|
"[ 3/16 → 1/4 | note:Gb1 ]",
|
||||||
|
"[ 1/4 → 5/16 | note:Gb1 ]",
|
||||||
|
"[ 5/16 → 3/8 | note:Gb1 ]",
|
||||||
|
"[ 3/8 → 7/16 | note:Gb1 ]",
|
||||||
|
"[ 7/16 → 1/2 | note:Gb1 ]",
|
||||||
|
"[ 1/2 → 9/16 | note:Gb1 ]",
|
||||||
|
"[ 9/16 → 5/8 | note:Gb1 ]",
|
||||||
|
"[ 5/8 → 11/16 | note:Gb1 ]",
|
||||||
|
"[ 11/16 → 3/4 | note:Gb1 ]",
|
||||||
|
"[ 3/4 → 13/16 | note:Gb1 ]",
|
||||||
|
"[ 13/16 → 7/8 | note:Gb1 ]",
|
||||||
|
"[ 7/8 → 15/16 | note:Gb1 ]",
|
||||||
|
"[ 15/16 → 1/1 | note:Gb1 ]",
|
||||||
|
"[ 1/1 → 17/16 | note:Cb3 ]",
|
||||||
|
"[ 17/16 → 9/8 | note:Cb3 ]",
|
||||||
|
"[ 9/8 → 19/16 | note:Cb3 ]",
|
||||||
|
"[ 19/16 → 5/4 | note:Cb3 ]",
|
||||||
|
"[ 5/4 → 21/16 | note:Cb3 ]",
|
||||||
|
"[ 21/16 → 11/8 | note:Cb3 ]",
|
||||||
|
"[ 11/8 → 23/16 | note:Cb3 ]",
|
||||||
|
"[ 23/16 → 3/2 | note:Cb3 ]",
|
||||||
|
"[ 3/2 → 25/16 | note:Cb3 ]",
|
||||||
|
"[ 25/16 → 13/8 | note:Cb3 ]",
|
||||||
|
"[ 13/8 → 27/16 | note:Cb3 ]",
|
||||||
|
"[ 27/16 → 7/4 | note:Cb3 ]",
|
||||||
|
"[ 7/4 → 29/16 | note:Cb3 ]",
|
||||||
|
"[ 29/16 → 15/8 | note:Cb3 ]",
|
||||||
|
"[ 15/8 → 31/16 | note:Cb3 ]",
|
||||||
|
"[ 31/16 → 2/1 | note:Cb3 ]",
|
||||||
|
"[ 2/1 → 33/16 | note:Eb4 ]",
|
||||||
|
"[ 33/16 → 17/8 | note:Eb4 ]",
|
||||||
|
"[ 17/8 → 35/16 | note:Eb4 ]",
|
||||||
|
"[ 35/16 → 9/4 | note:Eb4 ]",
|
||||||
|
"[ 9/4 → 37/16 | note:Eb4 ]",
|
||||||
|
"[ 37/16 → 19/8 | note:Eb4 ]",
|
||||||
|
"[ 19/8 → 39/16 | note:Eb4 ]",
|
||||||
|
"[ 39/16 → 5/2 | note:Eb4 ]",
|
||||||
|
"[ 5/2 → 41/16 | note:Eb4 ]",
|
||||||
|
"[ 41/16 → 21/8 | note:Eb4 ]",
|
||||||
|
"[ 21/8 → 43/16 | note:Eb4 ]",
|
||||||
|
"[ 43/16 → 11/4 | note:Eb4 ]",
|
||||||
|
"[ 11/4 → 45/16 | note:Eb4 ]",
|
||||||
|
"[ 45/16 → 23/8 | note:Eb4 ]",
|
||||||
|
"[ 23/8 → 47/16 | note:Eb4 ]",
|
||||||
|
"[ 47/16 → 3/1 | note:Eb4 ]",
|
||||||
|
"[ 3/1 → 49/16 | note:Db2 ]",
|
||||||
|
"[ 49/16 → 25/8 | note:Db2 ]",
|
||||||
|
"[ 25/8 → 51/16 | note:Db2 ]",
|
||||||
|
"[ 51/16 → 13/4 | note:Db2 ]",
|
||||||
|
"[ 13/4 → 53/16 | note:Db2 ]",
|
||||||
|
"[ 53/16 → 27/8 | note:Db2 ]",
|
||||||
|
"[ 27/8 → 55/16 | note:Db2 ]",
|
||||||
|
"[ 55/16 → 7/2 | note:Db2 ]",
|
||||||
|
"[ 7/2 → 57/16 | note:Db2 ]",
|
||||||
|
"[ 57/16 → 29/8 | note:Db2 ]",
|
||||||
|
"[ 29/8 → 59/16 | note:Db2 ]",
|
||||||
|
"[ 59/16 → 15/4 | note:Db2 ]",
|
||||||
|
"[ 15/4 → 61/16 | note:Db2 ]",
|
||||||
|
"[ 61/16 → 31/8 | note:Db2 ]",
|
||||||
|
"[ 31/8 → 63/16 | note:Db2 ]",
|
||||||
|
"[ 63/16 → 4/1 | note:Db2 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/2 | note:C3 ]",
|
"[ 0/1 → 1/2 | note:C3 ]",
|
||||||
|
|
|
||||||
5
website/public/fonts/tic80/license.txt
Normal file
5
website/public/fonts/tic80/license.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
The FontStruction “TIC-80 wide font”
|
||||||
|
(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed
|
||||||
|
under a Creative Commons CC0 Public Domain Dedication license
|
||||||
|
(http://creativecommons.org/publicdomain/zero/1.0/).
|
||||||
|
[ancestry]
|
||||||
16
website/public/fonts/tic80/readme.txt
Normal file
16
website/public/fonts/tic80/readme.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
The font file in this archive was created using Fontstruct the free, online
|
||||||
|
font-building tool.
|
||||||
|
This font was created by “nesbox”.
|
||||||
|
This font has a homepage where this archive and other versions may be found:
|
||||||
|
https://fontstruct.com/fontstructions/show/1388526
|
||||||
|
[ancestry]
|
||||||
|
Try Fontstruct at https://fontstruct.com
|
||||||
|
It’s easy and it’s fun.
|
||||||
|
|
||||||
|
Fontstruct is copyright ©2017-2025 Rob Meek
|
||||||
|
|
||||||
|
LEGAL NOTICE:
|
||||||
|
In using this font you must comply with the licensing terms described in the
|
||||||
|
file “license.txt” included with this archive.
|
||||||
|
If you redistribute the font file in this archive, it must be accompanied by all
|
||||||
|
the other files from this archive, including this one.
|
||||||
BIN
website/public/fonts/tic80/tic-80-wide-font.otf
Normal file
BIN
website/public/fonts/tic80/tic-80-wide-font.otf
Normal file
Binary file not shown.
|
|
@ -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/).
|
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.
|
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`.
|
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.
|
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**
|
**selecting sample numbers separately**
|
||||||
|
|
||||||
Instead of using ":", we can also use the `n` function to select sample numbers:
|
Instead of selecting sample numbers one by one:
|
||||||
|
|
||||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
|
||||||
|
|
||||||
This is shorter and more readable than:
|
|
||||||
|
|
||||||
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
|
<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
|
## Recap
|
||||||
|
|
||||||
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
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;
|
min-width: 300px !important;
|
||||||
max-height: 400px !important;
|
max-height: 400px !important;
|
||||||
background-color: var(--lineHighlight) !important;
|
background-color: var(--lineHighlight) !important;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--background) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main tooltip container */
|
/* Main tooltip container */
|
||||||
|
|
@ -91,7 +93,7 @@
|
||||||
font-size: var(--font-size, 13px);
|
font-size: var(--font-size, 13px);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
max-height: 400px;
|
height: 100%;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
overflow-y: auto !important;
|
overflow-y: auto !important;
|
||||||
|
|
@ -112,6 +114,13 @@
|
||||||
margin: 0 0 8px 0;
|
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 {
|
.autocomplete-info-function-description {
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import jsdocJson from '../../../../../doc.json';
|
import jsdocJson from '../../../../../doc.json';
|
||||||
import { Textbox } from '../textbox/Textbox';
|
import { Textbox } from '../textbox/Textbox';
|
||||||
const availableFunctions = jsdocJson.docs
|
|
||||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
||||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
|
||||||
|
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) => {
|
const getInnerText = (html) => {
|
||||||
var div = document.createElement('div');
|
var div = document.createElement('div');
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ const fontFamilyOptions = {
|
||||||
FiraCode: 'FiraCode',
|
FiraCode: 'FiraCode',
|
||||||
'FiraCode-SemiBold': 'FiraCode SemiBold',
|
'FiraCode-SemiBold': 'FiraCode SemiBold',
|
||||||
teletext: 'teletext',
|
teletext: 'teletext',
|
||||||
|
tic80: 'tic80',
|
||||||
mode7: 'mode7',
|
mode7: 'mode7',
|
||||||
BigBlueTerminal: 'BigBlueTerminal',
|
BigBlueTerminal: 'BigBlueTerminal',
|
||||||
x3270: 'x3270',
|
x3270: 'x3270',
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@
|
||||||
src: url('/fonts/teletext/EuropeanTeletext.ttf');
|
src: url('/fonts/teletext/EuropeanTeletext.ttf');
|
||||||
size-adjust: 90%;
|
size-adjust: 90%;
|
||||||
}
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'tic80';
|
||||||
|
src: url('/fonts/tic80/tic-80-wide-font.otf');
|
||||||
|
size-adjust: 60%;
|
||||||
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'mode7';
|
font-family: 'mode7';
|
||||||
src: url('/fonts/mode7/MODE7GX3.TTF');
|
src: url('/fonts/mode7/MODE7GX3.TTF');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue