diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 5f38d393a..208b5ae52 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -193,16 +193,34 @@ const generic_params = [ */ ['end'], /** - * Loops the sample (from `begin` to `end`) the specified number of times. - * Note that the tempo of the loop is not synced with the cycle tempo. + * Loops the sample from `loopBegin` to `loopEnd`. If one or both of those are set, + * the sample is looped to fill the event duration, instead of playing the whole sample. + * If only `loopBegin` is set, `loopEnd` defaults to 1. + * If only `loopEnd` is set, `loopBegin` defaults to 0. * - * @name loop - * @param {number | Pattern} times How often the sample is looped + * @name loopBegin + * @param {number | Pattern} position position from 0 - 1 where the loop starts * @example - * s("bd").loop("<1 2 3 4>").osc() + * note("").s("piano") + * .loopBegin("<0 .24 .32>").loopEnd(.5) * */ - ['loop'], + ['loopBegin'], + /** + * Loops the sample from `loopBegin` to `loopEnd`. If one or both of those are set, + * the sample is looped to fill the event duration, instead of playing the whole sample. + * If only `loopBegin` is set, `loopEnd` defaults to 1. + * If only `loopEnd` is set, `loopBegin` defaults to 0. + * + * @name loopEnd + * @param {number | Pattern} position position from 0 - 1 where the loop ends + * @example + * @example + * note("").s("piano") + * .loopBegin(.2).loopEnd("<.3 .4 .5 .6>") + * + */ + ['loopEnd'], // TODO: currently duplicated with "native" legato // TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419 /** diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 1f4e3375d..fc0280040 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -231,7 +231,8 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => { unit, nudge = 0, // TODO: is this in seconds? cut, - loop, + loopBegin, + loopEnd, orbit = 1, room, size = 2, @@ -307,17 +308,21 @@ export const webaudioOutput = async (hap, deadline, hapDuration, cps) => { // are there other units? bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * cps; } - let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value; + const loop = loopBegin !== undefined || loopEnd !== undefined; + let duration = + soundfont || clip || loop ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value; // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, // 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 * duration * bufferSource.playbackRate.value; duration = (end - begin) * duration; if (loop) { + loopBegin = loopBegin ?? 0; + loopEnd = loopEnd ?? 1; bufferSource.loop = true; - bufferSource.loopStart = offset; - bufferSource.loopEnd = offset + duration; - duration = loop * duration; + bufferSource.loopStart = offset + loopBegin * duration; + bufferSource.loopEnd = offset + duration - (1 - loopEnd) * duration; + duration = hapDuration; } t += nudge;