mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
Merge pull request #698 from Bubobubobubobubo/sampler
Adding loop points and thus wavetable synthesis
This commit is contained in:
commit
80ea9e8de4
@ -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("<bd sd>,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("<bd sd>,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
|
||||
*
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 ]",
|
||||
|
||||
@ -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 href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
|
||||
});
|
||||
const description =
|
||||
item.description?.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => {
|
||||
// console.log(_, 'a', a, 'b', b);
|
||||
return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
|
||||
}) || '';
|
||||
return (
|
||||
<>
|
||||
{!!h && <CustomHeading>{item.longname}</CustomHeading>}
|
||||
|
||||
@ -303,6 +303,18 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
||||
|
||||
<JsDoc client:idle name="Pattern.end" h={0} />
|
||||
|
||||
### loop
|
||||
|
||||
<JsDoc client:idle name="loop" h={0} />
|
||||
|
||||
### loopBegin
|
||||
|
||||
<JsDoc client:idle name="loopBegin" h={0} />
|
||||
|
||||
### loopEnd
|
||||
|
||||
<JsDoc client:idle name="loopEnd" h={0} />
|
||||
|
||||
### cut
|
||||
|
||||
<JsDoc client:idle name="cut" h={0} />
|
||||
@ -315,6 +327,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
||||
|
||||
<JsDoc client:idle name="Pattern.loopAt" h={0} />
|
||||
|
||||
### fit
|
||||
|
||||
<JsDoc client:idle name="fit" h={0} />
|
||||
|
||||
### chop
|
||||
|
||||
<JsDoc client:idle name="Pattern.chop" h={0} />
|
||||
@ -323,6 +339,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
||||
|
||||
<JsDoc client:idle name="Pattern.slice" h={0} />
|
||||
|
||||
### splice
|
||||
|
||||
<JsDoc client:idle name="splice" h={0} />
|
||||
|
||||
### speed
|
||||
|
||||
<JsDoc client:idle name="speed" h={0} />
|
||||
|
||||
@ -78,6 +78,23 @@ You can use fm with any of the above waveforms, although the below examples all
|
||||
|
||||
<JsDoc client:idle name="fmenv" h={0} />
|
||||
|
||||
## 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.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`samples('github:Bubobubobubobubo/Dough-Waveforms/main/');
|
||||
|
||||
note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")
|
||||
.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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user