I used Microsoft’s Copilot to add a Hold parameter to the envelope:
//-----------------------------------------------------------
getDescription = function () {
return "AHDSR MIDI Triggered";
}
//-----------------------------------------------------------
makeParameter = function (name) {
var value = 0;
var listeners = [];
return {
name: name,
units: "",
stepCount: 0,
toString: function (val) { return val; },
fromString: function (str) { return parseFloat(str); },
addListener: function (obj) { listeners.push(obj); },
getValue: function () { return value; },
getPlain: function () { return value; },
setValue: function (val) {
value = val;
for (i = 0; i < listeners.length; ++i)
listeners[i](this);
},
};
}
//-----------------------------------------------------------
makeBoolParameter = function (name) {
param = makeParameter(name);
param.stepCount = 1;
param.toString = function (value) { return value >= 0.5 ? "On" : "Off"; };
param.fromString = function (str) { if (str == "On") return 1.; if (str == "Off") return 0.; };
return param;
};
//-----------------------------------------------------------
makeRangeParameter = function (name, min, max, units, precision) {
if (precision == undefined)
precision = 2;
var precision = Math.pow(10, precision);
param = makeParameter(name);
param.units = units;
param.toString = function (value) {
value = normalizedToPlain(min, max, value);
value = Math.round(value * precision) / precision;
return value + (this.units ? " " + this.units : "");
};
param.fromString = function (str) {
var v = parseFloat(str);
if (!isNaN(v))
return plainToNormalized(min, max, v);
};
param.getPlain = function () { return normalizedToPlain(min, max, this.getValue()); };
return param;
}
//-----------------------------------------------------------
makeADSR = function () {
const Phase = { Attack: 1, Halten: 2, Decay: 3, Sustain: 4, Release: 5 };
var attack = 0;
var halten = 0;
var decay = 0;
var sustain = 0;
var release = 0;
var phase = Phase.Attack;
var value = 0;
var time = -1;
var doRetrigger = 0;
obj = {
setAttack: function (newValue) { attack = newValue; },
setHalten: function (newValue) { halten = newValue; },
setDecay: function (newValue) { decay = newValue; },
setSustain: function (newValue) { sustain = newValue / 100; }, // Sustain as percent (converted to [0..1])
setRelease: function (newValue) { release = newValue; },
setRetrigger: function (newValue) { doRetrigger = newValue > 0.5; },
trigger: function () {
time = doRetrigger ? 0 : value * attack;
phase = Phase.Attack;
},
release: function () {
if (phase == Phase.Sustain || phase == Phase.Decay || phase == Phase.Halten) {
phase = Phase.Release;
time = 0;
}
},
process: function (numSamples) {
if (time < 0) return 0;
time += numSamples;
if (phase == Phase.Attack) {
if (time > attack) {
phase = Phase.Halten;
value = 1.0;
time -= attack;
} else {
value = time / attack;
}
}
if (phase == Phase.Halten) {
if (time > halten) {
phase = Phase.Decay;
time -= halten;
}
}
if (phase == Phase.Decay) {
if (time > decay) {
phase = Phase.Sustain;
value = sustain;
time -= decay;
} else {
value = 1.0 - (1.0 - sustain) * (time / decay);
}
}
if (phase == Phase.Sustain) {
// Hold the Sustain level until Release phase
return value;
}
if (phase == Phase.Release) {
if (time > release) {
phase = Phase.Attack;
time = -1;
value = 0;
} else {
value = sustain * (1.0 - (time / release));
}
}
return value;
}
};
return obj;
}
//-----------------------------------------------------------
const AttackIndex = 1;
const HaltenIndex = 2;
const DecayIndex = 3;
const SustainIndex = 4;
const ReleaseIndex = 5;
const VelocityIndex = 6;
const RetriggerIndex = 7;
parameter = [];
parameter[AttackIndex] = makeRangeParameter("Attack", 0, 3000, "ms", 0);
parameter[HaltenIndex] = makeRangeParameter("Hold", 0, 1000, "ms", 0);
parameter[DecayIndex] = makeRangeParameter("Decay", 0, 3000, "ms", 0);
parameter[SustainIndex] = makeRangeParameter("Sustain", 0, 100, "%", 0); // Sustain now functional
parameter[ReleaseIndex] = makeRangeParameter("Release", 0, 3000, "ms", 0);
parameter[VelocityIndex] = makeRangeParameter("Velocity", 0, 100, "%", 0);
parameter[RetriggerIndex] = makeBoolParameter("Retrigger");
adsr = makeADSR();
sr = getSampleRate() / 1000;
parameter[AttackIndex].addListener(function (param) { adsr.setAttack(param.getPlain() * sr); });
parameter[HaltenIndex].addListener(function (param) { adsr.setHalten(param.getPlain() * sr); });
parameter[DecayIndex].addListener(function (param) { adsr.setDecay(param.getPlain() * sr); });
parameter[SustainIndex].addListener(function (param) { adsr.setSustain(param.getPlain()); });
parameter[ReleaseIndex].addListener(function (param) { adsr.setRelease(param.getPlain() * sr); });
parameter[RetriggerIndex].addListener(function (param) { adsr.setRetrigger(param.getPlain()); });
key = { noteID: -1, velocity: 0 };
//-----------------------------------------------------------
processModulation = function (inputValue, numSamples) {
return adsr.process(numSamples) * key.velocity;
}
//-----------------------------------------------------------
onNoteOnEvent = function (channel, pitch, velocity, tuning, noteID) {
if (key.noteID == -1) {
key.noteID = noteID;
velAmount = parameter[VelocityIndex].getValue();
key.velocity = 1. + (velocity * velAmount) - 1. * velAmount;
adsr.trigger();
}
}
//-----------------------------------------------------------
onNoteOffEvent = function (channel, pitch, velocity, tuning, noteID) {
if (key.noteID == noteID) {
key.noteID = -1;
adsr.release();
}
}
//-----------------------------------------------------------
onParamChange = function (paramIndex, newValue) {
if (parameter[paramIndex]) parameter[paramIndex].setValue(newValue);
}
//-----------------------------------------------------------
paramValueToString = function (paramIndex, paramValue) {
if (parameter[paramIndex]) return parameter[paramIndex].toString(paramValue);
}
//-----------------------------------------------------------
stringToParamValue = function (paramIndex, string) {
if (parameter[paramIndex]) return parameter[paramIndex].fromString(string);
}
//-----------------------------------------------------------
getParamTitle = function (paramIndex) {
if (parameter[paramIndex]) return parameter[paramIndex].name;
}
//-----------------------------------------------------------
getParamStepCount = function (paramIndex) {
if (parameter[paramIndex]) return parameter[paramIndex].stepCount;
}