diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 780b4a44..f988f0a7 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -299,16 +299,52 @@ const generic_params = [ */ ['end'], /** - * Loops the sample (from `begin` to `end`) the specified number of times. + * Loops the sample. * Note that the tempo of the loop is not synced with the cycle tempo. + * To change the loop region, use loopBegin / loopEnd. * * @name loop - * @param {number | Pattern} times How often the sample is looped + * @param {number | Pattern} on If 1, the sample is looped * @example - * s("bd").loop("<1 2 3 4>").osc() + * s("casio").loop(1) * */ ['loop'], + /** + * Begin to loop at a specific point in the sample (inbetween `begin` and `end`). + * Note that the loop point must be inbetween `begin` and `end`, and before `loopEnd`! + * Note: Samples starting with wt_ will automatically loop! (wt = wavetable) + * + * @name loopBegin + * @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample + * @synonyms loopb + * @example + * s("space").loop(1) + * .loopBegin("<0 .125 .25>").scope() + */ + ['loopBegin', 'loopb'], + /** + * + * End the looping section at a specific point in the sample (inbetween `begin` and `end`). + * Note that the loop point must be inbetween `begin` and `end`, and after `loopBegin`! + * + * @name loopEnd + * @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample + * @synonyms loope + * @example + * s("space").loop(1) + * .loopEnd("<1 .75 .5 .25>").scope() + */ + ['loopEnd', 'loope'], + /** + * bit crusher effect. + * + * @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>") + * + */ // TODO: currently duplicated with "native" legato // TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419 /** @@ -323,15 +359,6 @@ const generic_params = [ */ // ['legato'], // ['clhatdecay'], - /** - * bit crusher effect. - * - * @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>") - * - */ ['crush'], /** * fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers @@ -343,7 +370,6 @@ const generic_params = [ * */ ['coarse'], - /** * choose the channel the pattern is sent to in superdirt * diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 7e694f49..e3efb427 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2273,14 +2273,14 @@ export const slice = register( false, // turns off auto-patternification ); -/* +/** * Works the same as slice, but changes the playback speed of each slice to match the duration of its step. * @name splice - * @memberof Pattern - * @returns Pattern * @example * await samples('github:tidalcycles/Dirt-Samples/master') - * s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65) + * s("breaks165") + * .splice(8, "0 1 [2 3 0]@2 3 0@2 7") + * .hurry(0.65) */ export const splice = register( @@ -2307,9 +2307,16 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto return _loopAt(factor, pat, 1); }); -// this function will be redefined in repl.mjs to use the correct cps value. +// the fit function will be redefined in repl.mjs to use the correct cps value. // It is still here to work in cases where repl.mjs is not used - +/** + * Makes the sample fit its event duration. Good for rhythmical loops like drum breaks. + * Similar to loopAt. + * @name fit + * @example + * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) + * s("rhodes/4").fit() + */ export const fit = register('fit', (pat) => pat.withHap((hap) => hap.withValue((v) => ({ diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 02e5eada..76b6a542 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -196,7 +196,7 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option const cutGroups = []; export async function onTriggerSample(t, value, onended, bank, resolveUrl) { - const { + let { s, freq, unit, @@ -207,7 +207,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { n = 0, note, speed = 1, // sample playback speed + loopBegin = 0, begin = 0, + loopEnd = 1, end = 1, } = value; // load sample @@ -215,6 +217,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { // no playback return; } + loop = s.startsWith('wt_') ? 1 : value.loop; const ac = getAudioContext(); // 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 } = value; @@ -242,19 +245,12 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { // rather than the current playback rate, so even if the sound is playing at twice its normal speed, // the midway point through a 10-second audio buffer is still 5." const offset = begin * bufferSource.buffer.duration; - bufferSource.start(time, offset); - const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; - /*if (loop) { - // TODO: idea for loopBegin / loopEnd - // if one of [loopBegin,loopEnd] is <= 1, interpret it as normlized - // if [loopBegin,loopEnd] is bigger >= 1, interpret it as sample number - // this will simplify perfectly looping things, while still keeping the normalized option - // the only drawback is that looping between samples 0 and 1 is not possible (which is not real use case) + if (loop) { bufferSource.loop = true; - bufferSource.loopStart = offset; - bufferSource.loopEnd = offset + duration; - duration = loop * duration; - }*/ + bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; + bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset; + } + bufferSource.start(time, offset); const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 1, t); bufferSource.connect(envelope); const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... @@ -265,9 +261,10 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { out.disconnect(); onended(); }; - const stop = (endTime, playWholeBuffer = clip === undefined) => { + const stop = (endTime, playWholeBuffer = clip === undefined && loop === undefined) => { let releaseTime = endTime; if (playWholeBuffer) { + const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value; releaseTime = t + (end - begin) * bufferDuration; } bufferSource.stop(releaseTime + release); diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index cd7595b5..48835a48 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1828,6 +1828,15 @@ exports[`runs examples > example "firstOf" example index 0 1`] = ` ] `; +exports[`runs examples > example "fit" example index 0 1`] = ` +[ + "[ (0/1 → 1/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]", + "[ 0/1 ⇜ (1/1 → 2/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]", + "[ 0/1 ⇜ (2/1 → 3/1) ⇝ 4/1 | s:rhodes speed:0.25 unit:c ]", + "[ 0/1 ⇜ (3/1 → 4/1) | s:rhodes speed:0.25 unit:c ]", +] +`; + exports[`runs examples > example "floor" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:42 ]", @@ -2640,10 +2649,10 @@ exports[`runs examples > example "linger" example index 0 1`] = ` exports[`runs examples > example "loop" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | s:bd loop:1 ]", - "[ 1/1 → 2/1 | s:bd loop:2 ]", - "[ 2/1 → 3/1 | s:bd loop:3 ]", - "[ 3/1 → 4/1 | s:bd loop:4 ]", + "[ 0/1 → 1/1 | s:casio loop:1 ]", + "[ 1/1 → 2/1 | s:casio loop:1 ]", + "[ 2/1 → 3/1 | s:casio loop:1 ]", + "[ 3/1 → 4/1 | s:casio loop:1 ]", ] `; @@ -2665,6 +2674,24 @@ exports[`runs examples > example "loopAtCps" example index 0 1`] = ` ] `; +exports[`runs examples > example "loopBegin" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:space loop:1 loopBegin:0 analyze:1 ]", + "[ 1/1 → 2/1 | s:space loop:1 loopBegin:0.125 analyze:1 ]", + "[ 2/1 → 3/1 | s:space loop:1 loopBegin:0.25 analyze:1 ]", + "[ 3/1 → 4/1 | s:space loop:1 loopBegin:0 analyze:1 ]", +] +`; + +exports[`runs examples > example "loopEnd" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | s:space loop:1 loopEnd:1 analyze:1 ]", + "[ 1/1 → 2/1 | s:space loop:1 loopEnd:0.75 analyze:1 ]", + "[ 2/1 → 3/1 | s:space loop:1 loopEnd:0.5 analyze:1 ]", + "[ 3/1 → 4/1 | s:space loop:1 loopEnd:0.25 analyze:1 ]", +] +`; + exports[`runs examples > example "lpattack" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 lpattack:0.5 lpenv:4 ]", @@ -4323,6 +4350,36 @@ exports[`runs examples > example "speed" example index 1 1`] = ` ] `; +exports[`runs examples > example "splice" example index 0 1`] = ` +[ + "[ 0/1 → 5/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 5/26 → 5/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ 5/13 → 20/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 20/39 → 25/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 25/39 → 10/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 10/13 → 25/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ (25/26 → 1/1) ⇝ 35/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 25/26 ⇜ (1/1 → 35/26) | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 35/26 → 20/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 20/13 → 45/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 45/26 → 25/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ (25/13 → 2/1) ⇝ 80/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 25/13 ⇜ (2/1 → 80/39) | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 80/39 → 85/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 85/39 → 30/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 30/13 → 5/2 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 5/2 → 75/26 | speed:0.325 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ (75/26 → 3/1) ⇝ 40/13 | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 75/26 ⇜ (3/1 → 40/13) | speed:0.65 unit:c begin:0.875 end:1 _slices:8 s:breaks165 ]", + "[ 40/13 → 85/26 | speed:0.65 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ 85/26 → 45/13 | speed:0.65 unit:c begin:0.125 end:0.25 _slices:8 s:breaks165 ]", + "[ 45/13 → 140/39 | speed:0.9750000000000001 unit:c begin:0.25 end:0.375 _slices:8 s:breaks165 ]", + "[ 140/39 → 145/39 | speed:0.9750000000000001 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", + "[ 145/39 → 50/13 | speed:0.9750000000000001 unit:c begin:0 end:0.125 _slices:8 s:breaks165 ]", + "[ (50/13 → 4/1) ⇝ 105/26 | speed:0.65 unit:c begin:0.375 end:0.5 _slices:8 s:breaks165 ]", +] +`; + exports[`runs examples > example "square" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:C3 ]", diff --git a/website/src/docs/JsDoc.jsx b/website/src/docs/JsDoc.jsx index 88a775a5..1ba80e0a 100644 --- a/website/src/docs/JsDoc.jsx +++ b/website/src/docs/JsDoc.jsx @@ -12,10 +12,11 @@ export function JsDoc({ name, h = 3, hideDescription, punchcard, canvasHeight }) } const synonyms = getTag('synonyms', item)?.split(', ') || []; const CustomHeading = `h${h}`; - const description = item.description.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => { - // console.log(_, 'a', a, 'b', b); - return `${a}${b ? `#${b}` : ''}`; - }); + const description = + item.description?.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => { + // console.log(_, 'a', a, 'b', b); + return `${a}${b ? `#${b}` : ''}`; + }) || ''; return ( <> {!!h && {item.longname}} diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index 9f79e54a..10d8c730 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -303,6 +303,18 @@ Sampler effects are functions that can be used to change the behaviour of sample +### loop + + + +### loopBegin + + + +### loopEnd + + + ### cut @@ -315,6 +327,10 @@ Sampler effects are functions that can be used to change the behaviour of sample +### fit + + + ### chop @@ -323,6 +339,10 @@ Sampler effects are functions that can be used to change the behaviour of sample +### splice + + + ### speed diff --git a/website/src/pages/learn/synths.mdx b/website/src/pages/learn/synths.mdx index b24726cf..26e94467 100644 --- a/website/src/pages/learn/synths.mdx +++ b/website/src/pages/learn/synths.mdx @@ -78,6 +78,23 @@ You can use fm with any of the above waveforms, although the below examples all +## Wavetable Synthesis + +Strudel can also use the sampler to load custom waveforms as a replacement of the default waveforms used by WebAudio for the base synth. A default set of more than 1000 wavetables is accessible by default (coming from the [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) set). You can also import/use your own. A wavetable is a one-cycle waveform, which is then repeated to create a sound at the desired frequency. It is a classic but very effective synthesis technique. + +Any sample preceded by the `wt_` prefix will be loaded as a wavetable. This means that the `loop` argument will be set to `1` by defalt. You can scan over the wavetable by using `loopBegin` and `loopEnd` as well. + +") +.n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9) +.s('wt_flute').velocity(0.25).often(n => n.ply(2)) +.release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0) +.cutoff(2000).scope({}).cutoff("<1000 2000 4000>").fast(2)`} +/> + ## ZZFX The "Zuper Zmall Zound Zynth" [ZZFX](https://github.com/KilledByAPixel/ZzFX) is also integrated in strudel.