├── .gitignore
├── .editorconfig
├── LICENSE
├── debugger.html
├── README.md
├── demo.html
└── adsrnode.js
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | ADSRNode Demo
10 |
11 |
38 |
39 |
40 |
41 |
86 |
265 |
266 |
267 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------