mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 13:48:40 +00:00
Merge pull request #913 from tidalcycles/pitch-envelopes
pitch envelope
This commit is contained in:
commit
bd83d19197
@ -891,6 +891,82 @@ const generic_params = [
|
||||
*
|
||||
*/
|
||||
['freq'],
|
||||
// pitch envelope
|
||||
/**
|
||||
* Attack time of pitch envelope.
|
||||
*
|
||||
* @name pattack
|
||||
* @synonyms patt
|
||||
* @param {number | Pattern} time time in seconds
|
||||
* @example
|
||||
* note("<c eb g bb>").pattack("<0 .1 .25 .5>")
|
||||
*
|
||||
*/
|
||||
['pattack', 'patt'],
|
||||
/**
|
||||
* Decay time of pitch envelope.
|
||||
*
|
||||
* @name pdecay
|
||||
* @synonyms pdec
|
||||
* @param {number | Pattern} time time in seconds
|
||||
* @example
|
||||
* note("<c eb g bb>").pdecay("<0 .1 .25 .5>")
|
||||
*
|
||||
*/
|
||||
['pdecay', 'pdec'],
|
||||
// TODO: how to use psustain?!
|
||||
['psustain', 'psus'],
|
||||
/**
|
||||
* Release time of pitch envelope
|
||||
*
|
||||
* @name prelease
|
||||
* @synonyms prel
|
||||
* @param {number | Pattern} time time in seconds
|
||||
* @example
|
||||
* note("<c eb g bb> ~")
|
||||
* .release(.5) // to hear the pitch release
|
||||
* .prelease("<0 .1 .25 .5>")
|
||||
*
|
||||
*/
|
||||
['prelease', 'prel'],
|
||||
/**
|
||||
* Amount of pitch envelope. Negative values will flip the envelope.
|
||||
* If you don't set other pitch envelope controls, `pattack:.2` will be the default.
|
||||
*
|
||||
* @name penv
|
||||
* @param {number | Pattern} semitones change in semitones
|
||||
* @example
|
||||
* note("c")
|
||||
* .penv("<12 7 1 .5 0 -1 -7 -12>")
|
||||
*
|
||||
*/
|
||||
['penv'],
|
||||
/**
|
||||
* Curve of envelope. Defaults to linear. exponential is good for kicks
|
||||
*
|
||||
* @name pcurve
|
||||
* @param {number | Pattern} type 0 = linear, 1 = exponential
|
||||
* @example
|
||||
* note("g1*2")
|
||||
* .s("sine").pdec(.5)
|
||||
* .penv(32)
|
||||
* .pcurve("<0 1>")
|
||||
*
|
||||
*/
|
||||
['pcurve'],
|
||||
/**
|
||||
* Sets the range anchor of the envelope:
|
||||
* - anchor 0: range = [note, note + penv]
|
||||
* - anchor 1: range = [note - penv, note]
|
||||
* If you don't set an anchor, the value will default to the psustain value.
|
||||
*
|
||||
* @name panchor
|
||||
* @param {number | Pattern} anchor anchor offset
|
||||
* @example
|
||||
* note("c").penv(12).panchor("<0 .5 1 .5>")
|
||||
*
|
||||
*/
|
||||
['panchor'],
|
||||
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
|
||||
['gate', 'gat'],
|
||||
// ['hatgrain'],
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel.cycles/core';
|
||||
import { getAudioContext, registerSound, getParamADSR, getADSRValues } from '@strudel.cycles/webaudio';
|
||||
import {
|
||||
getAudioContext,
|
||||
registerSound,
|
||||
getParamADSR,
|
||||
getADSRValues,
|
||||
getPitchEnvelope,
|
||||
getVibratoOscillator,
|
||||
} from '@strudel.cycles/webaudio';
|
||||
import gm from './gm.mjs';
|
||||
|
||||
let loadCache = {};
|
||||
@ -149,10 +156,16 @@ export function registerSoundfonts() {
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, time, holdEnd, 'linear');
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time);
|
||||
// pitch envelope
|
||||
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
||||
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (releaseTime) => {};
|
||||
bufferSource.onended = () => {
|
||||
bufferSource.disconnect();
|
||||
vibratoOscillator?.stop();
|
||||
node.disconnect();
|
||||
onended();
|
||||
};
|
||||
|
||||
@ -31,10 +31,10 @@ export const getParamADSR = (
|
||||
decay = nanFallback(decay);
|
||||
sustain = nanFallback(sustain);
|
||||
release = nanFallback(release);
|
||||
|
||||
const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime';
|
||||
if (curve === 'exponential') {
|
||||
min = Math.max(0.0001, min);
|
||||
min = min === 0 ? 0.001 : min;
|
||||
max = max === 0 ? 0.001 : max;
|
||||
}
|
||||
const range = max - min;
|
||||
const peak = max;
|
||||
@ -42,12 +42,17 @@ export const getParamADSR = (
|
||||
const duration = end - begin;
|
||||
|
||||
const envValAtTime = (time) => {
|
||||
let val;
|
||||
if (attack > time) {
|
||||
let slope = getSlope(min, peak, 0, attack);
|
||||
return time * slope + (min > peak ? min : 0);
|
||||
val = time * slope + (min > peak ? min : 0);
|
||||
} else {
|
||||
return (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak;
|
||||
val = (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak;
|
||||
}
|
||||
if (curve === 'exponential') {
|
||||
val = val || 0.001;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
param.setValueAtTime(min, begin);
|
||||
@ -144,3 +149,40 @@ export function drywet(dry, wet, wetAmount = 0) {
|
||||
wet_gain.connect(mix);
|
||||
return mix;
|
||||
}
|
||||
|
||||
let curves = ['linear', 'exponential'];
|
||||
export function getPitchEnvelope(param, value, t, holdEnd) {
|
||||
// envelope is active when any of these values is set
|
||||
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
||||
if (!hasEnvelope) {
|
||||
return;
|
||||
}
|
||||
const penv = nanFallback(value.penv, 1, true);
|
||||
const curve = curves[value.pcurve ?? 0];
|
||||
let [pattack, pdecay, psustain, prelease] = getADSRValues(
|
||||
[value.pattack, value.pdecay, value.psustain, value.prelease],
|
||||
curve,
|
||||
[0.2, 0.001, 1, 0.001],
|
||||
);
|
||||
let panchor = value.panchor ?? psustain;
|
||||
const cents = penv * 100; // penv is in semitones
|
||||
const min = 0 - cents * panchor;
|
||||
const max = cents - cents * panchor;
|
||||
getParamADSR(param, pattack, pdecay, psustain, prelease, min, max, t, holdEnd, curve);
|
||||
}
|
||||
|
||||
export function getVibratoOscillator(param, value, t) {
|
||||
const { vibmod = 0.5, vib } = value;
|
||||
let vibratoOscillator;
|
||||
if (vib > 0) {
|
||||
vibratoOscillator = getAudioContext().createOscillator();
|
||||
vibratoOscillator.frequency.value = vib;
|
||||
const gain = getAudioContext().createGain();
|
||||
// Vibmod is the amount of vibrato, in semitones
|
||||
gain.gain.value = vibmod * 100;
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(param);
|
||||
vibratoOscillator.start(t);
|
||||
return vibratoOscillator;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
|
||||
import { getAudioContext, registerSound } from './index.mjs';
|
||||
import { getADSRValues, getParamADSR } from './helpers.mjs';
|
||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
@ -244,8 +244,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
loopEnd = 1,
|
||||
end = 1,
|
||||
duration,
|
||||
vib,
|
||||
vibmod = 0.5,
|
||||
} = value;
|
||||
// load sample
|
||||
if (speed === 0) {
|
||||
@ -263,17 +261,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
|
||||
|
||||
// vibrato
|
||||
let vibratoOscillator;
|
||||
if (vib > 0) {
|
||||
vibratoOscillator = getAudioContext().createOscillator();
|
||||
vibratoOscillator.frequency.value = vib;
|
||||
const gain = getAudioContext().createGain();
|
||||
// Vibmod is the amount of vibrato, in semitones
|
||||
gain.gain.value = vibmod * 100;
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(bufferSource.detune);
|
||||
vibratoOscillator.start(0);
|
||||
}
|
||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
@ -310,6 +298,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
|
||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||
|
||||
// pitch envelope
|
||||
getPitchEnvelope(bufferSource.detune, value, t, holdEnd);
|
||||
|
||||
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
||||
node.connect(out);
|
||||
bufferSource.onended = function () {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext } from './superdough.mjs';
|
||||
import { gainNode, getADSRValues, getParamADSR } from './helpers.mjs';
|
||||
import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
@ -105,15 +105,11 @@ export function waveformN(partials, type) {
|
||||
}
|
||||
|
||||
// expects one of waveforms as s
|
||||
export function getOscillator(
|
||||
s,
|
||||
t,
|
||||
{
|
||||
export function getOscillator(s, t, value) {
|
||||
let {
|
||||
n: partials,
|
||||
note,
|
||||
freq,
|
||||
vib = 0,
|
||||
vibmod = 0.5,
|
||||
noise = 0,
|
||||
// fm
|
||||
fmh: fmHarmonicity = 1,
|
||||
@ -126,8 +122,7 @@ export function getOscillator(
|
||||
fmvelocity: fmVelocity,
|
||||
fmwave: fmWaveform = 'sine',
|
||||
duration,
|
||||
},
|
||||
) {
|
||||
} = value;
|
||||
let ac = getAudioContext();
|
||||
let o;
|
||||
// If no partials are given, use stock waveforms
|
||||
@ -184,17 +179,10 @@ export function getOscillator(
|
||||
}
|
||||
|
||||
// Additional oscillator for vibrato effect
|
||||
let vibratoOscillator;
|
||||
if (vib > 0) {
|
||||
vibratoOscillator = getAudioContext().createOscillator();
|
||||
vibratoOscillator.frequency.value = vib;
|
||||
const gain = getAudioContext().createGain();
|
||||
// Vibmod is the amount of vibrato, in semitones
|
||||
gain.gain.value = vibmod * 100;
|
||||
vibratoOscillator.connect(gain);
|
||||
gain.connect(o.detune);
|
||||
vibratoOscillator.start(t);
|
||||
}
|
||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
||||
|
||||
// pitch envelope
|
||||
getPitchEnvelope(o.detune, value, t, t + duration);
|
||||
|
||||
let noiseMix;
|
||||
if (noise) {
|
||||
|
||||
@ -3332,6 +3332,55 @@ exports[`runs examples > example "pan" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "panchor" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c penv:12 panchor:0 ]",
|
||||
"[ 1/1 → 2/1 | note:c penv:12 panchor:0.5 ]",
|
||||
"[ 2/1 → 3/1 | note:c penv:12 panchor:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c penv:12 panchor:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pattack" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c pattack:0 ]",
|
||||
"[ 1/1 → 2/1 | note:eb pattack:0.1 ]",
|
||||
"[ 2/1 → 3/1 | note:g pattack:0.25 ]",
|
||||
"[ 3/1 → 4/1 | note:bb pattack:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pcurve" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]",
|
||||
"[ 1/2 → 1/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]",
|
||||
"[ 1/1 → 3/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]",
|
||||
"[ 3/2 → 2/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]",
|
||||
"[ 2/1 → 5/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]",
|
||||
"[ 5/2 → 3/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]",
|
||||
"[ 3/1 → 7/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]",
|
||||
"[ 7/2 → 4/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "pdecay" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c pdecay:0 ]",
|
||||
"[ 1/1 → 2/1 | note:eb pdecay:0.1 ]",
|
||||
"[ 2/1 → 3/1 | note:g pdecay:0.25 ]",
|
||||
"[ 3/1 → 4/1 | note:bb pdecay:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "penv" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | note:c penv:12 ]",
|
||||
"[ 1/1 → 2/1 | note:c penv:7 ]",
|
||||
"[ 2/1 → 3/1 | note:c penv:1 ]",
|
||||
"[ 3/1 → 4/1 | note:c penv:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "perlin" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:hh cutoff:512.5097280354112 ]",
|
||||
@ -3660,6 +3709,15 @@ exports[`runs examples > example "postgain" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "prelease" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | note:c release:0.5 prelease:0 ]",
|
||||
"[ 1/1 → 3/2 | note:eb release:0.5 prelease:0.1 ]",
|
||||
"[ 2/1 → 5/2 | note:g release:0.5 prelease:0.25 ]",
|
||||
"[ 3/1 → 7/2 | note:bb release:0.5 prelease:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "press" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:hh ]",
|
||||
|
||||
@ -138,6 +138,60 @@ There is one filter envelope for each filter type and thus one set of envelope f
|
||||
|
||||
<JsDoc client:idle name="lpenv" h={0} />
|
||||
|
||||
# Pitch Envelope
|
||||
|
||||
You can also control the pitch with envelopes!
|
||||
Pitch envelopes can breathe life into static sounds:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`n("<-4,0 5 2 1>*<2!3 4>")
|
||||
.scale("<C F>/8:pentatonic")
|
||||
.s("gm_electric_guitar_jazz")
|
||||
.penv("<.5 0 7 -2>*2").vib("4:.1")
|
||||
.phaser(2).delay(.25).room(.3)
|
||||
.size(4).fast(.75)`}
|
||||
/>
|
||||
|
||||
You also create some lovely chiptune-style sounds:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`n(run("<4 8>/16")).jux(rev)
|
||||
.chord("<C^7 <Db^7 Fm7>>")
|
||||
.dict('ireal')
|
||||
.voicing().add(note("<0 1>/8"))
|
||||
.dec(.1).room(.2)
|
||||
.segment("<4 [2 8]>")
|
||||
.penv("<0 <2 -2>>").patt(.02)`}
|
||||
/>
|
||||
|
||||
Let's break down all pitch envelope controls:
|
||||
|
||||
## pattack
|
||||
|
||||
<JsDoc client:idle name="pattack" h={0} />
|
||||
|
||||
## pdecay
|
||||
|
||||
<JsDoc client:idle name="pdecay" h={0} />
|
||||
|
||||
## prelease
|
||||
|
||||
<JsDoc client:idle name="prelease" h={0} />
|
||||
|
||||
## penv
|
||||
|
||||
<JsDoc client:idle name="penv" h={0} />
|
||||
|
||||
## pcurve
|
||||
|
||||
<JsDoc client:idle name="pcurve" h={0} />
|
||||
|
||||
## panchor
|
||||
|
||||
<JsDoc client:idle name="panchor" h={0} />
|
||||
|
||||
# Dynamics
|
||||
|
||||
## gain
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user