Merge branch 'main' into lu/flux

This commit is contained in:
froos 2025-10-28 23:53:44 +01:00
commit 85f6472894
88 changed files with 4164 additions and 778 deletions

View file

@ -87,6 +87,239 @@ export function registerControl(names, ...aliases) {
*/
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
/**
* Position in the wavetable of the wavetable oscillator
*
* @name wt
* @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition
* @example
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
*/
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name wtenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { wtenv } = registerControl('wtenv');
/**
* Attack time of the wavetable oscillator's position envelope
*
* @name wtattack
* @synonyms wtatt
* @param {number | Pattern} time attack time in seconds
*/
export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt');
/**
* Decay time of the wavetable oscillator's position envelope
*
* @name wtdecay
* @synonyms wtdec
* @param {number | Pattern} time decay time in seconds
*/
export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec');
/**
* Sustain time of the wavetable oscillator's position envelope
*
* @name wtsustain
* @synonyms wtsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus');
/**
* Release time of the wavetable oscillator's position envelope
*
* @name wtrelease
* @synonyms wtrel
* @param {number | Pattern} time release time in seconds
*/
export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel');
/**
* Rate of the LFO for the wavetable oscillator's position
*
* @name wtrate
* @param {number | Pattern} rate rate in hertz
*/
export const { wtrate } = registerControl('wtrate');
/**
* cycle synced rate of the LFO for the wavetable oscillator's position
*
* @name wtsync
* @param {number | Pattern} rate rate in cycles
*/
export const { wtsync } = registerControl('wtsync');
/**
* Depth of the LFO for the wavetable oscillator's position
*
* @name wtdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { wtdepth } = registerControl('wtdepth');
/**
* Shape of the LFO for the wavetable oscillator's position
*
* @name wtshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { wtshape } = registerControl('wtshape');
/**
* DC offset of the LFO for the wavetable oscillator's position
*
* @name wtdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { wtdc } = registerControl('wtdc');
/**
* Skew of the LFO for the wavetable oscillator's position
*
* @name wtskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { wtskew } = registerControl('wtskew');
/**
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
*
* @name warp
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
* @synonyms wavetableWarp
* @example
* s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("spin")
*/
export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp');
/**
* Attack time of the wavetable oscillator's warp envelope
*
* @name warpattack
* @synonyms warpatt
* @param {number | Pattern} time attack time in seconds
*/
export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt');
/**
* Decay time of the wavetable oscillator's warp envelope
*
* @name warpdecay
* @synonyms warpdec
* @param {number | Pattern} time decay time in seconds
*/
export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec');
/**
* Sustain time of the wavetable oscillator's warp envelope
*
* @name warpsustain
* @synonyms warpsus
* @param {number | Pattern} gain sustain level (0 to 1)
*/
export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus');
/**
* Release time of the wavetable oscillator's warp envelope
*
* @name warprelease
* @synonyms warprel
* @param {number | Pattern} time release time in seconds
*/
export const { warprelease, warprel } = registerControl('warprelease', 'warprel');
/**
* Rate of the LFO for the wavetable oscillator's warp
*
* @name warprate
* @param {number | Pattern} rate rate in hertz
*/
export const { warprate } = registerControl('warprate');
/**
* Depth of the LFO for the wavetable oscillator's warp
*
* @name warpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { warpdepth } = registerControl('warpdepth');
/**
* Shape of the LFO for the wavetable oscillator's warp
*
* @name warpshape
* @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..)
*/
export const { warpshape } = registerControl('warpshape');
/**
* DC offset of the LFO for the wavetable oscillator's warp
*
* @name warpdc
* @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar
*/
export const { warpdc } = registerControl('warpdc');
/**
* Skew of the LFO for the wavetable oscillator's warp
*
* @name warpskew
* @param {number | Pattern} skew How much to bend the LFO shape
*/
export const { warpskew } = registerControl('warpskew');
/**
* Type 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 warpmode
* @param {number | string | Pattern} mode Warp mode
* @synonyms wavetableWarpMode
* @example
* s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1")
* .warpmode("<asym bendp spin logistic sync wormhole brownian>*2")
*
*/
export const { warpmode, wavetableWarpMode } = registerControl('warpmode', '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
* @example
* s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>")
*
*/
export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand');
/**
* Amount of envelope applied wavetable oscillator's position envelope
*
* @name warpenv
* @param {number | Pattern} amount between 0 and 1
*/
export const { warpenv } = registerControl('warpenv');
/**
* cycle synced rate of the LFO for the wavetable warp position
*
* @name warpsync
* @param {number | Pattern} rate rate in cycles
*/
export const { warpsync } = registerControl('warpsync');
/**
* Define a custom webaudio node to use as a sound source.
*
@ -114,7 +347,7 @@ export const { n } = registerControl('n');
*
* - a letter (a-g or A-G)
* - 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`
*
@ -127,6 +360,8 @@ export const { n } = registerControl('n');
* note("c4 a4 f4 e4")
* @example
* note("60 69 65 64")
* @example
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
*/
export const { note } = registerControl(['note', 'n']);
@ -142,8 +377,8 @@ export const { note } = registerControl(['note', 'n']);
*/
export const { accelerate } = registerControl('accelerate');
/**
*
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
* @example
* s("hh*8")
@ -255,7 +490,7 @@ export const { fmenv } = registerControl('fmenv');
export const { fmattack } = registerControl('fmattack');
/**
* waveform of the fm modulator
* Waveform of the fm modulator
*
* @name fmwave
* @param {number | Pattern} wave waveform
@ -389,7 +624,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
// ['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
* @name begin
@ -450,7 +685,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
*/
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
/**
* bit crusher effect.
* Bit crusher effect.
*
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
@ -461,7 +696,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
// ['clhatdecay'],
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
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
@ -472,7 +707,7 @@ export const { crush } = registerControl('crush');
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
* @synonyms trem
@ -484,7 +719,7 @@ export const { coarse } = registerControl('coarse');
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
* @synonyms tremsync
@ -499,7 +734,7 @@ export const { tremolosync } = registerControl(
);
/**
* depth of amplitude modulation
* Depth of amplitude modulation
*
* @name tremolodepth
* @synonyms tremdepth
@ -510,7 +745,7 @@ export const { tremolosync } = registerControl(
*/
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
/**
* alter the shape of the modulation waveform
* Alter the shape of the modulation waveform
*
* @name tremoloskew
* @synonyms tremskew
@ -522,7 +757,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
/**
* alter the phase of the modulation waveform
* Alter the phase of the modulation waveform
*
* @name tremolophase
* @synonyms tremphase
@ -534,7 +769,7 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
/**
* shape of amplitude modulation
* Shape of amplitude modulation
*
* @name tremoloshape
* @synonyms tremshape
@ -545,7 +780,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
*/
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
/**
* filter overdrive for supported filter types
* Filter overdrive for supported filter types
*
* @name drive
* @param {number | Pattern} amount
@ -556,7 +791,9 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
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
* @synonyms duck
@ -564,30 +801,77 @@ export const { drive } = registerControl('drive');
* @example
* $: 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)
* @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');
/**
* 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
* @param {number | Pattern} depth depth of modulation from 0 to 1
* @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>"))
* @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');
/**
* 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
* @synonyms duckatt
* @param {number | Pattern} time
*
* @param {number | Pattern} time The attack time in seconds
* @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');
@ -632,7 +916,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
export const { channels, ch } = registerControl('channels', 'ch');
/**
* controls the pulsewidth of the pulse oscillator
* Controls the pulsewidth of the pulse oscillator
*
* @name pw
* @param {number | Pattern} pulsewidth
@ -644,7 +928,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
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
* @param {number | Pattern} rate
@ -656,7 +940,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
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
* @param {number | Pattern} sweep
@ -697,7 +981,7 @@ export const { phaserrate, ph, phaser } = registerControl(
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
* @synonyms phc
@ -725,7 +1009,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
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
* @param {number | Pattern} channel channel number
@ -1077,7 +1361,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
* @name djf
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
* @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');
@ -1603,19 +1887,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size',
export const { shape } = registerControl(['shape', 'shapevol']);
/**
* Wave shaping distortion. CAUTION: it can get loud.
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output.
* Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type.
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
*
* @name distort
* @synonyms dist
* @param {number | Pattern} distortion
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
* @param {number | string | Pattern} type type of distortion to apply
* @example
* s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
* @example
* note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")
* @example
* s("bd:4*4").bank("tr808").distort("3:0.5:diode")
*
*/
export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist');
export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist');
/**
* Postgain for waveshaping distortion.
*
* @name distortvol
* @synonyms distvol
* @param {number | Pattern} volume linear postgain of the distortion
* @example
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
*/
export const { distortvol } = registerControl('distortvol', 'distvol');
/**
* Type of waveshaping distortion to apply.
*
* @name distorttype
* @synonyms disttype
* @param {number | string | Pattern} type type of distortion to apply
* @example
* s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>")
*
* @example
* s("sine").note("F1*2").release(1)
* .penv(24).pdecay(0.05)
* .distort(rand.range(1, 8))
* .distorttype("<fold chebyshev scurve diode asym sinefold>")
*/
export const { distorttype } = registerControl('distorttype', 'disttype');
/**
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
@ -2003,6 +2320,24 @@ export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');
/**
* The host to send open sound control messages to. Requires running the OSC bridge.
* @name oschost
* @param {string | Pattern} oschost e.g. 'localhost'
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
*/
export const { oschost } = registerControl('oschost');
/**
* The port to send open sound control messages to. Requires running the OSC bridge.
* @name oscport
* @param {number | Pattern} oscport e.g. 57120
* @example
* note("c4").oschost('127.0.0.1').oscport(57120).osc();
*/
export const { oscport } = registerControl('oscport');
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);

View file

@ -57,7 +57,7 @@ export class Cyclist {
}
// query the pattern for events
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {

View file

@ -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;
};
@ -128,7 +128,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);
}
@ -139,7 +139,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];
}
@ -216,6 +216,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);
});

View file

@ -126,6 +126,8 @@ export const lcm = (...fractions) => {
);
};
export const isFraction = (x) => x instanceof Fraction;
fraction._original = Fraction;
export default fraction;

View file

@ -5,8 +5,9 @@ let debounce = 1000,
lastTime;
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
logger(`[${origin}] error: ${e.message}`);
}

View file

@ -38,8 +38,7 @@ export class NeoCyclist {
if (this.started === false) {
return;
}
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' });
haps.forEach((hap) => {
if (hap.hasOnset()) {
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);

View file

@ -1,6 +1,6 @@
{
"name": "@strudel/core",
"version": "1.2.4",
"version": "1.2.5",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",

View file

@ -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 Fraction, { lcm } from './fraction.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';
@ -98,10 +98,7 @@ export class Pattern {
// runs func on query state
withState(func) {
return this.withHaps((haps, state) => {
func(state);
return haps;
});
return new Pattern((state) => this.query(func(state)));
}
/**
@ -999,7 +996,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
// compose matrix functions
function _nonArrayObject(x) {
return !Array.isArray(x) && typeof x === 'object';
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
}
function _composeOp(a, b, func) {
if (_nonArrayObject(a) || _nonArrayObject(b)) {
@ -2428,6 +2425,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});
export const applyN = register('applyN', function (n, func, p) {
let result = p;
for (let i = 0; i < n; i++) {
result = func(result);
}
return result;
});
/**
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
* @name plyWith
* @synonyms plywith
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern
* @example
* "<0 [2 4]>"
* .plyWith(4, (p) => p.add(2))
* .scale("C:minor").note()
*/
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
* @name plyForEach
* @synonyms plyforeach
* @param {number} factor how many times to repeat
* @param {function} func function to apply, given the pattern and the iteration index
* @example
* "<0 [2 4]>"
* .plyForEach(4, (p,n) => p.add(n*2))
* .scale("C:minor").note()
*/
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
const result = pat
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
.squeezeJoin();
if (__steps) {
result._steps = Fraction(factor).mulmaybe(pat._steps);
}
return result;
});
/**
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
* @name iter
@ -2555,8 +2603,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'],
@ -2979,6 +3027,24 @@ export const extend = stepRegister('extend', function (factor, pat) {
return pat.fast(factor).expand(factor);
});
/**
* *Experimental*
*
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
*
* TODO: find out how this function differs from extend
* @example
* stepcat(
* sound("bd bd - cp").replicate(2),
* sound("bd - sd -")
* ).pace(8)
*/
export const replicate = stepRegister('replicate', function (factor, pat) {
return pat.repeatCycles(factor).fast(factor).expand(factor);
});
/**
* *Experimental*
*
@ -3514,3 +3580,76 @@ export const morph = (frompat, topat, bypat) => {
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};
/**
* Soft-clipping distortion
*
* @name soft
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Hard-clipping distortion
*
* @name hard
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Cubic polynomial distortion
*
* @name cubic
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Diode-emulating distortion
*
* @name diode
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Asymmetrical diode distortion
*
* @name asym
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Wavefolding distortion
*
* @name fold
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Wavefolding distortion composed with sinusoid
*
* @name sinefold
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
/**
* Distortion via Chebyshev polynomials
*
* @name chebyshev
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
*
*/
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
for (const name of distAlgoNames) {
// Add aliases for distortion algorithms
Pattern.prototype[name] = function (args) {
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
return this.distort(argsPat);
};
}

View file

@ -74,6 +74,14 @@ export function repl({
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) => {
pattern = editPattern?.(pattern) || pattern;
await scheduler.setPattern(pattern, autostart);
@ -85,7 +93,10 @@ export function repl({
const start = () => scheduler.start();
const pause = () => scheduler.pause();
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
@ -97,7 +108,10 @@ export function repl({
* setcpm(140/4) // =140 bpm in 4/4
* $: 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`..
@ -200,7 +214,10 @@ export function repl({
}
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) {
let patterns = Object.values(pPatterns);
let patterns = [];
for (const [key, value] of Object.entries(pPatterns)) {
patterns.push(value.withState((state) => state.setControls({ id: key })));
}
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
@ -214,6 +231,7 @@ export function repl({
pattern = allTransforms[i](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?' : '.'));

View file

@ -19,9 +19,9 @@ export class State {
return this.setSpan(func(this.span));
}
// Returns new State with different controls
// Returns new State with added controls.
setControls(controls) {
return new State(this.span, controls);
return new State(this.span, { ...this.controls, ...controls });
}
}

View file

@ -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]);
});
});

View file

@ -7,13 +7,13 @@ 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 [];
}
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) {
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) => {