Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs
This commit is contained in:
commit
cc15e3cd36
131 changed files with 13482 additions and 7546 deletions
49
packages/core/bench/signal.bench.mjs
Normal file
49
packages/core/bench/signal.bench.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, bench } from 'vitest';
|
||||
|
||||
import { calculateSteps, rand, useRNG } from '../index.mjs';
|
||||
|
||||
const testingResolution = 128;
|
||||
|
||||
const _generateRandomPattern = () => rand.iter(testingResolution).fast(testingResolution).firstCycle();
|
||||
|
||||
describe('old random', () => {
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
useRNG('legacy');
|
||||
_generateRandomPattern();
|
||||
},
|
||||
{
|
||||
time: 1000,
|
||||
teardown() {
|
||||
useRNG('legacy');
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
useRNG('precise');
|
||||
_generateRandomPattern();
|
||||
},
|
||||
{
|
||||
time: 1000,
|
||||
teardown() {
|
||||
useRNG('legacy');
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('random', () => {
|
||||
calculateSteps(true);
|
||||
bench('+tactus', _generateRandomPattern, { time: 1000 });
|
||||
|
||||
calculateSteps(false);
|
||||
bench('-tactus', _generateRandomPattern, { time: 1000 });
|
||||
});
|
||||
|
||||
calculateSteps(true);
|
||||
|
|
@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Pattern, register, reify } from './pattern.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { Pattern, pure, register, reify } from './pattern.mjs';
|
||||
|
||||
export function createParam(names) {
|
||||
let isMulti = Array.isArray(names);
|
||||
|
|
@ -72,6 +73,27 @@ export function registerControl(names, ...aliases) {
|
|||
return bag;
|
||||
}
|
||||
|
||||
export function registerMultiControl(names, maxControls, ...aliases) {
|
||||
names = Array.isArray(names) ? names : [names];
|
||||
let bag = {};
|
||||
for (let i = 1; i <= maxControls; i++) {
|
||||
let theseAliases = [...aliases];
|
||||
let theseNames = [...names];
|
||||
if (i === 1) {
|
||||
// adds e.g. fm1 as an alias for fm
|
||||
const aliases1 = theseAliases.map((a) => `${a}1`);
|
||||
const names1 = theseNames.map((n) => `${n}1`);
|
||||
theseAliases = theseAliases.concat(aliases1).concat(names1);
|
||||
} else {
|
||||
theseAliases = theseAliases.map((a) => `${a}${i}`);
|
||||
theseNames = theseNames.map((n) => `${n}${i}`);
|
||||
}
|
||||
const subBag = registerControl(theseNames, ...theseAliases);
|
||||
bag = { ...bag, ...subBag };
|
||||
}
|
||||
return bag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
||||
* separated by ':'.
|
||||
|
|
@ -412,12 +434,13 @@ export const { accelerate } = registerControl('accelerate');
|
|||
*
|
||||
* @name velocity
|
||||
* @tags fx, superdough, supradough
|
||||
* @synonyms vel
|
||||
* @example
|
||||
* s("hh*8")
|
||||
* .gain(".4!2 1 .4!2 1 .4 1")
|
||||
* .velocity(".4 1")
|
||||
*/
|
||||
export const { velocity } = registerControl('velocity');
|
||||
export const { velocity, vel } = registerControl('velocity', 'vel');
|
||||
/**
|
||||
* Controls the gain by an exponential amount.
|
||||
*
|
||||
|
|
@ -471,6 +494,9 @@ export const { attack, att } = registerControl('attack', 'att');
|
|||
* Whole numbers and simple ratios sound more natural,
|
||||
* while decimal numbers and complex ratios sound metallic.
|
||||
*
|
||||
* A number may be added afterwards to control the harmonicity of
|
||||
* any of the 8 individual FMs (e.g. `fmh2`)
|
||||
*
|
||||
* @name fmh
|
||||
* @tags fx, superdough, supradough
|
||||
* @param {number | Pattern} harmonicity
|
||||
|
|
@ -481,26 +507,41 @@ export const { attack, att } = registerControl('attack', 'att');
|
|||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
|
||||
export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh');
|
||||
|
||||
/**
|
||||
* Sets the Frequency Modulation of the synth.
|
||||
* Controls the modulation index, which defines the brightness of the sound.
|
||||
*
|
||||
* @name fm
|
||||
* A number may be added afterwards to control the modulation index of
|
||||
* any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into
|
||||
* each other with matrix commands like `fm13`, which would send `fm1` back into
|
||||
* `fm3`
|
||||
*
|
||||
* @name fmi
|
||||
* @tags fx, superdough, supradough
|
||||
* @param {number | Pattern} brightness modulation index
|
||||
* @synonyms fmi
|
||||
* @synonyms fm
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
* .fm("<0 1 2 8 32>")
|
||||
* ._scope()
|
||||
* @example
|
||||
* s("sine").note("F1").seg(8)
|
||||
* .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8))
|
||||
* .fmh(1.06).fmh2(10).fmh3(0.1)
|
||||
*
|
||||
*/
|
||||
export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
||||
export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } =
|
||||
registerMultiControl(['fmi', 'fmh'], 8, 'fm');
|
||||
|
||||
// fm envelope
|
||||
/**
|
||||
* Ramp type of fm envelope. Exp might be a bit broken..
|
||||
*
|
||||
* A number may be added afterwards to control the envelope of
|
||||
* any of the 8 individual FMs (e.g. `fmenv4`)
|
||||
*
|
||||
* @name fmenv
|
||||
* @tags fx, superdough, supradough
|
||||
* @param {number | Pattern} type lin | exp
|
||||
|
|
@ -513,12 +554,20 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
|||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmenv } = registerControl('fmenv');
|
||||
export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl(
|
||||
'fmenv',
|
||||
8,
|
||||
);
|
||||
|
||||
/**
|
||||
* Attack time for the FM envelope: time it takes to reach maximum modulation
|
||||
*
|
||||
* A number may be added afterwards to control the attack of the envelope of
|
||||
* any of the 8 individual FMs (e.g. `fmatt5`)
|
||||
*
|
||||
* @name fmattack
|
||||
* @tags fx, superdough, supradough
|
||||
* @synonyms fmatt
|
||||
* @param {number | Pattern} time attack time
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
|
|
@ -527,11 +576,33 @@ export const { fmenv } = registerControl('fmenv');
|
|||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmattack } = registerControl('fmattack');
|
||||
export const {
|
||||
fmattack,
|
||||
fmattack1,
|
||||
fmattack2,
|
||||
fmattack3,
|
||||
fmattack4,
|
||||
fmattack5,
|
||||
fmattack6,
|
||||
fmattack7,
|
||||
fmattack8,
|
||||
fmatt,
|
||||
fmatt1,
|
||||
fmatt2,
|
||||
fmatt3,
|
||||
fmatt4,
|
||||
fmatt5,
|
||||
fmatt6,
|
||||
fmatt7,
|
||||
fmatt8,
|
||||
} = registerMultiControl('fmattack', 8, 'fmatt');
|
||||
|
||||
/**
|
||||
* Waveform of the fm modulator
|
||||
*
|
||||
* A number may be added afterwards to control the waveform
|
||||
* any of the 8 individual FMs (e.g. `fmwave6`)
|
||||
*
|
||||
* @name fmwave
|
||||
* @tags fx, superdough, supradough
|
||||
* @param {number | Pattern} wave waveform
|
||||
|
|
@ -541,13 +612,20 @@ export const { fmattack } = registerControl('fmattack');
|
|||
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
||||
*
|
||||
*/
|
||||
export const { fmwave } = registerControl('fmwave');
|
||||
export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl(
|
||||
'fmwave',
|
||||
8,
|
||||
);
|
||||
|
||||
/**
|
||||
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
||||
*
|
||||
* A number may be added afterwards to control the decay of the envelope of
|
||||
* any of the 8 individual FMs (e.g. `fmdec6`)
|
||||
*
|
||||
* @name fmdecay
|
||||
* @tags fx, superdough, supradough
|
||||
* @synonyms fmdec
|
||||
* @param {number | Pattern} time decay time
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
|
|
@ -557,12 +635,36 @@ export const { fmwave } = registerControl('fmwave');
|
|||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmdecay } = registerControl('fmdecay');
|
||||
export const {
|
||||
fmdecay,
|
||||
fmdecay1,
|
||||
fmdecay2,
|
||||
fmdecay3,
|
||||
fmdecay4,
|
||||
fmdecay5,
|
||||
fmdecay6,
|
||||
fmdecay7,
|
||||
fmdecay8,
|
||||
fmdec,
|
||||
fmdec1,
|
||||
fmdec2,
|
||||
fmdec3,
|
||||
fmdec4,
|
||||
fmdec5,
|
||||
fmdec6,
|
||||
fmdec7,
|
||||
fmdec8,
|
||||
} = registerMultiControl('fmdecay', 8, 'fmdec');
|
||||
|
||||
/**
|
||||
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
|
||||
*
|
||||
* A number may be added afterwards to control the sustain of the envelope of
|
||||
* any of the 8 individual FMs (e.g. `fmsus7`)
|
||||
*
|
||||
* @name fmsustain
|
||||
* @tags fx, superdough, supradough
|
||||
* @synonyms fmsus
|
||||
* @param {number | Pattern} level sustain level
|
||||
* @example
|
||||
* note("c e g b g e")
|
||||
|
|
@ -572,10 +674,69 @@ export const { fmdecay } = registerControl('fmdecay');
|
|||
* ._scope()
|
||||
*
|
||||
*/
|
||||
export const { fmsustain } = registerControl('fmsustain');
|
||||
// these are not really useful... skipping for now
|
||||
export const { fmrelease } = registerControl('fmrelease');
|
||||
export const { fmvelocity } = registerControl('fmvelocity');
|
||||
export const {
|
||||
fmsustain,
|
||||
fmsustain1,
|
||||
fmsustain2,
|
||||
fmsustain3,
|
||||
fmsustain4,
|
||||
fmsustain5,
|
||||
fmsustain6,
|
||||
fmsustain7,
|
||||
fmsustain8,
|
||||
fmsus,
|
||||
fmsus1,
|
||||
fmsus2,
|
||||
fmsus3,
|
||||
fmsus4,
|
||||
fmsus5,
|
||||
fmsus6,
|
||||
fmsus7,
|
||||
fmsus8,
|
||||
} = registerMultiControl('fmsustain', 8, 'fmsus');
|
||||
|
||||
/**
|
||||
* Release time for the FM envelope: how much modulation is applied after the note is released
|
||||
*
|
||||
* A number may be added afterwards to control the release of the envelope of
|
||||
* any of the 8 individual FMs (e.g. `fmrel8`)
|
||||
*
|
||||
* @name fmrelease
|
||||
* @synonyms fmrel
|
||||
* @param {number | Pattern} time release time
|
||||
*
|
||||
*/
|
||||
export const {
|
||||
fmrelease,
|
||||
fmrelease1,
|
||||
fmrelease2,
|
||||
fmrelease3,
|
||||
fmrelease4,
|
||||
fmrelease5,
|
||||
fmrelease6,
|
||||
fmrelease7,
|
||||
fmrelease8,
|
||||
fmrel,
|
||||
fmrel1,
|
||||
fmrel2,
|
||||
fmrel3,
|
||||
fmrel4,
|
||||
fmrel5,
|
||||
fmrel6,
|
||||
fmrel7,
|
||||
fmrel8,
|
||||
} = registerMultiControl('fmrelease', 8, 'fmrel');
|
||||
|
||||
// FM Matrix
|
||||
// Note: we do not declare top-level exports here since it would add
|
||||
// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least
|
||||
// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it,
|
||||
// TODO to add it explicitly / go with the globalThis approach
|
||||
for (let i = 0; i <= 8; i++) {
|
||||
for (let j = 0; j <= 8; j++) {
|
||||
registerControl(`fmi${i}${j}`, `fm${i}${j}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
||||
|
|
@ -1387,6 +1548,193 @@ export const { fanchor } = registerControl('fanchor');
|
|||
*/
|
||||
// currently an alias of 'hcutoff' https://codeberg.org/uzu/strudel/issues/496
|
||||
// ['hpf'],
|
||||
|
||||
/**
|
||||
* Rate of the LFO for the lowpass filter
|
||||
*
|
||||
* @name lprate
|
||||
* @param {number | Pattern} rate rate in hertz
|
||||
* @example
|
||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>")
|
||||
*/
|
||||
export const { lprate } = registerControl('lprate');
|
||||
|
||||
/**
|
||||
* Cycle-synced rate of the LFO for the lowpass filter
|
||||
*
|
||||
* @name lpsync
|
||||
* @param {number | Pattern} rate rate in cycles
|
||||
* @example
|
||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>")
|
||||
*/
|
||||
export const { lpsync } = registerControl('lpsync');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the lowpass filter
|
||||
*
|
||||
* @name lpdepth
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
* @example
|
||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>")
|
||||
*/
|
||||
|
||||
export const { lpdepth } = registerControl('lpdepth');
|
||||
/**
|
||||
* Depth of the LFO for the lowpass filter, in HZ
|
||||
*
|
||||
* @name lpdepthfrequency
|
||||
* @synonyms lpdepthfreq
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
* @example
|
||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>")
|
||||
*/
|
||||
|
||||
export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequency', 'lpdepthfreq');
|
||||
|
||||
/**
|
||||
* Shape of the LFO for the lowpass filter
|
||||
*
|
||||
* @name lpshape
|
||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||
*/
|
||||
export const { lpshape } = registerControl('lpshape');
|
||||
|
||||
/**
|
||||
* DC offset of the LFO for the lowpass filter
|
||||
*
|
||||
* @name lpdc
|
||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||
*/
|
||||
export const { lpdc } = registerControl('lpdc');
|
||||
|
||||
/**
|
||||
* Skew of the LFO for the lowpass filter
|
||||
*
|
||||
* @name lpskew
|
||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||
*/
|
||||
export const { lpskew } = registerControl('lpskew');
|
||||
|
||||
/**
|
||||
* Rate of the LFO for the bandpass filter
|
||||
*
|
||||
* @name bprate
|
||||
* @param {number | Pattern} rate rate in hertz
|
||||
*/
|
||||
export const { bprate } = registerControl('bprate');
|
||||
|
||||
/**
|
||||
* Cycle-synced rate of the LFO for the bandpass filter
|
||||
*
|
||||
* @name bpsync
|
||||
* @param {number | Pattern} rate rate in cycles
|
||||
*/
|
||||
export const { bpsync } = registerControl('bpsync');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the bandpass filter
|
||||
*
|
||||
* @name bpdepth
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
*/
|
||||
export const { bpdepth } = registerControl('bpdepth');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the bandpass filter, in HZ
|
||||
*
|
||||
* @name bpdepthfrequency
|
||||
* @synonyms bpdepthfreq
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
* @example
|
||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>")
|
||||
*/
|
||||
|
||||
export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequency', 'bpdepthfreq');
|
||||
|
||||
/**
|
||||
* Shape of the LFO for the bandpass filter
|
||||
*
|
||||
* @name bpshape
|
||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||
*/
|
||||
export const { bpshape } = registerControl('bpshape');
|
||||
|
||||
/**
|
||||
* DC offset of the LFO for the bandpass filter
|
||||
*
|
||||
* @name bpdc
|
||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||
*/
|
||||
export const { bpdc } = registerControl('bpdc');
|
||||
|
||||
/**
|
||||
* Skew of the LFO for the bandpass filter
|
||||
*
|
||||
* @name bpskew
|
||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||
*/
|
||||
export const { bpskew } = registerControl('bpskew');
|
||||
|
||||
/**
|
||||
* Rate of the LFO for the highpass filter
|
||||
*
|
||||
* @name hprate
|
||||
* @param {number | Pattern} rate rate in hertz
|
||||
*/
|
||||
export const { hprate } = registerControl('hprate');
|
||||
|
||||
/**
|
||||
* Cycle-synced rate of the LFO for the highpass filter
|
||||
*
|
||||
* @name hpsync
|
||||
* @param {number | Pattern} rate rate in cycles
|
||||
*/
|
||||
export const { hpsync } = registerControl('hpsync');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the highpass filter
|
||||
*
|
||||
* @name hpdepth
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
*/
|
||||
export const { hpdepth } = registerControl('hpdepth');
|
||||
|
||||
/**
|
||||
* Depth of the LFO for the hipass filter, in hz
|
||||
*
|
||||
* @name hpdepthfrequency
|
||||
* @synonyms hpdepthfreq
|
||||
* @param {number | Pattern} depth depth of modulation
|
||||
* @example
|
||||
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>")
|
||||
*/
|
||||
|
||||
export const { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequency', 'hpdepthfreq');
|
||||
|
||||
/**
|
||||
* Shape of the LFO for the highpass filter
|
||||
*
|
||||
* @name hpshape
|
||||
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
|
||||
*/
|
||||
export const { hpshape } = registerControl('hpshape');
|
||||
|
||||
/**
|
||||
* DC offset of the LFO for the highpass filter
|
||||
*
|
||||
* @name hpdc
|
||||
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
|
||||
*/
|
||||
export const { hpdc } = registerControl('hpdc');
|
||||
|
||||
/**
|
||||
* Skew of the LFO for the highpass filter
|
||||
*
|
||||
* @name hpskew
|
||||
* @param {number | Pattern} skew How much to bend the LFO shape
|
||||
*/
|
||||
export const { hpskew } = registerControl('hpskew');
|
||||
|
||||
/**
|
||||
* Applies a vibrato to the frequency of the oscillator.
|
||||
*
|
||||
|
|
@ -1802,12 +2150,12 @@ export const { nudge } = registerControl('nudge');
|
|||
*
|
||||
* @name octave
|
||||
* @tags fx, superdirt
|
||||
* @synonyms oct
|
||||
* @param {number | Pattern} octave octave number
|
||||
* @example
|
||||
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
|
||||
* @superDirtOnly
|
||||
* n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>")
|
||||
*/
|
||||
export const { octave } = registerControl('octave');
|
||||
export const { octave, oct } = registerControl('octave', 'oct');
|
||||
|
||||
// ['ophatdecay'],
|
||||
// TODO: example
|
||||
|
|
@ -1816,6 +2164,7 @@ export const { octave } = registerControl('octave');
|
|||
*
|
||||
* @name orbit
|
||||
* @tags fx, superdough
|
||||
* @synonyms o
|
||||
* @param {number | Pattern} number
|
||||
* @example
|
||||
* stack(
|
||||
|
|
@ -1823,7 +2172,29 @@ export const { octave } = registerControl('octave');
|
|||
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
|
||||
* )
|
||||
*/
|
||||
export const { orbit } = registerControl('orbit');
|
||||
export const { orbit } = registerControl('orbit', 'o');
|
||||
|
||||
/**
|
||||
* A `bus` is a send which can be used for mixing patterns. It combines with..
|
||||
* s("bus") to play that bus through another pattern (for, say, applying non-linear
|
||||
* effects like distortion to multiple signals)
|
||||
*
|
||||
* otherPat.bmod(..) (to modulate another pattern with the bus)
|
||||
*
|
||||
* @name bus
|
||||
* @param {number | Pattern} number
|
||||
*/
|
||||
export const { bus } = registerControl('bus');
|
||||
|
||||
/**
|
||||
* Postgain multiplier prior to sending the signal to the audio bus.
|
||||
*
|
||||
* @name busgain
|
||||
* @synonyms bgain
|
||||
* @param {number | Pattern} number
|
||||
*/
|
||||
export const { busgain, bgain } = registerControl('busgain', 'bgain');
|
||||
|
||||
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
||||
export const { overgain } = registerControl('overgain');
|
||||
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
||||
|
|
@ -1869,8 +2240,7 @@ export const { panorient } = registerControl('panorient');
|
|||
// ['pitch2'],
|
||||
// ['pitch3'],
|
||||
// ['portamento'],
|
||||
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
export const { rate } = registerControl('rate');
|
||||
|
||||
// TODO: slide param for certain synths
|
||||
export const { slide } = registerControl('slide');
|
||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||
|
|
@ -1879,17 +2249,64 @@ export const { semitone } = registerControl('semitone');
|
|||
// TODO: synth param
|
||||
export const { voice } = registerControl('voice');
|
||||
// voicings // https://codeberg.org/uzu/strudel/issues/506
|
||||
// chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
|
||||
/**
|
||||
* The chord to voice
|
||||
* @name chord
|
||||
* @param {string | Pattern} symbols chord symbols to voice e.g., C, Eb, Fm7, G7. The symbols can be defined via addVoicings
|
||||
* @example
|
||||
* chord("<Am C D F Am E Am E>").voicing()
|
||||
**/
|
||||
export const { chord } = registerControl('chord');
|
||||
// which dictionary to use for the voicings
|
||||
/**
|
||||
* Which dictionary to use for the voicings. This falls back to the default dictionary if not provided
|
||||
*
|
||||
* @name dictionary
|
||||
* @param {string} dictionaryName which dictionary (having been defined with `addVoicings`) to use
|
||||
* @example
|
||||
* addVoicings('house', {
|
||||
'': ['7 12 16', '0 7 16', '4 7 12'],
|
||||
'm': ['0 3 7']
|
||||
})
|
||||
chord("<Am C D F Am E Am E>")
|
||||
.dict('house').anchor(66)
|
||||
.voicing().room(.5)
|
||||
**/
|
||||
export const { dictionary, dict } = registerControl('dictionary', 'dict');
|
||||
// the top note to align the voicing to, defaults to c5
|
||||
/** The top note to align the voicing to. Defaults to c5
|
||||
*
|
||||
* @name anchor
|
||||
* @param {string | Pattern} anchorNote the note to align the voicings to
|
||||
* @example
|
||||
* anchor("<c4 g4 c5 g5>").chord("C").voicing()
|
||||
**/
|
||||
export const { anchor } = registerControl('anchor');
|
||||
// how the voicing is offset from the anchored position
|
||||
/**
|
||||
* Sets how the voicing is offset from the anchored position
|
||||
*
|
||||
* @name offset
|
||||
* @param {number | Pattern} shift the amount to shift the voicing up or down
|
||||
* @example
|
||||
* chord("<Am C D F Am E Am E>").offset("<0 1 2 3 4 5>") // alter the voicing each time
|
||||
**/
|
||||
export const { offset } = registerControl('offset');
|
||||
// how many octaves are voicing steps spread apart, defaults to 1
|
||||
/**
|
||||
* How many octaves are voicing steps spread apart, defaults to 1
|
||||
*
|
||||
* @name octaves
|
||||
* @param {number | Pattern} count the number of octaves
|
||||
* @example
|
||||
* chord("<Am C D F Am E Am E>").octaves("<2 4>").voicing()
|
||||
**/
|
||||
export const { octaves } = registerControl('octaves');
|
||||
// below = anchor note will be removed from the voicing, useful for melody harmonization
|
||||
/**
|
||||
* Remove anchor note from the voicing. Useful for melody harmonization
|
||||
*
|
||||
* @name mode
|
||||
* @param {string | Pattern} modeName one of {below | above | duck | root}
|
||||
* @example
|
||||
* mode("<below above duck root>").chord("C").voicing()
|
||||
*
|
||||
**/
|
||||
export const { mode } = registerControl(['mode', 'anchor']);
|
||||
|
||||
/**
|
||||
|
|
@ -2220,7 +2637,6 @@ export const { tsdelay } = registerControl('tsdelay');
|
|||
export const { real } = registerControl('real');
|
||||
export const { imag } = registerControl('imag');
|
||||
export const { enhance } = registerControl('enhance');
|
||||
export const { partials } = registerControl('partials');
|
||||
export const { comb } = registerControl('comb');
|
||||
export const { smear } = registerControl('smear');
|
||||
export const { scram } = registerControl('scram');
|
||||
|
|
@ -2271,7 +2687,6 @@ export const { curve } = registerControl('curve');
|
|||
export const { deltaSlide } = registerControl('deltaSlide');
|
||||
export const { pitchJump } = registerControl('pitchJump');
|
||||
export const { pitchJumpTime } = registerControl('pitchJumpTime');
|
||||
export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime');
|
||||
// noise on the frequency or as bubo calls it "frequency fog" :)
|
||||
export const { znoise } = registerControl('znoise');
|
||||
export const { zmod } = registerControl('zmod');
|
||||
|
|
@ -2528,8 +2943,13 @@ export const as = register('as', (mapping, pat) => {
|
|||
mapping = Array.isArray(mapping) ? mapping : [mapping];
|
||||
return pat.fmap((v) => {
|
||||
v = Array.isArray(v) ? v : [v];
|
||||
v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]]));
|
||||
return v;
|
||||
const entries = [];
|
||||
for (let i = 0; i < mapping.length; ++i) {
|
||||
if (v[i] !== undefined) {
|
||||
entries.push([getControlName(mapping[i]), v[i]]);
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(entries);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -2562,3 +2982,267 @@ export const scrub = register(
|
|||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const subControlAliases = new Map();
|
||||
const registerSubControl = (control, subControl, ...aliases) => {
|
||||
const aliasMap = subControlAliases.get(control) ?? new Map();
|
||||
const allKeys = new Set([subControl, ...aliases]);
|
||||
for (const alias of allKeys) {
|
||||
aliasMap.set(String(alias).toLowerCase(), subControl);
|
||||
}
|
||||
subControlAliases.set(control, aliasMap);
|
||||
};
|
||||
|
||||
const registerSubControls = (control, subControlAliases = []) => {
|
||||
for (const [subControl, ...aliases] of subControlAliases) {
|
||||
registerSubControl(control, subControl, ...aliases);
|
||||
}
|
||||
};
|
||||
|
||||
const getMainSubcontrolName = (control, subKey) => {
|
||||
const aliasMap = subControlAliases.get(control);
|
||||
if (!aliasMap) return subKey;
|
||||
return aliasMap.get(String(subKey).toLowerCase()) ?? subKey;
|
||||
};
|
||||
|
||||
registerSubControls('lfo', [
|
||||
['control', 'c'],
|
||||
['subControl', 'sc'],
|
||||
['rate', 'r'],
|
||||
['depth', 'dep', 'dr'],
|
||||
['depthabs', 'da'],
|
||||
['dcoffset', 'dc'],
|
||||
['shape', 'sh'],
|
||||
['skew', 'sk'],
|
||||
['curve', 'cu'],
|
||||
['sync', 's'],
|
||||
['fxi'],
|
||||
]);
|
||||
registerSubControls('env', [
|
||||
['control', 'c'],
|
||||
['subControl', 'sc'],
|
||||
['attack', 'att', 'a'],
|
||||
['decay', 'dec', 'd'],
|
||||
['sustain', 'sus', 's'],
|
||||
['release', 'rel', 'r'],
|
||||
['depth', 'dep', 'dr'],
|
||||
['depthabs', 'da'],
|
||||
['acurve', 'ac'],
|
||||
['dcurve', 'dc'],
|
||||
['rcurve', 'rc'],
|
||||
['fxi'],
|
||||
]);
|
||||
registerSubControls('bmod', [
|
||||
['bus', 'b'],
|
||||
['control', 'c'],
|
||||
['subControl', 'sc'],
|
||||
['depth', 'dep', 'dr'],
|
||||
['depthabs', 'da'],
|
||||
['dc'],
|
||||
['fxi'],
|
||||
]);
|
||||
|
||||
Pattern.prototype.modulate = function (type, config, idPat) {
|
||||
config = { control: undefined, ...config };
|
||||
const modulatorKeys = ['lfo', 'env', 'bmod'];
|
||||
if (!modulatorKeys.includes(type)) {
|
||||
logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`);
|
||||
return this;
|
||||
}
|
||||
let output = this;
|
||||
let defaultValue = undefined;
|
||||
// Copy value into a temporary `v` container and attach a single `id` (to be shared across
|
||||
// each config entry). At the output we destructure and throw away the id
|
||||
output = output.fmap((v) => (id) => ({ v, id })).appLeft(reify(idPat));
|
||||
for (const [rawKey, value] of Object.entries(config)) {
|
||||
const key = getMainSubcontrolName(type, rawKey);
|
||||
const valuePat = reify(value);
|
||||
output = output
|
||||
.fmap(({ v, id }) => (c) => {
|
||||
if (defaultValue === undefined) {
|
||||
// default control to the control set just before this in the chain
|
||||
// e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO
|
||||
let control = getControlName(Object.keys(v).at(-1));
|
||||
if (modulatorKeys.includes(control)) {
|
||||
control = `${control}_${[...v[control].__ids].at(-1)}`;
|
||||
}
|
||||
defaultValue = control;
|
||||
}
|
||||
v[type] ??= { __ids: new Set() };
|
||||
const t = v[type];
|
||||
id ??= t.__ids.size;
|
||||
t[id] ??= { control: defaultValue };
|
||||
t.__ids.add(id); // keeps track of insertion order
|
||||
if (c === undefined) return { v, id };
|
||||
if (key === 'control' || key === 'subControl') {
|
||||
t[id][key] = getControlName(c);
|
||||
} else {
|
||||
t[id][key] = c;
|
||||
}
|
||||
return { v, id };
|
||||
})
|
||||
.appLeft(valuePat);
|
||||
}
|
||||
return output.fmap(({ v }) => v);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs.
|
||||
* There are two ways to declare which control will be modulated:
|
||||
* 1. Explicitly put `control` in the config (e.g. `lfo({ c: "lpf" })`)
|
||||
* 2. If the control parameter is absent, the control _immediately before_ the `lfo` call will be used
|
||||
* (e.g. `s("saw").lpf(500).lfo()` to modulate `lpf`)
|
||||
*
|
||||
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||
* a `sometimes`. See example below.
|
||||
*
|
||||
* @name lfo
|
||||
* @param {Object} config LFO configuration.
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: r
|
||||
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||
* @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc
|
||||
* @param {number | Pattern} [config.shape] Shape index. Aliases: sh
|
||||
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
|
||||
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu
|
||||
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
|
||||
* @param {number | Pattern} [config.fxi] FX index to target
|
||||
* @param {string | Pattern} id ID to use for this modulator
|
||||
* @returns Pattern
|
||||
*
|
||||
* @example
|
||||
* s("saw").note("F1").lpf(500).lfo()
|
||||
*
|
||||
* @example
|
||||
* s("saw").lfo().lpf(500).lfo({ s: 0.3 })
|
||||
*
|
||||
* @example
|
||||
* s("saw").lpf(500).diode(0.3)
|
||||
* .lfo({ c: "lpf" })
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).lfo()
|
||||
* .lfo({ c: "s" })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.lfo({ s: "8" }, 1)) // lfo #1 (0-indexed)
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).lfo({ depth: 4 }, 'lpf_mod')
|
||||
* .lfo({ c: "s" })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.lfo({ s: "8" }, 'lpf_mod'))
|
||||
*/
|
||||
Pattern.prototype.lfo = function (config, id) {
|
||||
return this.modulate('lfo', config, id);
|
||||
};
|
||||
export const lfo = (config) => pure({}).lfo(config);
|
||||
|
||||
/**
|
||||
* Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes
|
||||
* There are two ways to declare which control will be modulated:
|
||||
* 1. Explicitly put `control` in the config (e.g. `env({ c: "lpf" })`)
|
||||
* 2. If the control parameter is absent, the control _immediately before_ the `env` call will be used
|
||||
* (e.g. `s("saw").lpf(500).env({ a: 1 })` to modulate `lpf`)
|
||||
*
|
||||
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||
* a `sometimes`. See example below.
|
||||
*
|
||||
* @name env
|
||||
* @param {Object} config Envelope configuration.
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||
* @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a
|
||||
* @param {number | Pattern} [config.decay] Time to reach sustain. Aliases: dec, d
|
||||
* @param {number | Pattern} [config.sustain] Sustain depth. Aliases: sus, s
|
||||
* @param {number | Pattern} [config.release] Time to return to nominal value. Aliases: rel, r
|
||||
* @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac
|
||||
* @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc
|
||||
* @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc
|
||||
* @param {number | Pattern} [config.fxi] FX index to target
|
||||
* @param {string | Pattern} id ID to use for this modulator
|
||||
* @returns Pattern
|
||||
*
|
||||
* @example
|
||||
* s("saw").note("F1").lpf(500).env({ a: 1 })
|
||||
*
|
||||
* @example
|
||||
* s("saw").env({ d: 1 }).note("F1")
|
||||
* .lpq(4).lpf(50)
|
||||
* .env({ a: 0.1, d: 1, ac: 0.8, dc: 0.3, depth: 50 })
|
||||
*
|
||||
* @example
|
||||
* s("saw").lpf(500).diode(0.3)
|
||||
* .env({ c: "lpf", a: 0.5, d: 0.5 })
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).env({ a: 1 })
|
||||
* .env({ c: "s", a: 1 })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.env({ a: "0.5" }, 1)) // envelope #1 (0-indexed)
|
||||
*
|
||||
* @example
|
||||
* s("pulse").lpf(500).env({ a: 1 }, 'lpf_mod')
|
||||
* .env({ c: "s", a: 1 })
|
||||
* .diode(0.3)
|
||||
* .sometimes(x => x.env({ a: "0.5" }, 'lpf_mod'))
|
||||
*/
|
||||
Pattern.prototype.env = function (config, id) {
|
||||
return this.modulate('env', config, id);
|
||||
};
|
||||
export const env = (config) => pure({}).env(config);
|
||||
|
||||
/**
|
||||
* Modulates with the output from a given `bus`.
|
||||
* Can be called in sequence like pat.bmod(...).bmod(...) to set up multiple modulators
|
||||
*
|
||||
* Send to an audio bus with `otherPat.bus(..)`.
|
||||
*
|
||||
* There are two ways to declare which control will be modulated:
|
||||
* 1. Explicitly put `control` in the config (e.g. `bmod({ id: 2, c: "lpf" })`)
|
||||
* 2. If the control parameter is absent, the control _immediately before_ the `bmod` call will be used
|
||||
* (e.g. `s("saw").lpf(500).bmod({ id: 2 })` to modulate `lpf`)
|
||||
*
|
||||
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||
* a `sometimes`. See example below.
|
||||
*
|
||||
* @name bmod
|
||||
* @param {Object} config Bus modulation configuration.
|
||||
* @param {string | Pattern} [config.bus] Bus to get modulation signal from
|
||||
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||
* @param {number | Pattern} [config.dc] DC offset prior to application
|
||||
* @param {number | Pattern} [config.fxi] FX index to target
|
||||
* @param {string | Pattern} id ID to use for this modulator
|
||||
* @returns Pattern
|
||||
*
|
||||
* @example
|
||||
* modulator: s("one").seg(64).gain(slider(0, 0, 1)).bus(1).dry(0)
|
||||
* carrier: s("saw").bmod({ b: 1 })
|
||||
*
|
||||
*/
|
||||
Pattern.prototype.bmod = function (config, id) {
|
||||
return this.modulate('bmod', config, id);
|
||||
};
|
||||
export const bmod = (config) => pure({}).bmod(config);
|
||||
|
||||
/**
|
||||
* Transient shaper. Gives independent control over the emphasis on transients
|
||||
* and sustains
|
||||
*
|
||||
* @name transient
|
||||
* @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate)
|
||||
* @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate)
|
||||
* @example
|
||||
* s("bd").transient("<-1 -0.5 0 0.5 1>")
|
||||
* @example
|
||||
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
|
||||
*/
|
||||
export const { transient } = registerControl(['transient', 'transsustain']);
|
||||
|
||||
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
|
||||
|
|
|
|||
|
|
@ -35,18 +35,18 @@ const right = function (n, x) {
|
|||
return result;
|
||||
};
|
||||
|
||||
const _bjork = function (n, x) {
|
||||
const _bjorklund = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
|
||||
return Math.min(ons, offs) <= 1 ? [n, x] : _bjorklund(...(ons > offs ? left(n, x) : right(n, x)));
|
||||
};
|
||||
|
||||
export const bjork = function (ons, steps) {
|
||||
export const bjorklund = function (ons, steps) {
|
||||
const inverted = ons < 0;
|
||||
const absOns = Math.abs(ons);
|
||||
const offs = steps - absOns;
|
||||
const ones = Array(absOns).fill([1]);
|
||||
const zeros = Array(offs).fill([0]);
|
||||
const result = _bjork([absOns, offs], [ones, zeros]);
|
||||
const result = _bjorklund([absOns, offs], [ones, zeros]);
|
||||
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||
return inverted ? pattern.map((x) => 1 - x) : pattern;
|
||||
};
|
||||
|
|
@ -130,7 +130,7 @@ export const bjork = function (ons, steps) {
|
|||
*/
|
||||
|
||||
const _euclidRot = function (pulses, steps, rotation) {
|
||||
const b = bjork(pulses, steps);
|
||||
const b = bjorklund(pulses, steps);
|
||||
if (rotation) {
|
||||
return rotate(b, -rotation);
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
|
|||
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||
});
|
||||
|
||||
export const e = register('e', function (euc, pat) {
|
||||
export const bjork = register('bjork', function (euc, pat) {
|
||||
if (!Array.isArray(euc)) {
|
||||
euc = [euc];
|
||||
}
|
||||
|
|
@ -221,6 +221,6 @@ export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, s
|
|||
* .pan(sine.slow(8))
|
||||
*/
|
||||
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
||||
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
|
||||
const morphed = _morph(bjorklund(pulses, steps), new Array(pulses).fill(1), perc);
|
||||
return pat.struct(morphed).setSteps(steps);
|
||||
});
|
||||
|
|
|
|||
90
packages/core/impure.mjs
Normal file
90
packages/core/impure.mjs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
stateful.mjs - File of shame for stateful, impure and otherwise illegal pattern methods
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { register, reify, Pattern } from './pattern.mjs';
|
||||
|
||||
let timelines = {};
|
||||
|
||||
export const reset_state = function () {
|
||||
reset_timelines();
|
||||
};
|
||||
|
||||
export const reset_timelines = function () {
|
||||
timelines = {};
|
||||
};
|
||||
|
||||
/***
|
||||
* Allows you to switch a pattern between different 'timelines'. This is particularly useful when
|
||||
* live coding, for example when you want to cue a pattern up to play from its start.
|
||||
*
|
||||
* Timelines are specified by number, so that if you had a pattern like
|
||||
* `n("<0 1 2 3>").s("num").timeline(1)` playing, then changed the '1'
|
||||
* to '2', it would always align '0' to the nearest cycle. You will likely want to trigger
|
||||
* an evaluation a little bit before the cycle starts, to avoid missing events.
|
||||
*
|
||||
* After the first use, a timeline will continue with the same 'offset'. That is, if you change
|
||||
* a pattern without changing its timeline number, it will stay on that timeline without resetting.
|
||||
*
|
||||
* Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2`
|
||||
* and `2`. This is because when you negate a timeline it will always reset.
|
||||
*
|
||||
* You can also pattern the timeline if you want, to create strange resetting patterns.
|
||||
* @param {number | Pattern} timeline The timeline that the pattern should play on.
|
||||
* @example
|
||||
* n("<0 1 2 3>(3,8)")
|
||||
* .sound("num")
|
||||
* // resets the timeline every two cycles, by negating the timeline.
|
||||
* // in a lot of cases this will be edited by a human live coder
|
||||
* // rather than patterned!
|
||||
* .timeline("<2 -2>".slow(2))
|
||||
*/
|
||||
|
||||
export const timeline = register(
|
||||
'timeline',
|
||||
function (tpat, pat) {
|
||||
tpat = reify(tpat);
|
||||
const f = function (state) {
|
||||
// Is this called from the scheduler? (rather than from e.g. the visualiser)
|
||||
const scheduler = !!state.controls.cyclist;
|
||||
const timehaps = tpat.query(state);
|
||||
const result = [];
|
||||
for (const timehap of timehaps) {
|
||||
const tlid = timehap.value;
|
||||
let offset;
|
||||
if (tlid === 0) {
|
||||
offset = 0;
|
||||
} else if (tlid in timelines) {
|
||||
offset = timelines[tlid];
|
||||
} else {
|
||||
const timearc = timehap.wholeOrPart();
|
||||
if (!scheduler || state.span.begin.lt(timearc.midpoint())) {
|
||||
offset = timearc.begin;
|
||||
} else {
|
||||
// Sync to end of timearc if we first see it over halfway into its
|
||||
// timespan. Allows 'cuing up' next timeline when live coding.
|
||||
offset = timearc.end;
|
||||
}
|
||||
}
|
||||
if (scheduler) {
|
||||
// update state
|
||||
timelines[tlid] = offset;
|
||||
if (tlid !== 0) {
|
||||
delete timelines[-tlid];
|
||||
}
|
||||
}
|
||||
|
||||
const pathaps = pat
|
||||
.late(offset)
|
||||
.query(state.setSpan(timehap.part))
|
||||
.map((h) => h.setContext(h.combineContext(timehap)));
|
||||
result.push(...pathaps);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return new Pattern(f, pat._steps);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
index.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
|
@ -11,20 +11,21 @@ import createClock from './zyklus.mjs';
|
|||
import { logger } from './logger.mjs';
|
||||
export { Fraction, controls, createClock };
|
||||
export * from './controls.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './pick.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './cyclist.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './hap.mjs';
|
||||
export * from './impure.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './pick.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './signal.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './state.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './ui.mjs';
|
||||
export * from './util.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
stringifyValues,
|
||||
} from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
|
||||
let stringParser;
|
||||
|
||||
|
|
@ -420,7 +420,7 @@ export class Pattern {
|
|||
try {
|
||||
return this.query(new State(new TimeSpan(begin, end), controls));
|
||||
} catch (err) {
|
||||
logger(`[query]: ${err.message}`, 'error');
|
||||
errorLogger(err, 'query');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -592,7 +592,8 @@ export class Pattern {
|
|||
* @tags internals
|
||||
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
* @example
|
||||
* s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity)
|
||||
*/
|
||||
filterHaps(hap_test) {
|
||||
return new Pattern((state) => this.query(state).filter(hap_test));
|
||||
|
|
@ -604,7 +605,11 @@ export class Pattern {
|
|||
* @tags internals
|
||||
* @param {Function} value_test
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
* @example
|
||||
* const drums = s("bd sd bd sd")
|
||||
* kick: drums.filterValues((v) => v.s === 'bd').duck(2)
|
||||
* snare: drums.filterValues((v) => v.s === 'sd')
|
||||
* bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2)
|
||||
*/
|
||||
filterValues(value_test) {
|
||||
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps);
|
||||
|
|
@ -1646,7 +1651,13 @@ export const func = curry((a, b) => reify(b).func(a));
|
|||
*
|
||||
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
|
||||
* @param {function} func function with 1 or more params, where last is the current pattern
|
||||
* @noAutocomplete
|
||||
* @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be
|
||||
* in their raw form and it will be up to you to patternify them and/or query them for values
|
||||
* @example
|
||||
* const vlpf = register('vlpf', (freq, pat) => {
|
||||
* return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) }));
|
||||
* })
|
||||
* s("saw").seg(8).velocity(rand).vlpf(800)
|
||||
*
|
||||
*/
|
||||
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
|
||||
|
|
@ -2326,7 +2337,7 @@ export const brak = register('brak', function (pat) {
|
|||
});
|
||||
|
||||
/**
|
||||
* Reverse all haps in a pattern
|
||||
* Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern.
|
||||
*
|
||||
* @tags temporal
|
||||
* @name rev
|
||||
|
|
@ -2359,6 +2370,23 @@ export const rev = register(
|
|||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverse a whole pattern. See also `rev` for reversing each cycle.
|
||||
*
|
||||
* @name revv
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* // This is the same as `<[g e] [d c]>`. If `rev()` is used, you get
|
||||
* // the same as `<[d c] [g e]>`, where each cycle reverses, but the order of
|
||||
* // cycles stays the same.
|
||||
* note("<[c d] [e g]>").revv()
|
||||
*/
|
||||
export const revv = register('revv', function (pat) {
|
||||
const negateSpan = (span) => new TimeSpan(Fraction(0).sub(span.end), Fraction(0).sub(span.begin));
|
||||
return pat.withQuerySpan(negateSpan).withHapSpan(negateSpan);
|
||||
});
|
||||
|
||||
/** Like press, but allows you to specify the amount by which each
|
||||
* event is shifted. pressBy(0.5) is the same as press, while
|
||||
* pressBy(1/3) shifts each event by a third of its timespan.
|
||||
|
|
@ -2684,8 +2712,8 @@ export const { chunkBack, chunkback } = register(
|
|||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 8> 1 2 3 4 5 6 7"
|
||||
* .fastChunk(4, x => x.color('red')).slow(2)
|
||||
* .scale("C2:major").note()
|
||||
* .fastChunk(4, x => x.color('red')).slow(2)
|
||||
*/
|
||||
export const { fastchunk, fastChunk } = register(
|
||||
['fastchunk', 'fastChunk'],
|
||||
|
|
@ -2773,8 +2801,13 @@ export const hsl = register('hsl', (h, s, l, pat) => {
|
|||
* Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array).
|
||||
* @name tag
|
||||
* @tags temporal
|
||||
* @noAutocomplete
|
||||
* @param {string} tag anything unique
|
||||
* @example
|
||||
* s("saw!16").note("F1")
|
||||
* .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15)
|
||||
* .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered'))
|
||||
* .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered'))
|
||||
* .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered')))
|
||||
*/
|
||||
Pattern.prototype.tag = function (tag) {
|
||||
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
|
||||
|
|
@ -2786,7 +2819,7 @@ Pattern.prototype.tag = function (tag) {
|
|||
* @tags temporal
|
||||
* @param {Function} test function to test Hap
|
||||
* @example
|
||||
* s("hh!7 oh").filter(hap => hap.value.s==='hh')
|
||||
* s("hh!7 oh").filter(hap => hap.value.s === 'hh')
|
||||
*/
|
||||
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
|
||||
|
||||
|
|
@ -2794,8 +2827,9 @@ export const filter = register('filter', (test, pat) => pat.withHaps((haps) => h
|
|||
* Filters haps by their begin time
|
||||
* @name filterWhen
|
||||
* @tags temporal
|
||||
* @noAutocomplete
|
||||
* @param {Function} test function to test Hap.whole.begin
|
||||
* @example
|
||||
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
|
||||
*/
|
||||
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
|
||||
|
||||
|
|
@ -3776,3 +3810,98 @@ for (const name of distAlgoNames) {
|
|||
return this.distort(argsPat);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a list of patterns into a single pattern which outputs list-values
|
||||
*
|
||||
* @name parray
|
||||
* @returns Pattern
|
||||
*/
|
||||
export const parray = (pats) => {
|
||||
const pack = (...xs) => xs;
|
||||
let acc = pure(curry(pack, null, pats.length));
|
||||
for (const p of pats) acc = acc.appBoth(reify(p));
|
||||
return acc;
|
||||
};
|
||||
|
||||
const _ensureListPattern = (list) => {
|
||||
if (Array.isArray(list)) {
|
||||
return parray(list);
|
||||
}
|
||||
return reify(list);
|
||||
};
|
||||
|
||||
/**
|
||||
* Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
|
||||
*
|
||||
* Can also be used to create a new synth via `s('user').partials(...)`
|
||||
*
|
||||
* @name partials
|
||||
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
|
||||
* @example
|
||||
* s("user").seg(16).n(irand(8)).scale("A:major")
|
||||
* .partials([1, 0, 1, 0, 0, 1])
|
||||
* @example
|
||||
* s("saw").seg(8).n(irand(12)).scale("G#:minor")
|
||||
* .partials(binaryL(irand(256).add("1")))
|
||||
*/
|
||||
Pattern.prototype.partials = function (list) {
|
||||
return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list));
|
||||
};
|
||||
|
||||
// Also create a top-level function
|
||||
export const partials = (list) => {
|
||||
return _ensureListPattern(list).as('partials');
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
|
||||
*
|
||||
* @name phases
|
||||
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
|
||||
* @example
|
||||
* // Phase cancellation
|
||||
* s("saw").seg(8).n(irand(12)).scale("G#1:minor")
|
||||
* .partials(partials([1, 1, 1]))
|
||||
* .superimpose(x => x.phases([0.5, 0.5, 0.5]))
|
||||
*/
|
||||
Pattern.prototype.phases = function (list) {
|
||||
return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list));
|
||||
};
|
||||
|
||||
// Also create a top-level function
|
||||
export const phases = (list) => {
|
||||
return _ensureListPattern(list).as('phases');
|
||||
};
|
||||
|
||||
/**
|
||||
* Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2)..
|
||||
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
|
||||
* establish the controls of the given effect. See examples.
|
||||
* @name FX
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* $: s("[sbd <hh [bd | lt | oh]>]*4").dec(.4)
|
||||
* .FX(
|
||||
* phaser(0.5).gain(2),
|
||||
* bpf(800),
|
||||
* distort(1.3),
|
||||
* room(0.2),
|
||||
* delay(0.5).gain(1.25),
|
||||
* distort(0.3),
|
||||
* ).fxr(1.7) // sets release time of effects (like delay)
|
||||
* @example
|
||||
* $: s("saw").fm(0.5)
|
||||
* .delay(0.3) // outer effects are applied *last*
|
||||
* .FX(coarse(4)) // first coarse
|
||||
* .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf
|
||||
* .FX(distort(1)) // then distort
|
||||
*/
|
||||
Pattern.prototype.FX = function (...effects) {
|
||||
effects = effects.map(reify);
|
||||
return this.withValue((v) => (vEff) => {
|
||||
const currFX = v.FX ?? [];
|
||||
return { ...v, FX: currFX.concat(vEff) };
|
||||
}).appLeft(parray(effects));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { errorLogger, logger } from './logger.mjs';
|
|||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
import { reset_state } from './impure.mjs';
|
||||
|
||||
export function repl({
|
||||
defaultOutput,
|
||||
|
|
@ -52,6 +53,9 @@ export function repl({
|
|||
onToggle: (started) => {
|
||||
updateState({ started });
|
||||
onToggle?.(started);
|
||||
if (!started) {
|
||||
reset_state();
|
||||
}
|
||||
},
|
||||
setInterval,
|
||||
clearInterval,
|
||||
|
|
@ -157,9 +161,9 @@ export function repl({
|
|||
// allows muting a pattern x with x_ or _x
|
||||
return silence;
|
||||
}
|
||||
if (id === '$') {
|
||||
if (id.includes('$')) {
|
||||
// allows adding anonymous patterns with $:
|
||||
id = `$${anonymousIndex}`;
|
||||
id = `${id}${anonymousIndex}`;
|
||||
anonymousIndex++;
|
||||
}
|
||||
pPatterns[id] = this;
|
||||
|
|
@ -220,8 +224,19 @@ export function repl({
|
|||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = [];
|
||||
let soloActive = false;
|
||||
for (const [key, value] of Object.entries(pPatterns)) {
|
||||
patterns.push(value.withState((state) => state.setControls({ id: key })));
|
||||
// handle soloed patterns ex: S$: s("bd!4")
|
||||
const isSolod = key.length > 1 && key.startsWith('S');
|
||||
if (isSolod && soloActive === false) {
|
||||
// first time we see a soloed pattern, clear existing patterns
|
||||
patterns = [];
|
||||
soloActive = true;
|
||||
}
|
||||
if (!soloActive || (soloActive && isSolod)) {
|
||||
const valWithState = value.withState((state) => state.setControls({ id: key }));
|
||||
patterns.push(valWithState);
|
||||
}
|
||||
}
|
||||
if (eachTransform) {
|
||||
// Explicit lambda so only element (not index and array) are passed
|
||||
|
|
@ -232,14 +247,13 @@ export function repl({
|
|||
pattern = eachTransform(pattern);
|
||||
}
|
||||
if (allTransforms.length) {
|
||||
for (let i in allTransforms) {
|
||||
pattern = allTransforms[i](pattern);
|
||||
for (const transform of allTransforms) {
|
||||
pattern = transform(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
pattern = silence;
|
||||
}
|
||||
logger(`[eval] code updated`);
|
||||
pattern = await setPattern(pattern, autostart);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function steady(value) {
|
|||
}
|
||||
|
||||
export const signal = (func) => {
|
||||
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin))];
|
||||
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin, state.controls))];
|
||||
return new Pattern(query);
|
||||
};
|
||||
|
||||
|
|
@ -203,38 +203,97 @@ export const mouseY = signal(() => _mouseY);
|
|||
export const mousex = signal(() => _mouseX);
|
||||
export const mouseX = signal(() => _mouseX);
|
||||
|
||||
// random signals
|
||||
// Random number generators
|
||||
|
||||
const xorwise = (x) => {
|
||||
// Produce "Avalanche effect" where flipping a single bit of x
|
||||
// results in all output bits flipping with probability 0.5
|
||||
// See e.g. https://github.com/aappleby/smhasher/blob/0ff96f7835817a27d0487325b6c16033e2992eb5/src/MurmurHash3.cpp#L68-L77
|
||||
const _murmurHashFinalizer = (x) => {
|
||||
x |= 0;
|
||||
x ^= x >>> 16;
|
||||
x = Math.imul(x, 0x85ebca6b);
|
||||
x ^= x >>> 13;
|
||||
x = Math.imul(x, 0xc2b2ae35);
|
||||
x ^= x >>> 16;
|
||||
return x >>> 0; // unsigned
|
||||
};
|
||||
|
||||
// Convert t to a 32 bit integer, preserving temporal resolution down to 1/2^29
|
||||
const _tToT = (t) => {
|
||||
return Math.floor(t * 536870912);
|
||||
};
|
||||
|
||||
// Used to decorrelate nearby T, i, and seed prior to hashing
|
||||
const _decorrelate = (T, i = 0, seed = 0) => {
|
||||
const lowBits = (T >>> 0) >>> 0;
|
||||
const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32
|
||||
let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35);
|
||||
key ^= Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9);
|
||||
key ^= Math.imul(seed ^ 0x165667b1, 0x27d4eb2d);
|
||||
return key >>> 0;
|
||||
};
|
||||
|
||||
const randAt = (T, i = 0, seed = 0) => {
|
||||
return _murmurHashFinalizer(_decorrelate(T, i, seed)) / 4294967296; // 2^32
|
||||
};
|
||||
|
||||
// n samples at time t
|
||||
const timeToRands = (t, n, seed = 0) => {
|
||||
const T = _tToT(t);
|
||||
if (n === 1) {
|
||||
return randAt(T, 0, seed);
|
||||
}
|
||||
const out = new Array(n);
|
||||
for (let i = 0; i < n; i++) out[i] = randAt(T, i, seed);
|
||||
return out;
|
||||
};
|
||||
|
||||
// Old random signals. Currently the default, but can also be chosen via
|
||||
// `useRNG('legacy')`
|
||||
|
||||
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
|
||||
const __xorwise = (x) => {
|
||||
const a = (x << 13) ^ x;
|
||||
const b = (a >> 17) ^ a;
|
||||
return (b << 5) ^ b;
|
||||
};
|
||||
|
||||
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
|
||||
const _frac = (x) => x - Math.trunc(x);
|
||||
|
||||
const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
|
||||
|
||||
const intSeedToRand = (x) => (x % 536870912) / 536870912;
|
||||
|
||||
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
||||
|
||||
const timeToRandsPrime = (seed, n) => {
|
||||
const __frac = (x) => x - Math.trunc(x);
|
||||
const __timeToIntSeed = (x) => __xorwise(Math.trunc(__frac(x / 300) * 536870912));
|
||||
const __intSeedToRand = (x) => (x % 536870912) / 536870912;
|
||||
const __timeToRandsPrime = (seed, n) => {
|
||||
if (n === 1) {
|
||||
return Math.abs(__intSeedToRand(seed));
|
||||
}
|
||||
const result = [];
|
||||
// eslint-disable-next-line
|
||||
for (let i = 0; i < n; ++i) {
|
||||
result.push(intSeedToRand(seed));
|
||||
seed = xorwise(seed);
|
||||
for (let i = 0; i < n; i++) {
|
||||
result.push(__intSeedToRand(seed));
|
||||
seed = __xorwise(seed);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n);
|
||||
|
||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
// End old random
|
||||
|
||||
let RNG_MODE = 'legacy';
|
||||
export const getRandsAtTime = (t, n = 1, seed = 0) => {
|
||||
return RNG_MODE === 'legacy' ? __timeToRands(t + seed, n) : timeToRands(t, n, seed);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets which random number generator to use. Historically Strudel would
|
||||
* use `useRNG('legacy')`, which remains the default. To use a new more statistically
|
||||
* precise RNG, try `useRNG('precise')`.
|
||||
*
|
||||
* @name useRNG
|
||||
* @param {string} mod - Mode. One of 'legacy', 'precise'
|
||||
* @example
|
||||
* useRNG('legacy')
|
||||
* // Repeats every 300 cycles
|
||||
* $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32)
|
||||
* $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32)
|
||||
*/
|
||||
export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
|
||||
|
||||
/**
|
||||
* A discrete pattern of numbers from 0 to n-1
|
||||
|
|
@ -246,7 +305,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
|||
export const run = (n) => saw.range(0, n).round().segment(n);
|
||||
|
||||
/**
|
||||
* Creates a pattern from a binary number.
|
||||
* Creates a binary pattern from a number.
|
||||
*
|
||||
* @name binary
|
||||
* @tags generators
|
||||
|
|
@ -261,7 +320,7 @@ export const binary = (n) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Creates a pattern from a binary number, padded to n bits long.
|
||||
* Creates a binary pattern from a number, padded to n bits long.
|
||||
*
|
||||
* @name binaryN
|
||||
* @tags generators
|
||||
|
|
@ -278,10 +337,55 @@ export const binaryN = (n, nBits = 16) => {
|
|||
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a binary list pattern from a number.
|
||||
*
|
||||
* @name binaryL
|
||||
* @param {number} n - input number to convert to binary
|
||||
* s("saw").seg(8)
|
||||
* .partials(binaryL(irand(4096).add(1)))
|
||||
*/
|
||||
export const binaryL = (n) => {
|
||||
const nBits = reify(n).log2(0).floor().add(1);
|
||||
return binaryNL(n, nBits);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a binary list pattern from a number, padded to n bits long.
|
||||
*
|
||||
* @name binaryNL
|
||||
* @param {number} n - input number to convert to binary
|
||||
* @param {number} nBits - pattern length, defaults to 16
|
||||
*/
|
||||
export const binaryNL = (n, nBits = 16) => {
|
||||
return reify(n)
|
||||
.withValue((v) => (bits) => {
|
||||
const bList = [];
|
||||
for (let i = bits - 1; i >= 0; i--) {
|
||||
bList.push((v >> i) & 1);
|
||||
}
|
||||
return bList;
|
||||
})
|
||||
.appLeft(reify(nBits));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a list of random numbers of the given length
|
||||
*
|
||||
* @name randL
|
||||
* @param {number} n Number of random numbers to sample
|
||||
* @example
|
||||
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
|
||||
* .partials(randL(8))
|
||||
*/
|
||||
export const randL = (n) => {
|
||||
return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n));
|
||||
};
|
||||
|
||||
export const randrun = (n) => {
|
||||
return signal((t) => {
|
||||
return signal((t, controls) => {
|
||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||
const rands = timeToRands(t.floor().add(0.5), n);
|
||||
const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
||||
const nums = rands
|
||||
.map((n, i) => [n, i])
|
||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||
|
|
@ -324,6 +428,37 @@ export const scramble = register('scramble', (n, pat) => {
|
|||
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* Modify a pattern by applying a function to the `randomSeed` control if present
|
||||
*
|
||||
* @param {Function} func Function from seed (or undefined) to seed (or undefined)
|
||||
* @param {Pattern} pat Pattern to update
|
||||
* @returns Pattern
|
||||
*/
|
||||
export const withSeed = (func, pat) => {
|
||||
return new Pattern((state) => {
|
||||
let { randSeed, ...controls } = state.controls;
|
||||
randSeed = func(randSeed);
|
||||
return pat.query(state.setControls({ ...controls, randSeed }));
|
||||
}, pat._steps);
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the seed for random signals. Normally, random signals depend on time,
|
||||
* so two patterns at the same time will have the same random values. Specifying
|
||||
* a new seed changes the signal output by `rand`. This also affects other functions
|
||||
* that use randomness, like `shuffle` and `sometimes`.
|
||||
*
|
||||
* @name seed
|
||||
* @param {number} n A new seed. Can be any number.
|
||||
* @example
|
||||
* $: s("hh*4").degrade();
|
||||
* $: s("bd*4").degrade().seed(1); // Will degrade different events from the hi-hat
|
||||
*/
|
||||
export const seed = register('seed', (n, pat) => {
|
||||
return withSeed(() => n, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* A continuous pattern of random numbers, between 0 and 1.
|
||||
*
|
||||
|
|
@ -334,7 +469,7 @@ export const scramble = register('scramble', (n, pat) => {
|
|||
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
|
||||
*
|
||||
*/
|
||||
export const rand = signal(timeToRand);
|
||||
export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed));
|
||||
/**
|
||||
* A continuous pattern of random numbers, between -1 and 1
|
||||
* @tags generators
|
||||
|
|
@ -514,7 +649,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
|||
* @example
|
||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||
* @example
|
||||
* wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s()
|
||||
* wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note()
|
||||
* @example
|
||||
* // The probability can itself be a pattern
|
||||
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
||||
|
|
@ -523,36 +658,32 @@ export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pair
|
|||
|
||||
export const wrandcat = wchooseCycles;
|
||||
|
||||
function _perlin(t) {
|
||||
function _perlin(t, seed = 0) {
|
||||
let ta = Math.floor(t);
|
||||
let tb = ta + 1;
|
||||
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
|
||||
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
|
||||
const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb));
|
||||
const ra = getRandsAtTime(ta, 1, seed);
|
||||
const rb = getRandsAtTime(tb, 1, seed);
|
||||
const v = interp(t - ta)(ra)(rb);
|
||||
return v;
|
||||
}
|
||||
export const perlinWith = (tpat) => {
|
||||
return tpat.fmap(_perlin);
|
||||
};
|
||||
|
||||
function _berlin(t) {
|
||||
function _berlin(t, seed = 0) {
|
||||
const prevRidgeStartIndex = Math.floor(t);
|
||||
const nextRidgeStartIndex = prevRidgeStartIndex + 1;
|
||||
|
||||
const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex);
|
||||
const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint;
|
||||
const prevRidgeBottomPoint = getRandsAtTime(prevRidgeStartIndex, 1, seed);
|
||||
const height = getRandsAtTime(nextRidgeStartIndex, 1, seed);
|
||||
const nextRidgeTopPoint = prevRidgeBottomPoint + height;
|
||||
|
||||
const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex);
|
||||
const interp = (a, b, t) => {
|
||||
return a + (b - a) * t;
|
||||
return a + t * (b - a);
|
||||
};
|
||||
return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2;
|
||||
}
|
||||
|
||||
export const berlinWith = (tpat) => {
|
||||
return tpat.fmap(_berlin);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||
*
|
||||
|
|
@ -563,7 +694,7 @@ export const berlinWith = (tpat) => {
|
|||
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
|
||||
*
|
||||
*/
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
|
||||
|
||||
/**
|
||||
* Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful,
|
||||
|
|
@ -576,7 +707,7 @@ export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
|||
* n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor")
|
||||
*
|
||||
*/
|
||||
export const berlin = berlinWith(time.fmap((v) => Number(v)));
|
||||
export const berlin = signal((t, controls) => _berlin(t, controls.randSeed));
|
||||
|
||||
export const degradeByWith = register(
|
||||
'degradeByWith',
|
||||
|
|
@ -890,3 +1021,50 @@ export const whenKey = register('whenKey', function (input, func, pat) {
|
|||
export const keyDown = register('keyDown', function (pat) {
|
||||
return pat.fmap(_keyDown);
|
||||
});
|
||||
|
||||
/**
|
||||
* A pattern measuring the duration of events,
|
||||
* in cycles per event. `cyclesPer` doesn't have structure itself, but takes structure, and therefore
|
||||
* event durations, from the pattern that it is combined with.
|
||||
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
|
||||
* See also its reciprocal, `per`, also known as `perCycle`.
|
||||
* @example
|
||||
* // Shorter events are lower in pitch
|
||||
* sound("saw saw [saw saw] saw")
|
||||
* .note(cyclesPer.range(50, 100))
|
||||
* @example
|
||||
* sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]")
|
||||
* .note(cyclesPer.add(20))
|
||||
*/
|
||||
export const cyclesPer = new Pattern(function (state) {
|
||||
return [new Hap(undefined, state.span, state.span.duration)];
|
||||
});
|
||||
|
||||
/**
|
||||
* A pattern measuring the 'shortness' of events, or in other words, the duration of pattern events,
|
||||
* in events per cycle. `per` doesn't have structure itself, but takes structure, and therefore
|
||||
* event durations, from the pattern that it is combined with.
|
||||
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
|
||||
* See also its reciprocal, `cyclesPer`.
|
||||
* @synonyms perCycle
|
||||
* @example
|
||||
* // Shorter events are more distorted
|
||||
* n("0 0*2 0 0*2 0 [0 0 0]@2").sound("bd")
|
||||
* .distort(per.div(2))
|
||||
*/
|
||||
export const per = new Pattern(function (state) {
|
||||
return [new Hap(undefined, state.span, Fraction(1).div(state.span.duration))];
|
||||
});
|
||||
|
||||
export const perCycle = per;
|
||||
|
||||
/**
|
||||
* Like `per` but measures the shortness of events according to an exponential curve. In
|
||||
* particular, where the event duration halves, the
|
||||
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
|
||||
* the same as `"3 3 [4 [5 5]] 3"`.
|
||||
*/
|
||||
export const perx = new Pattern(function (state) {
|
||||
const n = Fraction(1).div(state.span.duration);
|
||||
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { bjork } from '../euclid.mjs';
|
||||
import { bjorklund } from '../euclid.mjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fastcat } from '../pattern.mjs';
|
||||
|
||||
describe('bjork', () => {
|
||||
it('should apply bjorklund to ons and steps', () => {
|
||||
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
||||
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
||||
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
||||
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
||||
describe('bjorklund', () => {
|
||||
it('should apply bjorklundlund to ons and steps', () => {
|
||||
expect(bjorklund(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
||||
expect(bjorklund(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
||||
expect(bjorklund(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
||||
expect(bjorklund(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
expect(bjorklund(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -586,6 +586,18 @@ describe('Pattern', () => {
|
|||
.map((a) => a.value),
|
||||
).toStrictEqual(['c', 'b', 'a']);
|
||||
});
|
||||
it('Does not reverse the order of cycles', () => {
|
||||
expect(fastcat('a', 'b', 'c', 'd').slow(2).rev().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
fastcat('b', 'a', 'd', 'c').firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('revv()', () => {
|
||||
it('Does reverse the order of cycles', () => {
|
||||
expect(fastcat('a', 'b', 'c', 'd').slow(2).revv().fast(2).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
fastcat('d', 'c', 'b', 'a').firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('sequence()', () => {
|
||||
it('Can work like fastcat', () => {
|
||||
|
|
@ -728,20 +740,6 @@ describe('Pattern', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
describe('signal()', () => {
|
||||
it('Can make saw/saw2', () => {
|
||||
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
|
||||
);
|
||||
|
||||
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
||||
});
|
||||
it('Can make isaw/isaw2', () => {
|
||||
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
||||
|
||||
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('_setContext()', () => {
|
||||
it('Can set the hap context', () => {
|
||||
expect(
|
||||
|
|
|
|||
61
packages/core/test/signal.test.mjs
Normal file
61
packages/core/test/signal.test.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
signal.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Fraction from 'fraction.js';
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
||||
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
|
||||
|
||||
const st = (begin, end) => new State(ts(begin, end));
|
||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||
|
||||
const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
const sameFirst = (a, b) => {
|
||||
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
|
||||
};
|
||||
|
||||
describe('signal()', () => {
|
||||
it('Can make saw/saw2', () => {
|
||||
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
|
||||
);
|
||||
|
||||
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
||||
});
|
||||
it('Can make isaw/isaw2', () => {
|
||||
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
||||
|
||||
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
||||
});
|
||||
});
|
||||
|
||||
describe('cyclesPer', () => {
|
||||
it('gives cycles per hap', () => {
|
||||
sameFirst(
|
||||
cyclesPer.struct(true, true, true, fastcat(true, true)),
|
||||
sequence(0.25, 0.25, 0.25, fastcat(0.125, 0.125)).fmap(Fraction),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('per', () => {
|
||||
it('gives haps per cycle', () => {
|
||||
sameFirst(per.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction));
|
||||
});
|
||||
});
|
||||
|
||||
describe('perx', () => {
|
||||
it('gives exponential haps per cycle', () => {
|
||||
sameFirst(
|
||||
perx.struct(true, true, true, fastcat(true, fastcat(true, true))),
|
||||
sequence(3, 3, 3, fastcat(4, fastcat(5, 5))),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,8 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||
import { logger } from './logger.mjs';
|
||||
|
||||
// returns true if the given string is a note
|
||||
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 isNoteWithOctave = (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) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
|
|
@ -23,6 +23,10 @@ export const tokenizeNote = (note) => {
|
|||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||
|
||||
export const getAccidentalsOffset = (accidentals) => {
|
||||
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
};
|
||||
|
||||
// turns the given note into its midi number representation
|
||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||
|
|
@ -30,7 +34,7 @@ export const noteToMidi = (note, defaultOctave = 3) => {
|
|||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = chromas[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
const offset = getAccidentalsOffset(acc);
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
export const midiToFreq = (n) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue