├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── adsrnode.js ├── debugger.html └── demo.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # see: http://editorconfig.org 2 | # 3 | # (c) Copyright 2017, Sean Connelly (@voidqk), http://syntheti.cc 4 | # MIT License 5 | 6 | root = true 7 | 8 | [*] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | charset = utf-8 13 | indent_style = tab 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # random crap that tends to appear over time 2 | Thumbs.db 3 | .DS_Store 4 | .DS_Store? 5 | *.bak 6 | *~ 7 | *# 8 | *.orig 9 | desktop.ini 10 | *.swp 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sean Connelly (@voidqk, web: sean.cm) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ADSRNode 2 | ======== 3 | 4 | ADSRNode is a single JavaScript function that creates an ADSR envelope for use in WebAudio. 5 | 6 | * [Demo](https://rawgit.com/voidqk/adsrnode/master/demo.html) 7 | 8 | Usage 9 | ----- 10 | 11 | ### ADSRNode(*audioCtx*, *opts*) 12 | 13 | ```javascript 14 | // create the Audio Context 15 | var ctx = new AudioContext(); 16 | 17 | // simple ADSR envelope 18 | var envelope = ADSRNode(ctx, { 19 | attack: 0.1, // seconds until hitting 1.0 20 | decay: 0.2, // seconds until hitting sustain value 21 | sustain: 0.5, // sustain value 22 | release: 0.3 // seconds until returning back to 0.0 23 | }); 24 | 25 | // advanced ADSR envelope 26 | var envelope = ADSRNode(ctx, { 27 | base: 5.0, // starting/ending value (default: 0) 28 | attack: 0.2, // seconds until hitting peak value (default: 0) 29 | attackCurve: 0.0, // amount of curve for attack (default: 0) 30 | peak: 9.0, // peak value (default: 1) 31 | hold: 0.3, // seconds to hold at the peak value (default: 0) 32 | decay: 0.4, // seconds until hitting sustain value (default: 0) 33 | decayCurve: 5.0, // amount of curve for decay (default: 0) 34 | sustain: 3.0, // sustain value (required) 35 | release: 0.5, // seconds until returning back to base value (default: 0) 36 | releaseCurve: 1.0 // amount of curve for release (default: 0) 37 | }); 38 | ``` 39 | 40 | The returned `envelope` object is a 41 | [ConstantSourceNode](https://developer.mozilla.org/en-US/docs/Web/API/ConstantSourceNode). 42 | 43 | It can be connected to other nodes using the normal 44 | [envelope.connect(...)](https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/connect) and 45 | [envelope.disconnect(...)](https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect) 46 | functions. 47 | 48 | It must be started with 49 | [envelope.start()](https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode/start), 50 | to begin outputting the `base` value. It can be stopped with 51 | [envelope.stop()](https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode/stop). 52 | 53 | The following methods/properties are added to the object: 54 | 55 | ### *envelope*.trigger([*when*]) 56 | 57 | Trigger the envelope. 58 | 59 | The `when` parameter is optional. It's the time, in seconds, at which the envelope should trigger. 60 | It is the same time measurement as 61 | [AudioContext.currentTime](https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/currentTime), 62 | just like many other functions that take timed events. I.e., to trigger in two seconds, you 63 | would do: `envelope.trigger(ctx.currentTime + 2)`. If omitted, it will trigger immediately. 64 | 65 | ### *envelope*.release([*when*]) 66 | 67 | Release a triggered envelope. 68 | 69 | The `when` parameter behaves just like `envelope.trigger`. 70 | 71 | ### *envelope*.reset() 72 | 73 | Reset an envelope immediately (i.e., output `base` value and wait for a trigger). 74 | 75 | ### *envelope*.update(*opts*) 76 | 77 | Update the values of the ADSR curve. All keys are optional. For example, to just update the 78 | peak, use `envelope.update({ peak: 2 })`. 79 | 80 | Updating the envelope will also `reset` it. 81 | 82 | ### *envelope*.baseTime 83 | 84 | This value is set after an `envelope.release(...)` to provide the exact moment (in absolute seconds) 85 | that the envelope will return to the base value. 86 | 87 | Triggering and Releasing 88 | ------------------------ 89 | 90 | [Special care](https://rawgit.com/voidqk/adsrnode/master/debugger.html) has been taken to ensure the 91 | envelope correctly responds to triggering and releasing while still outputting a partial envelope. 92 | 93 | For example, if a trigger happens in the middle of the release phase, the attack will pick up where 94 | the release left off -- as it should. Or if a release happens during the attack phase, it will 95 | correctly apply the release curve where the attack left off, etc. 96 | 97 | The only requirement from users of the library is to ensure calls to `trigger` and `release` happen 98 | in chronological order. 99 | 100 | For example, the following will fail, because it attempts to insert a trigger *before* a future 101 | trigger: 102 | 103 | ```javascript 104 | // this FAILS 105 | envelope.trigger(8); // schedule trigger at 8 second mark 106 | envelope.trigger(5); // schedule trigger at 5 second mark (error!) 107 | ``` 108 | 109 | This is easily fixed by simply ordering the calls: 110 | 111 | ```javascript 112 | // valid 113 | envelope.trigger(5); 114 | envelope.trigger(8); 115 | ``` 116 | -------------------------------------------------------------------------------- /adsrnode.js: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2018, Sean Connelly (@voidqk), http://sean.cm 2 | // MIT License 3 | // Project Home: https://github.com/voidqk/adsrnode 4 | 5 | (function(){ 6 | 7 | var DEBUG = false; 8 | 9 | function ADSRNode(ctx, opts){ 10 | // `ctx` is the AudioContext 11 | // `opts` is an object in the format: 12 | // { 13 | // base: , // output optional default: 0 14 | // attack: , // seconds optional default: 0 15 | // attackCurve: , // bend optional default: 0 16 | // peak: , // output optional default: 1 17 | // hold: , // seconds optional default: 0 18 | // decay: , // seconds optional default: 0 19 | // decayCurve: , // bend optional default: 0 20 | // sustain: , // output required 21 | // release: , // seconds optional default: 0 22 | // releaseCurve: // bend optional default: 0 23 | // } 24 | 25 | function getNum(opts, key, def){ 26 | if (typeof def === 'number' && typeof opts[key] === 'undefined') 27 | return def; 28 | if (typeof opts[key] === 'number') 29 | return opts[key]; 30 | throw new Error('[ADSRNode] Expecting "' + key + '" to be a number'); 31 | } 32 | 33 | var attack = 0, decay = 0, sustain, sustain_adj, release = 0; 34 | var base = 0, acurve = 0, peak = 1, hold = 0, dcurve = 0, rcurve = 0; 35 | 36 | function update(opts){ 37 | base = getNum(opts, 'base' , base ); 38 | attack = getNum(opts, 'attack' , attack ); 39 | acurve = getNum(opts, 'attackCurve' , acurve ); 40 | peak = getNum(opts, 'peak' , peak ); 41 | hold = getNum(opts, 'hold' , hold ); 42 | decay = getNum(opts, 'decay' , decay ); 43 | dcurve = getNum(opts, 'decayCurve' , dcurve ); 44 | sustain = getNum(opts, 'sustain' , sustain); 45 | release = getNum(opts, 'release' , release); 46 | rcurve = getNum(opts, 'releaseCurve', rcurve ); 47 | sustain_adj = adjustCurve(dcurve, peak, sustain); 48 | } 49 | 50 | // extract options 51 | update(opts); 52 | 53 | // create the node and inject the new methods 54 | var node = ctx.createConstantSource(); 55 | node.offset.value = base; 56 | 57 | // unfortunately, I can't seem to figure out how to use cancelAndHoldAtTime, so I have to have 58 | // code that calculates the ADSR curve in order to figure out the value at a given time, if an 59 | // interruption occurs 60 | // 61 | // the curve functions (linearRampToValueAtTime and setTargetAtTime) require an *event* 62 | // preceding the curve in order to calculate the correct start value... inserting the event 63 | // *should* work with cancelAndHoldAtTime, but it doesn't (or I misunderstand the API). 64 | // 65 | // therefore, for the curves to start at the correct location, I need to be able to calculate 66 | // the entire ADSR curve myself, so that I can correctly interrupt the curve at any moment. 67 | // 68 | // these values track the state of the trigger/release moments, in order to calculate the final 69 | // curve 70 | var lastTrigger = false; 71 | var lastRelease = false; 72 | 73 | // small epsilon value to check for divide by zero 74 | var eps = 0.00001; 75 | 76 | function curveValue(type, startValue, endValue, curTime, maxTime){ 77 | if (type === 0) 78 | return startValue + (endValue - startValue) * Math.min(curTime / maxTime, 1); 79 | // otherwise, exponential 80 | return endValue + (startValue - endValue) * Math.exp(-curTime * type / maxTime); 81 | } 82 | 83 | function adjustCurve(type, startValue, endValue){ 84 | // the exponential curve will never hit its target... but we can calculate an adjusted 85 | // target so that it will miss the adjusted value, but end up hitting the actual target 86 | if (type === 0) 87 | return endValue; // linear hits its target, so no worries 88 | var endExp = Math.exp(-type); 89 | return (endValue - startValue * endExp) / (1 - endExp); 90 | } 91 | 92 | function triggeredValue(time){ 93 | // calculates the actual value of the envelope at a given time, where `time` is the number 94 | // of seconds after a trigger (but before a release) 95 | var atktime = lastTrigger.atktime; 96 | if (time < atktime){ 97 | return curveValue(acurve, lastTrigger.v, 98 | adjustCurve(acurve, lastTrigger.v, peak), time, atktime); 99 | } 100 | if (time < atktime + hold) 101 | return peak; 102 | if (time < atktime + hold + decay) 103 | return curveValue(dcurve, peak, sustain_adj, time - atktime - hold, decay); 104 | return sustain; 105 | } 106 | 107 | function releasedValue(time){ 108 | // calculates the actual value of the envelope at a given time, where `time` is the number 109 | // of seconds after a release 110 | if (time < 0) 111 | return sustain; 112 | if (time > lastRelease.reltime) 113 | return base; 114 | return curveValue(rcurve, lastRelease.v, 115 | adjustCurve(rcurve, lastRelease.v, base), time, lastRelease.reltime); 116 | } 117 | 118 | function curveTo(param, type, value, time, duration){ 119 | if (type === 0 || duration <= 0) 120 | param.linearRampToValueAtTime(value, time + duration); 121 | else // exponential 122 | param.setTargetAtTime(value, time, duration / type); 123 | } 124 | 125 | node.trigger = function(when){ 126 | if (typeof when === 'undefined') 127 | when = this.context.currentTime; 128 | 129 | if (lastTrigger !== false){ 130 | if (when < lastTrigger.when) 131 | throw new Error('[ADSRNode] Cannot trigger before future trigger'); 132 | this.release(when); 133 | } 134 | var v = base; 135 | var interruptedLine = false; 136 | if (lastRelease !== false){ 137 | var now = when - lastRelease.when; 138 | v = releasedValue(now); 139 | // check if a linear release has been interrupted by this attack 140 | interruptedLine = rcurve === 0 && now >= 0 && now <= lastRelease.reltime; 141 | lastRelease = false; 142 | } 143 | var atktime = attack; 144 | if (Math.abs(base - peak) > eps) 145 | atktime = attack * (v - peak) / (base - peak); 146 | lastTrigger = { when: when, v: v, atktime: atktime }; 147 | 148 | this.offset.cancelScheduledValues(when); 149 | 150 | if (DEBUG){ 151 | // simulate curve using triggeredValue (debug purposes) 152 | for (var i = 0; i < 10; i += 0.01) 153 | this.offset.setValueAtTime(triggeredValue(i), when + i); 154 | return this; 155 | } 156 | 157 | if (interruptedLine) 158 | this.offset.linearRampToValueAtTime(v, when); 159 | else 160 | this.offset.setTargetAtTime(v, when, 0.001); 161 | curveTo(this.offset, acurve, adjustCurve(acurve, v, peak), when, atktime); 162 | this.offset.setTargetAtTime(peak, when + atktime, 0.001); 163 | if (hold > 0) 164 | this.offset.setTargetAtTime(peak, when + atktime + hold, 0.001); 165 | curveTo(this.offset, dcurve, sustain_adj, when + atktime + hold, decay); 166 | this.offset.setTargetAtTime(sustain, when + atktime + hold + decay, 0.001); 167 | return this; 168 | }; 169 | 170 | node.release = function(when){ 171 | if (typeof when === 'undefined') 172 | when = this.context.currentTime; 173 | 174 | if (lastTrigger === false) 175 | throw new Error('[ADSRNode] Cannot release without a trigger'); 176 | if (when < lastTrigger.when) 177 | throw new Error('[ADSRNode] Cannot release before the last trigger'); 178 | var tnow = when - lastTrigger.when; 179 | var v = triggeredValue(tnow); 180 | var reltime = release; 181 | if (Math.abs(sustain - base) > eps) 182 | reltime = release * (v - base) / (sustain - base); 183 | lastRelease = { when: when, v: v, reltime: reltime }; 184 | var atktime = lastTrigger.atktime; 185 | // check if a linear attack or a linear decay has been interrupted by this release 186 | var interruptedLine = 187 | (acurve === 0 && tnow >= 0 && tnow <= atktime) || 188 | (dcurve === 0 && tnow >= atktime + hold && tnow <= atktime + hold + decay); 189 | lastTrigger = false; 190 | 191 | this.offset.cancelScheduledValues(when); 192 | node.baseTime = when + reltime; 193 | 194 | if (DEBUG){ 195 | // simulate curve using releasedValue (debug purposes) 196 | for (var i = 0; true; i += 0.01){ 197 | this.offset.setValueAtTime(releasedValue(i), when + i); 198 | if (i >= reltime) 199 | break; 200 | } 201 | return this; 202 | } 203 | 204 | if (interruptedLine) 205 | this.offset.linearRampToValueAtTime(v, when); 206 | else 207 | this.offset.setTargetAtTime(v, when, 0.001); 208 | curveTo(this.offset, rcurve, adjustCurve(rcurve, v, base), when, reltime); 209 | this.offset.setTargetAtTime(base, when + reltime, 0.001); 210 | return this; 211 | }; 212 | 213 | node.reset = function(){ 214 | lastTrigger = false; 215 | lastRelease = false; 216 | var now = this.context.currentTime; 217 | this.offset.cancelScheduledValues(now); 218 | this.offset.setTargetAtTime(base, now, 0.001); 219 | node.baseTime = now; 220 | return this; 221 | }; 222 | 223 | node.update = function(opts){ 224 | update(opts); 225 | return this.reset(); 226 | }; 227 | 228 | node.baseTime = 0; 229 | 230 | return node; 231 | } 232 | 233 | // export appropriately 234 | if (typeof window === 'undefined') 235 | module.exports = ADSRNode; 236 | else 237 | window.ADSRNode = ADSRNode; 238 | 239 | })(); 240 | -------------------------------------------------------------------------------- /debugger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | ADSRNode Debugger 10 | 11 | 19 | 20 | 21 |

Move mouse to interact with interruption logic.

22 |

Click mouse to change mode.

23 | 24 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | ADSRNode Demo 10 | 11 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
Base:-5 5
Attack:0 5
Attack Curve:0 10
Peak:-5 5
Hold:0 5
Decay:0 5
Decay Curve:0 10
Sustain:-5 5
Release:0 5
Release Curve:0 10
86 | 265 | 266 | 267 | --------------------------------------------------------------------------------