diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 6b669660..4c5f40c8 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -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'], @@ -67,7 +67,7 @@ const generic_params = [ * @name gain * @param {number | Pattern} amount gain. * @example - * s("hh*8").gain(".4!2 1 .4!2 1 .4 1").out() + * 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,7 +140,7 @@ 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.'], @@ -152,7 +152,7 @@ const generic_params = [ * @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample * @example * samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/') - * s("rave").begin("<0 .25 .5 .75>").out() + * s("rave").begin("<0 .25 .5 .75>") * */ [ @@ -167,7 +167,7 @@ const generic_params = [ * @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>").out() + * s("bd*2,oh*4").end("<.1 .2 .5 1>") * */ [ @@ -205,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(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>").out() + * s(",hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>") * */ [ @@ -214,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>") * */ [ @@ -256,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 @@ -267,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 @@ -280,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>") * */ [ @@ -295,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.'], @@ -371,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() * */ [ @@ -496,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>") * */ [ @@ -599,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>") * */ [ @@ -665,7 +665,7 @@ const generic_params = [ * @param {string | Pattern} vowel You can use a e i o u. * @example * note("c2 >").s('sawtooth') - * .vowel(">").out() + * .vowel(">") * */ [ diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 96f5b40d..45a59783 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -32,52 +32,52 @@ const euclid = (pulses, steps, rotation = 0) => { * @returns Pattern * @example * // The Cuban tresillo pattern. - * n("c3").euclid(3,8).out() + * note("c3").euclid(3,8) */ /** * @example // A thirteenth century Persian rhythm called Khafif-e-ramal. - * n("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 children’s 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)); @@ -88,7 +88,7 @@ Pattern.prototype.euclid = function (pulses, steps, rotation = 0) { * @name euclidLegato * @memberof Pattern * @example - * n("g2").decay(.1).sustain(.3).euclidLegato(3,8).out() + * n("g2").decay(.1).sustain(.3).euclidLegato(3,8) */ Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) { const bin_pat = euclid(pulses, steps, rotation); diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 7c917be3..e98c69e1 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -472,7 +472,7 @@ export class Pattern { * @memberof Pattern * @returns Pattern * @example - * "0.5 1.5 2.5".round().scale('C major') + * "0.5 1.5 2.5".round().scale('C major').note() */ round() { return this._asNumber().fmap((v) => Math.round(v)); @@ -524,7 +524,7 @@ 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) { return this.mul(max - min).add(min); @@ -704,7 +704,7 @@ export class Pattern { * @name apply * @memberof Pattern * @example - * "".scale('C minor').apply(scaleTranspose("0,2,4")) + * "".scale('C minor').apply(scaleTranspose("0,2,4")).note() */ _apply(func) { return func(this); @@ -718,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))); @@ -756,11 +756,13 @@ export class Pattern { 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)) - ); + 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)._withHap(ef)._splitQueries(); @@ -796,7 +798,7 @@ export class Pattern { * @param {number | Pattern} factor speed up factor * @returns Pattern * @example - * s(" hh").fast(2).out() // s("[ hh]*2").out() + * s(" hh").fast(2) // s("[ hh]*2") */ _fast(factor) { const fastQuery = this.withQueryTime((t) => t.mul(factor)); @@ -811,7 +813,7 @@ export class Pattern { * @param {number | Pattern} factor slow down factor * @returns Pattern * @example - * s(" hh").slow(2).out() // s("[ hh]/2").out() + * s(" hh").slow(2) // s("[ hh]/2") */ _slow(factor) { return this._fast(Fraction(1).div(factor)); @@ -841,7 +843,7 @@ export class Pattern { * .chop(4) * .rev() // reverse order of chops * .loopAt(4,1) // fit sample into 4 cycles - * .out() + * */ _chop(n) { const slices = Array.from({ length: n }, (x, i) => i); @@ -872,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); @@ -887,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); @@ -923,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) @@ -982,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) @@ -1001,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))); @@ -1015,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; @@ -1032,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; @@ -1049,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; @@ -1075,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; @@ -1126,7 +1128,7 @@ export class Pattern { * @example * s("hh*2").stack( * n("c2(3,8)") - * ).out() + * ) */ stack(...pats) { return stack(this, ...pats); @@ -1143,7 +1145,7 @@ export class Pattern { * @example * s("hh*2").seq( * n("c2(3,8)") - * ).out() + * ) */ seq(...pats) { return sequence(this, ...pats); @@ -1156,7 +1158,7 @@ export class Pattern { * @example * s("hh*2").cat( * n("c2(3,8)") - * ).out() + * ) */ cat(...pats) { return cat(this, ...pats); @@ -1178,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))); @@ -1203,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))); @@ -1218,7 +1220,7 @@ export class Pattern { * @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))); @@ -1230,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)))); @@ -1242,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); @@ -1254,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); @@ -1269,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); @@ -1295,7 +1297,7 @@ export class Pattern { * @name legato * @memberof Pattern * @example - * n("c3 eb3 g3 c4").legato("<.25 .5 1 2>").out() + * 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)))); @@ -1308,7 +1310,7 @@ export class Pattern { * @example * s("hh*8") * .gain(".4!2 1 .4!2 1 .4 1") - * .velocity(".4 1").out() + * .velocity(".4 1") */ _velocity(velocity) { return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity })); @@ -1321,7 +1323,7 @@ export class Pattern { * @returns Pattern * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) - * s("rhodes").loopAt(4,1).out() + * s("rhodes").loopAt(4,1) */ _loopAt(factor, cps = 1) { return this.speed((1 / factor) * cps) @@ -1385,14 +1387,14 @@ function _composeOp(a, b, func) { * @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') + * "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') + * // "<[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>") + * "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>") + * // "48 52 55".add("<0 5 7 0>").note() */ add: [(a, b) => a + b, numOrString], // support string concatenation /** @@ -1401,7 +1403,7 @@ function _composeOp(a, b, func) { * @name sub * @memberof Pattern * @example - * "0 2 4".sub("<0 1 2 3>").scale('C4 minor') + * "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note() * // See add for more information. */ sub: [(a, b) => a - b, num], @@ -1411,7 +1413,7 @@ function _composeOp(a, b, func) { * @name mul * @memberof Pattern * @example - * "1 1.5 [1.66, <2 2.33>]".mul(150).freq().out() + * "1 1.5 [1.66, <2 2.33>]".mul(150).freq() */ mul: [(a, b) => a * b, num], /** @@ -1577,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.. @@ -1650,7 +1652,7 @@ export function fastcat(...pats) { * @param {...any} items - The items to concatenate * @return {Pattern} * @example - * cat(e5, b4, [d5, c5]) // "" + * cat(e5, b4, [d5, c5]).note() // "".note() * */ export function cat(...pats) { @@ -1660,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)); @@ -1681,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) { diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 8637c30e..71ee9078 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -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); diff --git a/packages/embed/README.md b/packages/embed/README.md index c3b7f3ee..b0ccf52b 100644 --- a/packages/embed/README.md +++ b/packages/embed/README.md @@ -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'}) --> diff --git a/packages/embed/example.html b/packages/embed/example.html index 820422b4..9e6fca15 100644 --- a/packages/embed/example.html +++ b/packages/embed/example.html @@ -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'}) --> diff --git a/packages/react/dist/index.cjs.js b/packages/react/dist/index.cjs.js index 83d8e376..2acdb6b7 100644 --- a/packages/react/dist/index.cjs.js +++ b/packages/react/dist/index.cjs.js @@ -1,3 +1,3 @@ -"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var t=require("react"),re=require("@uiw/react-codemirror"),k=require("@codemirror/view"),L=require("@codemirror/state"),oe=require("@codemirror/lang-javascript"),i=require("@lezer/highlight"),se=require("@uiw/codemirror-themes"),ne=require("react-hook-inview"),ce=require("@strudel.cycles/eval"),le=require("@strudel.cycles/core/util.mjs"),y=require("@strudel.cycles/tone"),B=require("@strudel.cycles/core"),E=require("@strudel.cycles/midi");function $(e){return e&&typeof e=="object"&&"default"in e?e:{default:e}}var m=$(t),ie=$(re),ue=se.createTheme({theme:"dark",settings:{background:"#222",foreground:"#75baff",caret:"#ffcc00",selection:"rgba(128, 203, 196, 0.5)",selectionMatch:"#036dd626",lineHighlight:"#8a91991a",gutterBackground:"transparent",gutterForeground:"#676e95"},styles:[{tag:i.tags.keyword,color:"#c792ea"},{tag:i.tags.operator,color:"#89ddff"},{tag:i.tags.special(i.tags.variableName),color:"#eeffff"},{tag:i.tags.typeName,color:"#f07178"},{tag:i.tags.atom,color:"#f78c6c"},{tag:i.tags.number,color:"#ff5370"},{tag:i.tags.definition(i.tags.variableName),color:"#82aaff"},{tag:i.tags.string,color:"#c3e88d"},{tag:i.tags.special(i.tags.string),color:"#f07178"},{tag:i.tags.comment,color:"#7d8799"},{tag:i.tags.variableName,color:"#f07178"},{tag:i.tags.tagName,color:"#ff5370"},{tag:i.tags.bracket,color:"#a2a1a4"},{tag:i.tags.meta,color:"#ffcb6b"},{tag:i.tags.attributeName,color:"#c792ea"},{tag:i.tags.propertyName,color:"#c792ea"},{tag:i.tags.className,color:"#decb6b"},{tag:i.tags.invalid,color:"#ffffff"}]});const P=L.StateEffect.define(),de=L.StateField.define({create(){return k.Decoration.none},update(e,r){try{for(let n of r.effects)if(n.is(P))if(n.value){const u=k.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=k.Decoration.set([u.range(0,r.newDoc.length)])}else e=k.Decoration.set([]);return e}catch(n){return console.warn("flash error",n),e}},provide:e=>k.EditorView.decorations.from(e)}),K=e=>{e.dispatch({effects:P.of(!0)}),setTimeout(()=>{e.dispatch({effects:P.of(!1)})},200)},A=L.StateEffect.define(),fe=L.StateField.define({create(){return k.Decoration.none},update(e,r){try{for(let n of r.effects)if(n.is(A)){const u=n.value.map(d=>(d.context.locations||[]).map(({start:l,end:c})=>{const f=d.context.color||"#FFCA28";let o=r.newDoc.line(l.line).from+l.column,a=r.newDoc.line(c.line).from+c.column;const w=r.newDoc.length;return o>w||a>w?void 0:k.Decoration.mark({attributes:{style:`outline: 1.5px solid ${f};`}}).range(o,a)})).flat().filter(Boolean)||[];e=k.Decoration.set(u,!0)}return e}catch{return k.Decoration.set([])}},provide:e=>k.EditorView.decorations.from(e)}),ge=[oe.javascript(),ue,fe,de];function Q({value:e,onChange:r,onViewChanged:n,onSelectionChange:u,options:d,editorDidMount:l}){const c=t.useCallback(a=>{r?.(a)},[r]),f=t.useCallback(a=>{n?.(a)},[n]),o=t.useCallback(a=>{a.selectionSet&&u&&u?.(a.state.selection)},[u]);return m.default.createElement(m.default.Fragment,null,m.default.createElement(ie.default,{value:e,onChange:c,onCreateEditor:f,onUpdate:o,extensions:ge}))}function J(e){const{onEvent:r,onQuery:n,onSchedule:u,ready:d=!0,onDraw:l}=e,[c,f]=t.useState(!1),o=1,a=()=>Math.floor(y.Tone.getTransport().seconds/o),w=(p=a())=>{const S=new B.TimeSpan(p,p+1),N=n?.(new B.State(S))||[];u?.(N,p);const F=S.begin.valueOf();y.Tone.getTransport().cancel(F);const M=(p+1)*o-.5,R=Math.max(y.Tone.getTransport().seconds,M)+.1;y.Tone.getTransport().schedule(()=>{w(p+1)},R),N?.filter(b=>b.part.begin.equals(b.whole?.begin)).forEach(b=>{y.Tone.getTransport().schedule(v=>{r(v,b,y.Tone.getContext().currentTime),y.Tone.Draw.schedule(()=>{l?.(v,b)},v)},b.part.begin.valueOf())})};t.useEffect(()=>{d&&w()},[r,u,n,l,d]);const C=async()=>{f(!0),await y.Tone.start(),y.Tone.getTransport().start("+0.1")},_=()=>{y.Tone.getTransport().pause(),f(!1)};return{start:C,stop:_,onEvent:r,started:c,setStarted:f,toggle:()=>c?_():C(),query:w,activeCycle:a}}function G(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(r=>window.postMessage(r,"*"),[])}let me=()=>Math.floor((1+Math.random())*65536).toString(16).substring(1);const he=e=>encodeURIComponent(btoa(e));function X({tune:e,defaultSynth:r,autolink:n=!0,onEvent:u,onDraw:d}){const l=t.useMemo(()=>me(),[]),[c,f]=t.useState(e),[o,a]=t.useState(),[w,C]=t.useState(""),[_,T]=t.useState(),[p,S]=t.useState(!1),[N,F]=t.useState(""),[M,R]=t.useState(),b=t.useMemo(()=>c!==o||_,[c,o,_]),v=t.useCallback(g=>C(s=>s+`${s?` +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var t=require("react"),ee=require("@uiw/react-codemirror"),E=require("@codemirror/view"),A=require("@codemirror/state"),te=require("@codemirror/lang-javascript"),l=require("@lezer/highlight"),ae=require("@uiw/codemirror-themes"),re=require("react-hook-inview"),oe=require("@strudel.cycles/eval"),p=require("@strudel.cycles/tone"),B=require("@strudel.cycles/core"),se=require("@strudel.cycles/webaudio"),M=require("@strudel.cycles/midi");function U(e){return e&&typeof e=="object"&&"default"in e?e:{default:e}}var d=U(t),ne=U(ee),ce=ae.createTheme({theme:"dark",settings:{background:"#222",foreground:"#75baff",caret:"#ffcc00",selection:"rgba(128, 203, 196, 0.5)",selectionMatch:"#036dd626",lineHighlight:"#8a91991a",gutterBackground:"transparent",gutterForeground:"#676e95"},styles:[{tag:l.tags.keyword,color:"#c792ea"},{tag:l.tags.operator,color:"#89ddff"},{tag:l.tags.special(l.tags.variableName),color:"#eeffff"},{tag:l.tags.typeName,color:"#f07178"},{tag:l.tags.atom,color:"#f78c6c"},{tag:l.tags.number,color:"#ff5370"},{tag:l.tags.definition(l.tags.variableName),color:"#82aaff"},{tag:l.tags.string,color:"#c3e88d"},{tag:l.tags.special(l.tags.string),color:"#f07178"},{tag:l.tags.comment,color:"#7d8799"},{tag:l.tags.variableName,color:"#f07178"},{tag:l.tags.tagName,color:"#ff5370"},{tag:l.tags.bracket,color:"#a2a1a4"},{tag:l.tags.meta,color:"#ffcb6b"},{tag:l.tags.attributeName,color:"#c792ea"},{tag:l.tags.propertyName,color:"#c792ea"},{tag:l.tags.className,color:"#decb6b"},{tag:l.tags.invalid,color:"#ffffff"}]});const O=A.StateEffect.define(),ie=A.StateField.define({create(){return E.Decoration.none},update(e,o){try{for(let s of o.effects)if(s.is(O))if(s.value){const i=E.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=E.Decoration.set([i.range(0,o.newDoc.length)])}else e=E.Decoration.set([]);return e}catch(s){return console.warn("flash error",s),e}},provide:e=>E.EditorView.decorations.from(e)}),$=e=>{e.dispatch({effects:O.of(!0)}),setTimeout(()=>{e.dispatch({effects:O.of(!1)})},200)},L=A.StateEffect.define(),le=A.StateField.define({create(){return E.Decoration.none},update(e,o){try{for(let s of o.effects)if(s.is(L)){const i=s.value.map(u=>(u.context.locations||[]).map(({start:n,end:m})=>{const a=u.context.color||"#FFCA28";let f=o.newDoc.line(n.line).from+n.column,r=o.newDoc.line(m.line).from+m.column;const v=o.newDoc.length;return f>v||r>v?void 0:E.Decoration.mark({attributes:{style:`outline: 1.5px solid ${a};`}}).range(f,r)})).flat().filter(Boolean)||[];e=E.Decoration.set(i,!0)}return e}catch{return E.Decoration.set([])}},provide:e=>E.EditorView.decorations.from(e)}),ue=[te.javascript(),ce,le,ie];function K({value:e,onChange:o,onViewChanged:s,onSelectionChange:i,options:u,editorDidMount:n}){const m=t.useCallback(r=>{o?.(r)},[o]),a=t.useCallback(r=>{s?.(r)},[s]),f=t.useCallback(r=>{r.selectionSet&&i&&i?.(r.state.selection)},[i]);return d.default.createElement(d.default.Fragment,null,d.default.createElement(ne.default,{value:e,onChange:m,onCreateEditor:a,onUpdate:f,extensions:ue}))}function Q(e){const{onEvent:o,onQuery:s,onSchedule:i,ready:u=!0,onDraw:n}=e,[m,a]=t.useState(!1),f=1,r=()=>Math.floor(p.Tone.getTransport().seconds/f),v=(w=r())=>{const R=new B.TimeSpan(w,w+1),k=s?.(new B.State(R))||[];i?.(k,w);const T=R.begin.valueOf();p.Tone.getTransport().cancel(T);const N=(w+1)*f-.5,C=Math.max(p.Tone.getTransport().seconds,N)+.1;p.Tone.getTransport().schedule(()=>{v(w+1)},C),k?.filter(h=>h.part.begin.equals(h.whole?.begin)).forEach(h=>{p.Tone.getTransport().schedule(S=>{o(S,h,p.Tone.getContext().currentTime),p.Tone.Draw.schedule(()=>{n?.(S,h)},S)},h.part.begin.valueOf())})};t.useEffect(()=>{u&&v()},[o,i,s,n,u]);const y=async()=>{a(!0),await p.Tone.start(),p.Tone.getTransport().start("+0.1")},b=()=>{p.Tone.getTransport().pause(),a(!1)};return{start:y,stop:b,onEvent:o,started:m,setStarted:a,toggle:()=>m?b():y(),query:v,activeCycle:r}}function J(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(o=>window.postMessage(o,"*"),[])}let de=()=>Math.floor((1+Math.random())*65536).toString(16).substring(1);const fe=e=>encodeURIComponent(btoa(e));function G({tune:e,autolink:o=!0,onEvent:s,onDraw:i}){const u=t.useMemo(()=>de(),[]),[n,m]=t.useState(e),[a,f]=t.useState(),[r,v]=t.useState(""),[y,b]=t.useState(),[W,w]=t.useState(!1),[R,k]=t.useState(""),[T,N]=t.useState(),C=t.useMemo(()=>n!==a||y,[n,a,y]),h=t.useCallback(g=>v(c=>c+`${c?` -`:""}${g}`),[]),W=t.useMemo(()=>{if(o&&!o.includes("strudel disable-highlighting"))return(g,s)=>d?.(g,s,o)},[o,d]),O=t.useMemo(()=>o&&o.includes("strudel hide-header"),[o]),H=t.useMemo(()=>o&&o.includes("strudel hide-console"),[o]),h=J({onDraw:W,onEvent:t.useCallback((g,s,ee)=>{try{u?.(s),s.context.logs?.length&&s.context.logs.forEach(v);const{onTrigger:q,velocity:te}=s.context;if(q)q(g,s,ee,1);else if(r){const ae=le.getPlayableNoteValue(s);r.triggerAttackRelease(ae,s.duration.valueOf(),g,te)}else throw new Error("no defaultSynth passed to useRepl.")}catch(q){console.warn(q),q.message="unplayable event: "+q?.message,v(q.message)}},[u,v,r]),onQuery:t.useCallback(g=>{try{return M?.query(g)||[]}catch(s){return console.warn(s),s.message="query error: "+s.message,T(s),[]}},[M]),onSchedule:t.useCallback((g,s)=>Z(g),[]),ready:!!M&&!!o}),z=G(({data:{from:g,type:s}})=>{s==="start"&&g!==l&&(h.setStarted(!1),a(void 0))}),j=t.useCallback(async(g=c)=>{if(o&&!b){T(void 0),h.start();return}try{S(!0);const s=await ce.evaluate(g);h.start(),z({type:"start",from:l}),R(()=>s.pattern),n&&(window.location.hash="#"+encodeURIComponent(btoa(c))),F(he(c)),T(void 0),a(g),S(!1)}catch(s){s.message="evaluation error: "+s.message,console.warn(s),T(s)}},[o,b,c,h,n,l,z]),Z=(g,s)=>{g.length};return{hideHeader:O,hideConsole:H,pending:p,code:c,setCode:f,pattern:M,error:_,cycle:h,setPattern:R,dirty:b,log:w,togglePlay:()=>{h.started?h.stop():j()},setActiveCode:a,activateCode:j,activeCode:o,pushLog:v,hash:N}}function V(...e){return e.filter(Boolean).join(" ")}let x=[],I;function Y({view:e,pattern:r,active:n}){t.useEffect(()=>{if(e)if(r&&n){let d=function(){try{const l=y.Tone.getTransport().seconds,f=[Math.max(I||l,l-1/10),l+1/60];I=l+1/60,x=x.filter(a=>a.whole.end>l);const o=r.queryArc(...f).filter(a=>a.hasOnset());x=x.concat(o),e.dispatch({effects:A.of(x)})}catch{e.dispatch({effects:A.of([])})}u=requestAnimationFrame(d)},u=requestAnimationFrame(d);return()=>{cancelAnimationFrame(u)}}else x=[],e.dispatch({effects:A.of([])})},[r,n,e])}const pe="_container_3i85k_1",be="_header_3i85k_5",ve="_buttons_3i85k_9",ye="_button_3i85k_9",we="_buttonDisabled_3i85k_17",Me="_error_3i85k_21",Ee="_body_3i85k_25";var D={container:pe,header:be,buttons:ve,button:ye,buttonDisabled:we,error:Me,body:Ee};function U({type:e}){return m.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",className:"sc-h-5 sc-w-5",viewBox:"0 0 20 20",fill:"currentColor"},{refresh:m.default.createElement("path",{fillRule:"evenodd",d:"M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",clipRule:"evenodd"}),play:m.default.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",clipRule:"evenodd"}),pause:m.default.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",clipRule:"evenodd"})}[e])}function ke({tune:e,defaultSynth:r,hideOutsideView:n=!1,theme:u,init:d,onEvent:l,enableKeyboard:c}){const{code:f,setCode:o,pattern:a,activeCode:w,activateCode:C,evaluateOnly:_,error:T,cycle:p,dirty:S,togglePlay:N,stop:F}=X({tune:e,defaultSynth:r,autolink:!1,onEvent:l});t.useEffect(()=>{d&&_()},[e,d]);const[M,R]=t.useState(),[b,v]=ne.useInView({threshold:.01}),W=t.useRef(),O=t.useMemo(()=>((v||!n)&&(W.current=!0),v||W.current),[v,n]);return Y({view:M,pattern:a,active:p.started&&!w?.includes("strudel disable-highlighting")}),t.useLayoutEffect(()=>{if(c){const H=async h=>{(h.ctrlKey||h.altKey)&&(h.code==="Enter"?(h.preventDefault(),K(M),await C()):h.code==="Period"&&(p.stop(),h.preventDefault()))};return window.addEventListener("keydown",H,!0),()=>window.removeEventListener("keydown",H,!0)}},[c,a,f,C,p,M]),m.default.createElement("div",{className:D.container,ref:b},m.default.createElement("div",{className:D.header},m.default.createElement("div",{className:D.buttons},m.default.createElement("button",{className:V(D.button,p.started?"sc-animate-pulse":""),onClick:()=>N()},m.default.createElement(U,{type:p.started?"pause":"play"})),m.default.createElement("button",{className:V(S?D.button:D.buttonDisabled),onClick:()=>C()},m.default.createElement(U,{type:"refresh"}))),T&&m.default.createElement("div",{className:D.error},T.message)),m.default.createElement("div",{className:D.body},O&&m.default.createElement(Q,{value:f,onChange:o,onViewChanged:R})))}function Ce(e){const{ready:r,connected:n,disconnected:u}=e,[d,l]=t.useState(!0),[c,f]=t.useState(E.WebMidi?.outputs||[]);return t.useEffect(()=>{E.enableWebMidi().then(()=>{E.WebMidi.addListener("connected",a=>{f([...E.WebMidi.outputs]),n?.(E.WebMidi,a)}),E.WebMidi.addListener("disconnected",a=>{f([...E.WebMidi.outputs]),u?.(E.WebMidi,a)}),r?.(E.WebMidi),l(!1)}).catch(a=>{if(a){console.error(a),console.warn("Web Midi could not be enabled..");return}})},[r,n,u,c]),{loading:d,outputs:c,outputByName:a=>E.WebMidi.getOutputByName(a)}}exports.CodeMirror=Q;exports.MiniRepl=ke;exports.cx=V;exports.flash=K;exports.useCycle=J;exports.useHighlighting=Y;exports.usePostMessage=G;exports.useRepl=X;exports.useWebMidi=Ce; +`:""}${g}`),[]),S=t.useMemo(()=>{if(a&&!a.includes("strudel disable-highlighting"))return(g,c)=>i?.(g,c,a)},[a,i]),H=t.useMemo(()=>a&&a.includes("strudel hide-header"),[a]),_=t.useMemo(()=>a&&a.includes("strudel hide-console"),[a]),q=Q({onDraw:S,onEvent:t.useCallback((g,c,Z)=>{try{s?.(c),c.context.logs?.length&&c.context.logs.forEach(h);const{onTrigger:x=se.webaudioOutputTrigger}=c.context;x(g,c,Z,1)}catch(x){console.warn(x),x.message="unplayable event: "+x?.message,h(x.message)}},[s,h]),onQuery:t.useCallback(g=>{try{return T?.query(g)||[]}catch(c){return console.warn(c),c.message="query error: "+c.message,b(c),[]}},[T]),onSchedule:t.useCallback((g,c)=>Y(g),[]),ready:!!T&&!!a}),z=J(({data:{from:g,type:c}})=>{c==="start"&&g!==u&&(q.setStarted(!1),f(void 0))}),V=t.useCallback(async(g=n)=>{if(a&&!C){b(void 0),q.start();return}try{w(!0);const c=await oe.evaluate(g);q.start(),z({type:"start",from:u}),N(()=>c.pattern),o&&(window.location.hash="#"+encodeURIComponent(btoa(n))),k(fe(n)),b(void 0),f(g),w(!1)}catch(c){c.message="evaluation error: "+c.message,console.warn(c),b(c)}},[a,C,n,q,o,u,z]),Y=(g,c)=>{g.length};return{hideHeader:H,hideConsole:_,pending:W,code:n,setCode:m,pattern:T,error:y,cycle:q,setPattern:N,dirty:C,log:r,togglePlay:()=>{q.started?q.stop():V()},setActiveCode:f,activateCode:V,activeCode:a,pushLog:h,hash:R}}function P(...e){return e.filter(Boolean).join(" ")}let F=[],j;function X({view:e,pattern:o,active:s}){t.useEffect(()=>{if(e)if(o&&s){let u=function(){try{const n=p.Tone.getTransport().seconds,a=[Math.max(j||n,n-1/10),n+1/60];j=n+1/60,F=F.filter(r=>r.whole.end>n);const f=o.queryArc(...a).filter(r=>r.hasOnset());F=F.concat(f),e.dispatch({effects:L.of(F)})}catch{e.dispatch({effects:L.of([])})}i=requestAnimationFrame(u)},i=requestAnimationFrame(u);return()=>{cancelAnimationFrame(i)}}else F=[],e.dispatch({effects:L.of([])})},[o,s,e])}const ge="_container_3i85k_1",me="_header_3i85k_5",he="_buttons_3i85k_9",be="_button_3i85k_9",pe="_buttonDisabled_3i85k_17",ve="_error_3i85k_21",ye="_body_3i85k_25";var D={container:ge,header:me,buttons:he,button:be,buttonDisabled:pe,error:ve,body:ye};function I({type:e}){return d.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",className:"sc-h-5 sc-w-5",viewBox:"0 0 20 20",fill:"currentColor"},{refresh:d.default.createElement("path",{fillRule:"evenodd",d:"M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",clipRule:"evenodd"}),play:d.default.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",clipRule:"evenodd"}),pause:d.default.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",clipRule:"evenodd"})}[e])}function we({tune:e,hideOutsideView:o=!1,init:s,onEvent:i,enableKeyboard:u}){const{code:n,setCode:m,pattern:a,activeCode:f,activateCode:r,evaluateOnly:v,error:y,cycle:b,dirty:W,togglePlay:w,stop:R}=G({tune:e,autolink:!1,onEvent:i});t.useEffect(()=>{s&&v()},[e,s]);const[k,T]=t.useState(),[N,C]=re.useInView({threshold:.01}),h=t.useRef(),S=t.useMemo(()=>((C||!o)&&(h.current=!0),C||h.current),[C,o]);return X({view:k,pattern:a,active:b.started&&!f?.includes("strudel disable-highlighting")}),t.useLayoutEffect(()=>{if(u){const H=async _=>{(_.ctrlKey||_.altKey)&&(_.code==="Enter"?(_.preventDefault(),$(k),await r()):_.code==="Period"&&(b.stop(),_.preventDefault()))};return window.addEventListener("keydown",H,!0),()=>window.removeEventListener("keydown",H,!0)}},[u,a,n,r,b,k]),d.default.createElement("div",{className:D.container,ref:N},d.default.createElement("div",{className:D.header},d.default.createElement("div",{className:D.buttons},d.default.createElement("button",{className:P(D.button,b.started?"sc-animate-pulse":""),onClick:()=>w()},d.default.createElement(I,{type:b.started?"pause":"play"})),d.default.createElement("button",{className:P(W?D.button:D.buttonDisabled),onClick:()=>r()},d.default.createElement(I,{type:"refresh"}))),y&&d.default.createElement("div",{className:D.error},y.message)),d.default.createElement("div",{className:D.body},S&&d.default.createElement(K,{value:n,onChange:m,onViewChanged:T})))}function Me(e){const{ready:o,connected:s,disconnected:i}=e,[u,n]=t.useState(!0),[m,a]=t.useState(M.WebMidi?.outputs||[]);return t.useEffect(()=>{M.enableWebMidi().then(()=>{M.WebMidi.addListener("connected",r=>{a([...M.WebMidi.outputs]),s?.(M.WebMidi,r)}),M.WebMidi.addListener("disconnected",r=>{a([...M.WebMidi.outputs]),i?.(M.WebMidi,r)}),o?.(M.WebMidi),n(!1)}).catch(r=>{if(r){console.error(r),console.warn("Web Midi could not be enabled..");return}})},[o,s,i,m]),{loading:u,outputs:m,outputByName:r=>M.WebMidi.getOutputByName(r)}}exports.CodeMirror=K;exports.MiniRepl=we;exports.cx=P;exports.flash=$;exports.useCycle=Q;exports.useHighlighting=X;exports.usePostMessage=J;exports.useRepl=G;exports.useWebMidi=Me; diff --git a/packages/react/dist/index.es.js b/packages/react/dist/index.es.js index c3540f7d..143fe4dd 100644 --- a/packages/react/dist/index.es.js +++ b/packages/react/dist/index.es.js @@ -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 } 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) => { @@ -484,10 +473,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 }); diff --git a/packages/react/src/App.jsx b/packages/react/src/App.jsx index 474010b3..61e81f2d 100644 --- a/packages/react/src/App.jsx +++ b/packages/react/src/App.jsx @@ -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 (
- +
); } diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index d4807138..afe804af 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -9,11 +9,10 @@ import './style.css'; import styles from './MiniRepl.module.css'; import { Icon } from './Icon'; -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, }); diff --git a/packages/react/src/hooks/useRepl.mjs b/packages/react/src/hooks/useRepl.mjs index d7f3041f..6da1b887 100644 --- a/packages/react/src/hooks/useRepl.mjs +++ b/packages/react/src/hooks/useRepl.mjs @@ -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) => { diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 10140e78..35fbaba3 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -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 */) { diff --git a/packages/tonal/voicings.mjs b/packages/tonal/voicings.mjs index 046fa02b..785f76d0 100644 --- a/packages/tonal/voicings.mjs +++ b/packages/tonal/voicings.mjs @@ -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("".voicings(), "") + * stack("".voicings(), "").note() */ Pattern.prototype.voicings = function (range) { diff --git a/packages/webaudio/README.md b/packages/webaudio/README.md index 6db6c1fa..e91086cd 100644 --- a/packages/webaudio/README.md +++ b/packages/webaudio/README.md @@ -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() ``` diff --git a/packages/webaudio/sampler.mjs b/packages/webaudio/sampler.mjs index 23b37b25..d3905c5b 100644 --- a/packages/webaudio/sampler.mjs +++ b/packages/webaudio/sampler.mjs @@ -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") * */ diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 41c6f748..d94f264f 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -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); }; diff --git a/repl/src/App.jsx b/repl/src/App.jsx index ae55fc78..a89c7a8f 100644 --- a/repl/src/App.jsx +++ b/repl/src/App.jsx @@ -7,14 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th import controls from '@strudel.cycles/core/controls.mjs'; import { evalScope, evaluate } from '@strudel.cycles/eval'; import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react'; -import { getDefaultSynth, cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone'; +import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone'; import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import './App.css'; import logo from './logo.svg'; import * as tunes from './tunes.mjs'; import { prebake } from './prebake.mjs'; import * as WebDirt from 'WebDirt'; -import { loadWebDirt } from '@strudel.cycles/webdirt'; import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio'; import { createClient } from '@supabase/supabase-js'; import { nanoid } from 'nanoid'; @@ -37,16 +36,10 @@ evalScope( import('@strudel.cycles/xen'), import('@strudel.cycles/webaudio'), import('@strudel.cycles/osc'), - import('@strudel.cycles/webdirt'), import('@strudel.cycles/serial'), import('@strudel.cycles/soundfonts'), ); -loadWebDirt({ - sampleMapUrl: 'EmuSP12.json', - sampleFolder: 'EmuSP12', -}); - prebake(); async function initCode() { @@ -87,7 +80,6 @@ function getRandomTune() { } const randomTune = getRandomTune(); -const defaultSynth = getDefaultSynth(); const isEmbedded = window.location !== window.parent.location; function App() { // const [editor, setEditor] = useState(); @@ -112,7 +104,6 @@ function App() { hideConsole, } = useRepl({ tune: '// LOADING...', - defaultSynth, }); useEffect(() => { initCode().then((decoded) => setCode(decoded || randomTune)); diff --git a/repl/src/prebake.mjs b/repl/src/prebake.mjs index f9e24e79..a6cc733c 100644 --- a/repl/src/prebake.mjs +++ b/repl/src/prebake.mjs @@ -1,7 +1,7 @@ import { Pattern, toMidi } from '@strudel.cycles/core'; import { samples } from '@strudel.cycles/webaudio'; -export async function prebake(isMock = false) { +export async function prebake({ isMock = false, baseDir = '.' } = {}) { samples( { piano: { @@ -39,12 +39,12 @@ export async function prebake(isMock = false) { }, // https://archive.org/details/SalamanderGrandPianoV3 // License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm - './piano/', + `${baseDir}/piano/`, ); if (!isMock) { await fetch('EmuSP12.json') .then((res) => res.json()) - .then((json) => samples(json, './EmuSP12/')); + .then((json) => samples(json, `${baseDir}/EmuSP12/`)); } } @@ -54,7 +54,7 @@ const panwidth = (pan, width) => pan * width + (1 - width) / 2; Pattern.prototype.piano = function () { return this.clip(1) .s('piano') - .release(.1) + .release(0.1) .fmap((value) => { const midi = typeof value.note === 'string' ? toMidi(value.note) : value.note; // pan by pitch diff --git a/repl/src/runtime.mjs b/repl/src/runtime.mjs index ee42ee22..abc84d6a 100644 --- a/repl/src/runtime.mjs +++ b/repl/src/runtime.mjs @@ -123,7 +123,7 @@ const uiHelpersMocked = { backgroundImage: id, }; -prebake(true); +prebake({ isMock: true }); // TODO: refactor to evalScope evalScope( diff --git a/repl/src/static.mjs b/repl/src/static.mjs index db3de4f1..f0746477 100644 --- a/repl/src/static.mjs +++ b/repl/src/static.mjs @@ -6,11 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th import { Tone } from '@strudel.cycles/tone'; import { State, TimeSpan } from '@strudel.cycles/core'; -import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs'; import { evaluate } from '@strudel.cycles/eval'; -import { getDefaultSynth } from '@strudel.cycles/tone'; - -const defaultSynth = getDefaultSynth(); +import { webaudioOutputTrigger } from '@strudel.cycles/webaudio'; // this is a test to play back events with as less runtime code as possible.. // the code asks for the number of seconds to prequery @@ -47,17 +44,8 @@ async function playStatic(code) { events.forEach((event) => { Tone.getTransport().schedule((time) => { try { - 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.'); - } - } else { - onTrigger(time, event); - } + const { onTrigger = webaudioOutputTrigger } = event.context; + onTrigger(time, event); } catch (err) { console.warn(err); err.message = 'unplayable event: ' + err?.message; diff --git a/repl/src/tunes.mjs b/repl/src/tunes.mjs index d1c7b987..225e84fe 100644 --- a/repl/src/tunes.mjs +++ b/repl/src/tunes.mjs @@ -155,7 +155,7 @@ export const zeldasRescue = `stack( .gain(.1) .s('triangle') .room(1) - .out()`; + `; export const caverave = `const keys = x => x.s('sawtooth').cutoff(1200).gain(.5).attack(0).decay(.16).sustain(.3).release(.1); @@ -186,7 +186,7 @@ const synths = stack( stack( drums.fast(2), synths -).slow(2).out()`; +).slow(2)`; export const sampleDrums = `samples({ bd: 'bd/BT0A0D0.wav', @@ -198,7 +198,7 @@ stack( "", "hh*4", "~ " -).s().out() +).s() `; // TODO: @@ -290,7 +290,7 @@ export const barryHarris = `backgroundImage( .scale('C bebop major') .transpose("<0 1 2 1>/8") .slow(2) - .note().piano().out() + .note().piano() `; export const blippyRhodes = `samples({ @@ -331,7 +331,7 @@ stack( .slow(2).superimpose(x=>x.add(.02)) .note().gain(.3) .s('sawtooth').cutoff(600), -).fast(3/2).out()`; +).fast(3/2)`; export const wavyKalimba = `samples({ 'kalimba': { c5:'https://freesound.org/data/previews/536/536549_11935698-lq.mp3' } @@ -359,7 +359,7 @@ stack( .clip(1) .s('kalimba') .delay(.2) - .out()`; + `; // TODO: rework tune to use freq /* @@ -401,7 +401,7 @@ stack( .scaleTranspose("0 4 0 6".early(".125 .5")).layer(scaleTranspose("0,<2 [4,6] [5,7]>/4")) ).slow(2) .velocity(sine.struct("x*8").add(3/5).mul(2/5).fast(8)) - .note().piano().out()`; + .note().piano()`; // iter, echo, echoWith export const undergroundPlumber = `backgroundImage('https://images.nintendolife.com/news/2016/08/video_exploring_the_funky_inspiration_for_the_super_mario_bros_underground_theme/large.jpg',{ className:'darken' }) @@ -425,7 +425,7 @@ stack( .echoWith(4, 1/8, (x,n)=>x.transpose(n*12).velocity(Math.pow(.4,n))) .legato(.1) .layer(h).note() -).out() +) .fast(2/3) .pianoroll({})`; @@ -444,7 +444,7 @@ stack( ).transpose(-1).note().piano(), s("mad").slow(2) ).cpm(78).slow(4) - .out() + .pianoroll() `; @@ -463,7 +463,7 @@ stack( .scale(scale) .scaleTranspose("<0>".slow(4)) .transpose(5) -.note().piano().out() +.note().piano() .velocity(.8) .slow(2) .pianoroll({maxMidi:100,minMidi:20})`; @@ -475,7 +475,7 @@ export const echoPiano = `"<0 2 [4 6](3,4,1) 3*2>" .off(1/2, x=>x.scaleTranspose(6).color('steelblue')) .legato(.5) .echo(4, 1/8, .5) -.note().piano().out() +.note().piano() .pianoroll()`; export const sml1 = ` @@ -511,7 +511,7 @@ stack( f3!2 e3!2 ab3!2 ~!2 >\` .legato(.5) -).fast(2) // .note().piano().out()`; +).fast(2) // .note().piano()`; /* // TODO: does not work on linux (at least for me..) @@ -554,7 +554,7 @@ stack( "".euclidLegato(6,8,1).note().s('bass').clip(1).gain(.8) ) .slow(6) - .out() + .pianoroll({minMidi:20,maxMidi:120,background:'transparent'}) `; @@ -570,7 +570,7 @@ export const waa2 = `n( .cutoff(cosine.range(500,4000).slow(16)) .gain(.5) .room(.5) - .out()`; + `; export const hyperpop = `const lfo = cosine.slow(15); const lfo2 = sine.slow(16); @@ -618,7 +618,7 @@ stack( "~ sn", "[~ hh3]*2" ).s().fast(2).gain(.7) -).slow(2).out() +).slow(2) // strudel disable-highlighting`; export const festivalOfFingers3 = `"[-7*3],0,2,6,[8 7]" @@ -634,7 +634,7 @@ export const festivalOfFingers3 = `"[-7*3],0,2,6,[8 7]" .scale(cat('D dorian','G mixolydian','C dorian','F mixolydian')) .legato(1) .slow(2) -.note().piano().out() +.note().piano() //.pianoroll({maxMidi:160})`; export const bossa = ` @@ -642,7 +642,7 @@ const scales = sequence('C minor', ['D locrian', 'G phrygian'], 'Bb2 minor', ['C stack( "".fast(2).struct("x ~ x@3 x ~ x ~ ~ ~ x ~ x@3".late(1/8)).early(1/8).slow(2).voicings(), "[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25) -).note().piano().out().slow(2)`; +).note().piano().slow(2)`; /* export const customTrigger = `stack( freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))), @@ -701,7 +701,7 @@ stack( .echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes //.hush() ) - .out() + .slow(3/2)`; export const swimmingWithSoundfonts = `stack( @@ -763,7 +763,7 @@ export const swimmingWithSoundfonts = `stack( "[G2 C2 F2 F2]" ).s('Acoustic Bass: Bass') ).slow(51) - .out()`; + `; export const outroMusic = `samples({ bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'], @@ -790,7 +790,7 @@ export const outroMusic = `samples({ .n(3).color('gray') ).slow(3/2) //.pianoroll({autorange:1,vertical:1,fold:0}) - .out()`; + `; export const bassFuge = `samples({ flbass: ['00_c2_finger_long_neck.wav','01_c2_finger_short_neck.wav','02_c2_finger_long_bridge.wav','03_c2_finger_short_bridge.wav','04_c2_pick_long.wav','05_c2_pick_short.wav','06_c2_palm_mute.wav'] }, 'github:cleary/samples-flbass/main/') @@ -814,7 +814,7 @@ x=>x.add(7).color('steelblue') //.hcutoff(400) .clip(1) .stack(s("bd:1*2,~ sd:0,[~ hh:0]*2")) -.out() + .pianoroll({vertical:1})`; export const bossaRandom = `const chords = "" @@ -826,7 +826,7 @@ stack( x? ~ ~ x@3 ~ x | x? ~ ~ x ~ x@3\`), roots.struct("x [~ x?0.2] x [~ x?] | x!4 | x@2 ~ ~ ~ x x x").transpose("0 7") -).slow(2).pianoroll().note().piano().out()`; +).slow(2).pianoroll().note().piano()`; export const chop = `samples({ p: 'https://cdn.freesound.org/previews/648/648433_11943129-lq.mp3' }) @@ -837,14 +837,14 @@ s("p") .shape(.4) .decay(.1) .sustain(.6) - .out()`; + `; export const delay = `stack( s("bd ") .delay("<0 .5>") .delaytime(".16 | .33") .delayfeedback(".6 | .8") - ).sometimes(x=>x.speed("-1")).out()`; + ).sometimes(x=>x.speed("-1"))`; export const orbit = `stack( s("bd ") @@ -856,4 +856,4 @@ export const orbit = `stack( .delaytime(.08) .delayfeedback(.7) .orbit(2) - ).sometimes(x=>x.speed("-1")).out()`; + ).sometimes(x=>x.speed("-1"))`; diff --git a/tutorial/MiniRepl.jsx b/tutorial/MiniRepl.jsx index e46e35bc..840e6077 100644 --- a/tutorial/MiniRepl.jsx +++ b/tutorial/MiniRepl.jsx @@ -1,24 +1,14 @@ -import { Tone } from '@strudel.cycles/tone'; import { evalScope } from '@strudel.cycles/eval'; import { MiniRepl as _MiniRepl } from '@strudel.cycles/react'; import controls from '@strudel.cycles/core/controls.mjs'; -import { loadWebDirt } from '@strudel.cycles/webdirt'; import { samples } from '@strudel.cycles/webaudio'; - -export const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination).set({ - oscillator: { type: 'triangle' }, - envelope: { - release: 0.01, - }, -}); +import { prebake } from '../repl/src/prebake.mjs'; fetch('https://strudel.tidalcycles.org/EmuSP12.json') .then((res) => res.json()) .then((json) => samples(json, 'https://strudel.tidalcycles.org/EmuSP12/')); - evalScope( - Tone, controls, import('@strudel.cycles/core'), import('@strudel.cycles/tone'), @@ -28,14 +18,10 @@ evalScope( import('@strudel.cycles/xen'), import('@strudel.cycles/webaudio'), import('@strudel.cycles/osc'), - import('@strudel.cycles/webdirt'), ); -loadWebDirt({ - sampleMapUrl: '../EmuSP12.json', - sampleFolder: '../EmuSP12', -}); +prebake(); export function MiniRepl({ tune }) { - return <_MiniRepl tune={tune} defaultSynth={defaultSynth} hideOutsideView={true} />; + return <_MiniRepl tune={tune} hideOutsideView={true} />; } diff --git a/tutorial/old.mdx b/tutorial/old.mdx new file mode 100644 index 00000000..c11e8e58 --- /dev/null +++ b/tutorial/old.mdx @@ -0,0 +1,152 @@ +# Old APIs + +These APIs are outdated and might break in the future. + +## Webdirt API (deprecated) + +You can use the powerful sampling engine [Webdirt](https://github.com/dktr0/WebDirt) with Strudel. + +{{ 'Pattern.webdirt' | jsdoc }} + +
+
+ +## Tone API (deprecated) + +The Tone API uses Tone.js instruments ands effects to create sounds. + + + +### tone(instrument) + +To change the instrument of a pattern, you can pass any [Tone.js Source](https://tonejs.github.io/docs/14.7.77/index.html) to .tone: + + + +While this works, it is a little bit verbose. To simplify things, all Tone Synths have a shortcut: + +```js +const amsynth = (options) => new AMSynth(options); +const duosynth = (options) => new DuoSynth(options); +const fmsynth = (options) => new FMSynth(options); +const membrane = (options) => new MembraneSynth(options); +const metal = (options) => new MetalSynth(options); +const monosynth = (options) => new MonoSynth(options); +const noise = (options) => new NoiseSynth(options); +const pluck = (options) => new PluckSynth(options); +const polysynth = (options) => new PolySynth(options); +const synth = (options) => new Synth(options); +const sampler = (options, baseUrl?) => new Sampler(options); // promisified, see below +const players = (options, baseUrl?) => new Sampler(options); // promisified, see below +``` + +### sampler + +With sampler, you can create tonal instruments from samples: + + + saw.struct("x*8").mul(16).round() + .legato(4).scale('D dorian').slow(2) + .tone(kalimba.toDestination()) +)`} +/> + +The sampler function promisifies [Tone.js Sampler](https://tonejs.github.io/docs/14.7.77/Sampler). + +Note that this function currently only works with this promise notation, but in the future, +it will be possible to use async instruments in a synchronous fashion. + +### players + +With players, you can create sound banks: + + + "bd hh sn hh".tone(drums.toDestination()) +) + `} +/> + +The sampler function promisifies [Tone.js Players](https://tonejs.github.io/docs/14.7.77/Players). + +Note that this function currently only works with this promise notation, but in the future, +it will be possible to use async instruments in a synchronous fashion. + +### out + +Shortcut for Tone.Destination. Intended to be used with Tone's .chain: + + + +This alone is not really useful, so read on.. + +### vol(volume) + +Helper that returns a Gain Node with the given volume. Intended to be used with Tone's .chain: + + + +### osc(type) + +Helper to set the waveform of a synth, monosynth or polysynth: + + + +The base types are `sine`, `square`, `sawtooth`, `triangle`. You can also append a number between 1 and 32 to reduce the harmonic partials. + +### lowpass(cutoff) + +Helper that returns a Filter Node of type lowpass with the given cutoff. Intended to be used with Tone's .chain: + + + +### highpass(cutoff) + +Helper that returns a Filter Node of type highpass with the given cutoff. Intended to be used with Tone's .chain: + + + +### adsr + +Helper to set the envelope of a Tone.js instrument. Intended to be used with Tone's .set: + + diff --git a/tutorial/tutorial.mdx b/tutorial/tutorial.mdx index bee7bef4..de12742e 100644 --- a/tutorial/tutorial.mdx +++ b/tutorial/tutorial.mdx @@ -17,182 +17,6 @@ The best place to actually make music with Strudel is the [Strudel REPL](https:/ To get a taste of what Strudel can do, check out this track: -/8"), -"~ ".tone(snare).bypass("<0@7 1>/4"), -"[~ c4]*2".tone(hihat) -); -const thru = (x) => x.transpose("<0 1>/8").transpose(-1); -const synths = stack( -"/2".scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).struct("[~ x]\*2") -.layer( -scaleTranspose(0).early(0), -scaleTranspose(2).early(1/8), -scaleTranspose(7).early(1/4), -scaleTranspose(8).early(3/8) -).layer(thru).tone(keys).bypass("<1 0>/16"), -"/2".struct("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).layer(thru).tone(bass), -"/2".struct("~ [x@0.1 ~]".fast(2)).voicings().layer(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4)) -) -stack( -drums.fast(2), -synths -).slow(2); -`} -/> - -[Open this track in the REPL](https://strudel.tidalcycles.org/#KCkgPT4gewogIGNvbnN0IGRlbGF5ID0gbmV3IEZlZWRiYWNrRGVsYXkoMS84LCAuNCkuY2hhaW4odm9sKDAuNSksIG91dCk7CiAgY29uc3Qga2ljayA9IG5ldyBNZW1icmFuZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBzbmFyZSA9IG5ldyBOb2lzZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBoaWhhdCA9IG5ldyBNZXRhbFN5bnRoKCkuc2V0KGFkc3IoMCwgLjA4LCAwLCAuMSkpLmNoYWluKHZvbCguMykuY29ubmVjdChkZWxheSksb3V0KTsKICBjb25zdCBiYXNzID0gbmV3IFN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC4xLCAuNCkgfSkuY2hhaW4obG93cGFzcyg5MDApLCB2b2woLjUpLCBvdXQpOwogIGNvbnN0IGtleXMgPSBuZXcgUG9seVN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC41LCAuMiwgLjcpIH0pLmNoYWluKGxvd3Bhc3MoMTIwMCksIHZvbCguNSksIG91dCk7CiAgCiAgY29uc3QgZHJ1bXMgPSBzdGFjaygKICAgICdjMSoyJy5tLnRvbmUoa2ljaykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0pLAogICAgJ34gPHghNyBbeEAzIHhdPicubS50b25lKHNuYXJlKS5ieXBhc3MoJzwwQDcgMT4vNCcubSksCiAgICAnW34gYzRdKjInLm0udG9uZShoaWhhdCkKICApOwogIAogIGNvbnN0IHRocnUgPSAoeCkgPT4geC50cmFuc3Bvc2UoJzwwIDE%2BLzgnLm0pLnRyYW5zcG9zZSgtMSk7CiAgY29uc3Qgc3ludGhzID0gc3RhY2soCiAgICAnPGViNCBkNCBjNCBiMz4vMicubS5zY2FsZSh0aW1lQ2F0KFszLCdDIG1pbm9yJ10sWzEsJ0MgbWVsb2RpYyBtaW5vciddKS5zbG93KDgpKS5ncm9vdmUoJ1t%2BIHhdKjInLm0pCiAgICAuZWRpdCgKICAgICAgc2NhbGVUcmFuc3Bvc2UoMCkuZWFybHkoMCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDIpLmVhcmx5KDEvOCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDcpLmVhcmx5KDEvNCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDgpLmVhcmx5KDMvOCkKICAgICkuZWRpdCh0aHJ1KS50b25lKGtleXMpLmJ5cGFzcygnPDEgMD4vMTYnLm0pLAogICAgJzxDMiBCYjEgQWIxIFtHMSBbRzIgRzFdXT4vMicubS5ncm9vdmUoJ1t4IFt%2BIHhdIDxbfiBbfiB4XV0hMyBbeCB4XT5AMl0vMicubS5mYXN0KDIpKS5lZGl0KHRocnUpLnRvbmUoYmFzcyksCiAgICAnPENtNyBCYjcgRm03IEc3YjEzPi8yJy5tLmdyb292ZSgnfiBbeEAwLjEgfl0nLm0uZmFzdCgyKSkudm9pY2luZ3MoKS5lZGl0KHRocnUpLmV2ZXJ5KDIsIGVhcmx5KDEvOCkpLnRvbmUoa2V5cykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0uZWFybHkoMS80KSkKICApCiAgcmV0dXJuIHN0YWNrKAogICAgZHJ1bXMuZmFzdCgyKSwgCiAgICBzeW50aHMKICApLnNsb3coMik7Cn0%3D) - -## Disclaimer - -- This project is still in its experimental state. In the future, parts of it might change significantly. -- This tutorial is far from complete. - -
- -# Mini Notation - -Similar to Tidal Cycles, Strudel has an embedded mini language that is designed to write rhythmic patterns in a short manner. -Before diving deeper into the details, here is a flavor of how the mini language looks like: - - - -The snippet above is enclosed in backticks (`), which allows you to write multi-line strings. -You can also use double quotes (") for single line mini notation. - -## Notes - -Notes are notated with the note letter, followed by the octave number. You can notate flats with `b` and sharps with `#`. - - - -Here, the same note is played over and over again, once a second. This one second is the default length of one so called "cycle". - -By the way, you can edit the contents of the player, and press "update" to hear your change! -You can also press "play" on the next player without needing to stop the last one. - -## Sequences - -We can play more notes by separating them with spaces: - - - -Here, those four notes are squashed into one cycle, so each note is a quarter second long. - -## Division - -We can slow the sequence down by enclosing it in brackets and dividing it by a number: - - - -The division by two means that the sequence will be played over the course of two cycles. -You can also use decimal numbers for any tempo you like. - -## Angle Brackets - -Using angle brackets, we can define the sequence length based on the number of children: - -"`} /> - -The above snippet is the same as: - - - -The advantage of the angle brackets, is that we can add more children without needing to change the number at the end. - -## Multiplication - -Contrary to division, a sequence can be sped up by multiplying it by a number: - - - -The multiplication by 2 here means that the sequence will play twice a cycle. - -## Bracket Nesting - -To create more interesting rhythms, you can nest sequences with brackets, like this: - - - -## Rests - -The "~" represents a rest: - - - -## Parallel - -Using commas, we can play chords: - - - -To play multiple chords in a sequence, we have to wrap them in brackets: - -"`} /> - -## Elongation - -With the "@" symbol, we can specify temporal "weight" of a sequence child: - -"`} /> - -Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1. - -## Replication - -Using "!" we can repeat without speeding up: - -"`} /> - -In essence, the `x!n` is like a shortcut for `[x*n]@n`. - -## Euclidian - -Using round brackets, we can create rhythmical sub-divisions based on three parameters: beats, segments and offset. -The first parameter controls how may beats will be played. -The second parameter controls the total amount of segments the beats will be distributed over. -The third (optional) parameter controls the starting position for distributing the beats. -One popular Euclidian rhythm (going by various names, such as "Pop Clave") is "(3,8,1)" or simply "(3,8)", -resulting in a rhythmical structure of "x ~ ~ x ~ ~ x ~" (3 beats over 8 segments, starting on position 1). - - - -
- -# Web Audio Output - -The default way to output sound is by using the Web Audio Output. -Here is a little beat to show some of the possibilities: - ],hh(3,4)") // drums .cutoff(500) // fixed cutoff .attack(1) // slowly fade in ) -.slow(3/2) -.out()`} +.slow(3/2)`} /> +## Disclaimer + +- This project is still in its experimental state. In the future, parts of it might change significantly. +- This tutorial is far from complete. + +
+ +# Playing Pitches + +Pitches are an essential building block for music. In Strudel, there are 3 different options to express a pitch: + +- `note`: letter notation +- `n`: number notation +- `freq`: frequency notation + +## note + +Notes are notated with the note letter, followed by the octave number. You can notate flats with `b` and sharps with `#`. + + + +By the way, you can edit the contents of the player, and press "update" to hear your change! +You can also press "play" on the next player without needing to stop the last one. + +## n + +If you don't like notes, you can also use numbers with `n` instead: + + + +These numbers are interpreted as so called midi numbers, where adjacent whole numbers are 1 semitone apart. +You could also write decimal numbers to get microtonal pitches: + + + +## freq + +To get maximum freedom, you can also use `freq` to directly control the frequency: + + + +In this example, we play A3 (220Hz), C#4 natural (275Hz), E4 (330Hz) and A4 (440Hz). + +
+ +# Playing Sounds + +Instead of pitches, we can also play sounds with `s`: + + + +Similarly, we can also use `s` to change the sound of our pitches: + + + +Try changing the sound to `square`, `triangle` or `sine`! + +We will go into the defails of sounds and synths [later](http://localhost:3000/tutorial/#web-audio-output). + +
+ +# Syntax + +So far, we've seen the following syntax: + +``` +xxx("foo").yyy("bar") +``` + +Generally, `xxx` and `yyy` are called functions, while `foo` and `bar` are called function arguments. +So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data. +The `yyy` function is called a chained function, because it is appended with a dot. + +Strudel makes heavy use of chained functions. Here is a more extreme example: + + + +The `//` is a line comment, resulting in the `delay` function being ignored. +It is a handy way to quickly turn stuff on and off. Try uncommenting this line by deleting `//`! + +The good news is, that this covers 99% of the JavaScript syntax needed for Strudel! + +Let's now look at the way we can express rhythms.. + +
+ +# Mini Notation + +Similar to Tidal Cycles, Strudel has an embedded mini language that is designed to write rhythmic patterns in a short manner. +Before diving deeper into the details, here is a flavor of how the mini language looks like: + + + +The snippet above is enclosed in backticks (`), which allows you to write multi-line strings. +You can also use double quotes (") for single line mini notation. + +## Sequences + +We can play more notes by separating them with spaces: + + + +Here, those four notes are squashed into one cycle, so each note is a quarter second long. +Try adding or removing notes and notice how the tempo changes! + +## Division + +We can slow the sequence down by enclosing it in brackets and dividing it by a number: + + + +The division by two means that the sequence will be played over the course of two cycles. +You can also use decimal numbers for any tempo you like. + +## Angle Brackets + +Using angle brackets, we can define the sequence length based on the number of children: + +")`} /> + +The above snippet is the same as: + + + +The advantage of the angle brackets, is that we can add more children without needing to change the number at the end. + +## Multiplication + +Contrary to division, a sequence can be sped up by multiplying it by a number: + + + +The multiplication by 2 here means that the sequence will play twice a cycle. + +## Bracket Nesting + +To create more interesting rhythms, you can nest sequences with brackets, like this: + + + +## Rests + +The "~" represents a rest: + + + +## Parallel + +Using commas, we can play chords: + + + +To play multiple chords in a sequence, we have to wrap them in brackets: + +")`} /> + +## Elongation + +With the "@" symbol, we can specify temporal "weight" of a sequence child: + +")`} /> + +Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1. + +## Replication + +Using "!" we can repeat without speeding up: + +")`} /> + +In essence, the `x!n` is like a shortcut for `[x*n]@n`. + +## Euclidian + +Using round brackets, we can create rhythmical sub-divisions based on three parameters: beats, segments and offset. +The first parameter controls how may beats will be played. +The second parameter controls the total amount of segments the beats will be distributed over. +The third (optional) parameter controls the starting position for distributing the beats. +One popular Euclidian rhythm (going by various names, such as "Pop Clave") is "(3,8,1)" or simply "(3,8)", +resulting in a rhythmical structure of "x ~ ~ x ~ ~ x ~" (3 beats over 8 segments, starting on position 1). + + + +
+ +# Synths, Samples & Effects + +Let's take a closer look at how we can control synths, sounds and effects. + ## Synths So far, all the mini notation examples all used the same sound, which is kind of boring. We can change the sound, using the `s` function: ->").s('sawtooth').out()`} /> +>").s('sawtooth')`} /> Here, we are wrapping our notes inside `note` and set the sound using `s`, connected by a dot. Those functions are only 2 of many ways to alter the properties, or _params_ of a sound. The power of patterns allows us to sequence any _param_ independently: ->").s("").out()`} /> +>").s("")`} /> Now we not only pattern the notes, but the sound as well! `sawtooth` `square` and `triangle` are the basic waveforms available in `s`. @@ -247,14 +287,14 @@ You can control the envelope of a synth using the `attack`, `decay`, `sustain` a >").s('sawtooth') - .attack(.1).decay(.1).sustain(.2).release(.1).out()`} + .attack(.1).decay(.1).sustain(.2).release(.1)`} /> ## Samples Besides Synths, `s` can also play back samples: - + To know which sounds are available, open the [default sample map](https://strudel.tidalcycles.org/EmuSP12.json) @@ -268,7 +308,7 @@ You can load your own sample map like this: sd: 'sd/rytm-01-classic.wav', hh: 'hh27/000_hh27closedhh.wav', }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/'); -s("bd sd,hh*8").out()`} +s("bd sd,hh*8")`} /> The `samples` function takes an object that maps sound names to audio file paths. @@ -282,7 +322,7 @@ Because github is a popular choice to dump samples, there is a shortcut for that sd: 'sd/rytm-01-classic.wav', hh: 'hh27/000_hh27closedhh.wav', }, 'github:tidalcycles/Dirt-Samples/master/'); -s("bd sd,hh*8").out()`} +s("bd sd,hh*8")`} /> The format is `github:user/repo/branch/`. @@ -297,7 +337,7 @@ It is also possible, to declare multiple files for one sound, using the array no sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'], hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'], }, 'github:tidalcycles/Dirt-Samples/master/'); -s(",~ ,[hh:0 hh:1]*2").out()`} +s(",~ ,[hh:0 hh:1]*2")`} /> The `:0` `:1` etc. are the indices of the array. @@ -309,7 +349,7 @@ The sample number can also be set using `n`: sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'], hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'], }, 'github:tidalcycles/Dirt-Samples/master/'); -s("bd,~ sd,hh*4").n("<0 1>").out()`} +s("bd,~ sd,hh*4").n("<0 1>")`} /> ### Pitched Sounds @@ -320,7 +360,7 @@ For pitched sounds, you can use `note`, just like with synths: tune={`samples({ "gtr": 'gtr/0001_cleanC.wav', }, 'github:tidalcycles/Dirt-Samples/master/'); -note("g3 [bb3 c4] @2").s('gtr').gain(.5).out()`} +note("g3 [bb3 c4] @2").s('gtr').gain(.5)`} /> Here, the guitar samples will overlap, because they always play till the end. @@ -331,7 +371,7 @@ If we want them to behave more like a synth, we can add `clip(1)`: "gtr": 'gtr/0001_cleanC.wav', }, 'github:tidalcycles/Dirt-Samples/master/'); note("g3 [bb3 c4] @2").s('gtr').clip(1) - .gain(.5).out()`} + .gain(.5)`} /> ### Base Pitch @@ -344,7 +384,7 @@ If we have 2 samples with different base pitches, we can make them in tune by sp "moog": { 'g3': 'moog/005_Mighty%20Moog%20G3.wav' }, }, 'github:tidalcycles/Dirt-Samples/master/'); note("g3 [bb3 c4] @2").s("gtr,moog").clip(1) - .gain(.5).out()`} + .gain(.5)`} /> If a sample has no pitch set, `c3` is the default. @@ -360,7 +400,7 @@ We can also declare different samples for different regions of the keyboard: }}, 'github:tidalcycles/Dirt-Samples/master/'); note("g2!2 !2, g4 f4]>") .s('moog').clip(1) - .gain(.5).out()`} + .gain(.5)`} /> The sampler will always pick the closest matching sample for the current note! @@ -407,33 +447,71 @@ Wether you're using a synth or a sample, you can apply these effects:
-# Core API +# JavaScript API While the mini notation is powerful on its own, there is much more to discover. Internally, the mini notation will expand to use the actual functional JavaScript API. +For example, this Pattern in Mini Notation: + + + +is equivalent to this Pattern without Mini Notation: + + + +Similarly, there is an equivalent function for every aspect of the mini notation. + +Which representation to use is a matter of context. As a rule of thumb, you can think of the JavaScript API +to fit better for the larger context, while mini notation is more practical for individiual rhythms. + +## Limits of Mini Notation + +While the Mini Notation is a powerful way to write rhythms shortly, it also has its limits. Take this example: + + + +Here, we are using mini notation for the individual rhythms, while using the function `stack` to mix them. +While stack is also available as `,` in mini notation, we cannot use it here, because we have different types of sounds. + ## Notes Notes are automatically available as variables: - + An important difference to the mini notation: For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name. The above is the same as: - + Using strings, you can also use "#". +## Alternative Syntax + +In the above example, we are nesting a function inside a function, which makes reading the parens a little more difficult. +To avoid getting to many nested parens, there is an alternative syntax to add a type to a pattern: + + + +You can use this with any function that declares a type (like `n`, `s`, `note`, `freq` etc), just make sure to leave the parens empty! + ## Pattern Factories The following functions will return a pattern. + {{ 'cat' | jsdoc }} @@ -477,7 +555,7 @@ You can freely mix JS patterns, mini patterns and values! For example, this patt stack(a3,c3,e4), stack(b3,d3,fs4), stack(b3,e4,g4) -)`} +).note()`} /> ...is equivalent to: @@ -488,12 +566,12 @@ You can freely mix JS patterns, mini patterns and values! For example, this patt "a3,c3,e4", "b3,d3,f#4", "b3,e4,g4" -)`} +).note()`} /> ... as well as: -"`} /> +")`} /> While mini notation is almost always shorter, it only has a handful of modifiers: \* / ! @. When using JS patterns, there is a lot more you can do. @@ -640,13 +718,13 @@ The Tonal API, uses [tonaljs](https://github.com/tonaljs/tonal) to provide helpe Transposes all notes to the given number of semitones: -".slow(2)).transpose(0)`} /> +".slow(2)).note()`} /> This method gets really exciting when we use it with a pattern as above. Instead of numbers, scientific interval notation can be used as well: -".slow(2)).transpose(1)`} /> +".slow(2)).note()`} /> ### scale(name) @@ -654,7 +732,8 @@ Turns numbers into notes in the scale (zero indexed). Also sets scale for other Note that the scale root is octaved here. You can also omit the octave, then index zero will default to octave 3. @@ -668,14 +747,15 @@ Transposes notes inside the scale by the number of steps: ")`} +.scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>") +.note()`} /> ### voicings(range?) Turns chord symbols into voicings, using the smoothest voice leading possible: -".voicings(), "")`} /> +".voicings(), "").note()`} /> @@ -683,7 +763,7 @@ Turns chord symbols into voicings, using the smoothest voice leading possible: Turns chord symbols into root notes of chords in given octave. -".rootNotes(3)`} /> +".rootNotes(3).note()`} /> Together with layer, struct and voicings, this can be used to create a basic backing track: @@ -691,7 +771,7 @@ Together with layer, struct and voicings, this can be used to create a basic bac tune={`"".layer( x => x.voicings(['d3','g4']).struct("~ x"), x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out())) -)`} +).note()`} /> @@ -755,152 +835,3 @@ The following functions can be used with superdirt: `s n note freq channel orbit cutoff resonance hcutoff hresonance bandf bandq djf vowel cut begin end loop fadeTime speed unitA gain amp accelerate crush coarse delay lock leslie lrate lsize pan panspan pansplay room size dry shape squiz waveloss attack decay octave detune tremolodepth` Please refer to [Tidal Docs](https://tidalcycles.org/) for more info. - -# Webdirt API (deprecated) - -You can use the powerful sampling engine [Webdirt](https://github.com/dktr0/WebDirt) with Strudel. - -{{ 'Pattern.webdirt' | jsdoc }} - -
-
- -# Tone API (deprecated) - -The Tone API uses Tone.js instruments ands effects to create sounds. - - - -### tone(instrument) - -To change the instrument of a pattern, you can pass any [Tone.js Source](https://tonejs.github.io/docs/14.7.77/index.html) to .tone: - - - -While this works, it is a little bit verbose. To simplify things, all Tone Synths have a shortcut: - -```js -const amsynth = (options) => new AMSynth(options); -const duosynth = (options) => new DuoSynth(options); -const fmsynth = (options) => new FMSynth(options); -const membrane = (options) => new MembraneSynth(options); -const metal = (options) => new MetalSynth(options); -const monosynth = (options) => new MonoSynth(options); -const noise = (options) => new NoiseSynth(options); -const pluck = (options) => new PluckSynth(options); -const polysynth = (options) => new PolySynth(options); -const synth = (options) => new Synth(options); -const sampler = (options, baseUrl?) => new Sampler(options); // promisified, see below -const players = (options, baseUrl?) => new Sampler(options); // promisified, see below -``` - -### sampler - -With sampler, you can create tonal instruments from samples: - - - saw.struct("x*8").mul(16).round() - .legato(4).scale('D dorian').slow(2) - .tone(kalimba.toDestination()) -)`} -/> - -The sampler function promisifies [Tone.js Sampler](https://tonejs.github.io/docs/14.7.77/Sampler). - -Note that this function currently only works with this promise notation, but in the future, -it will be possible to use async instruments in a synchronous fashion. - -### players - -With players, you can create sound banks: - - - "bd hh sn hh".tone(drums.toDestination()) -) - `} -/> - -The sampler function promisifies [Tone.js Players](https://tonejs.github.io/docs/14.7.77/Players). - -Note that this function currently only works with this promise notation, but in the future, -it will be possible to use async instruments in a synchronous fashion. - -### out - -Shortcut for Tone.Destination. Intended to be used with Tone's .chain: - - - -This alone is not really useful, so read on.. - -### vol(volume) - -Helper that returns a Gain Node with the given volume. Intended to be used with Tone's .chain: - - - -### osc(type) - -Helper to set the waveform of a synth, monosynth or polysynth: - - - -The base types are `sine`, `square`, `sawtooth`, `triangle`. You can also append a number between 1 and 32 to reduce the harmonic partials. - -### lowpass(cutoff) - -Helper that returns a Filter Node of type lowpass with the given cutoff. Intended to be used with Tone's .chain: - - - -### highpass(cutoff) - -Helper that returns a Filter Node of type highpass with the given cutoff. Intended to be used with Tone's .chain: - - - -### adsr - -Helper to set the envelope of a Tone.js instrument. Intended to be used with Tone's .set: - -