├── .gitignore ├── processors ├── gain.js ├── ring-modulator.js ├── bitcrusher.js ├── compressor.js ├── filter.js ├── freeverb.js ├── eq.js ├── overdrive.js ├── reverb.js ├── delay.js ├── ping-pong-delay.js ├── dipper.js └── pitchshift.js ├── lib ├── schedule-list.js ├── computed-next-tick.js ├── schedule-event.js ├── set-params.js ├── apply-scheduler.js ├── drums │ ├── rim-shot.js │ ├── kick-eight.js │ ├── hi-hat.js │ ├── kick-nine.js │ ├── snare.js │ └── clappy.js ├── build-impulse.js └── granular-sync.js ├── processor.js ├── examples └── monosynth.js ├── package.json ├── sources ├── cymbal.js ├── clap.js ├── kick.js ├── snare.js ├── noise.js ├── oscillator.js ├── sample.js └── granular.js ├── params ├── trigger-value.js ├── chromatic-scale.js ├── link-modulator.js ├── envelope.js └── lfo.js ├── triggerable.js ├── README.md ├── link-param.js ├── routable.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /processors/gain.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | var Param = require('audio-slot-param') 3 | var Apply = require('audio-slot-param/apply') 4 | 5 | module.exports = GainNode 6 | 7 | function GainNode (context) { 8 | var node = context.audio.createGain() 9 | node.gain.value = 0 10 | 11 | var obs = Processor(context, node, node, { 12 | gain: Param(context, node.gain.defaultValue) 13 | }) 14 | 15 | Apply(context, node.gain, obs.gain) 16 | 17 | return obs 18 | } 19 | -------------------------------------------------------------------------------- /processors/ring-modulator.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | var Param = require('audio-slot-param') 3 | var Apply = require('audio-slot-param/apply') 4 | var Oscillator = require('../sources/oscillator') 5 | 6 | module.exports = RingModulatorNode 7 | 8 | function RingModulatorNode (context) { 9 | var node = context.audio.createGain() 10 | 11 | var obs = Processor(context, node, node, { 12 | carrier: Oscillator(context) 13 | }) 14 | 15 | obs.carrier.connect(node.gain) 16 | 17 | return obs 18 | } 19 | -------------------------------------------------------------------------------- /processors/bitcrusher.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | var Property = require('observ-default') 3 | var Bitcrusher = require('bitcrusher') 4 | var watch = require('observ/watch') 5 | 6 | module.exports = BitcrusherNode 7 | 8 | function BitcrusherNode (context) { 9 | var node = Bitcrusher(context.audio, { bufferSize: 256 }) 10 | 11 | var obs = Processor(context, node, node, { 12 | bitDepth: Property(8), 13 | frequency: Property(1) 14 | }) 15 | 16 | watch(obs.bitDepth, function (value) { 17 | node.bitDepth = value 18 | }) 19 | 20 | watch(obs.frequency, function (value) { 21 | node.frequency = value 22 | }) 23 | 24 | return obs 25 | } 26 | -------------------------------------------------------------------------------- /lib/schedule-list.js: -------------------------------------------------------------------------------- 1 | module.exports = ScheduleList 2 | 3 | function ScheduleList () { 4 | var scheduled = [] 5 | 6 | scheduled.getLast = function () { 7 | return scheduled[scheduled.length - 1] 8 | } 9 | 10 | scheduled.truncateTo = function (pos) { 11 | while (scheduled[0] && scheduled[0].to && scheduled[0].to < pos) { 12 | destroy(scheduled.shift()) 13 | } 14 | } 15 | 16 | scheduled.truncateFrom = function (pos) { 17 | while (scheduled.length && scheduled.getLast().from >= pos) { 18 | destroy(scheduled.pop()) 19 | } 20 | } 21 | 22 | scheduled.destroy = function () { 23 | while (scheduled.length) { 24 | destroy(scheduled.pop()) 25 | } 26 | } 27 | 28 | return scheduled 29 | } 30 | 31 | function destroy (item) { 32 | item.stop() 33 | if (Array.isArray(item.releases)) { 34 | item.releases.forEach(invoke, item) 35 | } 36 | } 37 | 38 | function invoke (fn) { 39 | return fn.apply(this) 40 | } 41 | -------------------------------------------------------------------------------- /processors/compressor.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | var Param = require('audio-slot-param') 3 | var Apply = require('audio-slot-param/apply') 4 | 5 | module.exports = CompressorNode 6 | 7 | function CompressorNode (context) { 8 | var node = context.audio.createDynamicsCompressor() 9 | node.ratio.value = 20 10 | node.threshold.value = -1 11 | 12 | var obs = Processor(context, node, node, { 13 | threshold: Param(context, node.threshold.defaultValue), 14 | knee: Param(context, node.knee.defaultValue), 15 | ratio: Param(context, node.ratio.defaultValue), 16 | attack: Param(context, node.attack.defaultValue), 17 | release: Param(context, node.release.defaultValue) 18 | }) 19 | 20 | Apply(context, node.threshold, obs.threshold) 21 | Apply(context, node.knee, obs.knee) 22 | Apply(context, node.ratio, obs.ratio) 23 | Apply(context, node.attack, obs.attack) 24 | Apply(context, node.release, obs.release) 25 | 26 | return obs 27 | } 28 | -------------------------------------------------------------------------------- /lib/computed-next-tick.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var nextTick = require('next-tick') 3 | 4 | module.exports = computed 5 | 6 | function computed(observables, lambda) { 7 | var values = observables.map(function (o) { 8 | return o() 9 | }) 10 | 11 | var result = Observ(lambda.apply(null, values)) 12 | var pending = false 13 | 14 | observables.forEach(function (o, index) { 15 | o(function (newValue) { 16 | if (values[index] !== newValue) { 17 | values[index] = newValue 18 | if (!pending){ 19 | pending = true 20 | nextTick(result.forceUpdate) 21 | } 22 | } 23 | }) 24 | }) 25 | 26 | result.update = function(){ 27 | if (pending){ 28 | result.forceUpdate() 29 | } 30 | } 31 | 32 | result.forceUpdate = function(){ 33 | pending = false 34 | result.set(lambda.apply(null, values)) 35 | } 36 | 37 | return result 38 | } 39 | -------------------------------------------------------------------------------- /processors/filter.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | var Property = require('observ-default') 3 | 4 | var Param = require('audio-slot-param') 5 | var Apply = require('audio-slot-param/apply') 6 | var Transform = require('audio-slot-param/transform') 7 | 8 | module.exports = FilterNode 9 | 10 | function FilterNode (context) { 11 | var node = context.audio.createBiquadFilter() 12 | 13 | var obs = Processor(context, node, node, { 14 | frequency: Param(context, node.frequency.defaultValue), 15 | Q: Param(context, node.Q.defaultValue), 16 | gain: Param(context, node.gain.defaultValue), 17 | type: Property(node.type) 18 | }) 19 | 20 | obs.type(function (value) { 21 | node.type = value 22 | }) 23 | 24 | Apply(context, node.frequency, Transform(context, [ 25 | { param: obs.frequency, transform: clampMin20 } 26 | ])) 27 | Apply(context, node.Q, obs.Q) 28 | Apply(context, node.gain, obs.gain) 29 | 30 | return obs 31 | } 32 | 33 | function clampMin20 (_, val) { 34 | return Math.max(20, val) 35 | } 36 | -------------------------------------------------------------------------------- /lib/schedule-event.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ScheduleEvent 4 | 5 | function ScheduleEvent (from, source, choker, releases) { 6 | this.from = from 7 | this.to = NaN 8 | this.releases = releases 9 | this.source = source 10 | this.choker = choker 11 | this.choked = false 12 | } 13 | 14 | ScheduleEvent.prototype.choke = function (at) { 15 | if (!this.to || at < this.to) { 16 | this.choker.gain.cancelScheduledValues(this.choker.context.currentTime) 17 | this.source.stop(at + 0.1) 18 | this.choker.gain.setTargetAtTime(0, at, 0.02) 19 | this.choked = true 20 | this.to = at + 0.1 21 | } 22 | } 23 | 24 | ScheduleEvent.prototype.cancelChoke = function (at) { 25 | if (this.choked && this.stopAt) { 26 | this.choker.gain.cancelScheduledValues(this.to - 0.1) 27 | this.stop(this.stopAt) 28 | } 29 | } 30 | 31 | ScheduleEvent.prototype.stop = function (at) { 32 | at = at || this.source.context.currentTime 33 | this.choker.gain.cancelScheduledValues(this.choker.context.currentTime) 34 | this.source.stop(at) 35 | this.choked = false 36 | this.stopAt = at 37 | this.to = at 38 | } 39 | -------------------------------------------------------------------------------- /processors/freeverb.js: -------------------------------------------------------------------------------- 1 | var watch = require('observ/watch') 2 | 3 | var Freeverb = require('freeverb') 4 | var Processor = require('../processor.js') 5 | var Property = require('observ-default') 6 | 7 | var Param = require('audio-slot-param') 8 | var Apply = require('audio-slot-param/apply') 9 | var Transform = require('audio-slot-param/transform') 10 | 11 | module.exports = FreeverbNode 12 | 13 | function FreeverbNode (context) { 14 | var reverb = Freeverb(context.audio) 15 | var output = context.audio.createGain() 16 | reverb.connect(output) 17 | 18 | var obs = Processor(context, reverb, output, { 19 | roomSize: Property(0.8), 20 | dampening: Property(3000), 21 | wet: Param(context, 1), 22 | dry: Param(context, 1) 23 | }) 24 | 25 | watch(obs.roomSize, function (value) { 26 | reverb.roomSize = Math.min(1, Math.max(0, value)) 27 | }) 28 | 29 | watch(obs.dampening, function (value) { 30 | reverb.dampening = Math.min(20000, Math.max(0, value)) 31 | }) 32 | 33 | Apply(context, reverb.wet, Transform(context, [ 34 | obs.wet, { value: 4, transform: divide } 35 | ])) 36 | Apply(context, reverb.dry, obs.dry) 37 | 38 | return obs 39 | } 40 | 41 | function divide (a, b) { 42 | return a / b 43 | } 44 | -------------------------------------------------------------------------------- /processor.js: -------------------------------------------------------------------------------- 1 | var ObservStruct = require('observ-struct') 2 | var Param = require('audio-slot-param') 3 | 4 | module.exports = ProcessorNode 5 | 6 | function ProcessorNode (context, input, output, params, releases) { 7 | var obs = ObservStruct(params) 8 | 9 | obs.input = input 10 | obs.output = output 11 | obs.connect = output.connect.bind(output) 12 | obs.disconnect = output.disconnect.bind(output) 13 | obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) 14 | obs.context = context 15 | 16 | obs.triggerOn = function (at) { 17 | at = at || context.audio.currentTime 18 | Param.triggerOn(obs, at) 19 | } 20 | 21 | obs.triggerOff = function (at) { 22 | at = at || context.audio.currentTime 23 | var stopAt = obs.getReleaseDuration(at) + at 24 | Param.triggerOff(obs, stopAt) 25 | } 26 | 27 | obs.cancelFrom = function (at) { 28 | Param.cancelFrom(obs, at) 29 | } 30 | 31 | obs.destroy = function () { 32 | while (releases && releases.length) { 33 | releases.pop()() 34 | } 35 | Object.keys(obs).forEach(function (key) { 36 | if (obs[key] && typeof obs[key].destroy === 'function') { 37 | obs[key].destroy() 38 | } 39 | }) 40 | } 41 | 42 | return obs 43 | } 44 | -------------------------------------------------------------------------------- /examples/monosynth.js: -------------------------------------------------------------------------------- 1 | var Slot = require('../') 2 | 3 | var context = { 4 | audio: new window.AudioContext(), 5 | nodes: { 6 | oscillator: require('../sources/oscillator'), 7 | filter: require('../processors/filter'), 8 | envelope: require('../params/envelope'), 9 | lfo: require('../params/lfo') 10 | } 11 | } 12 | 13 | var synth = Slot(context) 14 | synth.set({ 15 | sources: [ 16 | { 17 | node: 'oscillator', 18 | shape: 'sawtooth', 19 | amp: { 20 | node: 'envelope', 21 | value: 0.6, 22 | attack: 0.1, 23 | release: 1 24 | }, 25 | octave: -1, 26 | detune: { 27 | value: 0, 28 | node: 'lfo', 29 | amp: 40, 30 | rate: 5, 31 | mode: 'add' 32 | } 33 | } 34 | ], 35 | processors: [ 36 | { 37 | node: 'filter', 38 | type: 'lowpass', 39 | frequency: { 40 | node: 'envelope', 41 | value: 10000, 42 | decay: 0.6, 43 | sustain: 0.05, 44 | release: 0.1 45 | } 46 | } 47 | ] 48 | }) 49 | 50 | synth.connect(context.audio.destination) 51 | 52 | // trigger! 53 | setTimeout(function () { 54 | synth.triggerOn(1) 55 | synth.triggerOff(2) 56 | synth.triggerOn(3) 57 | synth.triggerOff(4) 58 | synth.triggerOn(5) 59 | synth.triggerOff(7) 60 | }, 0.2) 61 | -------------------------------------------------------------------------------- /lib/set-params.js: -------------------------------------------------------------------------------- 1 | var blackList = ['start', 'stop', 'context', 'constructor', 'output'] 2 | 3 | module.exports = setParamsOn 4 | 5 | function setParamsOn (params, modulators, target) { 6 | var k = null 7 | for (k in params) { 8 | if (isAudioParam(target[k])) { 9 | if (target[k].value !== params[k] && isFinite(params[k])) { 10 | target[k].cancelScheduledValues(0) 11 | target[k].setValueAtTime(params[k], 0) 12 | target[k].value = params[k] 13 | } 14 | } else if (k in target && isPropertyTarget(k) && typeof target[k] !== 'function') { 15 | if (target[k] !== params[k]) { 16 | target[k] = params[k] 17 | } 18 | } 19 | } 20 | 21 | for (k in modulators) { 22 | if (isAudioParam(target[k])) { 23 | target[k].value = 0 24 | modulators[k].connect(target[k]) 25 | } else if (k in target && isPropertyTarget(k) && typeof target[k] !== 'function') { 26 | if (typeof modulators[k].resolved === 'function') { 27 | var val = modulators[k].resolved() 28 | if (val !== target[k]) { 29 | target[k] = val 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | function isPropertyTarget (key) { 37 | return !~blackList.indexOf(key) && key.charAt(0) !== '_' 38 | } 39 | 40 | function isAudioParam (node) { 41 | return (node instanceof Object && node.setValueAtTime) 42 | } 43 | -------------------------------------------------------------------------------- /lib/apply-scheduler.js: -------------------------------------------------------------------------------- 1 | var cache = new WeakMap() 2 | 3 | module.exports = function (context, target) { 4 | if (context.scheduler) { 5 | // use global scheduler 6 | return context.scheduler.onSchedule(target) 7 | } else if (context.audio) { 8 | var result = cache.get(context.audio) 9 | if (!result) { 10 | result = Scheduler(context.audio) 11 | cache.set(context.audio, result) 12 | } 13 | return result(target) 14 | } 15 | } 16 | 17 | function Scheduler (audioContext) { 18 | var listeners = [] 19 | var timer = null 20 | var lastTime = audioContext.currentTime 21 | 22 | var obs = function (listener) { 23 | if (!listeners.length) { 24 | timer = setInterval(schedule, 50) 25 | } 26 | listeners.push(listener) 27 | return function remove () { 28 | var index = listeners.indexOf(listener) 29 | if (~index) listeners.splice(index, 1) 30 | if (!listeners.length) { 31 | clearInterval(timer) 32 | } 33 | } 34 | } 35 | 36 | return obs 37 | 38 | // scoped 39 | 40 | function schedule () { 41 | var to = audioContext.currentTime + 0.1 42 | var data = { 43 | time: lastTime, 44 | duration: to - lastTime, 45 | from: lastTime, 46 | to: to, 47 | beatDuration: 1 48 | } 49 | lastTime = to 50 | for (var i = 0;i < listeners.length;i++) { 51 | listeners[i](data) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-slot", 3 | "version": "5.3.2", 4 | "description": "Web Audio API FRP wrapper for creating, routing, and triggering AudioNodes.", 5 | "main": "index.js", 6 | "scripts": { 7 | "example": "beefy examples/monosynth.js", 8 | "test": "standard" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mmckegg/audio-slot.git" 13 | }, 14 | "dependencies": { 15 | "array-union": "^1.0.1", 16 | "audio-slot-param": "~2.2.5", 17 | "bitcrusher": "^0.3.0", 18 | "deep-equal": "~0.2.1", 19 | "freeverb": "^1.0.2", 20 | "geval": "^2.1.1", 21 | "make-distortion-curve": "^1.0.0", 22 | "next-tick": "^1.0.0", 23 | "noise-buffer": "^2.0.0", 24 | "observ": "^0.2.0", 25 | "observ-default": "^1.0.0", 26 | "observ-node-array": "^1.14.0", 27 | "observ-struct": "^6.0.0", 28 | "setimmediate2": "^2.0.1", 29 | "xtend": "~4.0.0" 30 | }, 31 | "keywords": [ 32 | "frp", 33 | "observ", 34 | "waapi", 35 | "AudioNode", 36 | "modulator", 37 | "source", 38 | "AudioParam", 39 | "json", 40 | "midi" 41 | ], 42 | "author": "Matt McKegg", 43 | "license": "ISC", 44 | "bugs": { 45 | "url": "https://github.com/mmckegg/audio-slot/issues" 46 | }, 47 | "homepage": "https://github.com/mmckegg/audio-slot", 48 | "devDependencies": { 49 | "beefy": "^2.1.5", 50 | "standard": "^5.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sources/cymbal.js: -------------------------------------------------------------------------------- 1 | var Param = require('audio-slot-param') 2 | var Apply = require('audio-slot-param/apply') 3 | var Triggerable = require('../triggerable') 4 | var ScheduleEvent = require('../lib/schedule-event') 5 | 6 | var HiHat = require('../lib/drums/hi-hat') 7 | 8 | module.exports = CymbalNode 9 | 10 | function CymbalNode (context) { 11 | var output = context.audio.createGain() 12 | var amp = context.audio.createGain() 13 | amp.gain.value = 0 14 | amp.connect(output) 15 | 16 | var obs = Triggerable(context, { 17 | tune: Param(context, 0), // cents 18 | decay: Param(context, 0.3), // seconds 19 | amp: Param(context, 0.4) 20 | }, trigger) 21 | 22 | var currentParams = {} 23 | 24 | var ctor = HiHat(context.audio, currentParams) 25 | 26 | obs.context = context 27 | 28 | Apply(context, amp.gain, obs.amp) 29 | 30 | obs.connect = output.connect.bind(output) 31 | obs.disconnect = output.disconnect.bind(output) 32 | 33 | return obs 34 | 35 | // scoped 36 | function trigger (at) { 37 | // HACK: apply params 38 | currentParams.tune = obs.tune.getValueAt(at) + 64 39 | currentParams.decay = obs.decay.getValueAt(at) / 2.2 * 128 40 | 41 | var choker = context.audio.createGain() 42 | var source = ctor() 43 | source.connect(choker) 44 | choker.connect(amp) 45 | source.start(at) 46 | 47 | var event = new ScheduleEvent(at, source, choker, [ 48 | choker.disconnect.bind(choker) 49 | ]) 50 | 51 | event.oneshot = true 52 | event.to = at + obs.decay.getValueAt(at) 53 | return event 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /params/trigger-value.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var ObservStruct = require('observ-struct') 3 | var Param = require('audio-slot-param') 4 | var Event = require('geval') 5 | 6 | module.exports = ValueModulator 7 | 8 | function ValueModulator (parentContext) { 9 | var context = Object.create(parentContext) 10 | 11 | var obs = ObservStruct({ 12 | id: Observ(), 13 | value: Param(context, 1) 14 | }) 15 | 16 | obs._type = 'ValueModulator' 17 | context.slot = obs 18 | 19 | var broadcast = null 20 | obs.onSchedule = Event(function (b) { 21 | broadcast = b 22 | }) 23 | 24 | obs.value.onSchedule(function (value) { 25 | // only send modulations while triggering 26 | if (lastTriggerOn > lastTriggerOff) { 27 | broadcast(value) 28 | } 29 | }) 30 | 31 | obs.context = context 32 | 33 | var lastTriggerOn = -1 34 | var lastTriggerOff = 0 35 | 36 | obs.triggerOn = function (at) { 37 | at = at || context.audio.currentTime 38 | lastTriggerOn = at 39 | 40 | Param.triggerOn(obs, at) 41 | 42 | if (!obs.value.node) { 43 | broadcast({ value: obs.value.getValue(), at: at }) 44 | } 45 | } 46 | 47 | obs.triggerOff = function (at) { 48 | at = at || context.audio.currentTime 49 | 50 | var stopAt = obs.getReleaseDuration() + at 51 | Param.triggerOff(obs, stopAt) 52 | 53 | if (!obs.value.node) { 54 | broadcast({ value: 0, at: at }) 55 | } 56 | 57 | lastTriggerOff = at 58 | return stopAt 59 | } 60 | 61 | obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) 62 | obs.destroy = obs.value.destroy 63 | 64 | return obs 65 | } 66 | -------------------------------------------------------------------------------- /sources/clap.js: -------------------------------------------------------------------------------- 1 | var Param = require('audio-slot-param') 2 | var Apply = require('audio-slot-param/apply') 3 | var Triggerable = require('../triggerable') 4 | var ScheduleEvent = require('../lib/schedule-event') 5 | 6 | var Clappy = require('../lib/drums/clappy') 7 | 8 | module.exports = ClapNode 9 | 10 | function ClapNode (context) { 11 | var output = context.audio.createGain() 12 | var amp = context.audio.createGain() 13 | amp.gain.value = 0 14 | amp.connect(output) 15 | 16 | var obs = Triggerable(context, { 17 | tone: Param(context, 0.5), // ratio 18 | decay: Param(context, 0.5), // seconds 19 | density: Param(context, 0.1), // ratio 20 | amp: Param(context, 0.4) 21 | }, trigger) 22 | 23 | var currentParams = {} 24 | 25 | var ctor = Clappy(context.audio, currentParams) 26 | 27 | obs.context = context 28 | 29 | Apply(context, amp.gain, obs.amp) 30 | 31 | obs.connect = output.connect.bind(output) 32 | obs.disconnect = output.disconnect.bind(output) 33 | 34 | return obs 35 | 36 | // scoped 37 | function trigger (at) { 38 | // HACK: apply params 39 | currentParams.tone = obs.tone.getValueAt(at) * 128 40 | currentParams.decay = obs.decay.getValueAt(at) / 2.2 * 128 41 | currentParams.density = obs.density.getValueAt(at) * 128 42 | 43 | var choker = context.audio.createGain() 44 | var source = ctor() 45 | source.connect(choker) 46 | choker.connect(amp) 47 | source.start(at) 48 | 49 | var event = new ScheduleEvent(at, source, choker, [ 50 | choker.disconnect.bind(choker) 51 | ]) 52 | 53 | event.oneshot = true 54 | event.to = at + obs.decay.getValueAt(at) 55 | return event 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /processors/eq.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | var Property = require('observ-default') 3 | 4 | var Param = require('audio-slot-param') 5 | var Apply = require('audio-slot-param/apply') 6 | var Transform = require('audio-slot-param/transform') 7 | 8 | module.exports = EQNode 9 | 10 | function EQNode (context) { 11 | 12 | var lowshelf = context.audio.createBiquadFilter() 13 | lowshelf.type = 'lowshelf' 14 | lowshelf.frequency = 320 15 | 16 | var peaking = context.audio.createBiquadFilter() 17 | peaking.type = 'peaking' 18 | peaking.frequency = 1000 19 | peaking.Q = 0.5 20 | 21 | var highshelf = context.audio.createBiquadFilter() 22 | highshelf.type = 'highshelf' 23 | lowshelf.frequency = 3200 24 | 25 | var lowpass = context.audio.createBiquadFilter() 26 | lowpass.type = 'lowpass' 27 | 28 | var highpass = context.audio.createBiquadFilter() 29 | highpass.type = 'highpass' 30 | 31 | // chain 32 | lowshelf.connect(peaking) 33 | peaking.connect(highshelf) 34 | highshelf.connect(lowpass) 35 | lowpass.connect(highpass) 36 | 37 | var obs = Processor(context, lowshelf, highpass, { 38 | highcut: Param(context, 20000), 39 | lowcut: Param(context, 20), 40 | low: Param(context, 0), 41 | mid: Param(context, 0), 42 | high: Param(context, 0) 43 | }) 44 | 45 | Apply(context, lowpass.frequency, Transform(context, [ 46 | { param: obs.highcut, transform: clampMin20 } 47 | ])) 48 | 49 | Apply(context, highpass.frequency, Transform(context, [ 50 | { param: obs.lowcut, transform: clampMin20 } 51 | ])) 52 | 53 | Apply(context, lowshelf.gain, obs.low) 54 | Apply(context, peaking.gain, obs.mid) 55 | Apply(context, highshelf.gain, obs.high) 56 | 57 | return obs 58 | } 59 | 60 | function clampMin20 (_, val) { 61 | return Math.max(20, val) 62 | } 63 | -------------------------------------------------------------------------------- /params/chromatic-scale.js: -------------------------------------------------------------------------------- 1 | var ObservStruct = require('observ-struct') 2 | var Property = require('observ-default') 3 | 4 | var Param = require('audio-slot-param') 5 | var Transform = require('audio-slot-param/transform') 6 | 7 | module.exports = ScaleModulator 8 | 9 | var defaultScale = { 10 | offset: 0, 11 | notes: [0, 2, 4, 5, 7, 9, 11] 12 | } 13 | 14 | function ScaleModulator (context) { 15 | var obs = ObservStruct({ 16 | value: Param(context, 0), 17 | scale: Property(defaultScale) 18 | }) 19 | 20 | var transformedValue = Transform(context, [ 21 | { param: obs.value }, 22 | { param: context.offset, transform: add }, 23 | { param: obs.scale, transform: applyScale } 24 | ]) 25 | 26 | obs.onSchedule = transformedValue.onSchedule 27 | obs.getValueAt = transformedValue.getValueAt 28 | 29 | return obs 30 | } 31 | 32 | function add (base, value) { 33 | return base + value 34 | } 35 | 36 | function applyScale (base, scale) { 37 | var offset = scale && scale.offset || defaultScale.offset 38 | var notes = scale && scale.notes || defaultScale.notes 39 | 40 | var multiplier = Math.floor(base / notes.length) 41 | var scalePosition = mod(base, notes.length) 42 | var absScalePosition = Math.floor(scalePosition) 43 | var fraction = scalePosition - absScalePosition 44 | 45 | var note = notes[absScalePosition] + offset 46 | 47 | if (fraction) { 48 | var interval = getInterval(absScalePosition, notes) 49 | return note + (interval * fraction) + (multiplier * 12) 50 | } else { 51 | return note + (multiplier * 12) 52 | } 53 | } 54 | 55 | function getInterval (current, notes) { 56 | if (current >= notes.length - 1) { 57 | return 12 + notes[0] - notes[current] 58 | } else { 59 | return notes[current + 1] - notes[current] 60 | } 61 | } 62 | 63 | function mod (n, m) { 64 | return ((n % m) + m) % m 65 | } 66 | -------------------------------------------------------------------------------- /lib/drums/rim-shot.js: -------------------------------------------------------------------------------- 1 | // FROM: https://raw.githubusercontent.com/itsjoesullivan/rim-shot/master/index.js 2 | var makeDistortionCurve = require('make-distortion-curve'); 3 | var curve = makeDistortionCurve(1024); 4 | 5 | 6 | // partially informed by the rather odd http://www.kvraudio.com/forum/viewtopic.php?t=383536 7 | module.exports = function(context, parameters) { 8 | 9 | parameters = parameters || {}; 10 | parameters.tune = typeof parameters.tune === 'number' ? parameters.tune : 64; 11 | parameters.decay = typeof parameters.decay === 'number' ? parameters.decay : 64; 12 | 13 | return function() { 14 | 15 | var transpose = Math.pow(2, (parameters.tune - 64) / 1200); 16 | var max = 2.2; 17 | var min = 0.0001; 18 | var duration = (max - min) * (parameters.decay / 127) + min; 19 | 20 | var distortion = context.createWaveShaper(); 21 | distortion.curve = curve; 22 | 23 | var highpass = context.createBiquadFilter(); 24 | highpass.type = "highpass"; 25 | highpass.frequency.value = 700; 26 | 27 | distortion.connect(highpass); 28 | 29 | var oscs = [ 30 | context.createOscillator(), 31 | context.createOscillator(), 32 | ]; 33 | oscs.forEach(function(osc) { 34 | osc.type = "triangle"; 35 | }); 36 | oscs[0].frequency.value = 500 * transpose; 37 | oscs[1].frequency.value = 1720 * transpose; 38 | 39 | var gain = context.createGain(); 40 | 41 | oscs.forEach(function(osc) { 42 | osc.connect(distortion); 43 | }); 44 | highpass.connect(gain); 45 | gain.start = function(when) { 46 | oscs.forEach(function(osc) { 47 | osc.start(when); 48 | osc.stop(when + duration); 49 | }); 50 | gain.gain.setValueAtTime(0.8, when); 51 | gain.gain.exponentialRampToValueAtTime(0.00001, when + duration); 52 | } 53 | 54 | gain.stop = function (when) { 55 | oscs.forEach(function(osc) { 56 | osc.stop(when); 57 | }); 58 | } 59 | return gain; 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /processors/overdrive.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | 3 | var Param = require('audio-slot-param') 4 | var Transform = require('audio-slot-param/transform') 5 | var Apply = require('audio-slot-param/apply') 6 | 7 | module.exports = OverdriveNode 8 | 9 | var curve = generateCurve(22050) 10 | 11 | function OverdriveNode (context) { 12 | var input = context.audio.createGain() 13 | var output = context.audio.createGain() 14 | 15 | var bpWet = context.audio.createGain() 16 | var bpDry = context.audio.createGain() 17 | 18 | var bandpass = context.audio.createBiquadFilter() 19 | bandpass.type = 'bandpass' 20 | 21 | var lowpass = context.audio.createBiquadFilter() 22 | var waveshaper = context.audio.createWaveShaper() 23 | waveshaper.curve = curve 24 | 25 | input.connect(bpWet) 26 | input.connect(bpDry) 27 | 28 | bpWet.connect(bandpass) 29 | bpDry.connect(waveshaper) 30 | bandpass.connect(waveshaper) 31 | waveshaper.connect(lowpass) 32 | lowpass.connect(output) 33 | 34 | var obs = Processor(context, input, output, { 35 | preBand: Param(context, 0.5), 36 | color: Param(context, 800), 37 | postCut: Param(context, 3000), 38 | gain: Param(context, 1), 39 | amp: Param(context, 1) 40 | }) 41 | 42 | var invertedPreBand = Transform(context, [ 1, 43 | { param: obs.preBand, transform: subtract } 44 | ]) 45 | 46 | Apply(context, bpWet.gain, obs.preBand) 47 | Apply(context, bpDry.gain, invertedPreBand) 48 | Apply(context, bandpass.frequency, obs.color) 49 | Apply(context, lowpass.frequency, obs.postCut) 50 | Apply(context, input.gain, obs.gain) 51 | Apply(context, output.gain, obs.amp) 52 | 53 | return obs 54 | } 55 | 56 | function subtract (a, b) { 57 | return a - b 58 | } 59 | 60 | function generateCurve (steps) { 61 | var curve = new Float32Array(steps) 62 | var deg = Math.PI / 180 63 | 64 | for (var i = 0;i < steps;i++) { 65 | var x = i * 2 / steps - 1 66 | curve[i] = (3 + 10) * x * 20 * deg / (Math.PI + 10 * Math.abs(x)) 67 | } 68 | 69 | return curve 70 | } 71 | -------------------------------------------------------------------------------- /sources/kick.js: -------------------------------------------------------------------------------- 1 | var computed = require('../lib/computed-next-tick') 2 | var Property = require('observ-default') 3 | 4 | var Param = require('audio-slot-param') 5 | var Apply = require('audio-slot-param/apply') 6 | var Triggerable = require('../triggerable') 7 | var ScheduleEvent = require('../lib/schedule-event') 8 | 9 | var KickEight = require('../lib/drums/kick-eight') 10 | var KickNine = require('../lib/drums/kick-nine') 11 | 12 | module.exports = KickNode 13 | 14 | function KickNode (context) { 15 | var output = context.audio.createGain() 16 | var amp = context.audio.createGain() 17 | amp.gain.value = 0 18 | amp.connect(output) 19 | 20 | var obs = Triggerable(context, { 21 | type: Property('808'), 22 | tone: Param(context, 0.1), // ratio 23 | decay: Param(context, 0.5), // seconds 24 | tune: Param(context, 0), // cents 25 | amp: Param(context, 0.4) 26 | }, trigger) 27 | 28 | var currentParams = {} 29 | 30 | var getCtor = computed([obs.type], function (type) { 31 | if (type === '909') { 32 | return KickNine(context.audio, currentParams) 33 | } else { 34 | return KickEight(context.audio, currentParams) 35 | } 36 | }) 37 | 38 | obs.context = context 39 | 40 | Apply(context, amp.gain, obs.amp) 41 | 42 | obs.connect = output.connect.bind(output) 43 | obs.disconnect = output.disconnect.bind(output) 44 | 45 | return obs 46 | 47 | // scoped 48 | function trigger (at) { 49 | // HACK: apply params 50 | currentParams.tone = obs.tone.getValueAt(at) * 128 51 | currentParams.decay = obs.decay.getValueAt(at) / 2.2 * 128 52 | currentParams.tune = obs.tune.getValueAt(at) + 64 53 | 54 | var choker = context.audio.createGain() 55 | var source = getCtor()() 56 | source.connect(choker) 57 | choker.connect(amp) 58 | source.start(at) 59 | 60 | var event = new ScheduleEvent(at, source, choker, [ 61 | choker.disconnect.bind(choker) 62 | ]) 63 | 64 | event.oneshot = true 65 | event.to = at + obs.decay.getValueAt(at) 66 | return event 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /triggerable.js: -------------------------------------------------------------------------------- 1 | var ObservStruct = require('observ-struct') 2 | var Param = require('audio-slot-param') 3 | var ScheduleList = require('./lib/schedule-list') 4 | 5 | module.exports = Triggerable 6 | 7 | function Triggerable (context, params, trigger) { 8 | var scheduled = ScheduleList() 9 | 10 | var obs = ObservStruct(params) 11 | 12 | obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) 13 | 14 | obs.triggerOn = function (at) { 15 | obs.choke(at) 16 | var event = trigger(at) 17 | if (event) { 18 | scheduled.push(event) 19 | Param.triggerOn(obs, at) 20 | 21 | if (event.to) { 22 | var stopAt = event.to + obs.getReleaseDuration() 23 | stopAt = Math.min(stopAt, event.maxTo || stopAt) 24 | event.stop(stopAt) 25 | Param.triggerOff(obs, stopAt) 26 | } 27 | } 28 | } 29 | 30 | obs.triggerOff = function (at) { 31 | at = at || context.audio.currentTime 32 | scheduled.truncateTo(context.audio.currentTime) 33 | scheduled.truncateFrom(at) 34 | var event = scheduled.getLast() 35 | if (event) { 36 | if (!event.oneshot) { 37 | var stopAt = obs.getReleaseDuration() + at 38 | event.stop(stopAt) 39 | Param.triggerOff(obs, stopAt) 40 | } else { 41 | event.cancelChoke() 42 | } 43 | } 44 | } 45 | 46 | obs.cancelFrom = function (at) { 47 | scheduled.truncateTo(context.audio.currentTime) 48 | scheduled.truncateFrom(at) 49 | 50 | var event = scheduled.getLast() 51 | if (event) { 52 | Param.cancelFrom(obs, at) 53 | event.cancelChoke() 54 | } 55 | } 56 | 57 | obs.choke = function (at) { 58 | scheduled.truncateTo(context.audio.currentTime) 59 | scheduled.truncateFrom(at) 60 | 61 | var event = scheduled.getLast() 62 | if (event) { 63 | event.choke(at) 64 | } 65 | } 66 | 67 | obs.destroy = function () { 68 | Object.keys(obs).forEach(function (key) { 69 | if (obs[key] && typeof obs[key].destroy === 'function') { 70 | obs[key].destroy() 71 | } 72 | }) 73 | } 74 | 75 | return obs 76 | } 77 | -------------------------------------------------------------------------------- /sources/snare.js: -------------------------------------------------------------------------------- 1 | var computed = require('../lib/computed-next-tick') 2 | var Property = require('observ-default') 3 | 4 | var Param = require('audio-slot-param') 5 | var Apply = require('audio-slot-param/apply') 6 | var Triggerable = require('../triggerable') 7 | var ScheduleEvent = require('../lib/schedule-event') 8 | 9 | var Snare = require('../lib/drums/snare') 10 | var RimShot = require('../lib/drums/rim-shot') 11 | 12 | module.exports = SnareNode 13 | 14 | function SnareNode (context) { 15 | var output = context.audio.createGain() 16 | var amp = context.audio.createGain() 17 | amp.gain.value = 0 18 | amp.connect(output) 19 | 20 | var obs = Triggerable(context, { 21 | type: Property('snare'), 22 | tune: Param(context, 0), // cents 23 | tone: Param(context, 0.5), // ratio 24 | decay: Param(context, 0.2), // seconds 25 | snappy: Param(context, 0.5), // ratio 26 | amp: Param(context, 0.4) 27 | }, trigger) 28 | 29 | var currentParams = {} 30 | 31 | var getCtor = computed([obs.type], function (type) { 32 | if (type === 'rim') { 33 | return RimShot(context.audio, currentParams) 34 | } else { 35 | return Snare(context.audio, currentParams) 36 | } 37 | }) 38 | 39 | obs.context = context 40 | 41 | Apply(context, amp.gain, obs.amp) 42 | 43 | obs.connect = output.connect.bind(output) 44 | obs.disconnect = output.disconnect.bind(output) 45 | 46 | return obs 47 | 48 | // scoped 49 | function trigger (at) { 50 | 51 | // HACK: apply params 52 | currentParams.tune = obs.tune.getValueAt(at) + 64 53 | currentParams.tone = obs.tone.getValueAt(at) * 128 54 | currentParams.decay = obs.decay.getValueAt(at) / 2.2 * 128 55 | currentParams.snappy = obs.snappy.getValueAt(at) * 128 56 | 57 | var choker = context.audio.createGain() 58 | var source = getCtor()() 59 | source.connect(choker) 60 | choker.connect(amp) 61 | source.start(at) 62 | 63 | var event = new ScheduleEvent(at, source, choker, [ 64 | choker.disconnect.bind(choker) 65 | ]) 66 | 67 | event.oneshot = true 68 | event.to = at + obs.decay.getValueAt(at) 69 | return event 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /processors/reverb.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | var Property = require('observ-default') 3 | 4 | var Param = require('audio-slot-param') 5 | var Apply = require('audio-slot-param/apply') 6 | 7 | var buildImpulse = require('../lib/build-impulse.js') 8 | 9 | module.exports = ReverbNode 10 | 11 | function ReverbNode (context) { 12 | var input = context.audio.createGain() 13 | var output = context.audio.createGain() 14 | 15 | var convolver = context.audio.createConvolver(4) 16 | var filter = context.audio.createBiquadFilter() 17 | filter.Q.value = 0 18 | 19 | var dry = context.audio.createGain() 20 | var wet = context.audio.createGain() 21 | var building = false 22 | 23 | input.connect(dry) 24 | input.connect(convolver) 25 | 26 | convolver.connect(filter) 27 | filter.connect(wet) 28 | 29 | dry.connect(output) 30 | wet.connect(output) 31 | 32 | var obs = Processor(context, input, output, { 33 | time: Property(3), 34 | decay: Property(2), 35 | reverse: Property(false), 36 | 37 | cutoff: Param(context, 20000), 38 | filterType: Property('lowpass'), 39 | 40 | wet: Param(context, 1), 41 | dry: Param(context, 1) 42 | }, [cancel]) 43 | 44 | obs.time(refreshImpulse) 45 | obs.decay(refreshImpulse) 46 | obs.reverse(refreshImpulse) 47 | 48 | Apply(context, filter.frequency, obs.cutoff) 49 | obs.filterType(function (value) { 50 | filter.type = value 51 | }) 52 | 53 | Apply(context, wet.gain, obs.wet) 54 | Apply(context, dry.gain, obs.dry) 55 | 56 | return obs 57 | 58 | // scoped 59 | function cancel () { 60 | if (building) { 61 | buildImpulse.cancel(building) 62 | } 63 | } 64 | 65 | function refreshImpulse () { 66 | var rate = context.audio.sampleRate 67 | var length = Math.max(rate * obs.time(), 1) 68 | cancel() 69 | building = buildImpulse(length, obs.decay(), obs.reverse(), function (channels) { 70 | var impulse = context.audio.createBuffer(2, length, rate) 71 | impulse.getChannelData(0).set(channels[0]) 72 | impulse.getChannelData(1).set(channels[1]) 73 | convolver.buffer = impulse 74 | building = false 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/build-impulse.js: -------------------------------------------------------------------------------- 1 | module.exports = buildImpulse 2 | 3 | var chunkSize = 2048 4 | 5 | var queue = [] 6 | var targets = {} 7 | 8 | function buildImpulse (length, decay, reverse, cb) { 9 | var id = length + '/' + decay + '/' + reverse 10 | 11 | if (targets[id]) { 12 | targets[id].callbacks.push(cb) 13 | } else { 14 | var target = targets[id] = { 15 | id: id, 16 | callbacks: [cb], 17 | length: length, 18 | decay: decay, 19 | reverse: reverse, 20 | impulseL: new Float32Array(length), 21 | impulseR: new Float32Array(length) 22 | } 23 | queue.push([ target.id, 0, Math.min(chunkSize, length) ]) 24 | setTimeout(next, 1) 25 | } 26 | 27 | return { id: id, cb: cb } 28 | } 29 | 30 | buildImpulse.cancel = function (identifier) { 31 | if (targets[identifier.id]) { 32 | var target = targets[identifier.id] 33 | var index = target.callbacks.indexOf(identifier.cb) 34 | 35 | if (~index) { 36 | target.callbacks.splice(index, 1) 37 | } 38 | 39 | if (!target.callbacks.length) { 40 | delete targets[identifier.id] 41 | } 42 | return true 43 | } else { 44 | return false 45 | } 46 | } 47 | 48 | function next () { 49 | var item = queue.shift() 50 | if (item) { 51 | var target = targets[item[0]] 52 | if (target) { 53 | var length = target.length 54 | var decay = target.decay 55 | var reverse = target.reverse 56 | var from = item[1] 57 | var to = item[2] 58 | 59 | var impulseL = target.impulseL 60 | var impulseR = target.impulseR 61 | 62 | for (var i = from;i < to;i++) { 63 | var n = reverse ? length - i : i 64 | impulseL[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay) 65 | impulseR[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay) 66 | } 67 | 68 | if (to >= length - 1) { 69 | ;delete targets[item[0]] 70 | target.callbacks.forEach(function (cb) { 71 | cb([target.impulseL, target.impulseR]) 72 | }) 73 | } else { 74 | queue.push([ target.id, to, Math.min(to + chunkSize, length) ]) 75 | } 76 | } 77 | } 78 | 79 | if (queue.length) { 80 | setTimeout(next, 5) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /processors/delay.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | 3 | var Property = require('observ-default') 4 | 5 | var Param = require('audio-slot-param') 6 | var Transform = require('audio-slot-param/transform') 7 | var Apply = require('audio-slot-param/apply') 8 | 9 | module.exports = DelayNode 10 | 11 | function DelayNode (context) { 12 | var input = context.audio.createGain() 13 | var output = context.audio.createGain() 14 | 15 | var delay = context.audio.createDelay(4) 16 | var filter = context.audio.createBiquadFilter() 17 | 18 | var feedback = context.audio.createGain() 19 | var dry = context.audio.createGain() 20 | var wet = context.audio.createGain() 21 | 22 | // feedback loop 23 | input.connect(filter) 24 | filter.connect(delay) 25 | delay.connect(feedback) 26 | delay.connect(wet) 27 | feedback.connect(filter) 28 | 29 | input.connect(dry) 30 | dry.connect(output) 31 | wet.connect(output) 32 | 33 | var releases = [] 34 | 35 | var obs = Processor(context, input, output, { 36 | time: Param(context, 0.25), 37 | sync: Property(false), 38 | 39 | feedback: Param(context, 0.6), 40 | cutoff: Param(context, 20000), 41 | filterType: Property('lowpass'), 42 | 43 | wet: Param(context, 1), 44 | dry: Param(context, 1) 45 | }, releases) 46 | 47 | var rateMultiplier = Transform(context, [ 48 | { param: obs.sync }, 49 | { param: context.tempo, transform: getRateMultiplier } 50 | ]) 51 | 52 | // release context.tempo 53 | releases.push(rateMultiplier.destroy) 54 | 55 | var time = Transform(context, [ 56 | { param: obs.time }, 57 | { param: rateMultiplier, transform: multiply } 58 | ]) 59 | 60 | Apply(context, delay.delayTime, time) 61 | Apply(context, filter.frequency, obs.cutoff) 62 | Apply(context, feedback.gain, obs.feedback) 63 | obs.filterType(function (value) { 64 | filter.type = value 65 | }) 66 | 67 | filter.Q.value = 0 68 | 69 | Apply(context, wet.gain, obs.wet) 70 | Apply(context, dry.gain, obs.dry) 71 | 72 | return obs 73 | } 74 | 75 | function getRateMultiplier (sync, tempo) { 76 | if (sync) { 77 | return 60 / tempo 78 | } else { 79 | return 1 80 | } 81 | } 82 | 83 | function multiply (a, b) { 84 | return a * b 85 | } 86 | -------------------------------------------------------------------------------- /lib/drums/kick-eight.js: -------------------------------------------------------------------------------- 1 | // FROM: https://raw.githubusercontent.com/itsjoesullivan/kick-eight/master/index.js 2 | var NoiseBuffer = require('noise-buffer'); 3 | 4 | module.exports = function(context, parameters) { 5 | 6 | parameters = parameters || {}; 7 | parameters.tone = typeof parameters.tone === 'number' ? parameters.tone : 64; 8 | parameters.decay = typeof parameters.decay === 'number' ? parameters.decay : 64; 9 | parameters.tune = typeof parameters.tune === 'number' ? parameters.tune : 64 10 | 11 | var noiseBuffer = NoiseBuffer(1); 12 | 13 | return function() { 14 | var osc = context.createOscillator(); 15 | osc.frequency.value = 54 * Math.pow(2, (parameters.tune - 64) / 1200); 16 | var gain = context.createGain(); 17 | var oscGain = context.createGain(); 18 | oscGain.connect(gain); 19 | osc.connect(oscGain); 20 | 21 | var max = 2.2; 22 | var min = 0.09; 23 | var duration = (max - min) * (parameters.decay / 127) + min; 24 | 25 | var noise = context.createBufferSource(); 26 | noise.buffer = noiseBuffer; 27 | noise.loop = true; 28 | var noiseGain = context.createGain(); 29 | var noiseFilter = context.createBiquadFilter(); 30 | noiseFilter.type = "bandpass"; 31 | noiseFilter.frequency.value = 1380 * 2; 32 | noiseFilter.Q.value = 20; 33 | noise.connect(noiseFilter); 34 | noiseFilter.connect(noiseGain); 35 | noiseGain.connect(gain); 36 | 37 | 38 | gain.start = function(when) { 39 | if (typeof when !== 'number') { 40 | when = context.currentTime; 41 | } 42 | noise.start(when); 43 | 44 | noiseGain.gain.setValueAtTime(2 * Math.max((parameters.tone / 127), 0.0001), when); 45 | noiseGain.gain.exponentialRampToValueAtTime(0.0001, when + 0.01); 46 | noise.stop(when + duration); 47 | 48 | osc.start(when); 49 | osc.stop(when + duration); 50 | 51 | oscGain.gain.setValueAtTime(0.0001, when); 52 | oscGain.gain.exponentialRampToValueAtTime(1, when + 0.004); 53 | oscGain.gain.exponentialRampToValueAtTime(0.0001, when + duration); 54 | }; 55 | 56 | gain.stop = function(when) { 57 | if (typeof when !== 'number') { 58 | when = context.currentTime; 59 | } 60 | }; 61 | 62 | return gain; 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | audio-slot 2 | === 3 | 4 | Web Audio API FRP wrapper for creating, routing, and triggering AudioNodes. 5 | 6 | This module serves as the audio engine for [Loop Drop](https://github.com/mmckegg/loop-drop-app). 7 | 8 | [![NPM](https://nodei.co/npm/audio-slot.png)](https://nodei.co/npm/audio-slot/) 9 | 10 | ## Related modules / deps 11 | 12 | - [audio-slot-param](https://github.com/mmckegg/audio-slot-param) 13 | - [observ-node-array](https://github.com/mmckegg/observ-node-array) 14 | - [observ-struct](https://github.com/raynos/observ-struct) 15 | 16 | ## Example 17 | 18 | Create a simple monosynth: 19 | 20 | ```js 21 | var Slot = require('audio-slot') 22 | 23 | var context = { 24 | audio: new AudioContext(), 25 | nodes: { 26 | oscillator: require('audio-slot/sources/oscillator'), 27 | filter: require('audio-slot/processors/filter'), 28 | envelope: require('audio-slot/params/envelope'), 29 | lfo: require('audio-slot/params/lfo') 30 | } 31 | } 32 | 33 | var synth = Slot(context) 34 | synth.set({ 35 | sources: [ 36 | { 37 | node: 'oscillator', 38 | shape: 'sawtooth', 39 | amp: { 40 | node: 'envelope', 41 | value: 0.6, 42 | attack: 0.1, 43 | release: 1 44 | }, 45 | octave: -1, 46 | detune: { 47 | value: 0, 48 | node: 'lfo', 49 | amp: 40, 50 | rate: 5, 51 | mode: 'add' 52 | } 53 | } 54 | ], 55 | processors: [ 56 | { 57 | node: 'filter', 58 | type: 'lowpass', 59 | frequency: { 60 | node: 'envelope', 61 | value: 10000, 62 | decay: 0.6, 63 | sustain: 0.05, 64 | release: 0.1 65 | } 66 | } 67 | ] 68 | }) 69 | 70 | synth.connect(context.audio.destination) 71 | 72 | // trigger! 73 | setTimeout(function() { 74 | synth.triggerOn(1) 75 | synth.triggerOff(2) 76 | synth.triggerOn(3) 77 | synth.triggerOff(4) 78 | synth.triggerOn(5) 79 | synth.triggerOff(7) 80 | }, 0.2) 81 | 82 | ``` 83 | 84 | ## Included nodes 85 | 86 | ### Sources 87 | 88 | - oscillator 89 | - sample 90 | - granular 91 | - noise 92 | 93 | ### Processors 94 | 95 | - bitcrusher 96 | - delay 97 | - dipper 98 | - filter 99 | - freeverb 100 | - gain 101 | - overdrive 102 | - pitchshift 103 | - reverb 104 | 105 | ### Params 106 | 107 | - chromatic-scale 108 | - envelope 109 | - lfo 110 | - link-modulator 111 | - trigger-value -------------------------------------------------------------------------------- /processors/ping-pong-delay.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | 3 | var Property = require('observ-default') 4 | 5 | var Param = require('audio-slot-param') 6 | var Transform = require('audio-slot-param/transform') 7 | var Apply = require('audio-slot-param/apply') 8 | 9 | module.exports = PingPongDelayNode 10 | 11 | function PingPongDelayNode (context) { 12 | var input = context.audio.createGain() 13 | var output = context.audio.createGain() 14 | 15 | var delayL = context.audio.createDelay(4) 16 | var delayR = context.audio.createDelay(4) 17 | 18 | var filter = context.audio.createBiquadFilter() 19 | 20 | var feedback = context.audio.createGain() 21 | var dry = context.audio.createGain() 22 | var wet = context.audio.createGain() 23 | var releases = [] 24 | 25 | // feedback loop 26 | input.connect(filter) 27 | filter.connect(delayL) 28 | delayL.connect(delayR) 29 | delayR.connect(feedback) 30 | feedback.connect(filter) 31 | 32 | // wet 33 | var merger = context.audio.createChannelMerger(2) 34 | delayL.connect(merger, 0, 0) 35 | delayR.connect(merger, 0, 1) 36 | merger.connect(wet) 37 | wet.connect(output) 38 | 39 | input.connect(dry) 40 | dry.connect(output) 41 | 42 | var obs = Processor(context, input, output, { 43 | time: Param(context, 0.25), 44 | sync: Property(false), 45 | 46 | feedback: Param(context, 0.6), 47 | cutoff: Param(context, 20000), 48 | filterType: Property('lowpass'), 49 | 50 | wet: Param(context, 1), 51 | dry: Param(context, 1) 52 | }, releases) 53 | 54 | var rateMultiplier = Transform(context, [ 55 | { param: obs.sync }, 56 | { param: context.tempo, transform: getRateMultiplier } 57 | ]) 58 | 59 | // release context.tempo 60 | releases.push(rateMultiplier.destroy) 61 | 62 | var time = Transform(context, [ 63 | { param: obs.time }, 64 | { param: rateMultiplier, transform: multiply } 65 | ]) 66 | 67 | Apply(context, delayL.delayTime, time) 68 | Apply(context, delayR.delayTime, time) 69 | Apply(context, filter.frequency, obs.cutoff) 70 | Apply(context, feedback.gain, obs.feedback) 71 | obs.filterType(function (value) { 72 | filter.type = value 73 | }) 74 | 75 | filter.Q.value = 0 76 | 77 | Apply(context, wet.gain, obs.wet) 78 | Apply(context, dry.gain, obs.dry) 79 | 80 | return obs 81 | } 82 | 83 | function getRateMultiplier (sync, tempo) { 84 | if (sync) { 85 | return 60 / tempo 86 | } else { 87 | return 1 88 | } 89 | } 90 | 91 | function multiply (a, b) { 92 | return a * b 93 | } 94 | -------------------------------------------------------------------------------- /params/link-modulator.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var ObservStruct = require('observ-struct') 3 | var Event = require('geval') 4 | var setImmediate = require('setimmediate2').setImmediate 5 | 6 | var Param = require('audio-slot-param') 7 | var Transform = require('audio-slot-param/transform') 8 | 9 | module.exports = ParamModulator 10 | 11 | function ParamModulator (context) { 12 | var obs = ObservStruct({ 13 | param: Observ(), 14 | value: Param(context, 0) 15 | }) 16 | 17 | obs._type = 'ParamModulator' 18 | 19 | obs.context = context 20 | 21 | var currentParam = null 22 | 23 | var releaseSchedule = null 24 | var releaseParams = null 25 | 26 | var handleSchedule = null 27 | 28 | var eventSource = { 29 | onSchedule: Event(function (broadcast) { 30 | handleSchedule = broadcast 31 | }), 32 | getValueAt: function (at) { 33 | if (typeof currentParam === 'number') { 34 | return currentParam 35 | } else if (currentParam && currentParam.getValueAt) { 36 | return currentParam.getValueAt(at) 37 | } else { 38 | return 0 39 | } 40 | } 41 | } 42 | 43 | var transformedValue = Transform(context, [ 44 | { param: obs.value }, 45 | { param: eventSource, transform: operation } 46 | ]) 47 | 48 | obs.onSchedule = transformedValue.onSchedule 49 | obs.getValueAt = transformedValue.getValueAt 50 | 51 | if (context.paramLookup) { 52 | releaseParams = context.paramLookup(handleUpdate) 53 | } 54 | 55 | obs.param(handleUpdate) 56 | 57 | setImmediate(transformedValue.resend) 58 | 59 | obs.destroy = function () { 60 | releaseParams && releaseParams() 61 | releaseSchedule && releaseSchedule() 62 | releaseSchedule = releaseParams = null 63 | } 64 | 65 | return obs 66 | 67 | // scale 68 | 69 | function handleUpdate () { 70 | var param = context.paramLookup.get(obs.param()) 71 | if (currentParam !== param) { 72 | releaseSchedule && releaseSchedule() 73 | releaseSchedule = null 74 | if (param) { 75 | if (param.onSchedule) { 76 | releaseSchedule = param.onSchedule(handleSchedule) 77 | } else if (typeof param === 'function') { 78 | releaseSchedule = param(function (value) { 79 | handleSchedule({ 80 | value: value, 81 | at: context.audio.currentTime 82 | }) 83 | }) 84 | } 85 | } 86 | } 87 | currentParam = param 88 | } 89 | 90 | function operation (base, value) { 91 | return base + value 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /lib/drums/hi-hat.js: -------------------------------------------------------------------------------- 1 | // FROM: https://raw.githubusercontent.com/itsjoesullivan/hi-hat/master/index.js 2 | module.exports = function(context, parameters) { 3 | 4 | parameters = parameters || {}; 5 | parameters.tune = typeof parameters.tune === 'number' ? parameters.tune : 64 6 | parameters.decay = typeof parameters.decay === 'number' ? parameters.decay : 64; 7 | 8 | return function() { 9 | var audioNode = context.createGain(); 10 | 11 | var currentlyPlayingNodes = []; 12 | 13 | var max = 2.2; 14 | var min = 0.0001; 15 | var duration = (max - min) * (parameters.decay / 128) + min; 16 | var transpose = Math.pow(2, (parameters.tune - 64) / 1200); 17 | var fundamental = 40; 18 | var ratios = [2, 3, 4.16, 5.43, 6.79, 8.21]; 19 | 20 | // Highpass 21 | var highpass = context.createBiquadFilter(); 22 | highpass.type = "highpass"; 23 | highpass.frequency.value = 7000; 24 | 25 | // Bandpass 26 | var bandpass = context.createBiquadFilter(); 27 | bandpass.type = "bandpass"; 28 | bandpass.frequency.value = 10000; 29 | bandpass.connect(highpass); 30 | 31 | var gain = context.createGain(); 32 | gain.gain.value = 0; 33 | 34 | gain.connect(bandpass); 35 | highpass.connect(audioNode); 36 | 37 | // Create the oscillators 38 | var oscs = ratios.map(function(ratio) { 39 | var osc = context.createOscillator(); 40 | osc.type = "square"; 41 | // Frequency is the fundamental * this oscillator's ratio 42 | osc.frequency.value = fundamental * ratio * transpose; 43 | osc.connect(gain); 44 | return osc; 45 | }); 46 | 47 | audioNode.start = function(when) { 48 | currentlyPlayingNodes.forEach(function(node) { 49 | node.stop(when + 0.1); 50 | }); 51 | currentlyPlayingNodes = []; 52 | currentlyPlayingNodes.push(audioNode); 53 | if (typeof when !== "number") { 54 | when = context.currentTime; 55 | } 56 | oscs.forEach(function(osc) { 57 | osc.start(when); 58 | osc.stop(when + 0.01 + duration); 59 | }); 60 | // Define the volume envelope 61 | gain.gain.setValueAtTime(0.00001, when); 62 | gain.gain.exponentialRampToValueAtTime(1, when + 0.005); 63 | gain.gain.exponentialRampToValueAtTime(0.3, when + 0.03); 64 | gain.gain.exponentialRampToValueAtTime(0.00001, when + duration); 65 | gain.gain.setValueAtTime(0, when + 0.01 + duration); 66 | }; 67 | audioNode.stop = function(when) { 68 | oscs.forEach(function(osc) { 69 | osc.stop(when); 70 | }); 71 | }; 72 | return audioNode; 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/drums/kick-nine.js: -------------------------------------------------------------------------------- 1 | // FROM: https://raw.githubusercontent.com/itsjoesullivan/kick-nine/master/index.js 2 | var NoiseBuffer = require('noise-buffer'); 3 | var makeDistortionCurve = require('make-distortion-curve'); 4 | var distortionCurve = makeDistortionCurve(2) 5 | 6 | module.exports = function(context, parameters) { 7 | 8 | parameters = parameters || {}; 9 | parameters.tone = typeof parameters.tone === 'number' ? parameters.tone : 64; 10 | parameters.decay = typeof parameters.decay === 'number' ? parameters.decay : 64; 11 | parameters.tune = typeof parameters.tune === 'number' ? parameters.tune : 64 12 | 13 | var noiseBuffer = NoiseBuffer(0.2) 14 | var lastNode; 15 | 16 | return function() { 17 | var node = context.createGain(); 18 | var transpose = Math.pow(2, (parameters.tune - 64) / 1200) 19 | 20 | var osc = context.createOscillator(); 21 | osc.type = "sine"; 22 | osc.connect(node); 23 | 24 | var distortion = context.createWaveShaper(); 25 | distortion.curve = distortionCurve; 26 | distortion.oversample = '4x'; 27 | 28 | osc.connect(distortion); 29 | distortion.connect(node); 30 | 31 | var noiseSource = context.createBufferSource(); 32 | noiseSource.buffer = noiseBuffer; 33 | 34 | var noiseLowpass = context.createBiquadFilter(); 35 | noiseLowpass.type = "lowpass"; 36 | noiseLowpass.frequency.value = 1000; 37 | 38 | var max = 2.2; 39 | var min = 0.09; 40 | var duration = (max - min) * (parameters.decay / 127) + min; 41 | 42 | var noisePath = context.createGain(); 43 | noisePath.connect(node); 44 | noiseSource.connect(noiseLowpass); 45 | noiseLowpass.connect(noisePath); 46 | 47 | node.start = function(when) { 48 | if (typeof when !== 'number') { 49 | when = context.currentTime; 50 | } 51 | if (lastNode && lastNode.stop && lastNode !== node) { 52 | lastNode.stop(when); 53 | } 54 | lastNode = node; 55 | node.gain.setValueAtTime(1, when); 56 | node.gain.exponentialRampToValueAtTime(0.0001, when + duration) 57 | 58 | osc.start(when); 59 | osc.frequency.setValueAtTime(200 * transpose, when); 60 | osc.frequency.exponentialRampToValueAtTime(55 * transpose, when + 0.1); 61 | osc.stop(when + duration); 62 | 63 | noiseSource.start(when); 64 | noisePath.gain.exponentialRampToValueAtTime(2 * Math.max((parameters.tone / 127), 0.0001), when + 0.003); 65 | }; 66 | node.stop = function(when) { 67 | if (typeof when !== 'number') { 68 | when = context.currentTime; 69 | } 70 | osc.stop(when); 71 | }; 72 | return node; 73 | }; 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /processors/dipper.js: -------------------------------------------------------------------------------- 1 | var Processor = require('../processor.js') 2 | 3 | var Property = require('observ-default') 4 | var Param = require('audio-slot-param') 5 | var watch = require('observ/watch') 6 | 7 | var Apply = require('audio-slot-param/apply') 8 | 9 | module.exports = DipperNode 10 | 11 | function DipperNode (context) { 12 | var dipper = initializeMasterDipper(context.audio) 13 | var input = context.audio.createGain() 14 | var from = context.audio.createGain() 15 | var to = context.audio.createGain() 16 | var output = context.audio.createGain() 17 | 18 | input.connect(output) 19 | input.connect(to) 20 | dipper.connect(from) 21 | 22 | var obs = Processor(context, input, output, { 23 | mode: Property('modulate'), 24 | ratio: Param(context, 1) 25 | }, [ 26 | to.disconnect.bind(to), 27 | from.disconnect.bind(from) 28 | ]) 29 | 30 | watch(obs.mode, function (value) { 31 | if (value === 'source') { 32 | from.disconnect() 33 | to.connect(dipper) 34 | } else { 35 | to.disconnect() 36 | from.connect(output.gain) 37 | } 38 | }) 39 | 40 | Apply(context, to.gain, obs.ratio) 41 | Apply(context, from.gain, obs.ratio) 42 | 43 | return obs 44 | } 45 | 46 | function initializeMasterDipper (audioContext) { 47 | if (!audioContext.globalDipperProcessor) { 48 | audioContext.globalDipperProcessor = audioContext.createScriptProcessor(1024 * 2, 2, 1) 49 | var lastValue = 0 50 | var targetValue = 0 51 | 52 | audioContext.globalDipperProcessor.onaudioprocess = function (e) { 53 | var inputLength = e.inputBuffer.length 54 | var outputLength = e.inputBuffer.length 55 | var inputL = e.inputBuffer.getChannelData(0) 56 | var inputR = e.inputBuffer.getChannelData(1) 57 | var output = e.outputBuffer.getChannelData(0) 58 | 59 | targetValue = 0 60 | 61 | for (var i = 0;i < inputLength;i++) { 62 | targetValue += (Math.abs(inputL[i]) + Math.abs(inputR[i])) / 2 63 | } 64 | 65 | targetValue = (targetValue / inputLength) * 2 66 | 67 | for (var j = 0;j < outputLength;j++) { 68 | var difference = lastValue - targetValue 69 | if (difference > 0) { 70 | lastValue = lastValue - difference * 0.001 // release 71 | } else { 72 | lastValue = lastValue - difference * 0.001 // attack 73 | } 74 | output[j] = Math.max(-1, -lastValue) 75 | } 76 | 77 | } 78 | 79 | var pump = audioContext.createGain() 80 | pump.gain.value = 0 81 | pump.connect(audioContext.destination) 82 | audioContext.globalDipperProcessor.connect(pump) 83 | 84 | } 85 | return audioContext.globalDipperProcessor 86 | } 87 | -------------------------------------------------------------------------------- /params/envelope.js: -------------------------------------------------------------------------------- 1 | var ObservStruct = require('observ-struct') 2 | var Property = require('observ-default') 3 | var Event = require('geval') 4 | 5 | var Param = require('audio-slot-param') 6 | var Transform = require('audio-slot-param/transform') 7 | var setImmediate = require('setimmediate2').setImmediate 8 | 9 | module.exports = Envelope 10 | 11 | function Envelope (context) { 12 | var obs = ObservStruct({ 13 | attack: Param(context, 0), 14 | decay: Param(context, 0), 15 | release: Param(context, 0), 16 | sustain: Param(context, 1), 17 | retrigger: Property(false), 18 | value: Param(context, 1) 19 | }) 20 | 21 | var broadcast = null 22 | var eventSource = { 23 | onSchedule: Event(function (b) { 24 | broadcast = b 25 | }), 26 | getValueAt: function (at) { 27 | return 0 28 | } 29 | } 30 | 31 | var outputValue = Transform(context, [ 32 | { param: obs.value }, 33 | { param: eventSource, transform: multiply, watchingYou: true } 34 | ]) 35 | 36 | obs.getValueAt = outputValue.getValueAt 37 | obs.onSchedule = outputValue.onSchedule 38 | 39 | obs.context = context 40 | 41 | obs.triggerOn = function (at) { 42 | at = Math.max(at, context.audio.currentTime) 43 | 44 | var peakTime = at + (obs.attack() || 0.005) 45 | 46 | if (obs.attack()) { 47 | if (obs.retrigger()) { 48 | broadcast({ value: 0, at: at }) 49 | } 50 | broadcast({ 51 | value: 1, 52 | at: at, 53 | duration: obs.attack.getValueAt(at), 54 | mode: 'log' 55 | }) 56 | } else { 57 | broadcast({ value: 1, at: at }) 58 | } 59 | 60 | // decay / sustain 61 | broadcast({ 62 | value: obs.sustain.getValueAt(peakTime), 63 | at: peakTime, 64 | duration: obs.decay.getValueAt(peakTime), 65 | mode: 'log' 66 | }) 67 | } 68 | 69 | obs.triggerOff = function (at) { 70 | at = Math.max(at, context.audio.currentTime) 71 | 72 | var releaseTime = obs.release.getValueAt(at) 73 | 74 | // release 75 | if (releaseTime) { 76 | broadcast({ 77 | value: 0, at: at, 78 | duration: releaseTime, 79 | mode: 'log' 80 | }) 81 | } else { 82 | broadcast({ value: 0, at: at }) 83 | } 84 | 85 | return at + releaseTime 86 | } 87 | 88 | obs.cancelFrom = function (at) { 89 | at = Math.max(at, context.audio.currentTime) 90 | broadcast({ mode: 'cancel', at: at }) 91 | } 92 | 93 | obs.getReleaseDuration = function () { 94 | return obs.release.getValueAt(context.audio.currentTime) 95 | } 96 | 97 | setImmediate(function () { 98 | broadcast({ 99 | value: 0, 100 | at: context.audio.currentTime 101 | }) 102 | }) 103 | 104 | return obs 105 | } 106 | 107 | function multiply (a, b) { 108 | return a * b 109 | } 110 | -------------------------------------------------------------------------------- /sources/noise.js: -------------------------------------------------------------------------------- 1 | var computed = require('../lib/computed-next-tick') 2 | var ObservStruct = require('observ-struct') 3 | var Property = require('observ-default') 4 | 5 | var Param = require('audio-slot-param') 6 | var Apply = require('audio-slot-param/apply') 7 | var Triggerable = require('../triggerable') 8 | var ScheduleEvent = require('../lib/schedule-event') 9 | 10 | module.exports = NoiseNode 11 | 12 | function NoiseNode (context) { 13 | var output = context.audio.createGain() 14 | var amp = context.audio.createGain() 15 | amp.gain.value = 0 16 | amp.connect(output) 17 | 18 | var obs = Triggerable(context, { 19 | type: Property('white'), 20 | stereo: Property(false), 21 | amp: Param(context, 0.4) 22 | }, trigger) 23 | 24 | obs.resolvedBuffer = computed([obs.type, obs.stereo], function (type, stereo) { 25 | if (type === 'pink') { 26 | return generatePinkNoise(context.audio, 4096 * 4, stereo ? 2 : 1) 27 | } else { 28 | return generateWhiteNoise(context.audio, 4096 * 4, stereo ? 2 : 1) 29 | } 30 | }) 31 | 32 | obs.context = context 33 | 34 | Apply(context, amp.gain, obs.amp) 35 | 36 | obs.connect = output.connect.bind(output) 37 | obs.disconnect = output.disconnect.bind(output) 38 | 39 | return obs 40 | 41 | // scoped 42 | function trigger (at) { 43 | var buffer = obs.resolvedBuffer() 44 | 45 | if (buffer instanceof window.AudioBuffer) { 46 | var choker = context.audio.createGain() 47 | var player = context.audio.createBufferSource() 48 | player.connect(choker) 49 | choker.connect(amp) 50 | 51 | player.buffer = buffer 52 | player.loop = true 53 | player.start(at, 0) 54 | 55 | return new ScheduleEvent(at, player, choker, [ 56 | choker.disconnect.bind(choker) 57 | ]) 58 | } 59 | } 60 | } 61 | 62 | function generateWhiteNoise (audioContext, length, channels) { 63 | var buffer = audioContext.createBuffer(channels, length, audioContext.sampleRate) 64 | for (var i = 0;i < length;i++) { 65 | for (var j = 0;j < channels;j++) { 66 | buffer.getChannelData(j)[i] = Math.random() * 2 - 1 67 | } 68 | } 69 | return buffer 70 | } 71 | 72 | function generatePinkNoise (audioContext, length) { 73 | // TODO: support multichannel 74 | 75 | var b0, b1, b2, b3, b4, b5, b6 76 | b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0 77 | var buffer = audioContext.createBuffer(1, length, audioContext.sampleRate) 78 | var output = buffer.getChannelData(0) 79 | 80 | for (var i = 0;i < length;i++) { 81 | var white = Math.random() * 2 - 1 82 | b0 = 0.99886 * b0 + white * 0.0555179 83 | b1 = 0.99332 * b1 + white * 0.0750759 84 | b2 = 0.96900 * b2 + white * 0.1538520 85 | b3 = 0.86650 * b3 + white * 0.3104856 86 | b4 = 0.55000 * b4 + white * 0.5329522 87 | b5 = -0.7616 * b5 - white * 0.0168980 88 | output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362 89 | output[i] *= 0.11 // (roughly) compensate for gain 90 | b6 = white * 0.115926 91 | } 92 | 93 | return buffer 94 | } 95 | -------------------------------------------------------------------------------- /sources/oscillator.js: -------------------------------------------------------------------------------- 1 | var Triggerable = require('../triggerable') 2 | var Param = require('audio-slot-param') 3 | var Property = require('observ-default') 4 | var Transform = require('audio-slot-param/transform') 5 | var Apply = require('audio-slot-param/apply') 6 | var watch = require('observ/watch') 7 | 8 | var ScheduleEvent = require('../lib/schedule-event') 9 | 10 | module.exports = OscillatorNode 11 | 12 | function OscillatorNode (context) { 13 | var output = context.audio.createGain() 14 | var amp = context.audio.createGain() 15 | amp.gain.value = 0 16 | amp.connect(output) 17 | 18 | var obs = Triggerable(context, { 19 | amp: Param(context, 1), 20 | frequency: Param(context, 440), 21 | noteOffset: Param(context, 0), 22 | octave: Param(context, 0), 23 | detune: Param(context, 0), 24 | shape: Property('sine') // Param(context, multiplier.gain, 1) 25 | }, trigger) 26 | 27 | obs.context = context 28 | 29 | var frequency = Transform(context, [ 30 | { param: obs.frequency }, 31 | { param: obs.octave, transform: transformOctave }, 32 | { param: obs.noteOffset, transform: transformNote }, 33 | { param: context.noteOffset, transform: transformNote } 34 | ]) 35 | 36 | var powerRolloff = Transform(context, [ 37 | { param: frequency, transform: frequencyToPowerRolloff } 38 | ]) 39 | 40 | Apply(context, amp.gain, obs.amp) 41 | 42 | obs.connect = output.connect.bind(output) 43 | obs.disconnect = output.disconnect.bind(output) 44 | 45 | return obs 46 | 47 | // scoped 48 | function trigger (at) { 49 | var oscillator = context.audio.createOscillator() 50 | var power = context.audio.createGain() 51 | var choker = context.audio.createGain() 52 | oscillator.frequency.setValueAtTime(frequency.getValueAt(at), at) 53 | oscillator.start(at) 54 | oscillator.connect(power) 55 | power.connect(choker) 56 | choker.connect(amp) 57 | 58 | return new ScheduleEvent(at, oscillator, choker, [ 59 | Apply(context, oscillator.detune, obs.detune), 60 | Apply(context, oscillator.frequency, frequency), 61 | Apply(context, power.gain, powerRolloff), 62 | ApplyShape(context, oscillator, obs.shape), 63 | choker.disconnect.bind(choker) 64 | ]) 65 | } 66 | } 67 | 68 | function ApplyShape (context, target, shape) { 69 | return watch(shape, setShape.bind(this, context, target)) 70 | } 71 | 72 | function setShape (context, target, value) { 73 | if (value !== target.lastShape) { 74 | if (context.periodicWaves && context.periodicWaves[value]) { 75 | target.setPeriodicWave(context.periodicWaves[value]) 76 | } else { 77 | target.type = value 78 | } 79 | target.lastShape = value 80 | } 81 | } 82 | 83 | function transformOctave (baseFrequency, value) { 84 | return baseFrequency * Math.pow(2, value) 85 | } 86 | 87 | function transformNote (baseFrequency, value) { 88 | return baseFrequency * Math.pow(2, value / 12) 89 | } 90 | 91 | function frequencyToPowerRolloff (baseValue, value) { 92 | return 1 - ((value / 20000) || 0) 93 | } 94 | -------------------------------------------------------------------------------- /link-param.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var ObservStruct = require('observ-struct') 3 | var Prop = require('observ-default') 4 | 5 | var Param = require('audio-slot-param') 6 | var ParamProxy = require('audio-slot-param/proxy') 7 | var Transform = require('audio-slot-param/transform') 8 | 9 | module.exports = LinkParam 10 | 11 | function LinkParam (context) { 12 | var obs = ObservStruct({ 13 | param: Observ(), 14 | minValue: Param(context, 0), 15 | maxValue: Param(context, 1), 16 | mode: Prop('linear'), 17 | quantize: Prop(0) 18 | }) 19 | 20 | obs.value = ParamProxy(context, 0) 21 | obs._type = 'LinkParam' 22 | obs.context = context 23 | 24 | var updating = false 25 | var releaseParams = null 26 | var onDestroy = [] 27 | 28 | // transform: value * (maxValue - minValue) + minValue 29 | var outputValue = Transform(context, [ 30 | { param: obs.mode }, 31 | { param: obs.value, transform: applyInterpolation }, 32 | { param: Transform(context, 33 | [ { param: obs.maxValue }, 34 | { param: obs.minValue, transform: subtract } 35 | ]), transform: multiply 36 | }, 37 | { param: obs.minValue, transform: add }, 38 | { param: obs.quantize, transform: quantize } 39 | ]) 40 | 41 | obs.onSchedule = outputValue.onSchedule 42 | obs.getValueAt = outputValue.getValueAt 43 | 44 | obs.getValue = function () { 45 | return outputValue.getValueAt(context.audio.currentTime) 46 | } 47 | 48 | if (context.paramLookup) { 49 | releaseParams = context.paramLookup(handleUpdate) 50 | } 51 | 52 | if (context.active) { 53 | onDestroy.push(context.active(handleUpdate)) 54 | } 55 | 56 | obs.param(handleUpdate) 57 | 58 | obs.destroy = function () { 59 | while (onDestroy.length) { 60 | onDestroy.pop()() 61 | } 62 | releaseParams && releaseParams() 63 | releaseParams = null 64 | obs.value.destroy() 65 | } 66 | 67 | return obs 68 | 69 | // scoped 70 | 71 | function updateNow () { 72 | if (!context.active || context.active()) { 73 | var param = context.paramLookup.get(obs.param()) 74 | obs.value.setTarget(param) 75 | } else { 76 | obs.value.setTarget(null) 77 | } 78 | updating = false 79 | } 80 | 81 | function handleUpdate () { 82 | if (!updating) { 83 | updating = true 84 | setImmediate(updateNow) 85 | } 86 | } 87 | 88 | function applyInterpolation (mode, value) { 89 | if (mode === 'exp') { 90 | if (obs.minValue() < obs.maxValue()) { 91 | return value * value 92 | } else { 93 | var i = 1 - value 94 | return 1 - (i * i) 95 | } 96 | } else { // linear 97 | return value 98 | } 99 | } 100 | } 101 | 102 | function quantize (value, grid) { 103 | if (grid) { 104 | return Math.round(value * grid) / grid 105 | } else { 106 | return value 107 | } 108 | } 109 | 110 | // transform operations 111 | function add (a, b) { return a + b } 112 | function subtract (a, b) { return a - b } 113 | function multiply (a, b) { return a * b } 114 | -------------------------------------------------------------------------------- /routable.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var ObservStruct = require('observ-struct') 3 | var setImmediate = require('setimmediate2').setImmediate 4 | 5 | var Property = require('observ-default') 6 | var extend = require('xtend') 7 | 8 | module.exports = RoutableSlot 9 | 10 | function RoutableSlot (context, properties, input, output, releases) { 11 | var audioContext = context.audio 12 | 13 | output = output || input 14 | 15 | var refreshingConnections = false 16 | var connections = [] 17 | var extraConnections = [] 18 | 19 | var obs = ObservStruct(extend({ 20 | id: Observ(), 21 | output: Observ(), 22 | volume: Property(1) 23 | }, properties)) 24 | 25 | obs._type = 'RoutableSlot' 26 | obs.context = context 27 | obs.volume(function (value) { 28 | output.gain.value = value 29 | }) 30 | 31 | obs.input = input 32 | 33 | // main output 34 | obs.output(queueRefreshConnections) 35 | 36 | var removeSlotWatcher = context.slotLookup && context.slotLookup(queueRefreshConnections) 37 | 38 | obs.connect = function (destination) { 39 | output.connect(destination) 40 | extraConnections.push(destination) 41 | } 42 | 43 | obs.disconnect = function (destination) { 44 | if (destination) { 45 | remove(extraConnections, destination) 46 | output.disconnect(destination) 47 | } else { 48 | while (extraConnections.length) { 49 | output.disconnect(extraConnections.pop()) 50 | } 51 | } 52 | } 53 | 54 | obs.destroy = function () { 55 | Object.keys(obs).forEach(function (key) { 56 | if (obs[key] && typeof obs[key].destroy === 'function') { 57 | obs[key].destroy() 58 | } 59 | }) 60 | removeSlotWatcher && removeSlotWatcher() 61 | removeSlotWatcher = null 62 | } 63 | 64 | queueRefreshConnections() 65 | 66 | return obs 67 | 68 | // scoped 69 | 70 | function queueRefreshConnections () { 71 | if (!refreshingConnections) { 72 | refreshingConnections = true 73 | setImmediate(refreshConnections) 74 | } 75 | } 76 | 77 | function refreshConnections () { 78 | var outputs = [] 79 | refreshingConnections = false 80 | var outputNames = typeof obs.output() === 'string' ? [obs.output()] : obs.output() 81 | 82 | if (Array.isArray(outputNames)) { 83 | outputNames.forEach(function (name) { 84 | var destinationSlot = context.slotLookup.get(name) 85 | if (destinationSlot && destinationSlot.input) { 86 | outputs.push(destinationSlot.input) 87 | } 88 | }) 89 | } 90 | 91 | connections.forEach(function (node) { 92 | if (!~outputs.indexOf(node)) { 93 | output.disconnect(node) 94 | } 95 | }) 96 | 97 | outputs.forEach(function (node) { 98 | if (!~connections.indexOf(node)) { 99 | output.connect(node) 100 | } 101 | }) 102 | 103 | connections = outputs 104 | } 105 | } 106 | 107 | function remove (array, item) { 108 | var index = array.indexOf(item) 109 | if (~index) { 110 | array.splice(index, 1) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /sources/sample.js: -------------------------------------------------------------------------------- 1 | var Node = require('observ-node-array/single') 2 | var ResolvedValue = require('observ-node-array/resolved-value') 3 | var Param = require('audio-slot-param') 4 | var Property = require('observ-default') 5 | var Transform = require('audio-slot-param/transform') 6 | var Apply = require('audio-slot-param/apply') 7 | var watch = require('observ/watch') 8 | 9 | var Triggerable = require('../triggerable') 10 | var ScheduleEvent = require('../lib/schedule-event') 11 | 12 | module.exports = SampleNode 13 | 14 | function SampleNode (context) { 15 | var output = context.audio.createGain() 16 | var amp = context.audio.createGain() 17 | amp.gain.value = 0 18 | amp.connect(output) 19 | 20 | var obs = Triggerable(context, { 21 | mode: Property('hold'), 22 | offset: Property([0, 1]), 23 | buffer: Node(context), 24 | 25 | amp: Param(context, 1), 26 | transpose: Param(context, 0), 27 | tune: Param(context, 0) 28 | }, trigger) 29 | 30 | obs.resolvedBuffer = ResolvedValue(obs.buffer) 31 | obs.context = context 32 | 33 | var playbackRate = Transform(context, [ 1, 34 | { param: context.noteOffset, transform: noteOffsetToRate }, 35 | { param: obs.transpose, transform: noteOffsetToRate }, 36 | { param: obs.tune, transform: centsToRate } 37 | ]) 38 | 39 | Apply(context, amp.gain, obs.amp) 40 | 41 | obs.connect = output.connect.bind(output) 42 | obs.disconnect = output.disconnect.bind(output) 43 | 44 | return obs 45 | 46 | // scoped 47 | function trigger (at) { 48 | var buffer = obs.resolvedBuffer() 49 | var mode = obs.mode() 50 | 51 | if (buffer instanceof window.AudioBuffer) { 52 | var choker = context.audio.createGain() 53 | var player = context.audio.createBufferSource() 54 | player.connect(choker) 55 | choker.connect(amp) 56 | 57 | player.buffer = buffer 58 | player.loopStart = buffer.duration * obs.offset()[0] 59 | player.loopEnd = buffer.duration * obs.offset()[1] 60 | 61 | var event = new ScheduleEvent(at, player, choker, [ 62 | Apply(context, player.playbackRate, playbackRate), 63 | choker.disconnect.bind(choker) 64 | ]) 65 | 66 | event.maxTo = at + (buffer.duration - player.loopStart) / playbackRate.getValueAt(at) 67 | event.to = at + (player.loopEnd - player.loopStart) / playbackRate.getValueAt(at) 68 | 69 | if (mode === 'loop') { 70 | player.loop = true 71 | event.to = null 72 | } 73 | 74 | if (mode === 'release') { 75 | event.to = null 76 | event.stop = function (at) { 77 | if (at) { 78 | player.start(at, player.loopStart, (player.loopEnd - player.loopStart) / playbackRate.getValueAt(at)) 79 | } 80 | } 81 | } else { 82 | player.start(at, player.loopStart) 83 | } 84 | 85 | if (mode === 'oneshot') { 86 | event.oneshot = true 87 | } 88 | 89 | return event 90 | } 91 | } 92 | } 93 | 94 | function noteOffsetToRate (baseRate, value) { 95 | return baseRate * Math.pow(2, value / 12) 96 | } 97 | 98 | function centsToRate (baseRate, value) { 99 | return baseRate * Math.pow(2, value / 1200) 100 | } 101 | -------------------------------------------------------------------------------- /lib/granular-sync.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var Property = require('observ-default') 3 | 4 | module.exports = function (duration, offset, buffer) { 5 | var obs = Property(false) 6 | var refreshing = false 7 | var lastBuffer = null 8 | 9 | obs.beats = Observ() 10 | obs.trim = Observ() 11 | obs.beatOffset = Observ() 12 | obs.tempo = Observ() 13 | 14 | var setBeats = obs.beats.set 15 | var setTrim = obs.trim.set 16 | var setBeatOffset = obs.beatOffset.set 17 | var setTempo = obs.tempo.set 18 | 19 | obs.beats.set = function (value) { 20 | if (buffer()) { 21 | value = Math.max(1 / 32, value) 22 | var beats = (obs.tempo() / 60) * buffer().duration 23 | var length = value / beats 24 | offset.set([offset()[0], offset()[0] + length]) 25 | duration.set(value) 26 | } 27 | } 28 | 29 | obs.trim.set = function (value) { 30 | if (buffer()) { 31 | value = obs.beatOffset() + Math.max(0, Math.min(value, 0.99999)) 32 | var beats = (obs.tempo() / 60) * buffer().duration 33 | var pos = value / beats 34 | var diff = offset()[1] - offset()[0] 35 | offset.set([pos, pos + diff]) 36 | } 37 | } 38 | 39 | obs.beatOffset.set = function (value) { 40 | if (buffer()) { 41 | value = obs.trim() + Math.max(0, value) 42 | var beats = (obs.tempo() / 60) * buffer().duration 43 | var pos = value / beats 44 | var diff = offset()[1] - offset()[0] 45 | offset.set([pos, pos + diff]) 46 | } 47 | } 48 | 49 | obs.tempo.set = function (value) { 50 | if (buffer()) { 51 | var originalDuration = getOffsetDuration(buffer().duration, offset()) 52 | duration.set(value / 60 * originalDuration) 53 | } 54 | } 55 | 56 | duration(refresh) 57 | offset(refresh) 58 | buffer(refresh) 59 | 60 | return obs 61 | 62 | function refresh () { 63 | if (!refreshing) { 64 | refreshing = true 65 | setImmediate(refreshNow) 66 | } 67 | } 68 | 69 | function refreshNow () { 70 | refreshing = false 71 | if (buffer()) { 72 | var originalDuration = getOffsetDuration(buffer().duration, offset()) 73 | var tempo = duration() / originalDuration * 60 74 | var fullBeats = (tempo / 60) * buffer().duration 75 | var beats = getOffsetDuration(fullBeats, offset()) 76 | 77 | var fullOffset = Math.round(fullBeats * offset()[0] * 10000) / 10000 78 | var beatOffset = Math.floor(fullOffset) 79 | var trim = fullOffset - beatOffset 80 | 81 | if (tempo !== obs.tempo()) { 82 | setTempo(tempo) 83 | } 84 | 85 | if (beatOffset !== obs.beatOffset()) { 86 | setBeatOffset(beatOffset) 87 | } 88 | 89 | if (trim !== obs.trim()) { 90 | setTrim(trim) 91 | } 92 | 93 | if (beats !== obs.beats()) { 94 | setBeats(beats) 95 | } 96 | 97 | if (lastBuffer !== buffer()) { 98 | // bump refresh 99 | lastBuffer = buffer() 100 | if (obs()) { 101 | obs.set(true) 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | function getOffsetDuration (duration, offset) { 109 | return (offset[1] * duration) - (offset[0] * duration) 110 | } 111 | -------------------------------------------------------------------------------- /lib/drums/snare.js: -------------------------------------------------------------------------------- 1 | // FROM: https://raw.githubusercontent.com/itsjoesullivan/snare/master/index.js 2 | var NoiseBuffer = require('noise-buffer'); 3 | var noiseBuffer = NoiseBuffer(1); 4 | 5 | module.exports = function(context, parameters) { 6 | 7 | parameters = parameters || {}; 8 | parameters.tune = typeof parameters.tune === 'number' ? parameters.tune : 64 9 | parameters.tone = typeof parameters.tone === 'number' ? parameters.tone : 64 10 | parameters.snappy = typeof parameters.snappy === 'number' ? parameters.snappy : 64; 11 | parameters.decay = typeof parameters.decay === 'number' ? parameters.decay : 64; 12 | 13 | 14 | return function() { 15 | var transpose = Math.pow(2, (parameters.tune - 64) / 1200); 16 | 17 | var audioNode = context.createGain(); 18 | var masterBus = context.createGain(); 19 | masterBus.gain.value = 0.4; 20 | var masterHighBump = context.createBiquadFilter(); 21 | masterHighBump.type = "peaking"; 22 | var masterLowBump = context.createBiquadFilter(); 23 | masterLowBump.type = "peaking"; 24 | masterBus.connect(masterHighBump); 25 | masterHighBump.connect(masterLowBump); 26 | masterLowBump.connect(audioNode); 27 | masterHighBump.frequency.value = 4000; 28 | masterLowBump.frequency.value = 200; 29 | masterHighBump.gain.value = 6; 30 | masterLowBump.gain.value = 12; 31 | 32 | var noise = context.createBufferSource(); 33 | noise.buffer = noiseBuffer; 34 | noise.loop = true; 35 | 36 | var noiseGain = context.createGain(); 37 | var noiseHighpass = context.createBiquadFilter(); 38 | noiseHighpass.type = "highpass"; 39 | noise.connect(noiseGain); 40 | noiseGain.connect(noiseHighpass); 41 | noiseHighpass.connect(masterBus); 42 | noiseHighpass.frequency.value = 1200; 43 | 44 | var oscsGain = context.createGain(); 45 | var oscsHighpass = context.createBiquadFilter(); 46 | oscsGain.connect(oscsHighpass); 47 | oscsHighpass.type = "highpass"; 48 | oscsHighpass.frequency.value = 400; 49 | oscsHighpass.connect(masterBus); 50 | 51 | var max = 2.2; 52 | var min = 0.09; 53 | var duration = (max - min) * (parameters.decay / 127) + min; 54 | 55 | var oscs = [87.307, 329.628].map(function(frequency) { 56 | var osc = context.createOscillator(); 57 | osc.frequency.value = frequency * transpose; 58 | osc.connect(oscsGain); 59 | return osc; 60 | }); 61 | 62 | audioNode.start = function(when) { 63 | if (typeof when !== "number") { 64 | when = context.currentTime; 65 | } 66 | 67 | noiseGain.gain.setValueAtTime(0.00001, when); 68 | noiseGain.gain.exponentialRampToValueAtTime(Math.max(0.000001, parameters.snappy / 127), when + 0.005); 69 | noiseGain.gain.exponentialRampToValueAtTime(0.00001, when + 0.1 + duration); 70 | noise.start(when); 71 | 72 | oscsGain.gain.setValueAtTime(0.00001, when); 73 | oscsGain.gain.exponentialRampToValueAtTime(2 * Math.max((parameters.tone / 127), 0.0001), when + 0.005); 74 | oscsGain.gain.exponentialRampToValueAtTime(0.00001, when + duration); 75 | 76 | oscs.forEach(function(osc) { 77 | osc.start(when); 78 | }); 79 | }; 80 | audioNode.stop = function(when) { 81 | noise.stop(when); 82 | }; 83 | return audioNode; 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /lib/drums/clappy.js: -------------------------------------------------------------------------------- 1 | // FROM: https://raw.githubusercontent.com/itsjoesullivan/clappy/master/index.js 2 | var NoiseBuffer = require('noise-buffer'); 3 | var buffer = NoiseBuffer(1); 4 | 5 | module.exports = function(context, parameters) { 6 | 7 | parameters = parameters || {}; 8 | parameters.decay = typeof parameters.decay === 'number' ? parameters.decay : 64; 9 | parameters.tone = typeof parameters.tone === 'number' ? parameters.tone : 64 10 | parameters.density = typeof parameters.density === 'number' ? parameters.density : 20 11 | 12 | return function() { 13 | var transpose = Math.pow(2, (parameters.tone - 64) / 32); 14 | var max = 2.2; 15 | var min = 0.0001; 16 | var duration = (max - min) * (parameters.decay / 128) + min; 17 | var maxCycles = 10 18 | var minCycles = 1 19 | var cycles = (maxCycles - minCycles) * (parameters.density / 128) + minCycles; 20 | var clapFrequency = 80 * transpose; 21 | var clapLength = cycles / clapFrequency; 22 | 23 | var bandpass = context.createBiquadFilter(); 24 | bandpass.type = "bandpass"; 25 | bandpass.frequency.value = 800 * transpose; 26 | bandpass.Q.value = 0.7; 27 | 28 | var highpass = context.createBiquadFilter(); 29 | highpass.type = "highpass"; 30 | highpass.frequency.value = 600; 31 | bandpass.connect(highpass); 32 | 33 | /* 34 | All this does is feed white noise 35 | through two paths: one short burst 36 | that goes through an envelope mod- 37 | ified by an LFO at 80hz, simulat- 38 | ing a few rapid claps, and a long- 39 | er burst that simulates the rever- 40 | beration of the claps through the 41 | room. 42 | */ 43 | 44 | 45 | var audioNode = context.createGain(); 46 | 47 | var noise = context.createBufferSource(); 48 | noise.buffer = buffer; 49 | noise.loop = true; 50 | 51 | var clapDryEnvelope = context.createGain(); 52 | clapDryEnvelope.gain.value = 0; 53 | 54 | var clapDecayEnvelope = context.createGain(); 55 | clapDecayEnvelope.gain.value = 0; 56 | 57 | var lfoCarrier = context.createGain(); 58 | var lfo = context.createOscillator(); 59 | lfo.type = "sawtooth"; 60 | lfo.frequency.value = -clapFrequency; 61 | lfo.connect(lfoCarrier.gain); 62 | 63 | 64 | noise.connect(clapDryEnvelope); 65 | clapDryEnvelope.connect(lfoCarrier); 66 | lfoCarrier.connect(bandpass); 67 | 68 | noise.connect(clapDecayEnvelope); 69 | clapDecayEnvelope.connect(bandpass); 70 | 71 | highpass.connect(audioNode); 72 | 73 | audioNode.start = function(when) { 74 | 75 | clapDryEnvelope.gain.setValueAtTime(0.0001, when); 76 | clapDryEnvelope.gain.exponentialRampToValueAtTime(1, when + 0.001); 77 | clapDryEnvelope.gain.linearRampToValueAtTime(1, when + clapLength); 78 | clapDryEnvelope.gain.exponentialRampToValueAtTime(0.000000001, when + clapLength + 0.01); 79 | clapDryEnvelope.gain.setValueAtTime(0, when + clapLength + 0.02); 80 | 81 | clapDecayEnvelope.gain.setValueAtTime(0.0001, when); 82 | clapDecayEnvelope.gain.setValueAtTime(0.0001, when + clapLength); 83 | clapDecayEnvelope.gain.exponentialRampToValueAtTime(1, when + clapLength + 0.001); 84 | clapDecayEnvelope.gain.exponentialRampToValueAtTime(0.2, when + 0.1); 85 | clapDecayEnvelope.gain.exponentialRampToValueAtTime(0.000000001, when + duration); 86 | clapDecayEnvelope.gain.setValueAtTime(0, when + duration + 0.01); 87 | 88 | lfo.start(when); 89 | lfo.stop(when + duration); 90 | noise.start(when, Math.random() * noise.buffer.duration); 91 | noise.stop(when + duration); 92 | audioNode.gain.setValueAtTime(0, when + duration); 93 | }; 94 | 95 | audioNode.stop = function(when) { 96 | try { 97 | lfo.stop(when); 98 | } catch(e) { 99 | // likely already stopped 100 | } 101 | try { 102 | noise.stop(when); 103 | } catch(e) { 104 | // likely already stopped 105 | } 106 | }; 107 | 108 | return audioNode; 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var ObservStruct = require('observ-struct') 3 | var NodeArray = require('observ-node-array') 4 | var setImmediate = require('setimmediate2').setImmediate 5 | 6 | var Param = require('audio-slot-param') 7 | var Property = require('observ-default') 8 | var RoutableSlot = require('./routable') 9 | 10 | module.exports = AudioSlot 11 | 12 | function AudioSlot (parentContext, defaultValue) { 13 | var context = Object.create(parentContext) 14 | var audioContext = context.audio 15 | 16 | var input = audioContext.createGain() 17 | var pre = audioContext.createGain() 18 | var output = audioContext.createGain() 19 | 20 | var toProcessors = audioContext.createGain() 21 | var post = audioContext.createGain() 22 | 23 | var initialized = false 24 | var queue = [] 25 | 26 | input.connect(pre) 27 | pre.connect(toProcessors) 28 | toProcessors.connect(post) 29 | post.connect(output) 30 | 31 | var obs = RoutableSlot(context, { 32 | id: Observ(), 33 | sources: NodeArray(context), 34 | processors: NodeArray(context), 35 | noteOffset: Param(context, 0), 36 | output: Observ(), 37 | volume: Property(1) 38 | }, input, output) 39 | 40 | obs._type = 'AudioSlot' 41 | context.noteOffset = obs.noteOffset 42 | context.slot = obs 43 | 44 | // reconnect sources on add / update 45 | var connectedSources = [] 46 | obs.sources.onUpdate(function (diff) { 47 | while (connectedSources.length) { 48 | connectedSources.pop().disconnect() 49 | } 50 | obs.sources.forEach(function (source) { 51 | source.connect(pre) 52 | connectedSources.push(source) 53 | }) 54 | }) 55 | 56 | // reconnect processors on add / update 57 | var connectedProcessors = [ toProcessors ] 58 | var updatingProcessors = false 59 | 60 | obs.processors.onUpdate(function (diff) { 61 | if (!updatingProcessors) { 62 | setImmediate(updateProcessors) 63 | } 64 | updatingProcessors = true 65 | }) 66 | 67 | obs.triggerOn = function (at) { 68 | 69 | if (!initialized) { 70 | queue.push(function () { 71 | obs.triggerOn(at) 72 | }) 73 | return false 74 | } 75 | 76 | var offTime = null 77 | 78 | obs.sources.forEach(function (source) { 79 | var time = source.triggerOn(at) 80 | if (time && (!offTime || time > offTime)) { 81 | offTime = time 82 | } 83 | }) 84 | 85 | // for processor modulators 86 | obs.processors.forEach(function (processor) { 87 | var time = processor && processor.triggerOn(at) 88 | if (time && (!offTime || time > offTime)) { 89 | offTime = time 90 | } 91 | }) 92 | 93 | if (offTime) { 94 | obs.triggerOff(offTime) 95 | } 96 | } 97 | 98 | obs.triggerOff = function (at) { 99 | 100 | if (!initialized) { 101 | queue.push(function () { 102 | obs.triggerOff(at) 103 | }) 104 | return false 105 | } 106 | 107 | var maxProcessorDuration = 0 108 | var maxSourceDuration = 0 109 | 110 | var offEvents = [] 111 | 112 | obs.sources.forEach(function (source) { 113 | var releaseDuration = source.getReleaseDuration && source.getReleaseDuration() || 0 114 | if (releaseDuration > maxSourceDuration) { 115 | maxSourceDuration = releaseDuration 116 | } 117 | 118 | offEvents.push([source, releaseDuration]) 119 | }) 120 | 121 | obs.processors.forEach(function (processor) { 122 | var releaseDuration = processor.getReleaseDuration && processor.getReleaseDuration() || 0 123 | offEvents.push([processor, releaseDuration, true]) 124 | if (releaseDuration > maxProcessorDuration) { 125 | maxProcessorDuration = releaseDuration 126 | } 127 | }) 128 | 129 | var difference = maxProcessorDuration - maxSourceDuration 130 | var maxDuration = Math.max(maxSourceDuration, maxProcessorDuration) 131 | 132 | offEvents.forEach(function (event) { 133 | var target = event[0] 134 | var releaseDuration = event[1] 135 | 136 | if (event[2]) { 137 | target.triggerOff(at + maxDuration - releaseDuration) 138 | } else { 139 | target.triggerOff(at + Math.max(0, difference)) 140 | } 141 | }) 142 | } 143 | 144 | obs.cancelFrom = function (at) { 145 | obs.sources.forEach(function (source) { 146 | source.cancelFrom && source.cancelFrom(at) 147 | }) 148 | } 149 | 150 | obs.choke = function (at) { 151 | obs.sources.forEach(function (source) { 152 | source.choke && source.choke(at) 153 | }) 154 | } 155 | 156 | if (defaultValue) { 157 | obs.set(defaultValue) 158 | } 159 | 160 | setImmediate(function () { 161 | initialized = true 162 | while (queue.length) { 163 | queue.shift()() 164 | } 165 | }) 166 | 167 | return obs 168 | 169 | // scoped 170 | 171 | function updateProcessors () { 172 | if (checkProcessorsChanged()) { 173 | toProcessors.disconnect() 174 | while (connectedProcessors.length) { 175 | connectedProcessors.pop().disconnect() 176 | } 177 | 178 | var lastProcessor = toProcessors 179 | obs.processors.forEach(function (processor) { 180 | if (processor) { 181 | lastProcessor.connect(processor.input) 182 | lastProcessor = processor 183 | } 184 | connectedProcessors.push(processor) 185 | }) 186 | 187 | lastProcessor.connect(post) 188 | } 189 | 190 | updatingProcessors = false 191 | 192 | } 193 | 194 | function checkProcessorsChanged () { 195 | if (connectedProcessors.length !== obs.processors.getLength()) { 196 | return true 197 | } else { 198 | for (var i = 0;i < connectedProcessors.length;i++) { 199 | if (connectedProcessors[i] !== obs.processors.get(i)) { 200 | return true 201 | } 202 | } 203 | } 204 | 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /params/lfo.js: -------------------------------------------------------------------------------- 1 | var Observ = require('observ') 2 | var ObservStruct = require('observ-struct') 3 | var Property = require('observ-default') 4 | var Event = require('geval') 5 | 6 | var Param = require('audio-slot-param') 7 | var Transform = require('audio-slot-param/transform') 8 | var applyScheduler = require('../lib/apply-scheduler') 9 | 10 | module.exports = LFO 11 | 12 | function LFO (context) { 13 | var releaseSchedule = applyScheduler(context, handleSchedule) 14 | var active = [] 15 | var scheduledTo = 0 16 | var lastBeatDuration = 1 17 | 18 | var free = { 19 | start: context.audio.currentTime, 20 | nextTime: context.audio.currentTime 21 | } 22 | 23 | var obs = ObservStruct({ 24 | mode: Property('multiply'), 25 | sync: Property(false), 26 | trigger: Property(true), 27 | 28 | phaseOffset: Observ(), 29 | rate: Param(context, 1), 30 | amp: Param(context, 1), 31 | value: Param(context, 1), 32 | 33 | curve: Param(context, 1), 34 | skew: Param(context, 0) 35 | }) 36 | 37 | obs.trigger(function (value) { 38 | if (!value) { 39 | free.nextTime = context.audio.currentTime 40 | } 41 | }) 42 | 43 | obs.context = context 44 | 45 | var broadcast = null 46 | var eventSource = { 47 | onSchedule: Event(function (b) { 48 | broadcast = b 49 | }), 50 | 51 | getValueAt: function (at) { 52 | return 0 53 | } 54 | } 55 | 56 | var transform = Transform(context, [ 57 | { param: eventSource, transform: offsetForOperation }, 58 | { param: obs.amp, transform: multiply }, 59 | { param: obs.value, transform: operation } 60 | ]) 61 | 62 | obs.getValueAt = transform.getValueAt 63 | obs.onSchedule = transform.onSchedule 64 | obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) 65 | 66 | obs.triggerOn = function (at) { 67 | if (obs.trigger()) { 68 | at = at || context.audio.currentTime 69 | 70 | var event = { 71 | start: at, 72 | end: null, 73 | nextTime: at 74 | } 75 | 76 | truncate(at) 77 | 78 | Param.triggerOn(obs, at) 79 | 80 | active.push(event) 81 | 82 | broadcast({ 83 | at: at, 84 | value: 0 85 | }) 86 | 87 | if (at < scheduledTo) { 88 | scheduleEvent(event, at, scheduledTo, lastBeatDuration) 89 | } 90 | } 91 | } 92 | 93 | obs.triggerOff = function (at) { 94 | at = at || context.audio.currentTime 95 | var event = eventAt(at) 96 | if (event) { 97 | var stopAt = obs.getReleaseDuration() + at 98 | Param.triggerOff(obs, stopAt) 99 | truncate(stopAt) 100 | 101 | broadcast({ 102 | at: stopAt, 103 | value: 0 104 | }) 105 | 106 | event.end = stopAt 107 | } 108 | } 109 | 110 | obs.destroy = function () { 111 | releaseSchedule && releaseSchedule() 112 | releaseSchedule = null 113 | } 114 | 115 | return obs 116 | 117 | // scoped 118 | 119 | function handleSchedule (schedule) { 120 | var from = schedule.time 121 | var to = schedule.time + schedule.duration 122 | for (var i = active.length - 1;i >= 0;i--) { 123 | var event = active[i] 124 | 125 | // clean up old events 126 | if (event.end && event.end < context.audio.currentTime) { 127 | active.splice(i, 1) 128 | continue 129 | } 130 | 131 | scheduleEvent(event, from, to, schedule.beatDuration) 132 | } 133 | 134 | if (!obs.trigger() && (!context.active || context.active())) { 135 | scheduleEvent(free, from, to, schedule.beatDuration) 136 | } 137 | 138 | lastBeatDuration = schedule.beatDuration 139 | scheduledTo = to 140 | } 141 | 142 | function scheduleEvent (event, from, to, beatDuration) { 143 | if (event.nextTime < from) { 144 | event.nextTime = from 145 | } 146 | 147 | if (event.start <= to && (!event.end || event.end > from)) { 148 | var rate = obs.rate.getValueAt(from) 149 | 150 | if (obs.sync()) { 151 | rate = rate / beatDuration 152 | } 153 | 154 | var duration = 1 / rate 155 | 156 | while (event.nextTime < to) { 157 | step(event.nextTime, duration) 158 | event.nextTime += duration 159 | if (obs.mode() !== 'oneshot') { 160 | event.nextOffset = event.nextOffset % 1 161 | } 162 | } 163 | } 164 | } 165 | 166 | function step (start, duration) { 167 | var skew = clamp((obs.skew.getValueAt(start) + 1), 0, 1.9999999999) 168 | var curve = clamp(obs.curve.getValueAt(start), 0, 1) 169 | var stepDuration = duration / 4 170 | var up = stepDuration * skew * curve 171 | var pause = (stepDuration - curve * stepDuration) * 2 172 | var down = stepDuration * (2 - skew) * curve 173 | 174 | broadcast({ 175 | at: start, 176 | value: 1, 177 | duration: up 178 | }) 179 | 180 | broadcast({ 181 | at: start + up + pause, 182 | value: 0, 183 | duration: down 184 | }) 185 | 186 | broadcast({ 187 | at: start + up + pause + down, 188 | value: -1, 189 | duration: down 190 | }) 191 | 192 | broadcast({ 193 | at: start + up + pause + down + down + pause, 194 | value: 0, 195 | duration: up 196 | }) 197 | } 198 | 199 | function offsetForOperation (_, value) { 200 | var mode = obs.mode() 201 | if (mode === 'add') { 202 | return value 203 | } else if (mode === 'subtract') { 204 | return value 205 | } else { 206 | return (value + 1) / 2 207 | } 208 | } 209 | 210 | function operation (base, value) { 211 | var mode = obs.mode() 212 | if (mode === 'add') { 213 | return base + value 214 | } else if (mode === 'subtract') { 215 | return value - base 216 | } else { 217 | return base * value 218 | } 219 | } 220 | 221 | function truncate (at) { 222 | for (var i = active.length - 1;i >= 0;i--) { 223 | if (active[i].start >= at) { 224 | active.splice(i, 1) 225 | } else if (active[i].end && active[i].end > at) { 226 | active[i].end = at 227 | } 228 | } 229 | } 230 | 231 | function eventAt (time) { 232 | for (var i = 0;i < active.length;i++) { 233 | if (active[i].start <= time && (!active[i].end || active[i].end > time)) { 234 | return active[i] 235 | } 236 | } 237 | } 238 | } 239 | 240 | function clamp (value, min, max) { 241 | return Math.min(max, Math.max(min, value)) 242 | } 243 | 244 | function multiply (a, b) { 245 | return a * b 246 | } 247 | -------------------------------------------------------------------------------- /sources/granular.js: -------------------------------------------------------------------------------- 1 | var Node = require('observ-node-array/single') 2 | var ResolvedValue = require('observ-node-array/resolved-value') 3 | var Param = require('audio-slot-param') 4 | var Property = require('observ-default') 5 | var Transform = require('audio-slot-param/transform') 6 | var Apply = require('audio-slot-param/apply') 7 | 8 | var Triggerable = require('../triggerable') 9 | var ScheduleList = require('../lib/schedule-list') 10 | var ScheduleEvent = require('../lib/schedule-event') 11 | var SyncProperty = require('../lib/granular-sync') 12 | 13 | module.exports = GranularNode 14 | 15 | function GranularNode (context) { 16 | var output = context.audio.createGain() 17 | var amp = context.audio.createGain() 18 | amp.gain.value = 0 19 | amp.connect(output) 20 | 21 | var offset = Property([0, 1]) 22 | var buffer = Node(context) 23 | var resolvedBuffer = ResolvedValue(buffer) 24 | var duration = Property(1) 25 | var sync = SyncProperty(duration, offset, resolvedBuffer) 26 | 27 | var obs = Triggerable(context, { 28 | mode: Property('loop'), 29 | 30 | sync: sync, 31 | offset: offset, 32 | buffer: buffer, 33 | duration: duration, 34 | 35 | rate: Property(8), 36 | 37 | attack: Property(0.1), 38 | hold: Property(1), 39 | release: Property(0.1), 40 | 41 | transpose: Param(context, 0), 42 | tune: Param(context, 0), 43 | amp: Param(context, 1) 44 | }, trigger) 45 | 46 | obs.resolvedBuffer = resolvedBuffer 47 | obs.context = context 48 | 49 | var playbackRate = Transform(context, [ 1, 50 | { param: context.noteOffset, transform: noteOffsetToRate }, 51 | { param: obs.transpose, transform: noteOffsetToRate }, 52 | { param: obs.tune, transform: centsToRate } 53 | ]) 54 | 55 | Apply(context, amp.gain, obs.amp) 56 | 57 | obs.connect = output.connect.bind(output) 58 | obs.disconnect = output.disconnect.bind(output) 59 | 60 | return obs 61 | 62 | // scoped 63 | function trigger (at) { 64 | return new GranularSample(obs, amp, playbackRate, at) 65 | } 66 | } 67 | 68 | function noteOffsetToRate (baseRate, value) { 69 | return baseRate * Math.pow(2, value / 12) 70 | } 71 | 72 | function centsToRate (baseRate, value) { 73 | return baseRate * Math.pow(2, value / 1200) 74 | } 75 | 76 | // internal class 77 | 78 | function GranularSample (obs, output, playbackRate, from) { 79 | var clock = obs.context.scheduler 80 | var nextTime = clock.getNextScheduleTime() 81 | var schedule = { 82 | time: from, 83 | duration: nextTime - from, 84 | beatDuration: clock.getBeatDuration() 85 | } 86 | 87 | var length = obs.duration() 88 | if (obs.sync()) { 89 | length = length * schedule.beatDuration 90 | } 91 | 92 | this.context = obs.context 93 | this.obs = obs 94 | this.from = from 95 | this.to = NaN 96 | this.nextTime = from 97 | this.nextOffset = 0 98 | this.choker = obs.context.audio.createGain() 99 | this.choked = false 100 | this.oneshot = obs.mode() === 'oneshot' 101 | this.events = ScheduleList() 102 | this.releases = [this.events.destroy] 103 | this.playbackRate = playbackRate 104 | 105 | if (this.oneshot) { 106 | this.to = from + length 107 | } 108 | 109 | this.choker.connect(output) 110 | 111 | if (handleSchedule.call(this, schedule)) { 112 | this.releases.push(clock.onSchedule(handleSchedule.bind(this))) 113 | } 114 | } 115 | 116 | GranularSample.prototype.choke = function (at) { 117 | if (!this.to || at < this.to) { 118 | this.choker.gain.cancelScheduledValues(this.choker.context.currentTime) 119 | this.choker.gain.setTargetAtTime(0, at, 0.02) 120 | this.choked = true 121 | this.to = at + 0.1 122 | } 123 | } 124 | 125 | GranularSample.prototype.cancelChoke = function (at) { 126 | if (this.choked && this.stopAt) { 127 | this.choker.gain.cancelScheduledValues(this.to - 0.1) 128 | this.stop(this.stopAt) 129 | } 130 | } 131 | 132 | GranularSample.prototype.stop = function (at) { 133 | at = at || this.choker.context.currentTime 134 | this.events.truncateTo(this.context.audio.currentTime) 135 | this.choker.gain.cancelScheduledValues(this.choker.context.currentTime) 136 | this.choker.gain.setValueAtTime(0, at) 137 | this.choked = false 138 | this.stopAt = at 139 | this.to = at 140 | } 141 | 142 | function handleSchedule (schedule) { 143 | var obs = this.obs 144 | var endTime = schedule.time + schedule.duration 145 | 146 | this.events.truncateTo(this.context.audio.currentTime) 147 | if (endTime >= this.from && (!this.to || schedule.time < this.to)) { 148 | var length = obs.duration() 149 | var rate = obs.rate() 150 | 151 | if (obs.sync()) { 152 | length = length * schedule.beatDuration 153 | rate = rate / schedule.beatDuration 154 | } 155 | 156 | var slices = Math.max(1, rate) * length 157 | var duration = length / slices 158 | 159 | while (this.nextTime < endTime) { 160 | var event = play.call(this, this.nextTime, this.nextOffset, duration) 161 | if (event) { 162 | this.events.push(event) 163 | } 164 | this.nextTime += duration 165 | this.nextOffset += 1 / slices 166 | if (obs.mode() !== 'oneshot') { 167 | this.nextOffset = this.nextOffset % 1 168 | } 169 | } 170 | } 171 | 172 | if (!this.to || this.to > endTime) { 173 | return true 174 | } 175 | } 176 | 177 | function play (at, startOffset, grainDuration) { 178 | var obs = this.obs 179 | var context = this.context 180 | var buffer = obs.resolvedBuffer() 181 | if (buffer instanceof window.AudioBuffer && isFinite(startOffset) && grainDuration) { 182 | var source = context.audio.createBufferSource() 183 | source.buffer = buffer 184 | 185 | var offset = obs.offset() 186 | var start = offset[0] * source.buffer.duration 187 | var end = offset[1] * source.buffer.duration 188 | var duration = end - start 189 | 190 | var release = grainDuration * obs.release() 191 | var attack = grainDuration * obs.attack() 192 | 193 | // make sure it doesn't exceed the stop time 194 | var maxTime = (this.to || Infinity) - release 195 | var releaseAt = Math.min(at + grainDuration * obs.hold(), maxTime) 196 | 197 | source.playbackRate.value = this.playbackRate.getValueAt(at) 198 | 199 | if (obs.mode() !== 'oneshot' && releaseAt + release > startOffset * duration) { 200 | source.loop = true 201 | source.loopStart = start 202 | source.loopEnd = end 203 | } 204 | 205 | source.start(at, startOffset * duration + start) 206 | source.stop(releaseAt + release * 2) 207 | 208 | var envelope = context.audio.createGain() 209 | envelope.gain.value = 0 210 | source.connect(envelope) 211 | 212 | // envelope 213 | if (attack) { 214 | envelope.gain.setTargetAtTime(1, at, attack / 4) 215 | } 216 | envelope.gain.setTargetAtTime(0, releaseAt, release / 4) 217 | envelope.connect(this.choker) 218 | 219 | var event = new ScheduleEvent(at, source, envelope, [ 220 | Apply(context, source.playbackRate, this.playbackRate) 221 | ]) 222 | 223 | event.to = releaseAt + release 224 | 225 | return event 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /processors/pitchshift.js: -------------------------------------------------------------------------------- 1 | var watch = require('observ/watch') 2 | var Processor = require('../processor.js') 3 | var Property = require('observ-default') 4 | 5 | var Param = require('audio-slot-param') 6 | var Apply = require('audio-slot-param/apply') 7 | 8 | module.exports = PitchshiftNode 9 | 10 | function PitchshiftNode (context) { 11 | var instance = new Jungle(context.audio) 12 | 13 | var input = context.audio.createGain() 14 | var output = context.audio.createGain() 15 | 16 | var wet = context.audio.createGain() 17 | var dry = context.audio.createGain() 18 | 19 | input.connect(instance.input) 20 | instance.output.connect(wet) 21 | input.connect(dry) 22 | 23 | dry.connect(output) 24 | wet.connect(output) 25 | 26 | var obs = Processor(context, input, output, { 27 | transpose: Property(12), 28 | wet: Param(context, 1), 29 | dry: Param(context, 0) 30 | }) 31 | 32 | watch(obs.transpose, function (value) { 33 | instance.setPitchOffset(getMultiplier(value)) 34 | }) 35 | 36 | Apply(context, wet.gain, obs.wet) 37 | Apply(context, dry.gain, obs.dry) 38 | 39 | return obs 40 | 41 | } 42 | 43 | function getMultiplier (x) { 44 | // don't ask... 45 | if (x < 0) { 46 | return x / 12 47 | } else { 48 | var a5 = 1.8149080040913423e-7 49 | var a4 = -0.000019413043101157434 50 | var a3 = 0.0009795096626987743 51 | var a2 = -0.014147877819596033 52 | var a1 = 0.23005591195033048 53 | var a0 = 0.02278153473118749 54 | 55 | var x1 = x 56 | var x2 = x * x 57 | var x3 = x * x * x 58 | var x4 = x * x * x * x 59 | var x5 = x * x * x * x * x 60 | 61 | return a0 + x1 * a1 + x2 * a2 + x3 * a3 + x4 * a4 + x5 * a5 62 | } 63 | 64 | } 65 | 66 | // include https://github.com/cwilso/Audio-Input-Effects/blob/master/js/jungle.js 67 | 68 | // Copyright 2012, Google Inc. 69 | // All rights reserved. 70 | // 71 | // Redistribution and use in source and binary forms, with or without 72 | // modification, are permitted provided that the following conditions are 73 | // met: 74 | // 75 | // * Redistributions of source code must retain the above copyright 76 | // notice, this list of conditions and the following disclaimer. 77 | // * Redistributions in binary form must reproduce the above 78 | // copyright notice, this list of conditions and the following disclaimer 79 | // in the documentation and/or other materials provided with the 80 | // distribution. 81 | // * Neither the name of Google Inc. nor the names of its 82 | // contributors may be used to endorse or promote products derived from 83 | // this software without specific prior written permission. 84 | // 85 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 86 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 87 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 88 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 89 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 90 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 91 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 92 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 93 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 94 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 95 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 96 | 97 | function createFadeBuffer (context, activeTime, fadeTime) { 98 | var length1 = activeTime * context.sampleRate 99 | var length2 = (activeTime - 2 * fadeTime) * context.sampleRate 100 | var length = length1 + length2 101 | var buffer = context.createBuffer(1, length, context.sampleRate) 102 | var p = buffer.getChannelData(0) 103 | 104 | var fadeLength = fadeTime * context.sampleRate 105 | 106 | var fadeIndex1 = fadeLength 107 | var fadeIndex2 = length1 - fadeLength 108 | 109 | // 1st part of cycle 110 | for (var i = 0; i < length1; ++i) { 111 | var value 112 | 113 | if (i < fadeIndex1) { 114 | value = Math.sqrt(i / fadeLength) 115 | } else if (i >= fadeIndex2) { 116 | value = Math.sqrt(1 - (i - fadeIndex2) / fadeLength) 117 | } else { 118 | value = 1 119 | } 120 | 121 | p[i] = value 122 | } 123 | 124 | // 2nd part 125 | for (var j = length1; j < length; ++j) { 126 | p[j] = 0 127 | } 128 | 129 | return buffer 130 | } 131 | 132 | function createDelayTimeBuffer (context, activeTime, fadeTime, shiftUp) { 133 | var length1 = activeTime * context.sampleRate 134 | var length2 = (activeTime - 2 * fadeTime) * context.sampleRate 135 | var length = length1 + length2 136 | var buffer = context.createBuffer(1, length, context.sampleRate) 137 | var p = buffer.getChannelData(0) 138 | 139 | // 1st part of cycle 140 | for (var i = 0; i < length1; ++i) { 141 | if (shiftUp) { 142 | // This line does shift-up transpose 143 | p[i] = (length1 - i) / length 144 | } else { 145 | // This line does shift-down transpose 146 | p[i] = i / length1 147 | } 148 | } 149 | 150 | // 2nd part 151 | for (var j = length1; j < length; ++j) { 152 | p[j] = 0 153 | } 154 | 155 | return buffer 156 | } 157 | 158 | var delayTime = 0.100 159 | var fadeTime = 0.050 160 | var bufferTime = 0.100 161 | 162 | function Jungle (context) { 163 | this.context = context 164 | // Create nodes for the input and output of this "module". 165 | var input = context.createGain() 166 | var output = context.createGain() 167 | this.input = input 168 | this.output = output 169 | 170 | // Delay modulation. 171 | var mod1 = context.createBufferSource() 172 | var mod2 = context.createBufferSource() 173 | var mod3 = context.createBufferSource() 174 | var mod4 = context.createBufferSource() 175 | this.shiftDownBuffer = createDelayTimeBuffer(context, bufferTime, fadeTime, false) 176 | this.shiftUpBuffer = createDelayTimeBuffer(context, bufferTime, fadeTime, true) 177 | mod1.buffer = this.shiftDownBuffer 178 | mod2.buffer = this.shiftDownBuffer 179 | mod3.buffer = this.shiftUpBuffer 180 | mod4.buffer = this.shiftUpBuffer 181 | mod1.loop = true 182 | mod2.loop = true 183 | mod3.loop = true 184 | mod4.loop = true 185 | 186 | // for switching between oct-up and oct-down 187 | var mod1Gain = context.createGain() 188 | var mod2Gain = context.createGain() 189 | var mod3Gain = context.createGain() 190 | mod3Gain.gain.value = 0 191 | var mod4Gain = context.createGain() 192 | mod4Gain.gain.value = 0 193 | 194 | mod1.connect(mod1Gain) 195 | mod2.connect(mod2Gain) 196 | mod3.connect(mod3Gain) 197 | mod4.connect(mod4Gain) 198 | 199 | // Delay amount for changing pitch. 200 | var modGain1 = context.createGain() 201 | var modGain2 = context.createGain() 202 | 203 | var delay1 = context.createDelay() 204 | var delay2 = context.createDelay() 205 | mod1Gain.connect(modGain1) 206 | mod2Gain.connect(modGain2) 207 | mod3Gain.connect(modGain1) 208 | mod4Gain.connect(modGain2) 209 | modGain1.connect(delay1.delayTime) 210 | modGain2.connect(delay2.delayTime) 211 | 212 | // Crossfading. 213 | var fade1 = context.createBufferSource() 214 | var fade2 = context.createBufferSource() 215 | var fadeBuffer = createFadeBuffer(context, bufferTime, fadeTime) 216 | fade1.buffer = fadeBuffer 217 | fade2.buffer = fadeBuffer 218 | fade1.loop = true 219 | fade2.loop = true 220 | 221 | var mix1 = context.createGain() 222 | var mix2 = context.createGain() 223 | mix1.gain.value = 0 224 | mix2.gain.value = 0 225 | 226 | fade1.connect(mix1.gain) 227 | fade2.connect(mix2.gain) 228 | 229 | // Connect processing graph. 230 | input.connect(delay1) 231 | input.connect(delay2) 232 | delay1.connect(mix1) 233 | delay2.connect(mix2) 234 | mix1.connect(output) 235 | mix2.connect(output) 236 | 237 | // Start 238 | var t = context.currentTime + 0.050 239 | var t2 = t + bufferTime - fadeTime 240 | mod1.start(t) 241 | mod2.start(t2) 242 | mod3.start(t) 243 | mod4.start(t2) 244 | fade1.start(t) 245 | fade2.start(t2) 246 | 247 | this.mod1 = mod1 248 | this.mod2 = mod2 249 | this.mod1Gain = mod1Gain 250 | this.mod2Gain = mod2Gain 251 | this.mod3Gain = mod3Gain 252 | this.mod4Gain = mod4Gain 253 | this.modGain1 = modGain1 254 | this.modGain2 = modGain2 255 | this.fade1 = fade1 256 | this.fade2 = fade2 257 | this.mix1 = mix1 258 | this.mix2 = mix2 259 | this.delay1 = delay1 260 | this.delay2 = delay2 261 | 262 | this.setDelay(delayTime) 263 | } 264 | 265 | Jungle.prototype.setDelay = function (delayTime) { 266 | this.modGain1.gain.setTargetAtTime(0.5 * delayTime, 0, 0.010) 267 | this.modGain2.gain.setTargetAtTime(0.5 * delayTime, 0, 0.010) 268 | } 269 | 270 | Jungle.prototype.setPitchOffset = function (mult) { 271 | if (mult > 0) { // pitch up 272 | this.mod1Gain.gain.value = 0 273 | this.mod2Gain.gain.value = 0 274 | this.mod3Gain.gain.value = 1 275 | this.mod4Gain.gain.value = 1 276 | } else { // pitch down 277 | this.mod1Gain.gain.value = 1 278 | this.mod2Gain.gain.value = 1 279 | this.mod3Gain.gain.value = 0 280 | this.mod4Gain.gain.value = 0 281 | } 282 | this.setDelay(delayTime * Math.abs(mult)) 283 | } 284 | --------------------------------------------------------------------------------