Merge remote-tracking branch 'origin/main' into general-purpose-scheduler

This commit is contained in:
Felix Roos 2022-10-29 17:56:30 +02:00
commit 7e7e299bce
33 changed files with 3697 additions and 11642 deletions

View file

@ -23,7 +23,7 @@ const generic_params = [
* @name s
* @param {string | Pattern} sound The sound / pattern of sounds to pick
* @example
* s("bd hh").out()
* s("bd hh")
*
*/
['s', 's', 'sound'],
@ -62,12 +62,12 @@ const generic_params = [
*/
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
/**
* Like {@link amp}, but exponential.
* Controls the gain by an exponential amount.
*
* @name gain
* @param {number | Pattern} amount gain.
* @example
* s("bd*8").gain(".7*2 1 .7*2 1 .7 1").osc()
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
*
*/
[
@ -129,7 +129,7 @@ const generic_params = [
* @name bandf
* @param {number | Pattern} frequency center frequency
* @example
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>").out()
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>")
*
*/
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
@ -140,17 +140,19 @@ const generic_params = [
* @name bandq
* @param {number | Pattern} q q factor
* @example
* s("bd sd").bandf(500).bandq("<0 1 2 3>").out()
* s("bd sd").bandf(500).bandq("<0 1 2 3>")
*
*/
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
/**
* 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
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
* @example
* s("rave").begin("<0 .25 .5 .75>").osc()
* samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/')
* s("rave").begin("<0 .25 .5 .75>")
*
*/
[
@ -159,12 +161,13 @@ const generic_params = [
'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.',
],
/**
* The same as {@link begin}, but cuts off the end off each sample.
* The same as .begin, but cuts off the end off each sample.
*
* @memberof Pattern
* @name end
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
* @example
* s("bd*2,ho*4").end("<.1 .2 .5 1>").osc()
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
*
*/
[
@ -202,7 +205,7 @@ const generic_params = [
* @name crush
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>").out()
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
*
*/
[
@ -211,12 +214,12 @@ const generic_params = [
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
],
/**
* fake-resampling for lowering the sample rate
* 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.
* @example
* s("bd sd,hh*4").coarse("<1 4 8 16 32>").out()
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
*
*/
[
@ -253,7 +256,7 @@ const generic_params = [
* @name cutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @example
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>").out()
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>")
*
*/
// TODO: add lpf synonym
@ -264,7 +267,7 @@ const generic_params = [
* @name hcutoff
* @param {number | Pattern} frequency audible between 0 and 20000
* @example
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>").out()
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>")
*
*/
// TODO: add hpf synonym
@ -277,9 +280,9 @@ const generic_params = [
* Applies the resonance of the high-pass filter.
*
* @name hresonance
* @param {number | Pattern} q resonance factor between 0 and 1
* @param {number | Pattern} q resonance factor between 0 and 50
* @example
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>").out()
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>")
*
*/
[
@ -292,9 +295,9 @@ const generic_params = [
* Applies the cutoff frequency of the low-pass filter.
*
* @name resonance
* @param {number | Pattern} q resonance factor between 0 and 1
* @param {number | Pattern} q resonance factor between 0 and 50
* @example
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>").out()
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>")
*
*/
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
@ -368,7 +371,7 @@ const generic_params = [
* @name fadeTime
* @param {number | Pattern} time between 0 and 1
* @example
* s("ho*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
*
*/
[
@ -493,7 +496,7 @@ const generic_params = [
* @name pan
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>").out()
* s("[bd hh]*2").pan("<.5 1 .5 0>")
*
*/
[
@ -596,7 +599,7 @@ const generic_params = [
* @name shape
* @param {number | Pattern} distortion between 0 and 1
* @example
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>").out()
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
*
*/
[
@ -662,7 +665,7 @@ const generic_params = [
* @param {string | Pattern} vowel You can use a e i o u.
* @example
* note("c2 <eb2 <g2 g1>>").s('sawtooth')
* .vowel("<a e i <o u>>").out()
* .vowel("<a e i <o u>>")
*
*/
[

View file

@ -25,61 +25,70 @@ const euclid = (pulses, steps, rotation = 0) => {
* describe a large number of rhythms used in the most important music world traditions.
*
* @memberof Pattern
* @name euclid
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @param {number} rotation (optional) offset in steps
* @returns Pattern
* @example // The Cuban tresillo pattern.
* "c3".euclid(3,8)
* @example
* // The Cuban tresillo pattern.
* note("c3").euclid(3,8)
*/
/**
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
* "c3".euclid(2,5)
* note("c3").euclid(2,5)
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
* "c3".euclid(3,4)
* note("c3").euclid(3,4)
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
* "c3".euclid(3,5,2)
* note("c3").euclid(3,5,2)
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
* "c3".euclid(3,7)
* note("c3").euclid(3,7)
* @example // The Cuban tresillo pattern.
* "c3".euclid(3,8)
* note("c3").euclid(3,8)
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
* "c3".euclid(4,7)
* note("c3").euclid(4,7)
* @example // The Aksak rhythm of Turkey.
* "c3".euclid(4,9)
* note("c3").euclid(4,9)
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
* "c3".euclid(4,11)
* note("c3").euclid(4,11)
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
* "c3".euclid(5,6)
* note("c3").euclid(5,6)
* @example // The Nawakhat pattern, another popular Arab rhythm.
* "c3".euclid(5,7)
* note("c3").euclid(5,7)
* @example // The Cuban cinquillo pattern.
* "c3".euclid(5,8)
* note("c3").euclid(5,8)
* @example // A popular Arab rhythm called Agsag-Samai.
* "c3".euclid(5,9)
* note("c3").euclid(5,9)
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
* "c3".euclid(5,11)
* note("c3").euclid(5,11)
* @example // The Venda clapping pattern of a South African childrens song.
* "c3".euclid(5,12)
* note("c3").euclid(5,12)
* @example // The Bossa-Nova rhythm necklace of Brazil.
* "c3".euclid(5,16)
* note("c3").euclid(5,16)
* @example // A typical rhythm played on the Bendir (frame drum).
* "c3".euclid(7,8)
* note("c3").euclid(7,8)
* @example // A common West African bell pattern.
* "c3".euclid(7,12)
* note("c3").euclid(7,12)
* @example // A Samba rhythm necklace from Brazil.
* "c3".euclid(7,16,14)
* note("c3").euclid(7,16,14)
* @example // A rhythm necklace used in the Central African Republic.
* "c3".euclid(9,16)
* note("c3").euclid(9,16)
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
* "c3".euclid(11,24,14)
* note("c3").euclid(11,24,14)
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
* "c3".euclid(13,24,5)
* note("c3").euclid(13,24,5)
*/
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
return this.struct(euclid(pulses, steps, rotation));
};
/**
* Similar to {@link Pattern#euclid}, but each pulse is held until the next pulse, so there will be no gaps.
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
* @name euclidLegato
* @memberof Pattern
* @example
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
*/
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
const bin_pat = euclid(pulses, steps, rotation);

View file

@ -468,7 +468,11 @@ export class Pattern {
/**
* Assumes a numerical pattern. Returns a new pattern with all values rounded
* to the nearest integer.
* @name round
* @memberof Pattern
* @returns Pattern
* @example
* "0.5 1.5 2.5".round().scale('C major').note()
*/
round() {
return this._asNumber().fmap((v) => Math.round(v));
@ -520,9 +524,9 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4)).out()
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4))
*/
range(min, max) {
_range(min, max) {
return this.mul(max - min).add(min);
}
@ -534,8 +538,8 @@ export class Pattern {
* @param {Number} max
* @returns Pattern
*/
rangex(min, max) {
return this.range(Math.log(min), Math.log(max)).fmap(Math.exp);
_rangex(min, max) {
return this._range(Math.log(min), Math.log(max)).fmap(Math.exp);
}
/**
@ -545,8 +549,8 @@ export class Pattern {
* @param {Number} max
* @returns Pattern
*/
range2(min, max) {
return this._fromBipolar().range(min, max);
_range2(min, max) {
return this._fromBipolar()._range(min, max);
}
_bindWhole(choose_whole, func) {
@ -695,6 +699,13 @@ export class Pattern {
return this.fmap(func)._squeezeJoin();
}
/**
* Like layer, but with a single function:
* @name apply
* @memberof Pattern
* @example
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
*/
_apply(func) {
return func(this);
}
@ -707,7 +718,7 @@ export class Pattern {
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* .layer(x=>x.add("0,2"))
* .scale('C minor').note().out()
* .scale('C minor').note()
*/
layer(...funcs) {
return stack(...funcs.map((func) => func(this)));
@ -739,13 +750,22 @@ export class Pattern {
const end = cycle.add(span.end.sub(cycle).mul(factor).min(1));
return new TimeSpan(begin, end);
};
const ef = function (span) {
const cycle = span.begin.sam();
const begin = cycle.add(span.begin.sub(cycle).div(factor).min(1));
const end = cycle.add(span.end.sub(cycle).div(factor).min(1));
return new TimeSpan(begin, end);
const ef = function (hap) {
const begin = hap.part.begin;
const end = hap.part.end;
const cycle = begin.sam();
const beginPos = begin.sub(cycle).div(factor).min(1);
const endPos = end.sub(cycle).div(factor).min(1);
const newPart = new TimeSpan(cycle.add(beginPos), cycle.add(endPos));
const newWhole = !hap.whole
? undefined
: new TimeSpan(
newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)),
newPart.end.add(hap.whole.end.sub(end).div(factor)),
);
return new Hap(newWhole, newPart, hap.value, hap.context);
};
return this.withQuerySpan(qf).withHapSpan(ef)._splitQueries();
return this.withQuerySpan(qf)._withHap(ef)._splitQueries();
}
// Compress each cycle into the given timespan, leaving a gap
@ -765,7 +785,7 @@ export class Pattern {
_focus(b, e) {
return this._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
}
_focusSpan(span) {
return this._focus(span.begin, span.end);
}
@ -778,7 +798,7 @@ export class Pattern {
* @param {number | Pattern} factor speed up factor
* @returns Pattern
* @example
* s("<bd sd> hh").fast(2).out() // s("[<bd sd> hh]*2").out()
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
*/
_fast(factor) {
const fastQuery = this.withQueryTime((t) => t.mul(factor));
@ -793,7 +813,7 @@ export class Pattern {
* @param {number | Pattern} factor slow down factor
* @returns Pattern
* @example
* s("<bd sd> hh").slow(2).out() // s("[<bd sd> hh]/2").out()
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
*/
_slow(factor) {
return this._fast(Fraction(1).div(factor));
@ -811,6 +831,20 @@ export class Pattern {
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
}
/**
* Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'.
* It turns a pattern of samples into a pattern of parts of samples.
* @name chop
* @memberof Pattern
* @returns Pattern
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes")
* .chop(4)
* .rev() // reverse order of chops
* .loopAt(4,1) // fit sample into 4 cycles
*
*/
_chop(n) {
const slices = Array.from({ length: n }, (x, i) => i);
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
@ -840,7 +874,7 @@ export class Pattern {
* @param {number | Pattern} cycles number of cycles to nudge left
* @returns Pattern
* @example
* "bd ~".stack("hh ~".early(.1)).s().out()
* "bd ~".stack("hh ~".early(.1)).s()
*/
_early(offset) {
offset = Fraction(offset);
@ -855,7 +889,7 @@ export class Pattern {
* @param {number | Pattern} cycles number of cycles to nudge right
* @returns Pattern
* @example
* "bd ~".stack("hh ~".late(.1)).s().out()
* "bd ~".stack("hh ~".late(.1)).s()
*/
_late(offset) {
offset = Fraction(offset);
@ -891,9 +925,9 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* "c3,eb3,g3"
* note("c3,eb3,g3")
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(4).note().out()
* .slow(4)
*/
// struct(...binary_pats) {
// // Re structure the pattern according to a binary pattern (false values are dropped)
@ -950,7 +984,7 @@ export class Pattern {
* @param {function} func
* @returns Pattern
* @example
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5))
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
*/
when(binary_pat, func) {
//binary_pat = sequence(binary_pat)
@ -969,7 +1003,7 @@ export class Pattern {
* @param {function} func function to apply
* @returns Pattern
* @example
* "c3 eb3 g3".off(1/8, x=>x.add(7))
* "c3 eb3 g3".off(1/8, x=>x.add(7)).note()
*/
off(time_pat, func) {
return stack(this, func(this.late(time_pat)));
@ -983,7 +1017,7 @@ export class Pattern {
* @param {function} func function to apply
* @returns Pattern
* @example
* note("c3 d3 e3 g3").every(4, x=>x.rev()).out()
* note("c3 d3 e3 g3").every(4, x=>x.rev())
*/
every(n, func) {
const pat = this;
@ -1000,7 +1034,7 @@ export class Pattern {
* @param {function} func function to apply
* @returns Pattern
* @example
* note("c3 d3 e3 g3").every(4, x=>x.rev()).out()
* note("c3 d3 e3 g3").every(4, x=>x.rev())
*/
every(n, func) {
const pat = this;
@ -1017,7 +1051,7 @@ export class Pattern {
* @param {function} func function to apply
* @returns Pattern
* @example
* note("c3 d3 e3 g3").every(4, x=>x.rev()).out()
* note("c3 d3 e3 g3").every(4, x=>x.rev())
*/
each(n, func) {
const pat = this;
@ -1043,7 +1077,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* "c3 d3 e3 g3".rev()
* note("c3 d3 e3 g3").rev()
*/
rev() {
const pat = this;
@ -1094,7 +1128,7 @@ export class Pattern {
* @example
* s("hh*2").stack(
* n("c2(3,8)")
* ).out()
* )
*/
stack(...pats) {
return stack(this, ...pats);
@ -1111,7 +1145,7 @@ export class Pattern {
* @example
* s("hh*2").seq(
* n("c2(3,8)")
* ).out()
* )
*/
seq(...pats) {
return sequence(this, ...pats);
@ -1124,7 +1158,7 @@ export class Pattern {
* @example
* s("hh*2").cat(
* n("c2(3,8)")
* ).out()
* )
*/
cat(...pats) {
return cat(this, ...pats);
@ -1146,7 +1180,7 @@ export class Pattern {
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* .superimpose(x=>x.add(2))
* .scale('C minor').note().out()
* .scale('C minor').note()
*/
superimpose(...funcs) {
return this.stack(...funcs.map((func) => func(this)));
@ -1171,7 +1205,7 @@ export class Pattern {
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().legato(.2).out()
* .scale('C minor').note().legato(.2)
*/
_echoWith(times, time, func) {
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
@ -1182,8 +1216,11 @@ export class Pattern {
* @name echo
* @memberof Pattern
* @returns Pattern
* @param {number} times how many times to repeat
* @param {number} time cycle offset between iterations
* @param {number} feedback velocity multiplicator for each iteration
* @example
* s("bd sd").echo(3, 1/6, .8).out()
* s("bd sd").echo(3, 1/6, .8)
*/
_echo(times, time, feedback) {
return this._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
@ -1195,7 +1232,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* note("0 1 2 3".scale('A minor')).iter(4).out()
* note("0 1 2 3".scale('A minor')).iter(4)
*/
iter(times, back = false) {
return slowcat(...listRange(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
@ -1207,7 +1244,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* note("0 1 2 3".scale('A minor')).iterBack(4).out()
* note("0 1 2 3".scale('A minor')).iterBack(4)
*/
iterBack(times) {
return this.iter(times, true);
@ -1219,7 +1256,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note().out()
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
*/
_chunk(n, func, back = false) {
const binary = Array(n - 1).fill(false);
@ -1234,7 +1271,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note().out()
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
*/
_chunkBack(n, func) {
return this._chunk(n, func, true);
@ -1254,16 +1291,40 @@ export class Pattern {
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
}
// sets hap relative duration of haps
/**
*
* Multiplies the hap duration with the given factor.
* @name legato
* @memberof Pattern
* @example
* note("c3 eb3 g3 c4").legato("<.25 .5 1 2>")
*/
_legato(value) {
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
}
/**
*
* Sets the velocity from 0 to 1. Is multiplied together with gain.
* @name velocity
* @example
* s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1")
* .velocity(".4 1")
*/
_velocity(velocity) {
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
}
// move this to controls? (speed and unit are controls)
/**
* Makes the sample fit the given number of cycles by changing the speed.
* @name loopAt
* @memberof Pattern
* @returns Pattern
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes").loopAt(4,1)
*/
_loopAt(factor, cps = 1) {
return this.speed((1 / factor) * cps)
.unit('c')
@ -1319,9 +1380,48 @@ function _composeOp(a, b, func) {
keepif: [(a, b) => (b ? a : undefined)],
// numerical functions
/**
*
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
* @name add
* @memberof Pattern
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* "0 2 4".add("<0 3 4 0>").scale('C major').note()
* // Without add, the equivalent would be:
* // "<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major').note()
* @example
* // You can also use add with notes:
* "c3 e3 g3".add("<0 5 7 0>").note()
* // Behind the scenes, the notes are converted to midi numbers:
* // "48 52 55".add("<0 5 7 0>").note()
*/
add: [(a, b) => a + b, numOrString], // support string concatenation
/**
*
* Like add, but the given numbers are subtracted.
* @name sub
* @memberof Pattern
* @example
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
* // See add for more information.
*/
sub: [(a, b) => a - b, num],
/**
*
* Multiplies each number by the given factor.
* @name mul
* @memberof Pattern
* @example
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
*/
mul: [(a, b) => a * b, num],
/**
*
* Divides each number by the given factor.
* @name div
* @memberof Pattern
*/
div: [(a, b) => a / b, num],
mod: [mod, num],
pow: [Math.pow, num],
@ -1363,8 +1463,7 @@ function _composeOp(a, b, func) {
// avoid union, as we want to throw away the value of 'b' completely
result = pat['_op' + how](other, (a) => (b) => op(a, b));
result = result._removeUndefineds();
}
else {
} else {
result = pat['_op' + how](other, (a) => (b) => _composeOp(a, b, op));
}
return result;
@ -1480,7 +1579,7 @@ export function reify(thing) {
*
* @return {Pattern}
* @example
* stack(g3, b3, [e4, d4]) // "g3,b3,[e4,d4]"
* stack(g3, b3, [e4, d4]).note() // "g3,b3,[e4,d4]".note()
*/
export function stack(...pats) {
// Array test here is to avoid infinite recursions..
@ -1553,7 +1652,7 @@ export function fastcat(...pats) {
* @param {...any} items - The items to concatenate
* @return {Pattern}
* @example
* cat(e5, b4, [d5, c5]) // "<e5 b4 [d5 c5]>"
* cat(e5, b4, [d5, c5]).note() // "<e5 b4 [d5 c5]>".note()
*
*/
export function cat(...pats) {
@ -1563,7 +1662,7 @@ export function cat(...pats) {
/** Like {@link seq}, but each step has a length, relative to the whole.
* @return {Pattern}
* @example
* timeCat([3,e3],[1, g3]) // "e3@3 g3"
* timeCat([3,e3],[1, g3]).note() // "e3@3 g3".note()
*/
export function timeCat(...timepats) {
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
@ -1584,7 +1683,7 @@ export function sequence(...pats) {
/** Like **cat**, but the items are crammed into one cycle. Synonyms: fastcat, sequence
* @example
* seq(e5, b4, [d5, c5]) // "e5 b4 [d5 c5]"
* seq(e5, b4, [d5, c5]).note() // "e5 b4 [d5 c5]".note()
*
*/
export function seq(...pats) {
@ -1657,6 +1756,7 @@ export const mul = curry((a, pat) => pat.mul(a));
export const off = curry((t, f, pat) => pat.off(t, f));
export const ply = curry((a, pat) => pat.ply(a));
export const range = curry((a, b, pat) => pat.range(a, b));
export const rangex = curry((a, b, pat) => pat.rangex(a, b));
export const range2 = curry((a, b, pat) => pat.range2(a, b));
export const rev = (pat) => pat.rev();
export const slow = curry((a, pat) => pat.slow(a));
@ -1743,6 +1843,18 @@ Pattern.prototype.inside = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._inside)(...args, this);
};
Pattern.prototype.range = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._range)(...args, this);
};
Pattern.prototype.rangex = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._rangex)(...args, this);
};
Pattern.prototype.range2 = function (...args) {
args = args.map(reify);
return patternify2(Pattern.prototype._range2)(...args, this);
};
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
Pattern.prototype.bootstrap = function () {

View file

@ -27,9 +27,9 @@ export const isaw2 = isaw._toBipolar();
*
* @return {Pattern}
* @example
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4))
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
* @example
* saw.range(0,8).segment(8).scale('C major').slow(4)
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
*
*/
export const saw = signal((t) => t % 1);
@ -42,7 +42,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
*
* @return {Pattern}
* @example
* sine.segment(16).range(0,15).slow(2).scale('C minor')
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
*
*/
export const sine = sine2._fromBipolar();
@ -52,7 +52,7 @@ export const sine = sine2._fromBipolar();
*
* @return {Pattern}
* @example
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor')
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
*
*/
export const cosine = sine._early(Fraction(1).div(4));
@ -63,7 +63,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
*
* @return {Pattern}
* @example
* square.segment(2).range(0,7).scale('C minor')
* square.segment(2).range(0,7).scale('C minor').note()
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
@ -74,7 +74,7 @@ export const square2 = square._toBipolar();
*
* @return {Pattern}
* @example
* tri.segment(8).range(0,7).scale('C minor')
* tri.segment(8).range(0,7).scale('C minor').note()
*
*/
export const tri = fastcat(isaw, saw);
@ -120,7 +120,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
* @name rand
* @example
* // randomly change the cutoff
* s("bd sd,hh*4").cutoff(rand.range(500,2000)).out()
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
*
*/
export const rand = signal(timeToRand);
@ -142,7 +142,7 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
* @param {number} n max value (exclusive)
* @example
* // randomly select scale notes from 0 - 7 (= C to C)
* irand(8).struct("x(3,8)").scale('C minor').note().out()
* irand(8).struct("x(3,8)").scale('C minor').note()
*
*/
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
@ -208,9 +208,9 @@ Pattern.prototype.choose2 = function (...xs) {
* Picks one of the elements at random each cycle.
* @returns {Pattern}
* @example
* chooseCycles("bd", "hh", "sd").s().fast(4).out()
* chooseCycles("bd", "hh", "sd").s().fast(4)
* @example
* "bd | hh | sd".s().fast(4).out()
* "bd | hh | sd".s().fast(4)
*/
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
@ -252,7 +252,7 @@ export const perlinWith = (pat) => {
* @name perlin
* @example
* // randomly change the cutoff
* s("bd sd,hh*4").cutoff(perlin.range(500,2000)).out()
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
*
*/
export const perlin = perlinWith(time);
@ -271,9 +271,9 @@ Pattern.prototype._degradeByWith = function (withPat, x) {
* @param {number} amount - a number between 0 and 1
* @returns Pattern
* @example
* s("hh*8").degradeBy(0.2).out()
* s("hh*8").degradeBy(0.2)
* @example
* s("[hh?0.2]*8").out()
* s("[hh?0.2]*8")
*/
Pattern.prototype._degradeBy = function (x) {
return this._degradeByWith(rand, x);
@ -287,9 +287,9 @@ Pattern.prototype._degradeBy = function (x) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").degrade().out()
* s("hh*8").degrade()
* @example
* s("[hh?]*8").out()
* s("[hh?]*8")
*/
Pattern.prototype.degrade = function () {
return this._degradeBy(0.5);
@ -306,7 +306,7 @@ Pattern.prototype.degrade = function () {
* @param {number} amount - a number between 0 and 1
* @returns Pattern
* @example
* s("hh*8").undegradeBy(0.2).out()
* s("hh*8").undegradeBy(0.2)
*/
Pattern.prototype._undegradeBy = function (x) {
return this._degradeByWith(
@ -340,7 +340,7 @@ Pattern.prototype._sometimesBy = function (x, func) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5")).out()
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
*/
Pattern.prototype.sometimesBy = function (patx, func) {
const pat = this;
@ -370,7 +370,7 @@ Pattern.prototype.sometimesByPre = function (patx, func) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh*4").sometimes(x=>x.speed("0.5")).out()
* s("hh*4").sometimes(x=>x.speed("0.5"))
*/
Pattern.prototype.sometimes = function (func) {
return this._sometimesBy(0.5, func);
@ -398,7 +398,7 @@ Pattern.prototype._someCyclesBy = function (x, func) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5")).out()
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
*/
Pattern.prototype.someCyclesBy = function (patx, func) {
const pat = this;
@ -415,7 +415,7 @@ Pattern.prototype.someCyclesBy = function (patx, func) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh(3,8)").someCycles(x=>x.speed("0.5")).out()
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
*/
Pattern.prototype.someCycles = function (func) {
return this._someCyclesBy(0.5, func);
@ -429,7 +429,7 @@ Pattern.prototype.someCycles = function (func) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").often(x=>x.speed("0.5")).out()
* s("hh*8").often(x=>x.speed("0.5"))
*/
Pattern.prototype.often = function (func) {
return this.sometimesBy(0.75, func);
@ -443,7 +443,7 @@ Pattern.prototype.often = function (func) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").rarely(x=>x.speed("0.5")).out()
* s("hh*8").rarely(x=>x.speed("0.5"))
*/
Pattern.prototype.rarely = function (func) {
return this.sometimesBy(0.25, func);
@ -457,7 +457,7 @@ Pattern.prototype.rarely = function (func) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").almostNever(x=>x.speed("0.5")).out()
* s("hh*8").almostNever(x=>x.speed("0.5"))
*/
Pattern.prototype.almostNever = function (func) {
return this.sometimesBy(0.1, func);
@ -471,7 +471,7 @@ Pattern.prototype.almostNever = function (func) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").almostAlways(x=>x.speed("0.5")).out()
* s("hh*8").almostAlways(x=>x.speed("0.5"))
*/
Pattern.prototype.almostAlways = function (func) {
return this.sometimesBy(0.9, func);
@ -485,7 +485,7 @@ Pattern.prototype.almostAlways = function (func) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").never(x=>x.speed("0.5")).out()
* s("hh*8").never(x=>x.speed("0.5"))
*/
Pattern.prototype.never = function (func) {
return this;
@ -499,7 +499,7 @@ Pattern.prototype.never = function (func) {
* @memberof Pattern
* @returns Pattern
* @example
* s("hh*8").always(x=>x.speed("0.5")).out()
* s("hh*8").always(x=>x.speed("0.5"))
*/
Pattern.prototype.always = function (func) {
return func(this);

View file

@ -372,6 +372,11 @@ describe('Pattern', () => {
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
);
});
it('copes with breaking up events across cycles', () => {
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual(
[hap(ts(0, 1), ts(0, 0.5), 'a'), hap(ts(0.5, 1.5), ts(1, 1.5), 'a')]
);
});
});
describe('_compressSpan()', () => {
it('Can squash cycles of a pattern into a given timespan', () => {
@ -848,6 +853,13 @@ describe('Pattern', () => {
);
});
});
describe('range', () => {
it('Can be patterned', () => {
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(
sequence(0, 0.5).firstCycle(),
);
});
});
describe('range2', () => {
it('Can change the range of a bipolar pattern', () => {
expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(

View file

@ -1,11 +1,11 @@
/*
util.test.mjs - <short description TODO>
util.test.mjs - Tests for the core 'util' module
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.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 { pure } from '../pattern.mjs';
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency, getPlayableNoteValue } from '../util.mjs';
import { describe, it, expect } from 'vitest';
describe('isNote', () => {
@ -70,6 +70,10 @@ describe('toMidi', () => {
expect(toMidi('C#3')).toEqual(49);
expect(toMidi('C##3')).toEqual(50);
});
it('should throw an error when given a non-note', () => {
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
});
});
describe('fromMidi', () => {
it('should turn midi into frequency', () => {
@ -78,13 +82,27 @@ describe('fromMidi', () => {
});
});
describe('getFrequency', () => {
it('should turn midi into frequency', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
it('should turn note into frequency', () => {
expect(getFrequency(happify('a4'))).toEqual(440);
expect(getFrequency(happify('a3'))).toEqual(220);
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440); // TODO: migrate when values are objects..
});
it('should turn midi into frequency', () => {
expect(getFrequency(happify(69, { type: 'midi' }))).toEqual(440);
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
});
it('should return frequencies unchanged', () => {
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
});
it('should turn object with a "freq" property into frequency', () => {
expect(getFrequency(happify({freq: 220}))).toEqual(220)
expect(getFrequency(happify({freq: 440}))).toEqual(440)
});
it('should throw an error when given a non-note', () => {
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`)
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`)
});
});
describe('mod', () => {
@ -118,3 +136,26 @@ describe('compose', () => {
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
});
});
describe('getPlayableNoteValue', () => {
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
it('should return object "note" property', () => {
expect(getPlayableNoteValue(happify({note: "a4"}))).toEqual('a4')
});
it('should return object "n" property', () => {
expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4')
});
it('should return object "value" property', () => {
expect(getPlayableNoteValue(happify({value: "a4"}))).toEqual('a4')
});
it('should turn midi into frequency', () => {
expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220)
})
it('should return frequency value', () => {
expect(getPlayableNoteValue(happify(220, {type: 'frequency'}))).toEqual(220)
})
it('should throw an error if value is not an object, number, or string', () => {
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`)
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`)
})
});

View file

@ -31,6 +31,9 @@ export const fromMidi = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
*/
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return fromMidi(noteOrMidi);
@ -38,6 +41,9 @@ export const getFreq = (noteOrMidi) => {
return fromMidi(toMidi(noteOrMidi));
};
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
*/
export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1;
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
@ -78,7 +84,7 @@ export const getFrequency = (hap) => {
} else if (typeof value === 'string' && isNote(value)) {
value = fromMidi(toMidi(hap.value));
} else if (typeof value !== 'number') {
throw new Error('not a note or frequency:' + value);
throw new Error('not a note or frequency: ' + value);
}
return value;
};

View file

@ -18,7 +18,6 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
.legato(sine.range(0.3, 2).slow(28))
.wave("sawtooth square".fast(2))
.filter('lowpass', cosine.range(500,4000).slow(16))
.out()
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
-->
</strudel-repl>

View file

@ -10,7 +10,6 @@
.legato(sine.range(0.3, 2).slow(28))
.wave("sawtooth square".fast(2))
.filter('lowpass', cosine.range(500,4000).slow(16))
.out()
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
-->
</strudel-repl>

File diff suppressed because one or more lines are too long

View file

@ -7,9 +7,9 @@ import { tags } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import { useInView } from 'react-hook-inview';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import { Tone } from '@strudel.cycles/tone';
import { TimeSpan, State, Scheduler } from '@strudel.cycles/core';
import { TimeSpan, State } from '@strudel.cycles/core';
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
var strudelTheme = createTheme({
@ -251,7 +251,7 @@ let s4 = () => {
};
const generateHash = (code) => encodeURIComponent(btoa(code));
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
@ -282,26 +282,15 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
const { onTrigger, velocity } = event.context;
if (!onTrigger) {
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event, currentTime, 1 /* cps */);
}
const { onTrigger = webaudioOutputTrigger } = event.context;
onTrigger(time, event, currentTime, 1 /* cps */);
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
},
[onEvent, pushLog, defaultSynth],
[onEvent, pushLog],
),
onQuery: useCallback(
(state) => {
@ -483,10 +472,9 @@ function Icon({ type }) {
}[type]);
}
function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
tune,
defaultSynth,
autolink: false,
onEvent
});
@ -556,6 +544,133 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, on
})));
}
// will move to https://github.com/felixroos/zyklus
// TODO: started flag
function createClock(
getTime,
callback, // called slightly before each cycle
duration = 0.05, // duration of each cycle
interval = 0.1, // interval between callbacks
overlap = 0.1, // overlap between callbacks
) {
let tick = 0; // counts callbacks
let phase = 0; // next callback time
let precision = 10 ** 4; // used to round phase
let minLatency = 0.01;
const setDuration = (setter) => (duration = setter(duration));
overlap = overlap || interval / 2;
const onTick = () => {
const t = getTime();
const lookahead = t + interval + overlap; // the time window for this tick
if (phase === 0) {
phase = t + minLatency;
}
// callback as long as we're inside the lookahead
while (phase < lookahead) {
phase = Math.round(phase * precision) / precision;
phase >= t && callback(phase, duration, tick);
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
phase += duration; // increment phase by duration
tick++;
}
};
let intervalID;
const start = () => {
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => clearInterval(intervalID);
const pause = () => clear();
const stop = () => {
tick = 0;
phase = 0;
clear();
};
const getPhase = () => phase;
// setCallback
return { setDuration, start, stop, pause, duration, getPhase };
}
/*
cyclist.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/scheduler.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/>.
*/
class Cyclist {
worker;
pattern;
started = false;
cps = 1; // TODO
getTime;
phase = 0;
constructor({ interval, onTrigger, onError, getTime, latency = 0.1 }) {
this.getTime = getTime;
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
(phase, duration, tick) => {
if (tick === 0) {
this.origin = phase;
}
const begin = round(phase - this.origin);
this.phase = begin - latency;
const end = round(begin + duration);
const time = getTime();
try {
const haps = this.pattern.queryArc(begin, end); // get Haps
// console.log('haps', haps.map((hap) => hap.value.n).join(' '));
haps.forEach((hap) => {
// console.log('hap', hap.value.n, hap.part.begin);
if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = hap.whole.begin + this.origin - time + latency;
const duration = hap.duration * 1;
onTrigger?.(hap, deadline, duration);
}
});
} catch (e) {
console.warn('scheduler error', e);
onError?.(e);
}
}, // called slightly before each cycle
interval, // duration of each cycle
);
}
getPhase() {
return this.phase;
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
this.clock.start();
this.started = true;
}
pause() {
this.clock.stop();
delete this.origin;
this.started = false;
}
stop() {
delete this.origin;
this.clock.stop();
this.started = false;
}
setPattern(pat) {
this.pattern = pat;
}
setCps(cps = 1) {
this.cps = cps;
}
log(begin, end, haps) {
const onsets = haps.filter((h) => h.hasOnset());
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
}
}
// import { Scheduler } from '@strudel.cycles/core';
function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = false }) {
// scheduler
const [schedulerError, setSchedulerError] = useState();
@ -565,7 +680,8 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
const isDirty = code !== activeCode;
// TODO: how / when to remove schedulerError?
const scheduler = useMemo(
() => new Scheduler({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }),
// () => new Scheduler({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }),
() => new Cyclist({ interval, onTrigger: defaultOutput, onError: setSchedulerError, getTime }),
[defaultOutput, interval],
);
const evaluate$1 = useCallback(async () => {

View file

@ -1,13 +1,9 @@
import React from 'react';
import { MiniRepl } from './components/MiniRepl';
import 'tailwindcss/tailwind.css';
import { Tone, getDefaultSynth } from '@strudel.cycles/tone';
import { evalScope } from '@strudel.cycles/eval';
const defaultSynth = getDefaultSynth();
evalScope(
Tone,
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
@ -20,7 +16,7 @@ evalScope(
function App() {
return (
<div>
<MiniRepl tune={`"c3"`} defaultSynth={defaultSynth} />
<MiniRepl tune={`"c3"`} />
</div>
);
}

View file

@ -10,11 +10,10 @@ import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
import { Tone } from '@strudel.cycles/tone';
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
useRepl({
tune,
defaultSynth,
autolink: false,
onEvent,
});

View file

@ -6,9 +6,9 @@ This program is free software: you can redistribute it and/or modify it under th
import { useCallback, useState, useMemo } from 'react';
import { evaluate } from '@strudel.cycles/eval';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import useCycle from './useCycle.mjs';
import usePostMessage from './usePostMessage.mjs';
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
@ -17,7 +17,7 @@ let s4 = () => {
};
const generateHash = (code) => encodeURIComponent(btoa(code));
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
@ -48,26 +48,15 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
const { onTrigger, velocity } = event.context;
if (!onTrigger) {
if (defaultSynth) {
const note = getPlayableNoteValue(event);
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
} else {
throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event, currentTime, 1 /* cps */);
}
const { onTrigger = webaudioOutputTrigger } = event.context;
onTrigger(time, event, currentTime, 1 /* cps */);
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
},
[onEvent, pushLog, defaultSynth],
[onEvent, pushLog],
),
onQuery: useCallback(
(state) => {

View file

@ -69,9 +69,9 @@ function scaleOffset(scale, offset, note) {
* @memberof Pattern
* @name transpose
* @example
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).transpose(0)
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
* @example
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).transpose(0)
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
*/
Pattern.prototype._transpose = function (intervalOrSemitones) {
@ -107,6 +107,7 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
* "-8 [2,4,6]"
* .scale('C4 bebop major')
* .scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")
* .note()
*/
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
@ -134,9 +135,10 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
* @name scale
* @param {string} scale Name of scale
* @returns Pattern
* @example
* @example
* "0 2 4 6 4 2"
* .scale(seq('C2 major', 'C2 minor').slow(2))
* .note()
*/
Pattern.prototype._scale = function (scale /* : string */) {

View file

@ -40,7 +40,7 @@ Pattern.prototype.fmapNested = function (func) {
* @param {range} range note range for possible voicings (optional, defaults to `['F3', 'A4']`)
* @returns Pattern
* @example
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>").note()
*/
Pattern.prototype.voicings = function (range) {

View file

@ -15,13 +15,13 @@ npm i @strudel.cycles/webaudio --save
import { Scheduler, getAudioContext } from '@strudel.cycles/webaudio';
const scheduler = new Scheduler({
audioContext: getAudioContext(),
interval: 0.1,
onEvent: (e) => e.context?.createAudioNode?.(e),
});
const pattern = sequence([55, 99], 110).osc('sawtooth').out()
audioContext: getAudioContext(),
interval: 0.1,
onEvent: (e) => e.context?.createAudioNode?.(e),
});
const pattern = sequence([55, 99], 110).osc('sawtooth');
scheduler.setPattern(pattern);
scheduler.start()
scheduler.start();
//scheduler.stop()
```

View file

@ -18,3 +18,6 @@ if (typeof AudioContext !== 'undefined') {
return convolver;
};
}
// TODO: make the reverb more exciting
// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html

View file

@ -98,7 +98,7 @@ export const loadGithubSamples = async (path, nameFn) => {
* bd: '808bd/BD0000.WAV',
* sd: '808sd/SD0010.WAV'
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
* s("[bd ~]*2, [~ hh]*2, ~ sd").out()
* s("[bd ~]*2, [~ hh]*2, ~ sd")
*
*/

View file

@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
import * as strudel from '@strudel.cycles/core';
import { fromMidi, toMidi } from '@strudel.cycles/core';
import { fromMidi, isNote, toMidi } from '@strudel.cycles/core';
import './feedbackdelay.mjs';
import './reverb.mjs';
import { loadBuffer, reverseBuffer } from './sampler.mjs';
@ -115,7 +115,11 @@ const getSampleBufferSource = async (s, n, note, speed) => {
}
const bank = samples?.[s];
if (!bank) {
throw new Error(`sample not found: "${s}", try one of ${Object.keys(samples).join(', ')}`);
throw new Error(
`sample not found: "${s}", try one of ${Object.keys(samples)
.map((s) => `"${s}"`)
.join(', ')}.`,
);
}
if (typeof bank !== 'object') {
throw new Error('wrong format for sample bank:', s);
@ -234,6 +238,10 @@ function effectSend(input, effect, wet) {
export const webaudioOutput = async (hap, deadline, hapDuration) => {
try {
const ac = getAudioContext();
/* if (isNote(hap.value)) {
// supports primitive hap values that look like notes
hap.value = { note: hap.value };
} */
if (typeof hap.value !== 'object') {
throw new Error(
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
@ -249,7 +257,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
clip = 0, // if 1, samples will be cut off when the hap ends
n = 0,
note,
gain = 1,
gain = 0.8,
cutoff,
resonance = 1,
hcutoff,
@ -260,10 +268,6 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
crush,
shape,
pan,
attack = 0.001,
decay = 0.001,
sustain = 1,
release = 0.001,
speed = 1, // sample playback speed
begin = 0,
end = 1,
@ -291,6 +295,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
[note, n] = splitSN(note, n);
}
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
// with synths, n and note are the same thing
n = note || n || 36;
if (typeof n === 'string') {
@ -310,6 +316,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hapDuration);
chain.push(adsr);
} else {
// destructure adsr here, because the default should be different for synths and samples
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
// load sample
if (speed === 0) {
// no playback
@ -422,7 +430,9 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
}
};
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
Pattern.prototype.out = function () {
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
return this.onTrigger((t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps));
return this.onTrigger(webaudioOutputTrigger);
};