Merge pull request #913 from tidalcycles/pitch-envelopes

pitch envelope
This commit is contained in:
Felix Roos 2024-01-18 06:54:33 +01:00 committed by GitHub
commit bd83d19197
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 261 additions and 39 deletions

View File

@ -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'],

View File

@ -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();
};

View File

@ -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;
}
}

View File

@ -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 () {

View File

@ -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) {

View File

@ -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 ]",

View File

@ -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