Merge branch 'main' of github.com:daslyfe/strudel
This commit is contained in:
commit
a91f4576f3
16 changed files with 490 additions and 158 deletions
|
|
@ -809,15 +809,6 @@ export class Pattern {
|
|||
//////////////////////////////////////////////////////////////////////
|
||||
// Multi-pattern functions
|
||||
|
||||
/**
|
||||
* Stacks the given pattern(s) to the current pattern.
|
||||
* @name stack
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*4").stack(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
stack(...pats) {
|
||||
return stack(this, ...pats);
|
||||
}
|
||||
|
|
@ -826,30 +817,9 @@ export class Pattern {
|
|||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given pattern(s) to the current pattern.
|
||||
* @name seq
|
||||
* @memberof Pattern
|
||||
* @synonyms sequence, fastcat
|
||||
* @example
|
||||
* s("hh*4").seq(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
seq(...pats) {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given pattern(s) to the next cycle.
|
||||
* @name cat
|
||||
* @memberof Pattern
|
||||
* @synonyms slowcat
|
||||
* @example
|
||||
* s("hh*4").cat(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
cat(...pats) {
|
||||
return cat(this, ...pats);
|
||||
}
|
||||
|
|
@ -1280,6 +1250,12 @@ export function reify(thing) {
|
|||
* @example
|
||||
* stack("g3", "b3", ["e4", "d4"]).note()
|
||||
* // "g3,b3,[e4,d4]".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
* s("hh*4").stack(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
export function stack(...pats) {
|
||||
// Array test here is to avoid infinite recursions..
|
||||
|
|
@ -1408,6 +1384,11 @@ export function slowcatPrime(...pats) {
|
|||
* cat("e5", "b4", ["d5", "c5"]).note()
|
||||
* // "<e5 b4 [d5 c5]>".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
* s("hh*4").cat(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
export function cat(...pats) {
|
||||
return slowcat(...pats);
|
||||
|
|
@ -1430,6 +1411,36 @@ export function arrange(...sections) {
|
|||
return s_cat(...sections).slow(total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarly to `arrange`, allows you to arrange multiple patterns together over multiple cycles.
|
||||
* Unlike `arrange`, you specify a start and stop time for each pattern rather than duration, which
|
||||
* means that patterns can overlap.
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
seqPLoop([0, 2, "bd(3,8)"],
|
||||
[1, 3, "cp(3,8)"]
|
||||
)
|
||||
.sound()
|
||||
*/
|
||||
export function seqPLoop(...parts) {
|
||||
let total = Fraction(0);
|
||||
const pats = [];
|
||||
for (let part of parts) {
|
||||
if (part.length == 2) {
|
||||
part.unshift(total);
|
||||
}
|
||||
total = part[1];
|
||||
}
|
||||
|
||||
return stack(
|
||||
...parts.map(([start, stop, pat]) =>
|
||||
pure(reify(pat)).compress(Fraction(start).div(total), Fraction(stop).div(total)),
|
||||
),
|
||||
)
|
||||
.slow(total)
|
||||
.innerJoin(); // or resetJoin or restartJoin ??
|
||||
}
|
||||
|
||||
export function fastcat(...pats) {
|
||||
let result = slowcat(...pats);
|
||||
if (pats.length > 1) {
|
||||
|
|
@ -1448,12 +1459,18 @@ export function sequence(...pats) {
|
|||
}
|
||||
|
||||
/** Like **cat**, but the items are crammed into one cycle.
|
||||
* @synonyms fastcat, sequence
|
||||
* @synonyms sequence, fastcat
|
||||
* @example
|
||||
* seq("e5", "b4", ["d5", "c5"]).note()
|
||||
* // "e5 b4 [d5 c5]".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
* s("hh*4").seq(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
|
||||
export function seq(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
|
@ -2016,6 +2033,34 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
|
|||
return pat.zoom(a.begin, a.end);
|
||||
});
|
||||
|
||||
/**
|
||||
* Splits a pattern into the given number of slices, and plays them according to a pattern of slice numbers.
|
||||
* Similar to `slice`, but slices up patterns rather than sound samples.
|
||||
* @param {number} number of slices
|
||||
* @param {number} slices to play
|
||||
* @example
|
||||
* note("0 1 2 3 4 5 6 7".scale('c:mixolydian'))
|
||||
*.bite(4, "3 2 1 0")
|
||||
* @example
|
||||
* sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]")
|
||||
.bank("RolandTR909").speed(1.2)
|
||||
.bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3")
|
||||
*/
|
||||
export const bite = register(
|
||||
'bite',
|
||||
(npat, ipat, pat) => {
|
||||
return ipat
|
||||
.fmap((i) => (n) => {
|
||||
const a = Fraction(i).div(n).mod(1);
|
||||
const b = a.add(Fraction(1).div(n));
|
||||
return pat.zoom(a, b);
|
||||
})
|
||||
.appLeft(npat)
|
||||
.squeezeJoin();
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
/**
|
||||
* Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle.
|
||||
* @param {number} fraction fraction to select
|
||||
|
|
@ -2447,6 +2492,37 @@ Pattern.prototype.tag = function (tag) {
|
|||
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters haps using the given function
|
||||
* @name filter
|
||||
* @param {Function} test function to test Hap
|
||||
* @example
|
||||
* s("hh!7 oh").filter(hap => hap.value.s==='hh')
|
||||
*/
|
||||
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
|
||||
|
||||
/**
|
||||
* Filters haps by their begin time
|
||||
* @name filterWhen
|
||||
* @noAutocomplete
|
||||
* @param {Function} test function to test Hap.whole.begin
|
||||
*/
|
||||
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
|
||||
|
||||
/**
|
||||
* Use within to apply a function to only a part of a pattern.
|
||||
* @name within
|
||||
* @param {number} start start within cycle (0 - 1)
|
||||
* @param {number} end end within cycle (0 - 1). Must be > start
|
||||
* @param {Function} func function to be applied to the sub-pattern
|
||||
*/
|
||||
export const within = register('within', (a, b, fn, pat) =>
|
||||
stack(
|
||||
fn(pat.filterWhen((t) => t.cyclePos() >= a && t.cyclePos() <= b)),
|
||||
pat.filterWhen((t) => t.cyclePos() < a || t.cyclePos() > b),
|
||||
),
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Tactus-related functions, i.e. ones that do stepwise
|
||||
// transformations
|
||||
|
|
|
|||
|
|
@ -169,13 +169,18 @@ const refs = {};
|
|||
export async function midin(input) {
|
||||
if (isPattern(input)) {
|
||||
throw new Error(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
|
||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
const initial = await enableWebMidi(); // only returns on first init
|
||||
const device = getDevice(input, WebMidi.inputs);
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
|
||||
);
|
||||
}
|
||||
if (initial) {
|
||||
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
|
||||
logger(
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
|||
if (!(name in writeMessagers)) {
|
||||
getWriter(name, br);
|
||||
}
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
var message = '';
|
||||
var chk = 0;
|
||||
if (typeof hap.value === 'object') {
|
||||
|
|
@ -105,7 +105,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
|
|||
} else {
|
||||
message = hap.value;
|
||||
}
|
||||
const offset = (time - currentTime + latency) * 1000;
|
||||
const offset = (targetTime - currentTime + latency) * 1000;
|
||||
|
||||
window.setTimeout(function () {
|
||||
writeMessagers[name](message, chk);
|
||||
|
|
|
|||
|
|
@ -22,16 +22,13 @@ function humanFileSize(bytes, si) {
|
|||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => {
|
||||
let transpose = 0;
|
||||
if (freq !== undefined && note !== undefined) {
|
||||
logger('[sampler] hap has note and freq. ignoring note', 'warning');
|
||||
}
|
||||
let midi = valueToMidi({ freq, note }, 36);
|
||||
transpose = midi - 36; // C3 is middle C
|
||||
|
||||
const ac = getAudioContext();
|
||||
|
||||
// deduces relevant info for sample loading from hap.value and sample definition
|
||||
// it encapsulates the core sampler logic into a pure and synchronous function
|
||||
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
|
||||
export function getSampleInfo(hapValue, bank) {
|
||||
const { s, n = 0, speed = 1.0 } = hapValue;
|
||||
let midi = valueToMidi(hapValue, 36);
|
||||
let transpose = midi - 36; // C3 is middle C;
|
||||
let sampleUrl;
|
||||
let index = 0;
|
||||
if (Array.isArray(bank)) {
|
||||
|
|
@ -50,19 +47,54 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
|
|||
index = getSoundIndex(n, bank[closest].length);
|
||||
sampleUrl = bank[closest][index];
|
||||
}
|
||||
const label = `${s}:${index}`;
|
||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||
return { transpose, sampleUrl, index, midi, label, playbackRate };
|
||||
}
|
||||
|
||||
// takes hapValue and returns buffer + playbackRate.
|
||||
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||
if (resolveUrl) {
|
||||
sampleUrl = await resolveUrl(sampleUrl);
|
||||
}
|
||||
let buffer = await loadBuffer(sampleUrl, ac, s, index);
|
||||
if (speed < 0) {
|
||||
const ac = getAudioContext();
|
||||
const buffer = await loadBuffer(sampleUrl, ac, label);
|
||||
|
||||
if (hapValue.unit === 'c') {
|
||||
playbackRate = playbackRate * buffer.duration;
|
||||
}
|
||||
return { buffer, playbackRate };
|
||||
};
|
||||
|
||||
// creates playback ready AudioBufferSourceNode from hapValue
|
||||
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
|
||||
if (hapValue.speed < 0) {
|
||||
// should this be cached?
|
||||
buffer = reverseBuffer(buffer);
|
||||
}
|
||||
const ac = getAudioContext();
|
||||
const bufferSource = ac.createBufferSource();
|
||||
bufferSource.buffer = buffer;
|
||||
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
return bufferSource;
|
||||
|
||||
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
|
||||
// "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 * bufferSource.buffer.duration;
|
||||
|
||||
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
}
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
const sliceDuration = (end - begin) * bufferDuration;
|
||||
return { bufferSource, offset, bufferDuration, sliceDuration };
|
||||
};
|
||||
|
||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||
|
|
@ -232,10 +264,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
|
|||
const { prebake, tag } = options;
|
||||
processSampleMap(
|
||||
sampleMap,
|
||||
(key, value) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
|
||||
(key, bank) =>
|
||||
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||
type: 'sample',
|
||||
samples: value,
|
||||
samples: bank,
|
||||
baseUrl,
|
||||
prebake,
|
||||
tag,
|
||||
|
|
@ -249,38 +281,26 @@ const cutGroups = [];
|
|||
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
let {
|
||||
s,
|
||||
freq,
|
||||
unit,
|
||||
nudge = 0, // TODO: is this in seconds?
|
||||
cut,
|
||||
loop,
|
||||
clip = undefined, // if set, samples will be cut off when the hap ends
|
||||
n = 0,
|
||||
note,
|
||||
speed = 1, // sample playback speed
|
||||
loopBegin = 0,
|
||||
begin = 0,
|
||||
loopEnd = 1,
|
||||
end = 1,
|
||||
duration,
|
||||
} = value;
|
||||
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
// 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
|
||||
|
||||
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
||||
//const soundfont = getSoundfontKey(s);
|
||||
const time = t + nudge;
|
||||
|
||||
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
|
||||
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
|
|
@ -292,26 +312,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
||||
return;
|
||||
}
|
||||
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||
if (unit === 'c') {
|
||||
// are there other units?
|
||||
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps;
|
||||
}
|
||||
// "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 * bufferSource.buffer.duration;
|
||||
if (loop) {
|
||||
bufferSource.loop = true;
|
||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
}
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
|
||||
const time = t + nudge;
|
||||
bufferSource.start(time, offset);
|
||||
|
||||
const envGain = ac.createGain();
|
||||
const node = bufferSource.connect(envGain);
|
||||
|
||||
// if none of these controls is set, the duration of the sound will be set to the duration of the sample slice
|
||||
if (clip == null && loop == null && value.release == null) {
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
duration = (end - begin) * bufferDuration;
|
||||
duration = sliceDuration;
|
||||
}
|
||||
let holdEnd = t + duration;
|
||||
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ export const superdough = async (value, t, hapDuration) => {
|
|||
};
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
value.s = s;
|
||||
}
|
||||
|
||||
// get source AudioNode
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue