├── README.md
├── example.html
└── synth.js
/README.md:
--------------------------------------------------------------------------------
1 | SynthJS v0.2.0-alpha
2 | ===================
3 |
4 | This is a utility script that extends the Web Audio API context with a number of common synth and DSP nodes. So far this consists of:
5 |
6 | * White noise generator
7 | * Envelope generator
8 | * Feedback delay
9 | * Reverb
10 | * Drum sounds
11 | * Looping sequencer
12 |
13 | This is a work in progress.
14 |
15 | Note: Requires a browser that supports the webkit Web Audio API (see: [http://caniuse.com/audio-api](http://caniuse.com/audio-api)).
16 |
17 | Examples
18 | --------
19 |
20 | A square wave "ping" with delay.
21 |
22 | var context = new webkitAudioContext();
23 |
24 | //square wave at 440 Hz (default)
25 | var osc = context.createOscillator();
26 | osc.type = osc.SQUARE;
27 |
28 | //envelope with 0.001 sec attack and 0.5 sec decay
29 | var envelope = context.createEnvelope(0.001, 0.5, 0, 0);
30 |
31 | //feedback delay of 0.4 seconds with 0.5x feedback
32 | var feedbackDelay = context.createFeedbackDelay(0.4, 0.5);
33 |
34 | osc.connect(envelope);
35 | envelope.connect(feedbackDelay);
36 | envelope.connect(context.destination);
37 | feedbackDelay.connect(context.destination);
38 |
39 | //trigger the note and release after 0.6 seconds
40 | envelope.trigger(0.6);
41 |
42 | API
43 | ----
44 |
45 | ###Synths###
46 |
47 | context.**createNoiseGen**([stereo [, bufferSize]]) - White noise generator
48 |
49 | ###Envelope###
50 |
51 | context.**createEnvelope**(a, d, s, r)
52 |
53 | *Properties*
54 |
55 | * this.**att**
56 | * this.**dec**
57 | * this.**sus**
58 | * this.**rel**
59 |
60 | *Methods*
61 |
62 | * **trigger**([length]) - Triggers a note. If *length* is supplied, the note will automatically be released after *length* seconds.
63 | * **release**() - Releases the currently triggered note.
64 |
65 | ###Instruments###
66 |
67 | context.**createDrum**()
68 | context.**createHiHat**()
69 |
70 | *Methods*
71 |
72 | * **trigger**
73 |
74 | ###Effects###
75 |
76 | context.**createFeedbackDelay**(delay, feedback)
77 |
78 | * **delay** is in seconds
79 | * **feedback** is a value between 0 and 1.0
80 |
81 | context.**createReverbNode**(length[, options])
82 |
83 | * **length** is in seconds
84 |
85 | Currently available keys for reverb options object:
86 | * **decay** - how rapidly the sound decays (1 = linear decay, higher numbers are faster exponential decays) - defaults to 2
87 | * **reverse** - pass in `true` for reverse reverb
88 |
89 | ###Looping###
90 |
91 | **Loop** Class
92 |
93 | *Methods*
94 |
95 | * **setInstruments**
96 | * **setSequences**
97 | * **setBPM**
98 | * **setBeatUnit**
99 | * **startLoop**
100 | * **stopLoop**
101 |
102 | More detailed docs to come, for now check out example.html for an example of using the Loop class
103 |
--------------------------------------------------------------------------------
/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
77 |
122 |
123 |
124 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/synth.js:
--------------------------------------------------------------------------------
1 | (function(AudioContext){
2 |
3 | /* NoiseGen */
4 |
5 | function NoiseGenFactory(context, stereo, bufSize){
6 | bufSize = bufSize || 4096;
7 | var node = context.createJavaScriptNode(bufSize, 1, 2);
8 | node.onaudioprocess = function(e){
9 | var outBufferL = e.outputBuffer.getChannelData(0);
10 | var outBufferR = e.outputBuffer.getChannelData(1);
11 | for (var i = 0; i < bufSize; i++){
12 | outBufferL[i] = Math.random() * 2 - 1;
13 | outBufferR[i] = stereo ? Math.random() * 2 - 1 : outBufferL[i];
14 | }
15 | }
16 | return node;
17 | }
18 |
19 | /* EnvelopeNode */
20 |
21 | function EnvelopeNode(a, d, s, r){
22 | this.gain.value = 0;
23 | this.att = a;
24 | this.dec = d;
25 | this.sus = s;
26 | this.rel = r;
27 |
28 | this.trigger = function(length){
29 | var now = this.context.currentTime;
30 | var gain = this.gain;
31 | gain.cancelScheduledValues(now);
32 | gain.setValueAtTime(0, now);
33 | gain.linearRampToValueAtTime(1.0, now + this.att);
34 | now += this.att;
35 | gain.linearRampToValueAtTime(this.sus, now + this.dec);
36 | if (length){
37 | var self = this;
38 | setTimeout(function(){ self.release(); }, length * 1000);
39 | }
40 | };
41 | this.release = function(){
42 | var now = this.context.currentTime;
43 | var gain = this.gain;
44 | gain.cancelScheduledValues(now);
45 | gain.setValueAtTime(gain.value, now);
46 | gain.linearRampToValueAtTime(0, now + this.rel);
47 | }
48 | }
49 |
50 | function EnvelopeFactory(context, a, d, s, r){
51 | var gain = context.createGainNode();
52 | EnvelopeNode.call(gain, a, d, s, r);
53 | return gain;
54 | }
55 |
56 | /* FeedbackDelayNode */
57 |
58 | function FeedbackDelayNode(context, delay, feedback){
59 | this.delayTime.value = delay;
60 | this.gainNode = context.createGainNode();
61 | this.gainNode.gain.value = feedback;
62 | this.connect(this.gainNode);
63 | this.gainNode.connect(this);
64 | }
65 |
66 | function FeedbackDelayFactory(context, delayTime, feedback){
67 | var delay = context.createDelayNode(delayTime + 1);
68 | FeedbackDelayNode.call(delay, context, delayTime, feedback);
69 | return delay;
70 | }
71 |
72 | /* ReverbNode */
73 |
74 | function ReverbNodeFactory(context, seconds, options){
75 | options = options || {};
76 | var sampleRate = context.sampleRate;
77 | var length = sampleRate * seconds;
78 | var impulse = context.createBuffer(2, length, sampleRate);
79 | var impulseL = impulse.getChannelData(0);
80 | var impulseR = impulse.getChannelData(1);
81 | var decay = options.decay || 2;
82 | for (var i = 0; i < length; i++){
83 | var n = options.reverse ? length - i : i;
84 | impulseL[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay);
85 | impulseR[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay);
86 | }
87 | var convolver = context.createConvolver();
88 | convolver.buffer = impulse;
89 | return convolver;
90 | }
91 |
92 | AudioContext.prototype.createNoiseGen = function(stereo, bufSize){ return NoiseGenFactory(this, stereo, bufSize); };
93 | AudioContext.prototype.createEnvelope = function(a, s, d, r){ return EnvelopeFactory(this, a, s, d, r); };
94 | AudioContext.prototype.createFeedbackDelay = function(delay, feedback){ return FeedbackDelayFactory(this, delay, feedback); };
95 | AudioContext.prototype.createReverbNode = function(seconds, options){ return ReverbNodeFactory(this, seconds, options); };
96 |
97 | /** INSTRUMENTS **/
98 |
99 | function Drum(context){
100 | var osc = this.osc = context.createOscillator();
101 | osc.frequency.value = 45;
102 | osc.type = osc.SINE;
103 | var env = this.env = context.createEnvelope(0.001, 0.1, 0, 0.5);
104 | osc.connect(env);
105 | }
106 |
107 | Drum.prototype.trigger = function(){
108 | this.env.trigger(0.05);
109 | }
110 | Drum.prototype.connect = function(dest){
111 | this.env.connect(dest);
112 | }
113 |
114 | function HiHat(context){
115 | this.noiseGen = context.createNoiseGen();
116 | this.filter = context.createBiquadFilter();
117 | this.filter.type = this.filter.HIGHPASS;
118 | this.filter.frequency.value = 5000;
119 | this.noiseGen.connect(this.filter);
120 | this.env = context.createEnvelope(0.001, 0.05, 0, 0.2);
121 | this.filter.connect(this.env);
122 | }
123 |
124 | HiHat.prototype.trigger = function(){
125 | this.env.trigger(0.025);
126 | }
127 | HiHat.prototype.connect = function(dest){
128 | this.env.connect(dest);
129 | }
130 |
131 | AudioContext.prototype.createDrum = function(){ return new Drum(this); };
132 | AudioContext.prototype.createHiHat = function(){ return new HiHat(this); };
133 |
134 | /** LOOP **/
135 |
136 | function Loop(){
137 | this.tracks = {};
138 | this.stopped = true;
139 | this.interval = 500;
140 | this.beatUnit = 1/4;
141 | this.onPlay = function(){};
142 | }
143 |
144 | Loop.prototype.setInstruments = function(instruments){
145 | each(instruments, function(inst, label){
146 | this.tracks[label] = { instrument: inst };
147 | }, this);
148 | }
149 |
150 | Loop.prototype.setSequences = function(seqs){
151 | each(seqs, function(loop, label){
152 | this.tracks[label].loop = typeof loop === "string" ? loop.split('') : loop;
153 | this.tracks[label].loopPos = 0;
154 | }, this);
155 | }
156 |
157 | Loop.prototype.setBPM = function(BPM){
158 | this.interval = (60 / BPM) * 1000;
159 | }
160 |
161 | Loop.prototype.setBeatUnit = function(unit){
162 | this.beatUnit = unit;
163 | }
164 |
165 | Loop.prototype.startLoop = function(){
166 | this.stopped = false;
167 | this.playNext();
168 | }
169 |
170 | Loop.prototype.playNext = function(){
171 | if (this.stopped) return;
172 | each(this.tracks, function(track, name){
173 | var currNote = track.loop[track.loopPos];
174 | if (currNote === '*'){
175 | track.instrument.trigger();
176 | }
177 | this.onPlay(name, track.loopPos);
178 | if (++track.loopPos >= track.loop.length){
179 | track.loopPos = 0;
180 | }
181 | }, this);
182 | var self = this;
183 | setTimeout(function(){ self.playNext(); }, this.interval * this.beatUnit * 4);
184 | }
185 |
186 | Loop.prototype.stopLoop = function(){
187 | this.stopped = true;
188 | }
189 |
190 | function each(obj, callback, context){
191 | context = context || this;
192 | for (var prop in obj){
193 | if (obj.hasOwnProperty(prop)){
194 | callback.call(context, obj[prop], prop);
195 | }
196 | }
197 | }
198 |
199 | window.Loop = Loop;
200 |
201 | })(window.AudioContext || window.webkitAudioContext);
--------------------------------------------------------------------------------