├── 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 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
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); --------------------------------------------------------------------------------