├── .gitignore ├── LICENSE ├── README.md ├── dist ├── synth-kit.js ├── synth-kit.live.js ├── synth-kit.live.min.js └── synth-kit.min.js ├── docs ├── buffers.js.html ├── context.js.html ├── effects.js.html ├── envelopes.js.html ├── filters.js.html ├── fonts │ ├── Lato-Regular.ttf │ ├── OFL.txt │ └── SourceCodePro-Regular.ttf ├── index.html ├── instruments.js.html ├── load.js.html ├── module-buffers.html ├── module-context.html ├── module-effects.html ├── module-envelopes.html ├── module-filters.html ├── module-instruments.html ├── module-load.html ├── module-oscillators.html ├── module-routing.html ├── module-signals.html ├── module-synths.html ├── module-units.html ├── oscillators.js.html ├── routing.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js ├── signals.js.html ├── styles │ ├── ionicons.min.css │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── synths.js.html └── units.js.html ├── example ├── examples.js ├── index.html ├── livecoding.html ├── livecoding.js └── soundfont.js ├── instruments ├── b3.js ├── basic.js ├── emt140.js ├── meter.js ├── modern.js ├── sf-names.json ├── soundfont.js └── tr808.js ├── jsdoc.json ├── lib ├── buffers.js ├── context.js ├── effects.js ├── envelopes.js ├── filters.js ├── index.js ├── instruments.js ├── load.js ├── oscillators.js ├── routing.js ├── signals.js ├── synths.js ├── units.js └── utils.js ├── live.js ├── package.json ├── rollup.config.js └── test └── synth-kit-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 danigb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SynthKit 2 | 3 | ![stability](https://img.shields.io/badge/stability-experimental-red.svg?style=flat-square) 4 | 5 | > A (web audio) synth construction kit 6 | 7 | `synth-kit` is a collection of functions to make Web Audio API more enjoyable. Here is some taste of what you get: 8 | 9 | ```js 10 | import { conn, sine, saw, lowpass, adsr, dest, master, inst } from 'synth-kit' 11 | 12 | // start an OscillatorNode and connect it to AudioContext's destination 13 | sine('C4').connect(dest()).start() 14 | // start a chain of oscillator -> filter -> envelope -> destination 15 | conn(saw('Db3'), lowpass(500), adsr(), dest()).start() 16 | 17 | // start to master 18 | master.start(conn(square('C4'), adsr())) 19 | 20 | // create a instrument with an oscillator and an ADSR envelope: 21 | var synth = inst(function (freq) { 22 | return conn(saw(freq), adsr()) 23 | }) 24 | synth.start('C4') 25 | synth.start('G5') 26 | synth.stop() 27 | 28 | // create a substractive synth instrument with one oscillator: 29 | var mono = inst(substract) 30 | mono.start('C4') 31 | // a substractive synth instrument with three sine oscillators 32 | var duo = inst(substract, { oscillators: { ratios: [1, 2, 2.5 ] } }) 33 | duo.start({ note: 'C4', attack: 0.5 }) 34 | ``` 35 | 36 | ## Usage 37 | 38 | #### Oscillators, buffers and sources 39 | 40 | The `osc` function creates an OscillatorNode: 41 | 42 | ```js 43 | osc({ note: 'C4', type: 'square' }).connect(dest()).start() 44 | ``` 45 | 46 | You can use any of its aliases: `sine`, `saw`, `triangle` or `square`: 47 | 48 | ```js 49 | sine('Db3').connect(dest()).start() 50 | ``` 51 | 52 | You can load a sample with `load` function and play it with the `sample` function: 53 | 54 | ```js 55 | var kick = load('http://myserver.com/samples/kick.mp3') 56 | sample(kick).start() 57 | ``` 58 | 59 | Notice that the file must be fetched before get any sound, so if in the previous example `start` is called before loading the file, no sound in produced. 60 | 61 | 62 | #### Filters and envelopes 63 | 64 | Filters are created using `filter` function (or any of its aliases: `lowpass`, `hipass` and `bandpass`): 65 | 66 | ```js 67 | saw(440).connect(lowpass(200)).start() 68 | ``` 69 | 70 | There are several types of envelopes. The typical `adsr(options)`: 71 | 72 | ```js 73 | o = saw(440).connect(adsr({ release: 1 })) 74 | o.start() 75 | o.stop() // => will have 1 second of exponential decay 76 | ``` 77 | 78 | Or a percutive attack-decay envelope with `perc(attack, decay)`: 79 | 80 | ```js 81 | o = saw(440).connect(perc()).start() 82 | ``` 83 | 84 | #### Routing 85 | 86 | Routing audio unsing node's `connect` function is cumbersome, error prone and makes hard to understand the signal flow. 87 | 88 | You can route nodes in series using the `conn(...nodes)` function: 89 | 90 | ```js 91 | conn(sine(440), gain(0.5)) 92 | ``` 93 | 94 | Or route them in parrallel using the `add(...nodes)` function: 95 | 96 | ```js 97 | add(sine('c4'), sine('c5'), sine('g5')) 98 | ``` 99 | 100 | These two functions can be combined creating complex audio node graphs: 101 | 102 | ```js 103 | // Add two sines and connect themm to a filter and an envelope 104 | conn(add(sine(440), sine(444)), lowpass(500), adsr()) 105 | 106 | // Add two simple subtractive chain (osc -> filter -> adsr) 107 | add( 108 | conn(saw(400), lowpass(800), perc()), 109 | conn(square(800), hipass(1000), adsr()) 110 | ).connect(dest()).start() 111 | ``` 112 | 113 | #### Modulation 114 | 115 | You can pass a node to any audio parameter value to create modulations. For example, you can modulate the `detune` parameter of an oscillator with another oscillator to create a vibrato effect: 116 | 117 | ```js 118 | sine({ note: 'A4', detune: mul(50, sine(10)) }).start() 119 | ``` 120 | 121 | Or, for example, you can modulate a `gain` to create a tremolo effect: 122 | 123 | ```js 124 | connect(saw('C4'), gain({ value: sine(4) })).start() 125 | ``` 126 | 127 | #### Synths 128 | 129 | You can create a typical subtract synthetizer with an oscillator, a filter and two envelopes with the `subtract(freq, options)` function: 130 | 131 | ```js 132 | subtractive(880, { type: 'square', filter: { type: 'hipass' } }).connect(dest()).start() 133 | ``` 134 | 135 | Or a prototypical additive synthetizer: 136 | 137 | ```js 138 | additive([440, 880, 1200], { gains: [0.6, 0.3, 0.1] }).connect(dest()).start() 139 | ``` 140 | 141 | #### Instruments 142 | 143 | Sometime you want a more OOP interface. You want a instrument: 144 | 145 | ```js 146 | var marimba = inst(function (freq) { 147 | return add( 148 | conn(sine(fq), perc(0.1, 1)), 149 | conn(sine(2 * fq), perc(0.01, 0.1)) 150 | ) 151 | }) 152 | marimba.start('C4') 153 | marimba.start('G5') 154 | marimba.stop() 155 | ``` 156 | 157 | 158 | #### Conversion and utility functions 159 | 160 | SynthKit uses frequencies in hertzs to represent pitches, linear gain values to represent amplitudes and number of samples to represent buffer lengths. But sometimes you want to provide those using different units. Here are some conversion utilities: 161 | 162 | - `noteToFreq(name)`: convert a note name or note midi number to its frequency 163 | - `midiToFreq(midi)`: convert a note name or note midi number to its frequency 164 | - `tempoToFreq(bpm, subdivision)`: convert from beats per minute to hertzs 165 | - `dBToGain(db)`: convert from decibels to gain value 166 | - `levelToGain(level)`: convert from level (0 to 100 in logaritmic scale) to gain value 167 | - `gainToDb(gain)`: convert from gain to decibels 168 | - `secsToSamples(num)`: convert from seconds to number of samples 169 | 170 | ## Livecoding in the browser 171 | 172 | Setup `synth-kit` for live coding it's easy. Add the [distribution file] to you page: 173 | 174 | And then: 175 | 176 | ```js 177 | SynthKit.live() // => export all `synth-kit` functions to the global environment 178 | sine(note('A4')).start() 179 | ``` 180 | 181 | 182 | ## Test and examples 183 | 184 | To view some examples open: `example/index.html` 185 | 186 | ## Inspiration and references 187 | 188 | - The idea and API organization is taken from cljs-bach: https://github.com/ctford/cljs-bach Thanks ctford! 189 | - Of course, the synth secrets tutorial was the beginning of all: https://github.com/micjamking/synth-secrets (that's a easy to read version). Thanks Gordon Reid (here it is an [awesome PDF version](http://www.mediafire.com/file/7w2dcsqmkbeduea/Synth+Secrets+Complete.pdf)) 190 | - Vincent made some nice 808 drum synsthesis: https://github.com/vincentriemer/io-808 191 | - Percussion synthesis tutorial: http://www.cim.mcgill.ca/~clark/nordmodularbook/nm_percussion.html 192 | - Sound Design in Web Audio its an awesome two part blog post: http://nickwritesablog.com/sound-design-in-web-audio-neurofunk-bass-part-1/ and http://nickwritesablog.com/sound-design-in-web-audio-neurofunk-bass-part-2/ 193 | - There are a lot of resources about synthesis, here is a nice one: https://www.gearslutz.com/board/electronic-music-instruments-electronic-music-production/460283-how-do-you-synthesize-808-ish-drums.html 194 | 195 | ## License 196 | 197 | MIT License 198 | -------------------------------------------------------------------------------- /docs/buffers.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | buffers.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

buffers.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/**
 42 |  * In synth-kit buffers can be generated (with the `gen` function) or
 43 |  * retrieved from an audio file (with the `sample`) function
 44 |  *
 45 |  * @module buffers
 46 |  */
 47 | import { isA, OPTS } from './utils'
 48 | import { context, samplingRate } from './context'
 49 | import { lifecycle, plug } from './routing'
 50 | import { load } from './load'
 51 | 
 52 | /**
 53 |  * Create a buffer source (a BufferSourceNode)
 54 |  * @param {Buffer|Function} buffer - the buffer (or a function that returns a buffer)
 55 |  * @param {Object} options - (Optional) options can include:
 56 |  *
 57 |  * - loop: set to true to loop the buffer
 58 |  * - detune: the detune amount (can be a number or a signal)
 59 |  * - context: the audio context to use
 60 |  *
 61 |  * @return {AudioNode} a BufferSourceNode
 62 |  * @example
 63 |  * source(sample('snare.wav')).start()
 64 |  * source(sample('amen-break.mp3'), { loop: true })
 65 |  */
 66 | export function source (buffer, o) {
 67 |   o = o || OPTS
 68 |   var src = context(o.context).createBufferSource()
 69 |   src.buffer = isA('function', buffer) ? buffer() : buffer
 70 |   if (!src.buffer) console.warn('Buffer not ready.')
 71 |   if (o.loop) src.loop = true
 72 |   return lifecycle(src, [
 73 |     plug('detune', o.detune, src)
 74 |   ])
 75 | }
 76 | 
 77 | /**
 78 |  * Fetch a remote audio sample. You get a function that returns an AudioBuffer
 79 |  * when loaded or null otherwise. Or you can chain like a promise.
 80 |  *
 81 |  * @param {String} url - the url of the file
 82 |  * @param {Object} options - (Optional) options can include:
 83 |  *
 84 |  * - context: the audio context to use
 85 |  *
 86 |  * @return {Function} a function the returns the buffer
 87 |  *
 88 |  * @example
 89 |  * source(sample('server.com/audio-file.mp3')).start()
 90 |  * @example
 91 |  * // use the Promise like interface
 92 |  * sample('audio-file.mp3').then(function (buffer) {
 93 |  *     source(buffer).start()
 94 |  * })
 95 |  */
 96 | export function sample (url, opts) {
 97 |   var bf = null
 98 |   function buffer () { return bf }
 99 |   var promise = load(url, opts).then(function (buffer) {
100 |     buffer.url = url
101 |     bf = buffer
102 |     return bf
103 |   })
104 |   buffer.then = promise.then.bind(promise)
105 |   return buffer
106 | }
107 | 
108 | /**
109 |  * Generate a BufferNode. It returns a no-parameter function that
110 |  * returns a buffer. This way, it's easy to memoize (cache) buffers.
111 |  *
112 |  * @param {Function|Array<Function>} generators - a generator or a list of
113 |  * generators (to create a buffer with multiple channels)
114 |  * @param {Integer} samples - the length in samples
115 |  * @param {Object} options - (Optional) options can include:
116 |  *
117 |  * - reverse: set to true to reverse the generated buffer
118 |  * - context: the audio context to use
119 |  *
120 |  * @return {Function} a function with no parameters that returns the desired buffer
121 |  */
122 | export function gen (generators, samples, o) {
123 |   samples = samples || 2
124 |   o = o || OPTS
125 |   return function () {
126 |     if (!Array.isArray(generators)) generators = [ generators ]
127 |     var reverse = o.reverse
128 |     var numOfChannels = generators.length
129 |     var ctx = context(o.context)
130 | 
131 |     var buffer = ctx.createBuffer(numOfChannels, samples, samplingRate(ctx))
132 |     for (var ch = 0; ch < numOfChannels; ch++) {
133 |       generateData(generators[ch], buffer.getChannelData(ch), samples, reverse)
134 |     }
135 |     return buffer
136 |   }
137 | }
138 | 
139 | function generateData (generator, data, samples, reverse) {
140 |   for (var i = 0; i < samples; i++) {
141 |     data[i] = generator(reverse ? samples - i : i)
142 |   }
143 | }
144 | 
145 | /**
146 |  * White noise source node.
147 |  * @param {Integer} length - lenth in samples
148 |  * @param {Object} options - (Optional) the same options that `source` function
149 |  * @return {AudioNode} the white noise audio node generator
150 |  * @see source
151 |  * @example
152 |  * conn(white(seconds(1)), perc(), dest()).start()
153 |  */
154 | export function white (samples, options) {
155 |   if (!isA('number', samples)) samples = samplingRate(options)
156 |   return source(gen(white.generator, samples, options), options)
157 | }
158 | white.generator = function () { return Math.random() * 2 - 1 }
159 | 
160 |
161 |
162 | 163 | 164 | 165 | 166 |
167 | 168 |
169 | 170 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /docs/context.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | context.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

context.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/**
 42 |  * In synth-kit most of the functions accepts an optional AudioContext as
 43 |  * last parameter. If no one is provided, synth-kit creates a singleton
 44 |  * AudioContext using ['audio-context'](npmjs.com/package/audio-context) module.
 45 |  *
 46 |  * @module context
 47 |  */
 48 | import ac from 'audio-context'
 49 | 
 50 | // Shim to make connect chainable (soon to be implemented native)
 51 | if (ac && ac.createGain) {
 52 |   var proto = Object.getPrototypeOf(Object.getPrototypeOf(ac.createGain()))
 53 |   var _connect = proto.connect
 54 |   proto.connect = function () {
 55 |     _connect.apply(this, arguments)
 56 |     console.log('connect!', this, arguments[0])
 57 |     return this
 58 |   }
 59 | }
 60 | 
 61 | /**
 62 |  * Get the audio context.
 63 |  * @param {AudioContext} context - (Optional) if given, it return itself. If
 64 |  * nothing passed, it returns the AudioContext singleton instance
 65 |  * @return {AudioContext} the audio context
 66 |  * @example
 67 |  * // normally you won't do this:
 68 |  * var gain = context().createGain()
 69 |  */
 70 | export function context (ctx) { return ctx || ac }
 71 | 
 72 | /**
 73 |  * Get the audio context's destination
 74 |  * @param {AudioContext} context - (Optional) an alternate audio context
 75 |  * @return {AudioContext} the audio context destination
 76 |  * @example
 77 |  * conn(sine(300), dest()).start()
 78 |  */
 79 | export function dest (context) { return (context || ac).destination }
 80 | 
 81 | /**
 82 |  * Get audio context's current time
 83 |  * @param {AudioContext} context - (Optional) an optional audio context
 84 |  * @return {Number} the time in seconds relative to the AudioContext creation
 85 |  * @example
 86 |  * now() // => 3.3213
 87 |  */
 88 | export function now (context) { return (context || ac).currentTime }
 89 | 
 90 | /**
 91 |  * Get a valid time
 92 |  * @param {Float} time - the time (equal or greater than now(), otherwise, ignored)
 93 |  * @param {Float} delay - the delay
 94 |  * @param {AudioContext} context - (Optional) an optional audio context
 95 |  * @example
 96 |  * now() // => 0.7
 97 |  * time(0.2) // => 0.7
 98 |  * time(1) // => 1
 99 |  * time(0.2, 1) // => 1.7 (time is ignored because is < than now())
100 |  */
101 | export function when (time, delay, ctx) {
102 |   return Math.max(now(ctx), time || 0) + (delay || 0)
103 | }
104 | 
105 | /**
106 |  * Get time after n seconds (from now)
107 |  * @function
108 |  * @param {Float} delay - the delay
109 |  * @param {AudioContext} context - (Optional) an optional audio context
110 |  * @return {Float} time in seconds
111 |  * @example
112 |  * now() // => 0.785
113 |  * after(1) // => 1.785
114 |  */
115 | export var after = when.bind(0)
116 | 
117 | /**
118 |  * Get audio context sampling rate
119 |  * @param {AudioContext} context - (Optional) the audio context
120 |  * @return {Integer} the context's sampling rate
121 |  * @example
122 |  * samplingRate() // => 44100
123 |  */
124 | export function samplingRate (ctx) { return context(ctx).sampleRate }
125 | 
126 | /**
127 |  * Convert from seconds to samples (using AudioContext sampling rate)
128 |  * @param {Float} seconds - the number of seconds
129 |  * @param {AudioContext} context - (Optional) the audio context
130 |  * @return {Integer} the number of samples
131 |  * @example
132 |  * white(seconds(1.2)) // => generate 1.2 seconds of white noise
133 |  */
134 | export function timeToSamples (secs, context) { return secs * samplingRate(context) }
135 | 
136 |
137 |
138 | 139 | 140 | 141 | 142 |
143 | 144 |
145 | 146 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /docs/effects.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | effects.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

effects.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/** @module effects */
 42 | import { context, samplingRate } from './context'
 43 | import { add, conn, plug, lifecycle } from './routing'
 44 | import { bypass, gain } from './signals'
 45 | import { isA, OPTS } from './utils'
 46 | import { osc } from './oscillators'
 47 | import { lowpass } from './filters'
 48 | import { gen, white } from './buffers'
 49 | 
 50 | /**
 51 |  * Mix an effect with the signal. Can be partially applied to create an
 52 |  * effect bus.
 53 |  * @param {Number} wet - the amount of effect (0-1)
 54 |  * @param {AudioNode} fx - the effect unit
 55 |  * @param {Object} options - (Optional) may include:
 56 |  *
 57 |  * - compensate: it it's false, the dry signal gain will pass without reduction.
 58 |  * If true (it's true by default) the dry signal is reduced the gain of the wet signal.
 59 |  * - context: the audio context
 60 |  *
 61 |  * @return {AudioNode}
 62 |  * @example
 63 |  * mix(dB(-3), delay(ms(800)))
 64 |  * @example
 65 |  * // create an effect bus
 66 |  * var rev = mix(reverb(2))
 67 |  * conn(sine(300), rev(0.2))
 68 |  */
 69 | export function mix (wet, fx, opts) {
 70 |   if (arguments.length === 1) return function (w, o) { return mix(w, wet, o) }
 71 |   if (!isA('number', wet)) wet = 0.5
 72 |   opts = opts || OPTS
 73 |   var dry = opts.compensate === false ? 1 : 1 - wet
 74 |   console.log('MIX', wet, dry, opts)
 75 |   return add(gain(dry), conn(fx, gain(wet)))
 76 | }
 77 | 
 78 | /**
 79 |  * Create a feedback loop.
 80 |  * @param {Integer} amount - the amount of signal
 81 |  * @param {AudioNode} node - the node to feedback
 82 |  * @param {Object} options - (Optional) options may include:
 83 |  *
 84 |  * - context: the audio context to use
 85 |  *
 86 |  * @return {AudioNode} the original node (with a feedback loop)
 87 |  */
 88 | export function feedback (amount, node, options) {
 89 |   var feed = gain(amount, options)
 90 |   node.conn(feed)
 91 |   feed.conn(node)
 92 |   return node
 93 | }
 94 | 
 95 | /**
 96 |  * Create a tremolo
 97 |  */
 98 | export function tremolo (rate, type, ac) {
 99 |   type = type || 'sine'
100 |   return gain(osc(type, rate, ac), ac)
101 | }
102 | 
103 | /**
104 |  * Create a delay (a DelayNode object)
105 |  */
106 | export function dly (time, opts) {
107 |   opts = opts || OPTS
108 |   var dly = context(opts.context).createDelay(5)
109 |   return lifecycle(dly, [
110 |     plug('delayTime', time, dly)
111 |   ])
112 | }
113 | 
114 | /**
115 |  * Create a convolver
116 |  * @param {AudioBuffer} buffer
117 |  * @param {Object} options - (Optional) options may include:
118 |  *
119 |  * - normalize: true to normalize the buffer
120 |  * - context: the audio context to use
121 |  *
122 |  * @return {AudioNode} the convolver (ConvolverNode)
123 |  * @example
124 |  * reverb = mix(convolve(sample('emt-140.wav')))
125 |  * connect(subtractive(880, { perc: [0.1, 1] }), reverb(0.2))
126 |  */
127 | export function convolve (buffer, o) {
128 |   o = o || OPTS
129 |   var c = context(o.context).createConvolver()
130 |   c.buffer = isA('function', buffer) ? buffer() : buffer
131 |   if (o.normalize === true) c.normalize = true
132 |   return c
133 | }
134 | 
135 | /**
136 |  * Create a reverb impulse response using a logarithmic decay white noise
137 |  * @param {Number} duration - the duration in samples
138 |  * @param {Object} options - (Optional) options may include:
139 |  *
140 |  * - decay: the decay length in samples
141 |  * - attack: the attack time in samples
142 |  * - reverse: get the reversed impulse
143 |  * - context: the context to use
144 |  *
145 |  * @return {AudioBuffer} the impulse response audio buffer
146 |  */
147 | export function decayIR (samples, opts) {
148 |   samples = samples || 10000
149 |   opts = opts || OPTS
150 |   var attack = opts.attack || Math.floor(samples / 10)
151 |   var decay = opts.decay || samples - attack
152 |   // 60dB is a factor of 1 million in power, or 1000 in amplitude.
153 |   var base = Math.pow(1 / 1000, 1 / decay)
154 |   return gen(function (i) {
155 |     var dec = base ? Math.pow(base, i) : 1
156 |     var att = i < attack ? (i / attack) : 1
157 |     return att * white.generator(i) * dec
158 |   }, samples, opts)
159 | }
160 | 
161 | /**
162 |  * Create a simple reverb
163 |  * @param {Number} duration - in seconds? WTF?
164 |  */
165 | export function reverb (duration, opts) {
166 |   opts = opts || OPTS
167 |   var rate = samplingRate(opts.context)
168 |   return convolve(decayIR(duration * rate, opts), opts)
169 | }
170 | 
171 | /**
172 |  * A mono or stereo delay with filtered feedback
173 |  */
174 | export function delay (time, filter, feedAmount, ac) {
175 |   if (!isA('number', feedAmount)) feedAmount = 0.3
176 |   filter = isA('number', filter) ? lowpass(filter, null, null, ac) : filter || bypass(ac)
177 |   return feedback(feedAmount, dly(time, ac), filter, ac)
178 | }
179 | 
180 |
181 |
182 | 183 | 184 | 185 | 186 |
187 | 188 |
189 | 190 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /docs/envelopes.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | envelopes.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

envelopes.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/**
 42 |  *
 43 |  * @module envelopes
 44 |  */
 45 | import { when } from './context'
 46 | import { gain, scale } from './signals'
 47 | import { isA, OPTS } from './utils'
 48 | function eachStage (stages, fn) { stages.forEach(function (s) { fn.apply(null, s) }) }
 49 | 
 50 | /**
 51 |  * Create an attack-decay envelope with fixed duration. It's composed by a
 52 |  * linear attack ramp and an exponential decay. This envelope doesn't
 53 |  * have release, so it stops after the duration (attack + decay).
 54 |  *
 55 |  * @param {Number} attack - (Optional) the attack time, defaults to 0.01
 56 |  * @param {Number} decay - (Optional) the decay time, defaults to 0.2
 57 |  * @param {Object} options - (Optional) an options with a context
 58 |  * @return {AudioNode} the signal envelope
 59 |  * @example
 60 |  * conn(sine(1000), perc(0.01, 0.5))
 61 |  * conn(sine(1000), perc(null, 1)) // default attack
 62 |  * conn(sine(1000), perc()) // default values
 63 |  */
 64 | export function perc (attack, decay, opts) {
 65 |   if (isA('object', attack)) return perc(attack.attack, attack.decay, attack)
 66 |   var a = [ [0, 0, 'set'], [attack || 0.01, 1, 'lin'], [decay || 0.2, 0, 'exp'] ]
 67 |   return envelope(a, null, opts)
 68 | }
 69 | 
 70 | var ADSR = [0.01, 0.1, 0.8, 0.3]
 71 | /**
 72 |  * Create an adsr envelope
 73 |  * @params {Object} options - (Optional) the options may include:
 74 |  *
 75 |  * - adsr: an array with the form [attack, decay, sustain, release]
 76 |  * - attack: the attack time (will override the a in the adsr param)
 77 |  * - decay: the decay time (will override the d in adsr param)
 78 |  * - sustain: the sustain gain value (will override the s in the adsr param)
 79 |  * - release: the release time (will override the r in the adsr param)
 80 |  */
 81 | export function adsr (o) {
 82 |   o = o || OPTS
 83 |   var adsr = o.adsr || ADSR
 84 |   if (!isA('number', o.attack)) o.attack = adsr[0]
 85 |   if (!isA('number', o.decay)) o.decay = adsr[1]
 86 |   if (!isA('number', o.sustain)) o.sustain = adsr[2]
 87 |   if (!isA('number', o.release)) o.release = adsr[3]
 88 |   var a = [ [0, 0, 'set'], [o.attack, 1, 'lin'], [o.decay, o.sustain, 'exp'] ]
 89 |   var r = [ [0, o.sustain, 'set'], [o.release, 0, 'exp'] ]
 90 |   return envelope(a, r, o)
 91 | }
 92 | 
 93 | /**
 94 |  * A frequency envelope. Basically the setup to provide an adsr over a
 95 |  * number of octaves.
 96 |  * @param {Number} frequency - the initial frequency
 97 |  * @param {Number} octaves - (Optional) the number of octaves of the envelope (1 by default)
 98 |  * @param {Object} options - the same options as an ADSR envelope
 99 |  * @see adsr
100 |  * @example
101 |  * conn(saw(1200), lowpass(freqEnv(440, 2, { release: 1 })))
102 |  */
103 | export function freqEnv (freq, octs, a, d, s, r, ac) {
104 |   return scale(freq, freq * Math.pow(2, octs), adsr(a, d, s, 0, r, ac))
105 | }
106 | 
107 | /**
108 |  * Create a gain envelope
109 |  * @param {Array<Stage>} attStages - the attack part of the envelope
110 |  * @param {Array<Stage>} relStages - the release part of the envelope
111 |  * @private
112 |  */
113 | export function envelope (attEnvelope, relEnvelope, opts) {
114 |   var g = gain(0, opts)
115 |   g.start = apply(g.gain, attEnvelope)
116 |   if (!relEnvelope) {
117 |     g.duration = duration(attEnvelope)
118 |     g.stop = function () {}
119 |   } else {
120 |     g.stop = apply(g.gain, relEnvelope)
121 |     g.release = duration(relEnvelope)
122 |   }
123 |   return g
124 | }
125 | 
126 | /**
127 |  * Apply a contour to a parameter
128 |  * @param {AudioParam} param - the parameter to apply the contour to
129 |  * @param {Array<Stage>} contour - a list of countour stages, each of which
130 |  * is composed of [time, value, type].
131 |  * @example
132 |  * apply(filter.frequency, [ [0, 440, 'set'], [5, 880, 'lin'] ])
133 |  * @private
134 |  */
135 | function apply (param, contour) {
136 |   return function (t) {
137 |     t = when(t, 0, param.context)
138 |     eachStage(contour, function (time, value, type) {
139 |       t += time
140 |       if (type === 'set') param.setValueAtTime(value, t)
141 |       else if (type === 'lin') param.linearRampToValueAtTime(value, t)
142 |       else if (type === 'exp') param.exponentialRampToValueAtTime(value !== 0 ? value : 0.00001, t)
143 |       else console.warn('Invalid stage type', time, value, type)
144 |     })
145 |   }
146 | }
147 | 
148 | /**
149 |  * Calculate the duration of a contour
150 |  * @private
151 |  */
152 | function duration (contour) {
153 |   return contour.reduce(function (dur, stage) {
154 |     return dur + stage[0]
155 |   }, 0)
156 | }
157 | 
158 |
159 |
160 | 161 | 162 | 163 | 164 |
165 | 166 |
167 | 168 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /docs/filters.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | filters.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

filters.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/**
 42 |  * This module provides some syntactic sugar over the AudioContext.createBiquadFilter
 43 |  * function.
 44 |  *
 45 |  * @example
 46 |  * import { conn, square, lowpass, filter, master } from 'synth-kit'
 47 |  *
 48 |  * master.start(conn(square('C4'), lowpass('C5')))
 49 |  * master.start(conn(square('A4'), filter({ type: 'hipass', Q: 10, freq: 1000 }))
 50 |  * @module filters
 51 |  */
 52 | import { OPTS } from './utils'
 53 | import { getFreq } from './oscillators'
 54 | import { context } from './context'
 55 | import { plug, lifecycle } from './routing'
 56 | 
 57 | // private: create a filter
 58 | function create (type, opts) {
 59 |   opts = opts || OPTS
 60 |   var filter = context(opts.context).createBiquadFilter()
 61 |   filter.type = type || opts.type || 'lowpass'
 62 |   return lifecycle(filter, [
 63 |     plug('frequency', getFreq(opts), filter),
 64 |     plug('Q', opts.Q, filter),
 65 |     plug('detune', opts.detune, filter)
 66 |   ])
 67 | }
 68 | 
 69 | /**
 70 |  * Create a filter (a [BiquadFilterNode](https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode))
 71 |  *
 72 |  * @function
 73 |  * @param {Object} config - it may include:
 74 |  *
 75 |  * - frequency (or freq, or note or midi): the frequency expressed in hertzs
 76 |  * (or midi note number or note name)
 77 |  * - type: one of the BiquadFilterNode types
 78 |  * - detune: the detune of the frequency in cents (can be a number or a signal)
 79 |  * - Q: the Q of the filter (can be a number or a signal)
 80 |  * - context: the audio context to use
 81 |  *
 82 |  * Instead of a configuration object you can pass a frequency number of note
 83 |  * name just to specify the frequency.
 84 |  *
 85 |  * @return {AudioNode} the BiquadFilterNode
 86 |  * @example
 87 |  * conn(square(800), filter({ type: 'lowpass', freq: 600 }))
 88 |  */
 89 | export var filter = create.bind(null, null)
 90 | 
 91 | /**
 92 |  * Create a lowpass filter. An alias for `filter({ type: 'lowpass', ... })`
 93 |  *
 94 |  * @function
 95 |  * @param {Object} config - same as `filter` function but without type
 96 |  * @return {AudioNode} the lowpass filter
 97 |  * @see filter
 98 |  * @example
 99 |  * conn(square('C4'), lowpass(400))
100 |  */
101 | export const lowpass = create.bind(null, 'lowpass')
102 | 
103 | /**
104 |  * Create a hipass filter. An alias for `filter({ type: 'hipass', ... })`
105 |  *
106 |  * @function
107 |  * @param {Object} config - same as `filter` function but without type
108 |  * @return {AudioNode} the hipass filter
109 |  * @see filter
110 |  * @example
111 |  * conn(square(800), hipass(400))
112 |  */
113 | export const hipass = create.bind(null, 'highpass')
114 | 
115 | /**
116 |  * Create a bandpass filter. An alias for `filter({ type: 'bandpass', ... })`
117 |  *
118 |  * @function
119 |  * @param {Object} config - same as `filter` function but without type
120 |  * @return {AudioNode} the bandpass filter
121 |  * @see filter
122 |  * @example
123 |  * conn(square(800), bandpass(400))
124 |  */
125 | export const bandpass = create.bind(null, 'hipass')
126 | 
127 |
128 |
129 | 130 | 131 | 132 | 133 |
134 | 135 |
136 | 137 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danigb/synth-kit/ac6421d1fd05e6c73cf2174f84667c35ac567c26/docs/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" 2 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 3 | This license is copied below, and is also available with a FAQ at: 4 | http://scripts.sil.org/OFL 5 | 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /docs/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danigb/synth-kit/ac6421d1fd05e6c73cf2174f84667c35ac567c26/docs/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /docs/instruments.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | instruments.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

instruments.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/**
 42 |  * Instruments provide a OOP style interface to the synths. Something with
 43 |  * `start` and `stop` methods, than can be called several times and know about
 44 |  * note frequencies and midi connections.
 45 |  *
 46 |  * @example
 47 |  * var simple = inst((fq) => conn(sine(fq), adsr()))
 48 |  * simple.start('C4')
 49 |  * simple.start('G5')
 50 |  * simple.stop() // => stop all notes
 51 |  * @module instruments
 52 |  */
 53 | import { isA, OPTS, toArr, slice } from './utils'
 54 | import { context, when, dest } from './context'
 55 | import { withDest } from './synths'
 56 | 
 57 | /**
 58 |  * A master output instrument. You can use it to start and stop nodes. All
 59 |  * started nodes will be connected to the AudioContext destination.
 60 |  *
 61 |  * @example
 62 |  * master.start(sine(300)) // connect to destination and start
 63 |  * master.start(sine(600), 0, 1) // connect to destination and start after 1 second
 64 |  * master.stop() // stop all
 65 |  */
 66 | export var master = inst(null, dest())
 67 | 
 68 | /**
 69 |  * Create an object-oriented-style instrument player. It wraps a synth function
 70 |  * (a function that create nodes) into in a convenient player API. It can
 71 |  * be used to limit the polyphony.
 72 |  *
 73 |  * The player object have the following methods:
 74 |  *
 75 |  * - `start(node, when, delay, duration)`: start a node
 76 |  * - `stop`: stop all nodes
 77 |  * - `on(name, callback)`: add an event callback
 78 |  * - `event(name, ...values)`: fire an event
 79 |  *
 80 |  *
 81 |  * @param {Function} synth - the synth function (a function that returns a node graph)
 82 |  * @param {AudioNode} destination - if present, all nodes will be connected to
 83 |  * this destination
 84 |  * @param {Object} options - (Optional) the options may include:
 85 |  *
 86 |  * - maxVoices: the maximum number of simultaneous voices. No value (by default)
 87 |  * means no limit.
 88 |  *
 89 |  * @return {Player} a player object
 90 |  *
 91 |  * @example
 92 |  * // an instrument with destination
 93 |  * var synth = inst((fq) => sine(fq), dest())
 94 |  * synth.start('A4')
 95 |  * synth.stopAll()
 96 |  * @example
 97 |  * // only the destination
 98 |  * var master = inst(null, conn(mix(0.2), reverb(), dest()))
 99 |  * master.start(sine(300))
100 |  * master.start(sine(400))
101 |  * master.stopAll()
102 |  */
103 | export function inst (synth, destination, options) {
104 |   synth = withDest(synth, destination || dest())
105 |   return tracker(synth, options || OPTS)
106 | }
107 | 
108 | /**
109 |  * tracker: (fn: (object) => Node, options: object) => interface { start: fn, stop: fn }
110 |  * @private
111 |  */
112 | function tracker (synth, opts) {
113 |   var ob = observable({})
114 |   var limit = opts ? opts.maxVoices : 0
115 |   var voices = { limit: limit, all: {}, nextId: 0, current: 0, pool: new Array(limit) }
116 | 
117 |   function track (node) {
118 |     node.id = voices.nextId++
119 |     voices.all[node.id] = node
120 |     on(node, 'ended', function () {
121 |       delete voices.all[node.id]
122 |       ob.event('voices', Object.keys(voices.all), limit)
123 |     })
124 |     return node
125 |   }
126 | 
127 |   ob.start = function (value, time, delay, duration) {
128 |     var node = synth(value)
129 |     if (node.start) {
130 |       track(node)
131 |       time = start(node, time, delay, duration).startedAt
132 |       ob.event('start', node.id, time)
133 |       ob.event('voices', Object.keys(voices.all), limit)
134 |     }
135 |     return node
136 |   }
137 |   ob.stop = function (ids, time, delay) {
138 |     var t = 0
139 |     ids = toArr(ids || ids === 0 ? ids : Object.keys(voices.all))
140 |     ids.forEach(function (id) {
141 |       if (voices.all[id]) {
142 |         if (!t) t = when(time, delay, voices.all[id].context)
143 |         voices.all[id].stop(t)
144 |       }
145 |     })
146 |   }
147 |   return ob
148 | }
149 | 
150 | function start (node, time, delay, duration) {
151 |   if (time && !isA('number', time)) throw Error('Invalid time (maybe forgot connect?): ', time, delay, duration, node)
152 |   time = when(time, delay, context(node.context))
153 |   node.start(time)
154 |   node.startedAt = time
155 |   var d = duration || node.duration
156 |   if (d) node.stop(time + d)
157 |   return node
158 | }
159 | 
160 | // EVENTS
161 | // ======
162 | // decorate an objet to have `on` and `event` methods
163 | function observable (obj) {
164 |   obj.on = on.bind(null, obj)
165 |   obj.event = event.bind(null, obj)
166 |   return obj
167 | }
168 | 
169 | // add a listener to a target
170 | function on (target, name, callback) {
171 |   if (!name || name === '*') name = 'event'
172 |   var prev = target['on' + name]
173 |   target['on' + name] = function () {
174 |     if (prev) prev.apply(null, arguments)
175 |     callback.apply(null, arguments)
176 |   }
177 |   return target
178 | }
179 | 
180 | // fire an event
181 | function event (target, name /*, ...values */) {
182 |   var args = slice.call(arguments, 1)
183 |   if (isA('function', target['on' + name])) target['on' + name].apply(null, args)
184 |   if (isA('function', target.onevent)) target.onevent.apply(null, args)
185 |   return target
186 | }
187 | 
188 |
189 |
190 | 191 | 192 | 193 | 194 |
195 | 196 |
197 | 198 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /docs/load.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | load.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

load.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/* global XMLHttpRequest */
 42 | import { context } from './context'
 43 | 
 44 | var NONE = {}
 45 | /**
 46 |  * This module contains some functions to fetch and decode audio files.
 47 |  *
 48 |  * @module load
 49 |  */
 50 | 
 51 | /**
 52 |  * Load a remote audio file and return a promise.
 53 |  * @param {String} url
 54 |  * @param {Object} options - (Optional) may include:
 55 |  *
 56 |  * - context: the audio context
 57 |  *
 58 |  * @return {Promise<AudioBuffer>} a promise that resolves to an AudioBuffer
 59 |  * @example
 60 |  * load('sound.mp3').then(function (buffer) {
 61 |  *   sample(buffer, true).start()
 62 |  * }
 63 |  */
 64 | export function load (url, opts) {
 65 |   opts = opts || NONE
 66 |   return fetch(url, 'arraybuffer').then(decodeAudio(context(opts.context)))
 67 | }
 68 | 
 69 | /**
 70 |  * Fetch an url and return a promise
 71 |  * @param {String} url - the url
 72 |  * @param {String} type - can be 'text' or 'arraybuffer'
 73 |  * @return {Promise} a promise to the result
 74 |  */
 75 | export function fetch (url, type) {
 76 |   type = type === 'arraybuffer' ? type : 'text'
 77 |   return new Promise(function (resolve, reject) {
 78 |     var xhr = new XMLHttpRequest()
 79 | 
 80 |     xhr.open('GET', url, true)
 81 |     xhr.responseType = type
 82 | 
 83 |     xhr.onload = function () {
 84 |       if (xhr.response) {
 85 |         resolve(xhr.response)
 86 |       }
 87 |     }
 88 |     xhr.onerror = reject
 89 | 
 90 |     xhr.send()
 91 |   })
 92 | }
 93 | 
 94 | /**
 95 |  * Decode an array buffer into an AudioBuffer.
 96 |  * @param {AudioContext} context - (Optional) the context to be used (can be null to use
 97 |  * synth-kit's default audio context)
 98 |  * @param {Array} array - (Optional) the array to be decoded. If not given,
 99 |  * it returns a function that decodes the buffer so it can be chained with
100 |  * fetch function (see example)
101 |  * @return {Promise<AudioBuffer>} a promise that resolves to an audio buffer
102 |  * @example
103 |  * fecth('sound.mp3').then(decodeAudio())
104 |  */
105 | export function decodeAudio (ac, arrayBuffer) {
106 |   if (arguments.length === 1) return function (array) { return decodeAudio(ac, array) }
107 |   return new Promise(function (resolve, reject) {
108 |     ac.decodeAudioData(arrayBuffer, resolve, reject)
109 |   })
110 | }
111 | 
112 |
113 |
114 | 115 | 116 | 117 | 118 |
119 | 120 |
121 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/oscillators.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | oscillators.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

oscillators.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/**
 42 |  * This module provides some syntactic sugar over the AudioContext.createOscillator
 43 |  * function
 44 |  *
 45 |  * @example
 46 |  * import { sine, square, master } from 'synth-kit'
 47 |  *
 48 |  * master.start(sine('A4'))
 49 |  * master.start(square({ note: 'c3', detune: -10 }))
 50 |  * @module oscillators
 51 |  */
 52 | import { noteToFreq, midiToFreq, tempoToFreq } from './units'
 53 | import { context } from './context'
 54 | import { mult } from './signals'
 55 | import { plug, lifecycle } from './routing'
 56 | import { OPTS, isA } from './utils'
 57 | 
 58 | // Create a OscillatorNode of the given type and configuration
 59 | // this is a private function intended to be partially applied
 60 | function create (type, opts) {
 61 |   opts = opts || OPTS
 62 |   type = type || opts.type
 63 |   var osc = context(opts.context).createOscillator()
 64 |   if (type) osc.type = type
 65 |   return lifecycle(osc, [
 66 |     plug('frequency', getFreq(opts), osc),
 67 |     plug('detune', opts.detune, osc)
 68 |   ])
 69 | }
 70 | 
 71 | // given a configuration object, get the frequency
 72 | export function getFreq (obj) {
 73 |   return obj.frequency ? obj.frequency
 74 |     : obj.freq ? obj.freq
 75 |     : obj.midi ? midiToFreq(obj.midi)
 76 |     : obj.note ? noteToFreq(obj.note)
 77 |     : isA('string', obj) ? noteToFreq(obj)
 78 |     : isA('number', obj) ? obj
 79 |     : null
 80 | }
 81 | 
 82 | /**
 83 |  * Create an oscillator (an OscillatorNode)
 84 |  *
 85 |  * @function
 86 |  * @param {Object} config - may include:
 87 |  *
 88 |  * - type: one of the OscillatorNode types
 89 |  * - frequency (or freq): the oscillator frequency (can be a signal)
 90 |  * - detune: the detune in cents (can be a signal)
 91 |  * - note: the note name to get the frequency from (if frequency is not present)
 92 |  * - midi: the note midi number to get the frequency from (if frequency is not present)
 93 |  * - context: the audio context to use
 94 |  *
 95 |  * Notice that instead of a config object, this function also accepts a note
 96 |  * name or a frequency value (see examples)
 97 |  *
 98 |  * @return {AudioNode} the oscillator
 99 |  * @example
100 |  * // basic usage
101 |  * osc({ type: 'sine', frequency: 880 })
102 |  * osc({ note: 'C4', type: 'square', detune: -10 })
103 |  * // parameter modulation
104 |  * osc({ freq: 1500, detune: osc({ freq: 20}) })
105 |  * osc({ freq: envelope(...), type: 'square' })
106 |  * // without configuration object
107 |  * osc('C4')
108 |  * osc(1200)
109 |  */
110 | export const osc = create.bind(null, null)
111 | 
112 | /**
113 |  * Create a sine oscillator. An alias for `osc({ type: 'sine', ... })`
114 |  * @function
115 |  * @see osc
116 |  * @param {Object} config - Same as `osc` function, but without 'type'
117 |  * @return {AudioNode} the oscillator
118 |  * @example
119 |  * sine('C4')
120 |  * sine({ midi: 70, detune: -50 })
121 |  */
122 | export const sine = create.bind(null, 'sine')
123 | 
124 | /**
125 |  * Create a sawtooth oscillator. An alias for `osc({ type: 'sawtooth', ... })`
126 |  * @function
127 |  * @see osc
128 |  * @param {Object} config - Same as `osc` function, but without 'type'
129 |  * @return {AudioNode} the oscillator
130 |  * @example
131 |  * saw('A3')
132 |  * saw({ freq: 440, detune: lfo(5, 10) })
133 |  */
134 | export const saw = create.bind(null, 'sawtooth')
135 | /**
136 |  * Create a square oscillator. An alias for `osc({ type: 'square', ... })`
137 |  * @function
138 |  * @see osc
139 |  * @param {Object} config - Same as `osc` function, but without 'type'
140 |  * @return {AudioNode} the oscillator
141 |  * @example
142 |  * square({ note: 'c#3', context: offline() })
143 |  */
144 | export const square = create.bind(null, 'square')
145 | 
146 | /**
147 |  * Create a triangle oscillator. An alias for `osc({ type: 'triangle', ... })`
148 |  * @function
149 |  * @see osc
150 |  * @param {Object} config - Same as `osc` function, but without 'type'
151 |  * @return {AudioNode} the oscillator
152 |  * @example
153 |  * triangle({ note: 'Bb4', detune: -10 })
154 |  */
155 | export const triangle = osc.bind(null, 'triangle')
156 | 
157 | /**
158 |  * Create an LFO (low frequency oscillator). It's a standard oscillator with
159 |  * some goodies to reduce the boilerplate code when used as signal modulator.
160 |  *
161 |  * @see osc
162 |  * @param {Options} config - May include any of the `osc` function plus:
163 |  *
164 |  * - tempo: the tempo used to calculate the frequency (it overrides the frequency parameter)
165 |  * - division: the number of subdivisions of the tempo (defaults to 1)
166 |  * - amplitude: the amplitude of the oscillator
167 |  *
168 |  * @example
169 |  * sine({ note: 'C4', detune: lfo({ freq: 5, amplitude: 50 }) })
170 |  * sine({ note: 'A4', detune: lfo({ amplitude: 10, tempo: 120, division: 3 })
171 |  */
172 | export function lfo (opts) {
173 |   opts = opts || OPTS
174 |   var node = osc(opts)
175 |   if (opts.tempo) node.frequency.value = tempoToFreq(opts.tempo, opts.division)
176 |   return mult(opts.amplitude || 1, node)
177 | }
178 | 
179 |
180 |
181 | 182 | 183 | 184 | 185 |
186 | 187 |
188 | 189 | 192 | 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/routing.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | routing.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

routing.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
import { when } from './context'
 42 | import { slice, isArray } from './utils'
 43 | 
 44 | /**
 45 |  * This module provides two ways to route nodes:
 46 |  *
 47 |  * - In series: A -> B -> C -> D, using the `connect` function
 48 |  * - In parallel: in -> [A, B, C] -> out, using the `add` function
 49 |  * @module routing
 50 |  */
 51 | 
 52 | /**
 53 |  * Connect nodes in series: A -> B -> C -> D.
 54 |  * @param {Array<AudioNode>} nodes - the list of nodes to be connected
 55 |  * @return {AudioNode} the resulting audio node
 56 |  */
 57 | export function conn (nodes) {
 58 |   nodes = isArray(nodes) ? nodes : slice.call(arguments)
 59 |   if (!nodes.length) return null
 60 |   else if (nodes.length === 1) return nodes[0]
 61 | 
 62 |   var node = nodes[0]
 63 |   if (!node.duration) node.duration = 0
 64 |   var last = nodes.reduce(function (src, dest) {
 65 |     src.connect(dest)
 66 |     node.duration = Math.max(node.duration, dest.duration || 0)
 67 |     return dest
 68 |   })
 69 |   overrideConnect(node, last)
 70 |   var startables = nodes.slice(1).filter(isStartable)
 71 |   if (startables.length) {
 72 |     var _start = node.start
 73 |     node.start = function (time) {
 74 |       if (_start) _start.call(node, time)
 75 |       startables.forEach(function (node) { node.start(time) })
 76 |       if (node.duration) node.stop(time + node.duration)
 77 |     }
 78 |     var _stop = node.stop
 79 |     node.stop = function (time) {
 80 |       var t = 0
 81 |       startables.reverse()
 82 |       startables.forEach(function (node) {
 83 |         t = t || when(time, null, node.context)
 84 |         node.stop(t)
 85 |         t += node.release || 0
 86 |       })
 87 |       if (_stop) _stop.call(node, t)
 88 |     }
 89 |   }
 90 |   return node
 91 | }
 92 | 
 93 | // TODO: A RESOLVER: add debe tener onended cuando acaben todos sus nodos
 94 | /**
 95 |  * Connect nodes in parallel in order to add signals. This is one of the
 96 |  * routing functions (the other is `connect`).
 97 |  * @param {...AudioNode} nodes - the nodes to be connected
 98 |  * @return {AudioNode} the resulting audio node
 99 |  * @example
100 |  * add(sine(400), sine(401)).start()
101 |  */
102 | export function add (nodes) {
103 |   nodes = isArray(nodes) ? nodes : slice.call(arguments)
104 |   if (!nodes.length) return null
105 |   else if (nodes.length === 1) return nodes[0]
106 | 
107 |   var context = nodes[0].context
108 |   var input = context.createGain()
109 |   input.id = 'ADDin'
110 |   var output = context.createGain()
111 |   output.id = 'ADDout'
112 |   // the connection loop: connect input to all nodes. all nodes to output.
113 |   nodes.forEach(function (node) {
114 |     if (node.numberOfInputs) input.connect(node)
115 |     node.connect(output)
116 |   })
117 |   // this must be after the connection loop
118 |   overrideConnect(input, output)
119 |   var node = lifecycle(input, nodes)
120 |   addOnEndedEvent(node)
121 |   return node
122 | }
123 | 
124 | // make trigger an onended event when all startable node ended
125 | function addOnEndedEvent (node) {
126 |   if (!node.dependents) return
127 |   var length = node.dependents.length
128 |   function triggerEnded () {
129 |     length--
130 |     if (!length && node.onended) node.onended()
131 |   }
132 |   node.dependents.forEach(function (node) {
133 |     node.onended = triggerEnded
134 |   })
135 | }
136 | 
137 | // overrides the node's connect function to use output node
138 | function overrideConnect (node, output) {
139 |   node.output = node
140 |   node.connect = function (dest) {
141 |     output.connect(dest)
142 |     return node
143 |   }
144 |   return node
145 | }
146 | 
147 | /**
148 |  * Return if a node is startable or note
149 |  * @private
150 |  */
151 | function isStartable (node) { return node && typeof node.start === 'function' }
152 | 
153 | /**
154 |  * Plug something (a value, a node) into a node parameter
155 |  * @param {String} name - the parameter name
156 |  * @param {AudioNode|Object} value - the value (can be a signal)
157 |  * @param {AudioNode} target - the target audio node
158 |  * @return {AudioNode} the modulator signal if any or undefined
159 |  * @private
160 |  */
161 | export function plug (name, value, node) {
162 |   if (value === null || typeof value === 'undefined') {
163 |     // do nothing
164 |   } else if (typeof value.connect === 'function') {
165 |     node[name].value = 0
166 |     value.conn(node[name])
167 |     return value
168 |   } else if (node[name]) {
169 |     node[name].value = value
170 |   }
171 | }
172 | 
173 | /**
174 |  * Override start and stop functions (if necessary) to handle node dependencies
175 |  * lifecycle.
176 |  * @private
177 |  */
178 | export function lifecycle (node, dependents) {
179 |   dependents = dependents.filter(isStartable)
180 |   if (dependents.length) {
181 |     var _start = node.start
182 |     var _stop = node.stop
183 |     node.start = function (time) {
184 |       var res = _start ? _start.call(node, time) : void 0
185 |       dependents.forEach(function (d) { if (d.start) d.start(time) })
186 |       if (node.start.then) node.start.then(time, node, dependents)
187 |       return res
188 |     }
189 |     node.stop = function (time) {
190 |       var res = _stop ? _stop.call(node, time) : void 0
191 |       dependents.forEach(function (d) { if (d.stop) d.stop(time) })
192 |       return res
193 |     }
194 |     node.dependents = dependents
195 |   }
196 |   return node
197 | }
198 | 
199 |
200 |
201 | 202 | 203 | 204 | 205 |
206 | 207 |
208 | 209 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/signals.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | signals.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

signals.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/** @module signals */
 42 | 
 43 | import { OPTS, isA } from './utils'
 44 | import { context } from './context'
 45 | import { add, conn, lifecycle, plug } from './routing'
 46 | import { dBToGain, levelToGain } from './units'
 47 | 
 48 | /**
 49 |  * Create a GainNode
 50 |  *
 51 |  * @param {Object} config - may include:
 52 |  *
 53 |  * - value (or gain): the gain (can be a number or a signal)
 54 |  * - dB (or db): the gain in dB (only if gain is not specified)
 55 |  * - level: the gain in a logaritmic scale from 0 to 100
 56 |  * - context: the audio context to use to create the signal
 57 |  *
 58 |  * This funcion accepts a number with the gain value instead of a config object.
 59 |  *
 60 |  * @return {AudioNode} a GainNode
 61 |  * @example
 62 |  * gain({ dB: -3, context: <AudioContext> })
 63 |  * // with modulation (kind of tremolo)
 64 |  * conn(sine(400), gain({ value: sine(10) }))
 65 |  * // passing a number instead of an object
 66 |  * conn(sine('C4'), gain(0.3))
 67 |  */
 68 | export function gain (opts) {
 69 |   opts = opts || OPTS
 70 |   var node = context(opts.context).createGain()
 71 |   return lifecycle(node, [
 72 |     plug('gain', getGain(opts), node)
 73 |   ])
 74 | }
 75 | 
 76 | // given an config object, return the gain
 77 | function getGain (opts) {
 78 |   return isA('number', opts) ? opts
 79 |     : opts.value ? opts.value
 80 |     : opts.gain ? opts.gain
 81 |     : isA('number', opts.dB) ? dBToGain(opts.dB)
 82 |     : isA('number', opts.db) ? dBToGain(opts.db)
 83 |     : isA('number', opts.level) ? levelToGain(opts.level)
 84 |     : null
 85 | }
 86 | 
 87 | /**
 88 |  * Create a constant signal. Normally you will use it in combination with
 89 |  * envelopes or modulators.
 90 |  *
 91 |  * @param {Integer} value - the value of the constant
 92 |  * @param {Object} options - (Optional) options may include:
 93 |  *
 94 |  * - context: the audio context to use to create the signal
 95 |  *
 96 |  * @return {AudioNode} the constant audio node
 97 |  * @example
 98 |  * sine(constant(440)).start()
 99 |  */
100 | export function constant (value, o) {
101 |   // TODO: cache buffer
102 |   var ctx = context(o ? o.context : null)
103 |   var source = ctx.createBufferSource()
104 |   source.loop = true
105 |   source.buffer = ctx.createBuffer(1, 2, ctx.sampleRate)
106 |   var data = source.buffer.getChannelData(0)
107 |   data[0] = data[1] = value
108 |   return source
109 | }
110 | 
111 | /**
112 |  * Create a signal source. You will use signals to change parameters of a
113 |  * audio node after starting. See example.
114 |  * @param {Integer} value - the value of the constant
115 |  * @param {Object} options - (Optional) options may include:
116 |  *
117 |  * - context: the audio context to use to create the signal
118 |  *
119 |  * @return {AudioParam} the constant audio node
120 |  * @example
121 |  * var freq = signal(440)
122 |  * sine(freq).start()
123 |  * freq.value.linearRampToValueAtTime(880, after(5))
124 |  */
125 | export function signal (value, opts) {
126 |   var signal = gain(value, opts)
127 |   conn(constant(1, opts), signal).start()
128 |   signal.signal = signal.gain
129 |   return signal
130 | }
131 | 
132 | /**
133 |  * Create a node that bypasses the signal
134 |  * @param {Object} config - may include:
135 |  *
136 |  * - context: the audio context to use
137 |  *
138 |  * @return {AudioNode} the bypass audio node
139 |  * @example
140 |  * conn(sine('C4'), add(bypass(), dly(0.2)))
141 |  */
142 | export function bypass (o) {
143 |   return context(o ? o.context : null).createGain()
144 | }
145 | 
146 | /**
147 |  * Multiply a signal.
148 |  *
149 |  * @param {Integer} value - the value
150 |  * @param {Integer|AudioNode} signal - the signal to multiply by
151 |  * @example
152 |  * // a vibrato effect
153 |  * sine(440, { detune: mult(500, sine(2)) })
154 |  */
155 | export function mult (value, signal) {
156 |   if (isA('number', signal)) return value * signal
157 |   return conn(signal, gain({ value: value, context: signal.context }))
158 | }
159 | 
160 | /**
161 |  * Scale a signal. Given a signal (between -1 and 1) scale it to fit in a range.
162 |  * @param {Integer} min - the minimum of the range
163 |  * @param {Integer} max - the minimum of the range
164 |  * @param {AudioNode} source - the signal to scale
165 |  * @return {AudioNode} the scaled signal node
166 |  * @example
167 |  * // create a frequency envelope between 440 and 880 Hz
168 |  * sine(scale(440, 880, adsr(0.1, 0.01, 1, 1)))
169 |  */
170 | export function scale (min, max, source) {
171 |   var ctx = source
172 |   if (source.numberOfInputs) source = conn(constant(1, ctx), source)
173 |   var delta = max - min
174 |   return add(constant(min, ctx), mult(delta, source))
175 | }
176 | 
177 |
178 |
179 | 180 | 181 | 182 | 183 |
184 | 185 |
186 | 187 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: hsl(104, 100%, 24%); 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: hsl(104, 100%, 24%); } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: hsl(240, 100%, 50%); } 17 | 18 | /* a comment */ 19 | .com { 20 | color: hsl(0, 0%, 60%); } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: hsl(240, 100%, 32%); } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: hsl(240, 100%, 40%); } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #000000; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #000000; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #000000; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /docs/units.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | units.js - Postman Documentation 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 28 | 29 |
30 | 31 |

units.js

32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
/**
 42 |  * This module provides some conversion utilities between different units
 43 |  * used when dealing with audio
 44 |  *
 45 |  * @example
 46 |  * import { noteToFreq } from 'synth-kit'
 47 |  *
 48 |  * noteToFreq('A4') // => 440
 49 |  * @module units
 50 |  */
 51 | import Note from 'note-parser'
 52 | var pow = Math.pow
 53 | 
 54 | /**
 55 |  * Convert from note midi number to frequency
 56 |  * @param {Number} midi - the midi note number (can have decimals)
 57 |  * @param {Number} tuning - (Optional) the frequency of a reference A4 note (440 by default)
 58 |  * @return {Number} the note frequency
 59 |  * @example
 60 |  * midiToFreq(69) // => 440
 61 |  * midiToFreq(69.5) // => 452.8929841231365
 62 |  * midiToFreq(70) // => 466.1637615180899
 63 |  */
 64 | export function midiToFreq (value, base) { return pow(2, (+value - 69) / 12) * (base || 440) }
 65 | 
 66 | /**
 67 |  * Convert from note name to frequency
 68 |  * @function
 69 |  * @param {String} note - the note name
 70 |  * @param {Number} tuning - (Optional) the tuning of A4 (440 by default)
 71 |  * @return {Number} the note frequency
 72 |  * @example
 73 |  * noteToFreq('C3') // => 130.8127826502993
 74 |  */
 75 | export var noteToFreq = Note.freq
 76 | 
 77 | /**
 78 |  * Convert from beats per minute to hertzs
 79 |  *
 80 |  * @param {Integer} bpm - the tempo
 81 |  * @param {Integer} sub - (Optional) subdivision (default 1)
 82 |  * @return {Float} the tempo expressed in hertzs
 83 |  * @example
 84 |  * tempoToFreq(120) // => 2
 85 |  * tempoToFreq(120, 4) // => 8
 86 |  */
 87 | export function tempoToFreq (bpm, sub) { return (bpm / 60) * (sub || 1) }
 88 | 
 89 | /**
 90 |  * Convert decibels into gain.
 91 |  * @param  {Number} db
 92 |  * @return {Number} the gain (from 0 to 1)
 93 |  * @example
 94 |  * dBToGain(-3) // => 0.7071067811865475
 95 |  */
 96 | export function dBToGain (db) { return pow(2, db / 6) }
 97 | 
 98 | /**
 99 |  * Convert from level (an equal power scale from 0 to 100) into gain.
100 |  * @param {Number} level - from 0 to 100
101 |  * @return {Number} gain (from 0 to 1)
102 |  * @example
103 |  * levelToGain(80) // => 0.9510565162951535
104 |  */
105 | export function levelToGain (level) { return Math.sin(0.5 * Math.PI * level / 100) }
106 | 
107 | /**
108 |  * Convert gain to decibels.
109 |  * @param  {Number} gain (0-1)
110 |  * @return {Decibels}
111 |  * @example
112 |  * s.gainToDb(0.3) // => -10.457574905606752
113 |  */
114 | export function gainToDb (gain) { return 20 * (Math.log(gain) / Math.LN10) }
115 | 
116 |
117 |
118 | 119 | 120 | 121 | 122 |
123 | 124 |
125 | 126 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /example/examples.js: -------------------------------------------------------------------------------- 1 | /* global SynthKit connect sine saw square triangle gain lowpass 2 | detune scale add perc 3 | */ 4 | SynthKit.live() 5 | 6 | // var out = SynthKit(ac, Meter(), true).start() 7 | 8 | // append(MeterCanvas(), document.getElementById('meter')) 9 | 10 | addTitle(1, 'Synthkit node examples') 11 | 12 | addTitle(2, 'Oscillators') 13 | addExample('Sine', function () { 14 | connect(sine(800)).start(0, 0, 1) 15 | }) 16 | 17 | addExample('Sawtooth', function () { 18 | connect(saw(440), gain(0.3)).start(0, 0, 1) 19 | }) 20 | addExample('Square', function () { 21 | connect(square(440), gain(0.4)).start(0, 0, 1) 22 | }) 23 | addExample('Triangle', function () { 24 | connect(triangle(440), gain(0.5)).start(0, 0, 1) 25 | }) 26 | addExample('PulseWave', function () { 27 | }) 28 | 29 | addTitle(2, 'Routing') 30 | addExample('Serial routing', function () { 31 | connect(square(800), lowpass(300), gain(5)).start(0, 0, 1) 32 | }) 33 | addExample('Parallel routing', function () { 34 | add(sine(440), sine(442)).start(0, 0, 1) 35 | }) 36 | addTitle(2, 'Modulation') 37 | addExample('Detune', function () { 38 | sine(1000, detune(15, 10)).start(0, 0, 1) 39 | }) 40 | addExample('Frequency by an oscillator', function () { 41 | sine(scale(400, 800, sine(2.5))).start(0, 0, 1) 42 | }) 43 | addExample('Frequency by an envelope', function () { 44 | sine(scale(400, 800, perc(0.5, 0.5))).start(0, 0, 1) 45 | }) 46 | 47 | addTitle(2, 'Envelopes') 48 | addExample('Percutive', function () { 49 | connect(square(880), perc()).start(0, 0, 1) 50 | }) 51 | addExample('AHD', function () { 52 | connect(sine(880), ahd(1, 1, 1)).start(0, 0, 5) 53 | }) 54 | addExample('ADSR', function () { 55 | connect(sine(880), adsr()).start(0, 0, 1) 56 | }) 57 | 58 | 59 | addTitle(2, 'Filters') 60 | addExample('lowpass', function () { 61 | connect(saw(1200), lowpass(400)).start(0, 0, 1) 62 | }) 63 | 64 | addTitle(2, 'Buffers') 65 | addExample('White Noise', function () { 66 | connect(white(), perc(null, 1)).start(0, 0, 1) 67 | }) 68 | addExample('Pulse', function () { 69 | var synth = Pulse(0.01) 70 | }) 71 | 72 | addTitle(2, 'Effects') 73 | addExample('DelaySignal', function () { 74 | var synth = (freq) => connect(Saw(freq), Perc(), Mix(0.2, DelaySignal(0.2))) 75 | }) 76 | addExample('Reverb', function () { 77 | var synth = (revLevel) => connect(sine(800), Perc(), Reverb(revLevel, 4, 3)) 78 | SynthKit(ac, synth(0), out).start(ac.currentTime) 79 | SynthKit(ac, synth(0.4), out).start(ac.currentTime + 1) 80 | SynthKit(ac, synth(0.8), out).start(ac.currentTime + 2) 81 | }) 82 | addExample('Overdrive', function () { 83 | var Synth = (drive) => connect( 84 | Saw(440), Overdrive(drive), Perc(0.1, 0.2, 2) 85 | ) 86 | start(ac, Synth(0), output, ac.currentTime) 87 | start(ac, Synth(0.5), output, ac.currentTime + 1) 88 | start(ac, Synth(0.9), output, ac.currentTime + 2) 89 | }) 90 | 91 | addTitle(2, 'Instruments') 92 | addTitle(3, 'Drums') 93 | addExample('Cowbell', function () { 94 | start(ac, Cowbell(), output) 95 | }) 96 | addExample('Clave', function () { 97 | start(ac, Clave(), output) 98 | }) 99 | 100 | addExample('Kick', function () { 101 | start(ac, Kick(), output) 102 | }) 103 | 104 | addExample('Snare', function () { 105 | start(ac, Snare(), output) 106 | }) 107 | 108 | addExample('Maracas', function () { 109 | start(ac, Maracas(), output) 110 | }) 111 | 112 | addExample('Cymbal', function () { 113 | start(ac, Cymbal(), output) 114 | }) 115 | 116 | addTitle(3, 'Synths') 117 | addExample('Bells', function () { 118 | start(ac, Bell(880), output) 119 | }) 120 | addExample('Organ', function () { 121 | start(ac, Organ(880), output, null, 1) 122 | }) 123 | addExample('Marimba', function () { 124 | start(ac, Marimba(440), output) 125 | }) 126 | addExample('One', function () { 127 | start(ac, One(440), output, null, 0.2) 128 | }) 129 | addExample('Duo', function () { 130 | start(ac, Duo(440), output, null, 0.2) 131 | start(ac, Duo(500), output, ac.currentTime + 1, 0.2) 132 | }) 133 | 134 | addExample('AM', function () { 135 | function Togain () { 136 | return WaveShaper(WaveShaperCurve('audioTogain', function (i) { 137 | return (i + 1) / 2 138 | })) 139 | } 140 | function FiltEnv (freq, octaves, attack, release) { 141 | return Scale(freq, freq * octaves, Perc(attack, release)) 142 | } 143 | var Synth = function (freq, harmonicity) { 144 | harmonicity = harmonicity || 3 145 | return connect( 146 | sine(freq), 147 | gain(connect( 148 | square(freq * harmonicity), 149 | ADSR(0.5, 0, 1, 1), 150 | Togain() 151 | )), 152 | lowpass(FiltEnv(freq, 7, 0.5, 2)), 153 | ADSR(null, null, null, 2) 154 | ) 155 | } 156 | start(ac, Synth(440), output, null, 0.2) 157 | }) 158 | 159 | // //// HELPERS ////// 160 | 161 | var main 162 | function append (el, parent) { 163 | main = main || document.getElementById('main') 164 | parent = parent || main 165 | parent.appendChild(el) 166 | } 167 | 168 | function addTitle (num, text) { 169 | var title = document.createElement('h' + num) 170 | title.innerHTML = text 171 | append(title) 172 | } 173 | 174 | function addExample (name, cb) { 175 | var a = document.createElement('a') 176 | a.innerHTML = name 177 | a.href = '#' 178 | a.onclick = function (e) { 179 | e.preventDefault() 180 | cb() 181 | } 182 | append(a) 183 | var sep = document.createElement('span') 184 | sep.innerHTML = ' | ' 185 | append(sep) 186 | } 187 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SynthKit Examples 5 | 6 | 7 | 8 | 14 | 15 |
16 |
SynthKit
17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/livecoding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SynthKit Examples 5 | 6 | 7 | 8 | 14 | 15 |
16 |

SynthKit livecoding

17 |

Open the development console

18 |
19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/livecoding.js: -------------------------------------------------------------------------------- 1 | var SynthKit = require('../live') 2 | 3 | SynthKit.live() 4 | -------------------------------------------------------------------------------- /example/soundfont.js: -------------------------------------------------------------------------------- 1 | var soundfont = require('../instruments/soundfont') 2 | 3 | var marimba = soundfont.instrument('Marimba') 4 | 5 | function range (start, count) { 6 | return Array.apply(0, Array(count + 1)).map(function (element, index) { return index + start }) 7 | } 8 | 9 | var piano = soundfont.instrument('Acoustic Grand Piano') 10 | piano.on('ready', function () { 11 | console.log('Piano') 12 | 'C4 E4 G5 C5 E4 G5 C4'.split(' ').forEach(function (note, i) { 13 | piano.start(note, null, i * 0.5) 14 | }) 15 | }) 16 | 17 | marimba.on('ready', function () { 18 | console.log('Marimba') 19 | range(60, 12).forEach(function (note, i) { 20 | marimba.start(note, null, i * 0.5) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /instruments/b3.js: -------------------------------------------------------------------------------- 1 | // https://teichman.org/blog/2011/05/roto.html 2 | // http://electricdruid.net/technical-aspects-of-the-hammond-organ/ 3 | // http://www.dairiki.org/HammondWiki/OriginalHammondLeslieFaq 4 | // http://www.stefanv.com/electronics/hammond_drawbar_science.html 5 | var _ = require('..') 6 | 7 | // http://electricdruid.net/technical-aspects-of-the-hammond-organ/ 8 | var RATIOS = [0.5, 1.498823530, 1, 2, 2.997647060, 4, 5.040941178, 5.995294120, 8] 9 | 10 | /** 11 | * Create a poor man's hammond-like sound 12 | */ 13 | function b3 (preset, frequency) { 14 | if (typeof preset === 'string') preset = b3.presets[preset] || b3.parse(preset) 15 | else if (!Array.isArray(preset)) throw Error('Not valid B3 registration', preset) 16 | var osc = drawbars(preset) 17 | return frequency ? osc(frequency) : osc 18 | } 19 | 20 | b3.parse = function (str) { 21 | return (str.replace(/[^12345678]/g, '') + '00000000').slice(0, 9).split('').map(Math.abs) 22 | } 23 | 24 | // http://www.dairiki.org/HammondWiki/PopularDrawbarRegistrations 25 | b3.presets = { 26 | gospel: b3.parse('88 8000 008'), 27 | blues: b3.parse('88 8800 000'), 28 | bluesB: b3.parse('88 5324 588'), 29 | booker: b3.parse('88 8630 000'), 30 | onions: b3.parse('80 8800 008'), 31 | smith: b3.parse('88 8000 000'), 32 | mcgriff: b3.parse('86 8600 006'), 33 | errol: b3.parse('80 0008 888'), 34 | genesis: b3.parse('33 6866 330') 35 | } 36 | 37 | function drawbars (preset) { 38 | var gains = null // lazy 39 | return function (frequency) { 40 | gains = gains || preset.map(function (n) { 41 | return _.dB(-3 * (8 - n)) 42 | }) 43 | return _.oscBank(frequency, RATIOS, ['sine'], gains) 44 | } 45 | } 46 | 47 | module.exports = b3 48 | -------------------------------------------------------------------------------- /instruments/basic.js: -------------------------------------------------------------------------------- 1 | // Basic syntetizers 2 | var Kit = require('..') 3 | 4 | function ping (fq, opts) { 5 | opts = opts || {} 6 | fq = fq || 1000 7 | return Kit.conn(Kit.osc(opts.type, fq), Kit.perc(opts)) 8 | } 9 | 10 | module.exports = { ping: ping } 11 | -------------------------------------------------------------------------------- /instruments/emt140.js: -------------------------------------------------------------------------------- 1 | var Kit = require('..') 2 | var URL = 'https://danigb.github.io/sampled/IR/EMT140-Plate/samples/' 3 | 4 | function emt140 (name) { 5 | var c = Kit.convolve(null) 6 | var promise = emt140.fetch(name).then(function (buffer) { 7 | c.buffer = buffer 8 | console.log('Reverb ready', name, buffer) 9 | return c 10 | }) 11 | c.then = promise.then.bind(promise) 12 | return c 13 | } 14 | 15 | emt140.fetch = function (name) { 16 | name = name || 'Emt 140 Medium 2' 17 | name = name.toLowerCase().replace(/\s+/g, '_') + '.wav' 18 | return Kit.sample(URL + name) 19 | } 20 | 21 | emt140.NAMES = [ 'Emt 140 Bright 1', 'Emt 140 Bright 2', 'Emt 140 Bright 3', 22 | 'Emt 140 Bright 4', 'Emt 140 Dark 1', 'Emt 140 Bright 5', 'Emt 140 Dark 2', 23 | 'Emt 140 Dark 3', 'Emt 140 Dark 4', 'Emt 140 Dark 5', 'Emt 140 Medium 1', 24 | 'Emt 140 Medium 3', 'Emt 140 Medium 2', 'Emt 140 Medium 4', 'Emt 140 Medium 5' ] 25 | 26 | module.exports = emt140 27 | -------------------------------------------------------------------------------- /instruments/meter.js: -------------------------------------------------------------------------------- 1 | 2 | var cache = {} 3 | 4 | export function MeterCanvas (id, width, height) { 5 | id = id || '' 6 | if (!cache[id]) { 7 | cache[id] = document.createElement('canvas') 8 | cache[id].width = 600 9 | cache[id].height = 100 10 | } 11 | return cache[id] 12 | } 13 | 14 | export function Meter (props) { 15 | return function (ac) { 16 | var canvas = canvas || MeterCanvas() 17 | var ctx = canvas.getContext('2d') 18 | var w = canvas.width 19 | var h = canvas.height 20 | var running = false 21 | var analyser = ac.createAnalyser() 22 | var spectrum = Spectrum(analyser, w, h) 23 | var oscilloscope = Oscilloscope(analyser, w, h) 24 | analyser.fftSize = 1024 25 | analyser.start = function () { 26 | console.log('Start meter') 27 | running = true 28 | function paint () { 29 | ctx.clearRect(0, 0, w, h) 30 | spectrum(ctx) 31 | oscilloscope(ctx) 32 | if (running) window.requestAnimationFrame(paint) 33 | } 34 | window.requestAnimationFrame(paint) 35 | } 36 | return analyser 37 | } 38 | } 39 | 40 | function Spectrum (analyser, width, height) { 41 | var length = analyser.frequencyBinCount 42 | var data = new Uint8Array(length) 43 | var limit = Math.min(width, length) 44 | var ratio = height / 280 45 | var barHeight 46 | 47 | return function (ctx) { 48 | analyser.getByteFrequencyData(data) 49 | ctx.lineWidth = 0.5 50 | for (var i = 0; i < limit; i++) { 51 | barHeight = data[i] 52 | if (barHeight >= data[i + 1] && barHeight > data[i - 1]) { 53 | ctx.fillStyle = 'rgb(' + (barHeight + 100) + ',50,50)' 54 | } else { 55 | ctx.fillStyle = '#dedede' 56 | } 57 | ctx.fillRect(i, height - barHeight * ratio, 1, barHeight * ratio) 58 | } 59 | } 60 | } 61 | 62 | function Oscilloscope (analyser, width, height) { 63 | var zero, x 64 | var length = analyser.frequencyBinCount 65 | var limit = Math.min(length, width) 66 | var buffer = new Uint8Array(length) 67 | var scaling = height / 256 68 | 69 | return function draw (ctx) { 70 | analyser.getByteTimeDomainData(buffer) 71 | ctx.lineWidth = 0.5 72 | ctx.beginPath() 73 | zero = findFirstPositiveZeroCrossing(buffer, limit) 74 | for (x = 0; x < limit; x++) { 75 | ctx.lineTo(x, (256 - buffer[x + zero]) * scaling) 76 | } 77 | ctx.stroke() 78 | } 79 | } 80 | 81 | var ZERO = 128 82 | var MINVAL = ZERO + 6 83 | 84 | function findFirstPositiveZeroCrossing (buf, length) { 85 | var i = 0 86 | var lastZero = -1 87 | 88 | // advance until we're zero or negative 89 | while (i < length && buf[i] > ZERO) i++ 90 | 91 | if (i >= length) return 0 92 | 93 | // advance until we're above MINVAL, keeping track of last zero. 94 | while (i < length && buf[i] < MINVAL) { 95 | if (buf[i] >= 128 && lastZero === -1) { 96 | lastZero = i 97 | } 98 | i++ 99 | } 100 | 101 | // we may have jumped over MINVAL in one sample. 102 | if (lastZero === -1) lastZero = i 103 | 104 | if (i === length) return 0 // We didn't find any positive zero crossings 105 | 106 | return lastZero 107 | } 108 | -------------------------------------------------------------------------------- /instruments/modern.js: -------------------------------------------------------------------------------- 1 | import { OPTS, isA } from './utils' 2 | import { withDefaults } from './synth' 3 | import { samplingRate } from './context' 4 | import { osc } from './oscillators' 5 | import { constant, signal, gain } from './signals' 6 | import { conn, add } from './routing' 7 | import { filter, lowpass, hipass } from './filters' 8 | import { adsr } from './envelops' 9 | 10 | // A modern syntetizer 11 | 12 | var OSC_TYPES = ['sine', 'triangle', 'sawtooth', 'square'] 13 | 14 | export const synth = function (opts) { 15 | return conn( 16 | oscillators(opts.oscillators), 17 | vcf(opts.filter), vca(opts.vca)) 18 | } 19 | 20 | /** 21 | * Create a signal buy playin several oscillators at the same time 22 | * @param {Object} options - (Optional) with the following defaults: 23 | * 24 | * @example 25 | * oscillators({ 26 | * frequency: 440, 27 | * oscillators: { 28 | * sine: { gain 0.5, octave: 0 }, 29 | * sawtooth: { gain: 0.2, octave: -1, detine: 3 } 30 | * } 31 | * }) 32 | */ 33 | export const oscillators = withDefaults(function (opts) { 34 | var oscs = OSC_TYPES.map(function (name) { 35 | var opt = opts.oscillators['name'] 36 | return conn(osc(name, opts.frequency, opt), gain(opt.gain)) 37 | }) 38 | return conn(add(oscs), tone(opts.oscillators.tone)) 39 | }, { 40 | frequency: 440, 41 | sine: { gain: 0, octave: 0 }, 42 | triangle: { gain: 0, octave: 0 }, 43 | sawtooth: { gain: 0, octave: 0 }, 44 | square: { gain: 0, octave: 0 }, 45 | tone: 0 46 | }) 47 | 48 | /** 49 | * Create a tone node. 50 | * @param {Number} value - tone value (from -1 to 1) 51 | * @param {Object} options - (Optional) 52 | * @return {AudioNode} the tone 53 | * @example 54 | * conn(sine(300), tone(-0.5)) 55 | * @example 56 | * tone({ value: -0.5, context: }) 57 | */ 58 | export function tone (value, opts) { 59 | if (arguments.length === 1 && isA('object', value)) return tone(value.value, value) 60 | opts = opts || OPTS 61 | var nyquist = samplingRate(opts.context) / 2 62 | 63 | var src = conn(signal(nyquist, opts), gain(value, opts)) 64 | var tone = add( 65 | lowpass(add(src, constant(nyquist))), 66 | hipass(add(src, constant(-nyquist))) 67 | ) 68 | tone.value = signal.signal 69 | return tone 70 | } 71 | -------------------------------------------------------------------------------- /instruments/sf-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "Piano": [ 3 | "Acoustic Grand Piano", 4 | "Bright Acoustic Piano", 5 | "Electric Grand Piano", 6 | "Electric Piano 1", 7 | "Electric Piano 2", 8 | "Clavichord", 9 | "Harpsichord", 10 | "Honkytonk Piano" 11 | ], 12 | "Organ": [ 13 | "Church Organ", 14 | "Drawbar Organ", 15 | "Reed Organ", 16 | "Rock Organ", 17 | "Percussive Organ" 18 | ], 19 | "Guitar": [ 20 | "Acoustic Guitar Nylon", 21 | "Acoustic Guitar Steel", 22 | "Electric Guitar Clean", 23 | "Electric Guitar Jazz", 24 | "Electric Guitar Muted", 25 | "Distortion Guitar", 26 | "Guitar Fret Noise", 27 | "Guitar Harmonics", 28 | "Overdriven Guitar" 29 | ], 30 | "Bass": [ 31 | "Acoustic Bass", 32 | "Electric Bass Finger", 33 | "Electric Bass Pick", 34 | "Contrabass", 35 | "Fretless Bass", 36 | "Synth Bass 1", 37 | "Synth Bass 2", 38 | "Slap Bass 1", 39 | "Slap Bass 2" 40 | ], 41 | "Strings": [ 42 | "Cello", 43 | "Viola", 44 | "Violin", 45 | "Tremolo Strings", 46 | "Pizzicato Strings", 47 | "Synthstrings 1", 48 | "Synthstrings 2" 49 | ], 50 | "Reed": [ 51 | "Soprano Sax", 52 | "Alto Sax", 53 | "Tenor Sax", 54 | "Baritone Sax", 55 | "Bassoon", 56 | "Clarinet", 57 | "Oboe" 58 | ], 59 | "Brass": [ 60 | "Brass Section", 61 | "English Horn", 62 | "French Horn", 63 | "Muted Trumpet", 64 | "Trombone", 65 | "Trumpet", 66 | "Tuba", 67 | "Synthbrass 1", 68 | "Synthbrass 2" 69 | ], 70 | "Ensemble": [ 71 | "Choir Aahs", 72 | "Orchestra Hit", 73 | "Orchestral Harp", 74 | "String Ensemble 1", 75 | "String Ensemble 2" 76 | ], 77 | "Pipe": [ 78 | "Flute", 79 | "Ocarina", 80 | "Pan Flute", 81 | "Piccolo", 82 | "Recorder", 83 | "Whistle" 84 | ], 85 | "World": [ 86 | "Accordion", 87 | "Tango Accordion", 88 | "Bag Pipe", 89 | "Banjo", 90 | "Fiddle", 91 | "Harmonica", 92 | "Kalimba", 93 | "Koto", 94 | "Shakuhachi", 95 | "Shamisen", 96 | "Shanai", 97 | "Sitar" 98 | ], 99 | "Chromatic Percussion": [ 100 | "Celesta", 101 | "Dulcimer", 102 | "Glockenspiel", 103 | "Marimba", 104 | "Music Box", 105 | "Xylophone", 106 | "Tubular Bells", 107 | "Vibraphone", 108 | "Steel Drums", 109 | "Tinkle Bell" 110 | ], 111 | "Percussion": [ 112 | "Agogo", 113 | "Melodic Tom", 114 | "Woodblock", 115 | "Reverse Cymbal", 116 | "Timpani", 117 | "Synth Drum", 118 | "Taiko Drum" 119 | ], 120 | "Synth Lead": [ 121 | "Lead 1 Square", 122 | "Lead 2 Sawtooth", 123 | "Lead 3 Calliope", 124 | "Lead 4 Chiff", 125 | "Lead 5 Charang", 126 | "Lead 6 Voice", 127 | "Lead 7 Fifths", 128 | "Lead 8 Bass Lead" 129 | ], 130 | "Synth Pad": [ 131 | "Pad 1 New Age", 132 | "Pad 2 Warm", 133 | "Pad 3 Polysynth", 134 | "Pad 4 Choir", 135 | "Pad 5 Bowed", 136 | "Pad 6 Metallic", 137 | "Pad 7 Halo", 138 | "Pad 8 Sweep" 139 | ], 140 | "Sound Effects": [ 141 | "Applause", 142 | "Bird Tweet", 143 | "Blown Bottle", 144 | "Breath Noise", 145 | "Gunshot", 146 | "Fx 1 Rain", 147 | "Fx 2 Soundtrack", 148 | "Fx 3 Crystal", 149 | "Fx 4 Atmosphere", 150 | "Fx 5 Brightness", 151 | "Fx 6 Goblins", 152 | "Fx 7 Echoes", 153 | "Fx 8 Scifi", 154 | "Helicopter", 155 | "Seashore", 156 | "Synth Voice", 157 | "Telephone Ring", 158 | "Voice Oohs" 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /instruments/soundfont.js: -------------------------------------------------------------------------------- 1 | var _ = require('..') 2 | var Note = require('note-parser') 3 | function is (t, x) { return typeof x === t } 4 | 5 | var GLEITZ = 'https://gleitz.github.io/midi-js-soundfonts/' 6 | function isSoundfontURL (name) { return /\.js(\?.*)?$/i.test(name) } 7 | /** 8 | * Get the url of a soundfont name 9 | */ 10 | function url (name, sf, format) { 11 | if (isSoundfontURL(name)) return name 12 | format = format === 'ogg' ? '-ogg' : '-mp3' 13 | sf = sf === 'FluidR3_GM' ? 'FluidR3_GM/' : 'MusyngKite/' 14 | return GLEITZ + sf + name.toLowerCase().replace(/\s+/g, '_') + format + '.js' 15 | } 16 | 17 | var LOAD_DEFAULTS = { 18 | format: 'mp3', soundfont: 'MusyngKite', 19 | fetch: _.fetch, decodeAudio: _.decodeAudio 20 | } 21 | 22 | /** 23 | * Load a MIDI.js prerendered soundfont file. 24 | * load('Acoustic Grand Piano').then(function (buffers) { 25 | * buffer[60] // => for midi note 60 ('C4') 26 | }) 27 | */ 28 | function load (name, options) { 29 | var o = Object.assign({}, options, LOAD_DEFAULTS) 30 | var promise = is('function', name.then) ? name 31 | : o.fetch(url(name, o.soundfont, o.format), 'text') 32 | return promise.then(decodeSoundfont(o.context, o.decodeAudio)) 33 | } 34 | 35 | var INST_DEFAULTS = { 36 | gain: 1, filter: { type: 'lowpass', frequency: '22000' }, 37 | attack: 0.01, decay: 0.1, sustain: 0.8, release: 0.4 38 | } 39 | 40 | /** 41 | * Create a soundfont instrument. 42 | * The configuration object can contain the following options: 43 | * @param {String} name - the soundfont name 44 | * @param {Object} options - (Optional) soundfont options 45 | * @return {Instrument} the soundfont instrument 46 | * @example 47 | * var piano = Soundfont.instrument('Acoustic Grand Piano', { gain: 1.5 }) 48 | * piano.on('ready', function () { 49 | * piano.start('C4') 50 | * }) 51 | */ 52 | function instrument (name, config) { 53 | var buffers = null 54 | var sample = function (midi) { 55 | if (buffers) return buffers[midi] 56 | console.warn('Instrument "' + name + '" not loaded yet.') 57 | return _.gen(function () { return 0 }, 2) 58 | } 59 | var synth = function (o) { 60 | console.log('sf synth', o) 61 | return _.connect( 62 | _.source(sample(o.midi), o), 63 | _.filter(o.filter.type, o.filter.frequency, o.filter), 64 | /* adsr(o), */ 65 | _.gain(o) 66 | ) 67 | } 68 | 69 | synth = _.withOptions(synth, { 70 | defaults: Object.assign({}, INST_DEFAULTS, config), 71 | toOptions: toOptions, 72 | prepare: prepareOptions 73 | }) 74 | var sf = _.inst(synth, _.dest()) 75 | load(name, config).then(function (result) { 76 | buffers = result 77 | sf.event('ready', sf, name) 78 | }) 79 | return sf 80 | } 81 | 82 | function toOptions (note, context) { 83 | return is('number', note) ? { midi: note, context: context } 84 | : { note: note, context: context } 85 | } 86 | 87 | function prepareOptions (o) { 88 | var note = Note.parse(o.name || o.note) 89 | if (!o.note && note) o.note = Note.build(note) 90 | if (!o.midi && note) o.midi = note.midi 91 | if (!o.frequency && o.midi) o.frequency = Math.pow(2, (o.midi - 69) / 12) * 440 92 | if (!o.detune) { 93 | o.detune = Math.floor((o.midi % 1) * 100) 94 | if (o.detune) o.midi = Math.floor(o.midi) 95 | } 96 | return o 97 | } 98 | 99 | /** 100 | * Decode a soundfont text into a map of midi notes to audio buffers. 101 | * Can be partially applied. 102 | * 103 | * @param {AudioContext} - the audio context (or null to use the default context) 104 | * @param {String} content - the midi.js encoded soundfont text 105 | */ 106 | function decodeSoundfont (ac, decodeAudio) { 107 | var ctx = _.context(ac) 108 | return function (content) { 109 | var sf = parseMidijs(content) 110 | var names = Object.keys(sf) 111 | var promises = names.map(function (name) { 112 | return Promise.resolve(sf[name]).then(decodeBase64Audio).then(decodeAudio(ctx)) 113 | }) 114 | return Promise.all(promises).then(function (bufferList) { 115 | return names.reduce(function (buffers, name, i) { 116 | buffers[Note.midi(name)] = bufferList[i] 117 | return buffers 118 | }, {}) 119 | }) 120 | } 121 | } 122 | 123 | function parseMidijs (data) { 124 | var begin = data.indexOf('MIDI.Soundfont.') 125 | if (begin < 0) throw Error('Invalid MIDI.js Soundfont format') 126 | begin = data.indexOf('=', begin) + 2 127 | var end = data.lastIndexOf(',') 128 | return JSON.parse(data.slice(begin, end) + '}') 129 | } 130 | 131 | function decodeBase64Audio (source) { 132 | var i = source.indexOf(',') 133 | return base64Decode(source.slice(i + 1)).buffer 134 | } 135 | 136 | // DECODE UTILITIES 137 | function b64ToUint6 (nChr) { 138 | return nChr > 64 && nChr < 91 ? nChr - 65 139 | : nChr > 96 && nChr < 123 ? nChr - 71 140 | : nChr > 47 && nChr < 58 ? nChr + 4 141 | : nChr === 43 ? 62 142 | : nChr === 47 ? 63 143 | : 0 144 | } 145 | 146 | // Decode Base64 to Uint8Array 147 | // --------------------------- 148 | function base64Decode (sBase64, nBlocksSize) { 149 | var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, '') 150 | var nInLen = sB64Enc.length 151 | var nOutLen = nBlocksSize 152 | ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize 153 | : nInLen * 3 + 1 >> 2 154 | var taBytes = new Uint8Array(nOutLen) 155 | 156 | for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { 157 | nMod4 = nInIdx & 3 158 | nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4 159 | if (nMod4 === 3 || nInLen - nInIdx === 1) { 160 | for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { 161 | taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255 162 | } 163 | nUint24 = 0 164 | } 165 | } 166 | return taBytes 167 | } 168 | 169 | module.exports = { url: url, load: load, instrument: instrument } 170 | -------------------------------------------------------------------------------- /instruments/tr808.js: -------------------------------------------------------------------------------- 1 | var _ = require('..') 2 | 3 | function snare () { 4 | return _.connect(_.white(), _.perc()) 5 | } 6 | 7 | module.exports = { 8 | snare: snare 9 | } 10 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": ["lib", "README.md"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|docs)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": true, 16 | "monospaceLinks": true 17 | }, 18 | "opts": { 19 | "destination": "docs/", 20 | "encoding": "utf8", 21 | "private": false, 22 | "recurse": false, 23 | "template": "./node_modules/postman-jsdoc-theme" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/buffers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In synth-kit buffers can be generated (with the `gen` function) or 3 | * retrieved from an audio file (with the `sample`) function 4 | * 5 | * @module buffers 6 | */ 7 | import { isA, OPTS } from './utils' 8 | import { context, samplingRate } from './context' 9 | import { lifecycle, plug } from './routing' 10 | import { load } from './load' 11 | 12 | /** 13 | * Create a buffer source (a BufferSourceNode) 14 | * @param {Buffer|Function} buffer - the buffer (or a function that returns a buffer) 15 | * @param {Object} options - (Optional) options can include: 16 | * 17 | * - loop: set to true to loop the buffer 18 | * - detune: the detune amount (can be a number or a signal) 19 | * - context: the audio context to use 20 | * 21 | * @return {AudioNode} a BufferSourceNode 22 | * @example 23 | * source(sample('snare.wav')).start() 24 | * source(sample('amen-break.mp3'), { loop: true }) 25 | */ 26 | export function source (buffer, o) { 27 | o = o || OPTS 28 | var src = context(o.context).createBufferSource() 29 | src.buffer = isA('function', buffer) ? buffer() : buffer 30 | if (!src.buffer) console.warn('Buffer not ready.') 31 | if (o.loop) src.loop = true 32 | return lifecycle(src, [ 33 | plug('detune', o.detune, src) 34 | ]) 35 | } 36 | 37 | /** 38 | * Fetch a remote audio sample. You get a function that returns an AudioBuffer 39 | * when loaded or null otherwise. Or you can chain like a promise. 40 | * 41 | * @param {String} url - the url of the file 42 | * @param {Object} options - (Optional) options can include: 43 | * 44 | * - context: the audio context to use 45 | * 46 | * @return {Function} a function the returns the buffer 47 | * 48 | * @example 49 | * source(sample('server.com/audio-file.mp3')).start() 50 | * @example 51 | * // use the Promise like interface 52 | * sample('audio-file.mp3').then(function (buffer) { 53 | * source(buffer).start() 54 | * }) 55 | */ 56 | export function sample (url, opts) { 57 | var bf = null 58 | function buffer () { return bf } 59 | var promise = load(url, opts).then(function (buffer) { 60 | buffer.url = url 61 | bf = buffer 62 | return bf 63 | }) 64 | buffer.then = promise.then.bind(promise) 65 | return buffer 66 | } 67 | 68 | /** 69 | * Generate a BufferNode. It returns a no-parameter function that 70 | * returns a buffer. This way, it's easy to memoize (cache) buffers. 71 | * 72 | * @param {Function|Array} generators - a generator or a list of 73 | * generators (to create a buffer with multiple channels) 74 | * @param {Integer} samples - the length in samples 75 | * @param {Object} options - (Optional) options can include: 76 | * 77 | * - reverse: set to true to reverse the generated buffer 78 | * - context: the audio context to use 79 | * 80 | * @return {Function} a function with no parameters that returns the desired buffer 81 | */ 82 | export function gen (generators, samples, o) { 83 | samples = samples || 2 84 | o = o || OPTS 85 | return function () { 86 | if (!Array.isArray(generators)) generators = [ generators ] 87 | var reverse = o.reverse 88 | var numOfChannels = generators.length 89 | var ctx = context(o.context) 90 | 91 | var buffer = ctx.createBuffer(numOfChannels, samples, samplingRate(ctx)) 92 | for (var ch = 0; ch < numOfChannels; ch++) { 93 | generateData(generators[ch], buffer.getChannelData(ch), samples, reverse) 94 | } 95 | return buffer 96 | } 97 | } 98 | 99 | function generateData (generator, data, samples, reverse) { 100 | for (var i = 0; i < samples; i++) { 101 | data[i] = generator(reverse ? samples - i : i) 102 | } 103 | } 104 | 105 | /** 106 | * White noise source node. 107 | * @param {Integer} length - lenth in samples 108 | * @param {Object} options - (Optional) the same options that `source` function 109 | * @return {AudioNode} the white noise audio node generator 110 | * @see source 111 | * @example 112 | * conn(white(seconds(1)), perc(), dest()).start() 113 | */ 114 | export function white (samples, options) { 115 | if (!isA('number', samples)) samples = samplingRate(options) 116 | return source(gen(white.generator, samples, options), options) 117 | } 118 | white.generator = function () { return Math.random() * 2 - 1 } 119 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * In synth-kit most of the functions accepts an optional AudioContext as 3 | * last parameter. If no one is provided, synth-kit creates a singleton 4 | * AudioContext using ['audio-context'](npmjs.com/package/audio-context) module. 5 | * 6 | * @module context 7 | */ 8 | import ac from 'audio-context' 9 | 10 | // Shim to make connect chainable (soon to be implemented native) 11 | if (ac && ac.createGain) { 12 | var proto = Object.getPrototypeOf(Object.getPrototypeOf(ac.createGain())) 13 | var _connect = proto.connect 14 | proto.connect = function () { 15 | _connect.apply(this, arguments) 16 | console.log('connect!', this, arguments[0]) 17 | return this 18 | } 19 | } 20 | 21 | /** 22 | * Get the audio context. 23 | * @param {AudioContext} context - (Optional) if given, it return itself. If 24 | * nothing passed, it returns the AudioContext singleton instance 25 | * @return {AudioContext} the audio context 26 | * @example 27 | * // normally you won't do this: 28 | * var gain = context().createGain() 29 | */ 30 | export function context (ctx) { return ctx || ac } 31 | 32 | /** 33 | * Get the audio context's destination 34 | * @param {AudioContext} context - (Optional) an alternate audio context 35 | * @return {AudioContext} the audio context destination 36 | * @example 37 | * conn(sine(300), dest()).start() 38 | */ 39 | export function dest (context) { return (context || ac).destination } 40 | 41 | /** 42 | * Get audio context's current time 43 | * @param {AudioContext} context - (Optional) an optional audio context 44 | * @return {Number} the time in seconds relative to the AudioContext creation 45 | * @example 46 | * now() // => 3.3213 47 | */ 48 | export function now (context) { return (context || ac).currentTime } 49 | 50 | /** 51 | * Get a valid time 52 | * @param {Float} time - the time (equal or greater than now(), otherwise, ignored) 53 | * @param {Float} delay - the delay 54 | * @param {AudioContext} context - (Optional) an optional audio context 55 | * @example 56 | * now() // => 0.7 57 | * time(0.2) // => 0.7 58 | * time(1) // => 1 59 | * time(0.2, 1) // => 1.7 (time is ignored because is < than now()) 60 | */ 61 | export function when (time, delay, ctx) { 62 | return Math.max(now(ctx), time || 0) + (delay || 0) 63 | } 64 | 65 | /** 66 | * Get time after n seconds (from now) 67 | * @function 68 | * @param {Float} delay - the delay 69 | * @param {AudioContext} context - (Optional) an optional audio context 70 | * @return {Float} time in seconds 71 | * @example 72 | * now() // => 0.785 73 | * after(1) // => 1.785 74 | */ 75 | export var after = when.bind(0) 76 | 77 | /** 78 | * Get audio context sampling rate 79 | * @param {AudioContext} context - (Optional) the audio context 80 | * @return {Integer} the context's sampling rate 81 | * @example 82 | * samplingRate() // => 44100 83 | */ 84 | export function samplingRate (ctx) { return context(ctx).sampleRate } 85 | 86 | /** 87 | * Convert from seconds to samples (using AudioContext sampling rate) 88 | * @param {Float} seconds - the number of seconds 89 | * @param {AudioContext} context - (Optional) the audio context 90 | * @return {Integer} the number of samples 91 | * @example 92 | * white(seconds(1.2)) // => generate 1.2 seconds of white noise 93 | */ 94 | export function timeToSamples (secs, context) { return secs * samplingRate(context) } 95 | -------------------------------------------------------------------------------- /lib/effects.js: -------------------------------------------------------------------------------- 1 | /** @module effects */ 2 | import { context, samplingRate } from './context' 3 | import { add, conn, plug, lifecycle } from './routing' 4 | import { bypass, gain } from './signals' 5 | import { isA, OPTS } from './utils' 6 | import { osc } from './oscillators' 7 | import { lowpass } from './filters' 8 | import { gen, white } from './buffers' 9 | 10 | /** 11 | * Mix an effect with the signal. Can be partially applied to create an 12 | * effect bus. 13 | * @param {Number} wet - the amount of effect (0-1) 14 | * @param {AudioNode} fx - the effect unit 15 | * @param {Object} options - (Optional) may include: 16 | * 17 | * - compensate: it it's false, the dry signal gain will pass without reduction. 18 | * If true (it's true by default) the dry signal is reduced the gain of the wet signal. 19 | * - context: the audio context 20 | * 21 | * @return {AudioNode} 22 | * @example 23 | * mix(dB(-3), delay(ms(800))) 24 | * @example 25 | * // create an effect bus 26 | * var rev = mix(reverb(2)) 27 | * conn(sine(300), rev(0.2)) 28 | */ 29 | export function mix (wet, fx, opts) { 30 | if (arguments.length === 1) return function (w, o) { return mix(w, wet, o) } 31 | if (!isA('number', wet)) wet = 0.5 32 | opts = opts || OPTS 33 | var dry = opts.compensate === false ? 1 : 1 - wet 34 | console.log('MIX', wet, dry, opts) 35 | return add(gain(dry), conn(fx, gain(wet))) 36 | } 37 | 38 | /** 39 | * Create a feedback loop. 40 | * @param {Integer} amount - the amount of signal 41 | * @param {AudioNode} node - the node to feedback 42 | * @param {Object} options - (Optional) options may include: 43 | * 44 | * - context: the audio context to use 45 | * 46 | * @return {AudioNode} the original node (with a feedback loop) 47 | */ 48 | export function feedback (amount, node, options) { 49 | var feed = gain(amount, options) 50 | node.conn(feed) 51 | feed.conn(node) 52 | return node 53 | } 54 | 55 | /** 56 | * Create a tremolo 57 | */ 58 | export function tremolo (rate, type, ac) { 59 | type = type || 'sine' 60 | return gain(osc(type, rate, ac), ac) 61 | } 62 | 63 | /** 64 | * Create a delay (a DelayNode object) 65 | */ 66 | export function dly (time, opts) { 67 | opts = opts || OPTS 68 | var dly = context(opts.context).createDelay(5) 69 | return lifecycle(dly, [ 70 | plug('delayTime', time, dly) 71 | ]) 72 | } 73 | 74 | /** 75 | * Create a convolver 76 | * @param {AudioBuffer} buffer 77 | * @param {Object} options - (Optional) options may include: 78 | * 79 | * - normalize: true to normalize the buffer 80 | * - context: the audio context to use 81 | * 82 | * @return {AudioNode} the convolver (ConvolverNode) 83 | * @example 84 | * reverb = mix(convolve(sample('emt-140.wav'))) 85 | * connect(subtractive(880, { perc: [0.1, 1] }), reverb(0.2)) 86 | */ 87 | export function convolve (buffer, o) { 88 | o = o || OPTS 89 | var c = context(o.context).createConvolver() 90 | c.buffer = isA('function', buffer) ? buffer() : buffer 91 | if (o.normalize === true) c.normalize = true 92 | return c 93 | } 94 | 95 | /** 96 | * Create a reverb impulse response using a logarithmic decay white noise 97 | * @param {Number} duration - the duration in samples 98 | * @param {Object} options - (Optional) options may include: 99 | * 100 | * - decay: the decay length in samples 101 | * - attack: the attack time in samples 102 | * - reverse: get the reversed impulse 103 | * - context: the context to use 104 | * 105 | * @return {AudioBuffer} the impulse response audio buffer 106 | */ 107 | export function decayIR (samples, opts) { 108 | samples = samples || 10000 109 | opts = opts || OPTS 110 | var attack = opts.attack || Math.floor(samples / 10) 111 | var decay = opts.decay || samples - attack 112 | // 60dB is a factor of 1 million in power, or 1000 in amplitude. 113 | var base = Math.pow(1 / 1000, 1 / decay) 114 | return gen(function (i) { 115 | var dec = base ? Math.pow(base, i) : 1 116 | var att = i < attack ? (i / attack) : 1 117 | return att * white.generator(i) * dec 118 | }, samples, opts) 119 | } 120 | 121 | /** 122 | * Create a simple reverb 123 | * @param {Number} duration - in seconds? WTF? 124 | */ 125 | export function reverb (duration, opts) { 126 | opts = opts || OPTS 127 | var rate = samplingRate(opts.context) 128 | return convolve(decayIR(duration * rate, opts), opts) 129 | } 130 | 131 | /** 132 | * A mono or stereo delay with filtered feedback 133 | */ 134 | export function delay (time, filter, feedAmount, ac) { 135 | if (!isA('number', feedAmount)) feedAmount = 0.3 136 | filter = isA('number', filter) ? lowpass(filter, null, null, ac) : filter || bypass(ac) 137 | return feedback(feedAmount, dly(time, ac), filter, ac) 138 | } 139 | -------------------------------------------------------------------------------- /lib/envelopes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @module envelopes 4 | */ 5 | import { when } from './context' 6 | import { gain, scale } from './signals' 7 | import { isA, OPTS } from './utils' 8 | function eachStage (stages, fn) { stages.forEach(function (s) { fn.apply(null, s) }) } 9 | 10 | /** 11 | * Create an attack-decay envelope with fixed duration. It's composed by a 12 | * linear attack ramp and an exponential decay. This envelope doesn't 13 | * have release, so it stops after the duration (attack + decay). 14 | * 15 | * @param {Number} attack - (Optional) the attack time, defaults to 0.01 16 | * @param {Number} decay - (Optional) the decay time, defaults to 0.2 17 | * @param {Object} options - (Optional) an options with a context 18 | * @return {AudioNode} the signal envelope 19 | * @example 20 | * conn(sine(1000), perc(0.01, 0.5)) 21 | * conn(sine(1000), perc(null, 1)) // default attack 22 | * conn(sine(1000), perc()) // default values 23 | */ 24 | export function perc (attack, decay, opts) { 25 | if (isA('object', attack)) return perc(attack.attack, attack.decay, attack) 26 | var a = [ [0, 0, 'set'], [attack || 0.01, 1, 'lin'], [decay || 0.2, 0, 'exp'] ] 27 | return envelope(a, null, opts) 28 | } 29 | 30 | var ADSR = [0.01, 0.1, 0.8, 0.3] 31 | /** 32 | * Create an adsr envelope 33 | * @params {Object} options - (Optional) the options may include: 34 | * 35 | * - adsr: an array with the form [attack, decay, sustain, release] 36 | * - attack: the attack time (will override the a in the adsr param) 37 | * - decay: the decay time (will override the d in adsr param) 38 | * - sustain: the sustain gain value (will override the s in the adsr param) 39 | * - release: the release time (will override the r in the adsr param) 40 | */ 41 | export function adsr (o) { 42 | o = o || OPTS 43 | var adsr = o.adsr || ADSR 44 | if (!isA('number', o.attack)) o.attack = adsr[0] 45 | if (!isA('number', o.decay)) o.decay = adsr[1] 46 | if (!isA('number', o.sustain)) o.sustain = adsr[2] 47 | if (!isA('number', o.release)) o.release = adsr[3] 48 | var a = [ [0, 0, 'set'], [o.attack, 1, 'lin'], [o.decay, o.sustain, 'exp'] ] 49 | var r = [ [0, o.sustain, 'set'], [o.release, 0, 'exp'] ] 50 | return envelope(a, r, o) 51 | } 52 | 53 | /** 54 | * A frequency envelope. Basically the setup to provide an adsr over a 55 | * number of octaves. 56 | * @param {Number} frequency - the initial frequency 57 | * @param {Number} octaves - (Optional) the number of octaves of the envelope (1 by default) 58 | * @param {Object} options - the same options as an ADSR envelope 59 | * @see adsr 60 | * @example 61 | * conn(saw(1200), lowpass(freqEnv(440, 2, { release: 1 }))) 62 | */ 63 | export function freqEnv (freq, octs, a, d, s, r, ac) { 64 | return scale(freq, freq * Math.pow(2, octs), adsr(a, d, s, 0, r, ac)) 65 | } 66 | 67 | /** 68 | * Create a gain envelope 69 | * @param {Array} attStages - the attack part of the envelope 70 | * @param {Array} relStages - the release part of the envelope 71 | * @private 72 | */ 73 | export function envelope (attEnvelope, relEnvelope, opts) { 74 | var g = gain(0, opts) 75 | g.start = apply(g.gain, attEnvelope) 76 | if (!relEnvelope) { 77 | g.duration = duration(attEnvelope) 78 | g.stop = function () {} 79 | } else { 80 | g.stop = apply(g.gain, relEnvelope) 81 | g.release = duration(relEnvelope) 82 | } 83 | return g 84 | } 85 | 86 | /** 87 | * Apply a contour to a parameter 88 | * @param {AudioParam} param - the parameter to apply the contour to 89 | * @param {Array} contour - a list of countour stages, each of which 90 | * is composed of [time, value, type]. 91 | * @example 92 | * apply(filter.frequency, [ [0, 440, 'set'], [5, 880, 'lin'] ]) 93 | * @private 94 | */ 95 | function apply (param, contour) { 96 | return function (t) { 97 | t = when(t, 0, param.context) 98 | eachStage(contour, function (time, value, type) { 99 | t += time 100 | if (type === 'set') param.setValueAtTime(value, t) 101 | else if (type === 'lin') param.linearRampToValueAtTime(value, t) 102 | else if (type === 'exp') param.exponentialRampToValueAtTime(value !== 0 ? value : 0.00001, t) 103 | else console.warn('Invalid stage type', time, value, type) 104 | }) 105 | } 106 | } 107 | 108 | /** 109 | * Calculate the duration of a contour 110 | * @private 111 | */ 112 | function duration (contour) { 113 | return contour.reduce(function (dur, stage) { 114 | return dur + stage[0] 115 | }, 0) 116 | } 117 | -------------------------------------------------------------------------------- /lib/filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides some syntactic sugar over the AudioContext.createBiquadFilter 3 | * function. 4 | * 5 | * @example 6 | * import { conn, square, lowpass, filter, master } from 'synth-kit' 7 | * 8 | * master.start(conn(square('C4'), lowpass('C5'))) 9 | * master.start(conn(square('A4'), filter({ type: 'hipass', Q: 10, freq: 1000 })) 10 | * @module filters 11 | */ 12 | import { OPTS } from './utils' 13 | import { getFreq } from './oscillators' 14 | import { context } from './context' 15 | import { plug, lifecycle } from './routing' 16 | 17 | // private: create a filter 18 | function create (type, opts) { 19 | opts = opts || OPTS 20 | var filter = context(opts.context).createBiquadFilter() 21 | filter.type = type || opts.type || 'lowpass' 22 | return lifecycle(filter, [ 23 | plug('frequency', getFreq(opts), filter), 24 | plug('Q', opts.Q, filter), 25 | plug('detune', opts.detune, filter) 26 | ]) 27 | } 28 | 29 | /** 30 | * Create a filter (a [BiquadFilterNode](https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode)) 31 | * 32 | * @function 33 | * @param {Object} config - it may include: 34 | * 35 | * - frequency (or freq, or note or midi): the frequency expressed in hertzs 36 | * (or midi note number or note name) 37 | * - type: one of the BiquadFilterNode types 38 | * - detune: the detune of the frequency in cents (can be a number or a signal) 39 | * - Q: the Q of the filter (can be a number or a signal) 40 | * - context: the audio context to use 41 | * 42 | * Instead of a configuration object you can pass a frequency number of note 43 | * name just to specify the frequency. 44 | * 45 | * @return {AudioNode} the BiquadFilterNode 46 | * @example 47 | * conn(square(800), filter({ type: 'lowpass', freq: 600 })) 48 | */ 49 | export var filter = create.bind(null, null) 50 | 51 | /** 52 | * Create a lowpass filter. An alias for `filter({ type: 'lowpass', ... })` 53 | * 54 | * @function 55 | * @param {Object} config - same as `filter` function but without type 56 | * @return {AudioNode} the lowpass filter 57 | * @see filter 58 | * @example 59 | * conn(square('C4'), lowpass(400)) 60 | */ 61 | export const lowpass = create.bind(null, 'lowpass') 62 | 63 | /** 64 | * Create a hipass filter. An alias for `filter({ type: 'hipass', ... })` 65 | * 66 | * @function 67 | * @param {Object} config - same as `filter` function but without type 68 | * @return {AudioNode} the hipass filter 69 | * @see filter 70 | * @example 71 | * conn(square(800), hipass(400)) 72 | */ 73 | export const hipass = create.bind(null, 'highpass') 74 | 75 | /** 76 | * Create a bandpass filter. An alias for `filter({ type: 'bandpass', ... })` 77 | * 78 | * @function 79 | * @param {Object} config - same as `filter` function but without type 80 | * @return {AudioNode} the bandpass filter 81 | * @see filter 82 | * @example 83 | * conn(square(800), bandpass(400)) 84 | */ 85 | export const bandpass = create.bind(null, 'hipass') 86 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export { context, now, dest, samplingRate, timeToSamples } from './context' 2 | export { levelToGain, midiToFreq, noteToFreq, tempoToFreq, dBToGain, gainToDb } from './units' 3 | export { conn, add } from './routing' 4 | export { constant, bypass, gain, signal, mult, scale } from './signals' 5 | export { osc, sine, saw, square, triangle, lfo } from './oscillators' 6 | export { filter, lowpass, hipass, bandpass } from './filters' 7 | export { sample, gen, source, white } from './buffers' 8 | export { load, decodeAudio, fetch } from './load' 9 | export { perc, adsr } from './envelopes' 10 | export { mix, feedback, tremolo, dly, delay, 11 | convolve, decayIR, reverb } from './effects' 12 | export { withDefaults, vca, vcf, subtractive, bank } from './synths' 13 | export { inst, master } from './instruments' 14 | -------------------------------------------------------------------------------- /lib/instruments.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Instruments provide a OOP style interface to the synths. Something with 3 | * `start` and `stop` methods, than can be called several times and know about 4 | * note frequencies and midi connections. 5 | * 6 | * @example 7 | * var simple = inst((fq) => conn(sine(fq), adsr())) 8 | * simple.start('C4') 9 | * simple.start('G5') 10 | * simple.stop() // => stop all notes 11 | * @module instruments 12 | */ 13 | import { isA, OPTS, toArr, slice } from './utils' 14 | import { context, when, dest } from './context' 15 | import { withDest } from './synths' 16 | 17 | /** 18 | * A master output instrument. You can use it to start and stop nodes. All 19 | * started nodes will be connected to the AudioContext destination. 20 | * 21 | * @example 22 | * master.start(sine(300)) // connect to destination and start 23 | * master.start(sine(600), 0, 1) // connect to destination and start after 1 second 24 | * master.stop() // stop all 25 | */ 26 | export var master = inst(null, dest()) 27 | 28 | /** 29 | * Create an object-oriented-style instrument player. It wraps a synth function 30 | * (a function that create nodes) into in a convenient player API. It can 31 | * be used to limit the polyphony. 32 | * 33 | * The player object have the following methods: 34 | * 35 | * - `start(node, when, delay, duration)`: start a node 36 | * - `stop`: stop all nodes 37 | * - `on(name, callback)`: add an event callback 38 | * - `event(name, ...values)`: fire an event 39 | * 40 | * 41 | * @param {Function} synth - the synth function (a function that returns a node graph) 42 | * @param {AudioNode} destination - if present, all nodes will be connected to 43 | * this destination 44 | * @param {Object} options - (Optional) the options may include: 45 | * 46 | * - maxVoices: the maximum number of simultaneous voices. No value (by default) 47 | * means no limit. 48 | * 49 | * @return {Player} a player object 50 | * 51 | * @example 52 | * // an instrument with destination 53 | * var synth = inst((fq) => sine(fq), dest()) 54 | * synth.start('A4') 55 | * synth.stopAll() 56 | * @example 57 | * // only the destination 58 | * var master = inst(null, conn(mix(0.2), reverb(), dest())) 59 | * master.start(sine(300)) 60 | * master.start(sine(400)) 61 | * master.stopAll() 62 | */ 63 | export function inst (synth, destination, options) { 64 | synth = withDest(synth, destination || dest()) 65 | return tracker(synth, options || OPTS) 66 | } 67 | 68 | /** 69 | * tracker: (fn: (object) => Node, options: object) => interface { start: fn, stop: fn } 70 | * @private 71 | */ 72 | function tracker (synth, opts) { 73 | var ob = observable({}) 74 | var limit = opts ? opts.maxVoices : 0 75 | var voices = { limit: limit, all: {}, nextId: 0, current: 0, pool: new Array(limit) } 76 | 77 | function track (node) { 78 | node.id = voices.nextId++ 79 | voices.all[node.id] = node 80 | on(node, 'ended', function () { 81 | delete voices.all[node.id] 82 | ob.event('voices', Object.keys(voices.all), limit) 83 | }) 84 | return node 85 | } 86 | 87 | ob.start = function (value, time, delay, duration) { 88 | var node = synth(value) 89 | if (node.start) { 90 | track(node) 91 | time = start(node, time, delay, duration).startedAt 92 | ob.event('start', node.id, time) 93 | ob.event('voices', Object.keys(voices.all), limit) 94 | } 95 | return node 96 | } 97 | ob.stop = function (ids, time, delay) { 98 | var t = 0 99 | ids = toArr(ids || ids === 0 ? ids : Object.keys(voices.all)) 100 | ids.forEach(function (id) { 101 | if (voices.all[id]) { 102 | if (!t) t = when(time, delay, voices.all[id].context) 103 | voices.all[id].stop(t) 104 | } 105 | }) 106 | } 107 | return ob 108 | } 109 | 110 | function start (node, time, delay, duration) { 111 | if (time && !isA('number', time)) throw Error('Invalid time (maybe forgot connect?): ', time, delay, duration, node) 112 | time = when(time, delay, context(node.context)) 113 | node.start(time) 114 | node.startedAt = time 115 | var d = duration || node.duration 116 | if (d) node.stop(time + d) 117 | return node 118 | } 119 | 120 | // EVENTS 121 | // ====== 122 | // decorate an objet to have `on` and `event` methods 123 | function observable (obj) { 124 | obj.on = on.bind(null, obj) 125 | obj.event = event.bind(null, obj) 126 | return obj 127 | } 128 | 129 | // add a listener to a target 130 | function on (target, name, callback) { 131 | if (!name || name === '*') name = 'event' 132 | var prev = target['on' + name] 133 | target['on' + name] = function () { 134 | if (prev) prev.apply(null, arguments) 135 | callback.apply(null, arguments) 136 | } 137 | return target 138 | } 139 | 140 | // fire an event 141 | function event (target, name /*, ...values */) { 142 | var args = slice.call(arguments, 1) 143 | if (isA('function', target['on' + name])) target['on' + name].apply(null, args) 144 | if (isA('function', target.onevent)) target.onevent.apply(null, args) 145 | return target 146 | } 147 | -------------------------------------------------------------------------------- /lib/load.js: -------------------------------------------------------------------------------- 1 | /* global XMLHttpRequest */ 2 | import { context } from './context' 3 | 4 | var NONE = {} 5 | /** 6 | * This module contains some functions to fetch and decode audio files. 7 | * 8 | * @module load 9 | */ 10 | 11 | /** 12 | * Load a remote audio file and return a promise. 13 | * @param {String} url 14 | * @param {Object} options - (Optional) may include: 15 | * 16 | * - context: the audio context 17 | * 18 | * @return {Promise} a promise that resolves to an AudioBuffer 19 | * @example 20 | * load('sound.mp3').then(function (buffer) { 21 | * sample(buffer, true).start() 22 | * } 23 | */ 24 | export function load (url, opts) { 25 | opts = opts || NONE 26 | return fetch(url, 'arraybuffer').then(decodeAudio(context(opts.context))) 27 | } 28 | 29 | /** 30 | * Fetch an url and return a promise 31 | * @param {String} url - the url 32 | * @param {String} type - can be 'text' or 'arraybuffer' 33 | * @return {Promise} a promise to the result 34 | */ 35 | export function fetch (url, type) { 36 | type = type === 'arraybuffer' ? type : 'text' 37 | return new Promise(function (resolve, reject) { 38 | var xhr = new XMLHttpRequest() 39 | 40 | xhr.open('GET', url, true) 41 | xhr.responseType = type 42 | 43 | xhr.onload = function () { 44 | if (xhr.response) { 45 | resolve(xhr.response) 46 | } 47 | } 48 | xhr.onerror = reject 49 | 50 | xhr.send() 51 | }) 52 | } 53 | 54 | /** 55 | * Decode an array buffer into an AudioBuffer. 56 | * @param {AudioContext} context - (Optional) the context to be used (can be null to use 57 | * synth-kit's default audio context) 58 | * @param {Array} array - (Optional) the array to be decoded. If not given, 59 | * it returns a function that decodes the buffer so it can be chained with 60 | * fetch function (see example) 61 | * @return {Promise} a promise that resolves to an audio buffer 62 | * @example 63 | * fecth('sound.mp3').then(decodeAudio()) 64 | */ 65 | export function decodeAudio (ac, arrayBuffer) { 66 | if (arguments.length === 1) return function (array) { return decodeAudio(ac, array) } 67 | return new Promise(function (resolve, reject) { 68 | ac.decodeAudioData(arrayBuffer, resolve, reject) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /lib/oscillators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides some syntactic sugar over the AudioContext.createOscillator 3 | * function 4 | * 5 | * @example 6 | * import { sine, square, master } from 'synth-kit' 7 | * 8 | * master.start(sine('A4')) 9 | * master.start(square({ note: 'c3', detune: -10 })) 10 | * @module oscillators 11 | */ 12 | import { noteToFreq, midiToFreq, tempoToFreq } from './units' 13 | import { context } from './context' 14 | import { mult } from './signals' 15 | import { plug, lifecycle } from './routing' 16 | import { OPTS, isA } from './utils' 17 | 18 | // Create a OscillatorNode of the given type and configuration 19 | // this is a private function intended to be partially applied 20 | function create (type, opts) { 21 | opts = opts || OPTS 22 | type = type || opts.type 23 | var osc = context(opts.context).createOscillator() 24 | if (type) osc.type = type 25 | return lifecycle(osc, [ 26 | plug('frequency', getFreq(opts), osc), 27 | plug('detune', opts.detune, osc) 28 | ]) 29 | } 30 | 31 | // given a configuration object, get the frequency 32 | export function getFreq (obj) { 33 | return obj.frequency ? obj.frequency 34 | : obj.freq ? obj.freq 35 | : obj.midi ? midiToFreq(obj.midi) 36 | : obj.note ? noteToFreq(obj.note) 37 | : isA('string', obj) ? noteToFreq(obj) 38 | : isA('number', obj) ? obj 39 | : null 40 | } 41 | 42 | /** 43 | * Create an oscillator (an OscillatorNode) 44 | * 45 | * @function 46 | * @param {Object} config - may include: 47 | * 48 | * - type: one of the OscillatorNode types 49 | * - frequency (or freq): the oscillator frequency (can be a signal) 50 | * - detune: the detune in cents (can be a signal) 51 | * - note: the note name to get the frequency from (if frequency is not present) 52 | * - midi: the note midi number to get the frequency from (if frequency is not present) 53 | * - context: the audio context to use 54 | * 55 | * Notice that instead of a config object, this function also accepts a note 56 | * name or a frequency value (see examples) 57 | * 58 | * @return {AudioNode} the oscillator 59 | * @example 60 | * // basic usage 61 | * osc({ type: 'sine', frequency: 880 }) 62 | * osc({ note: 'C4', type: 'square', detune: -10 }) 63 | * // parameter modulation 64 | * osc({ freq: 1500, detune: osc({ freq: 20}) }) 65 | * osc({ freq: envelope(...), type: 'square' }) 66 | * // without configuration object 67 | * osc('C4') 68 | * osc(1200) 69 | */ 70 | export const osc = create.bind(null, null) 71 | 72 | /** 73 | * Create a sine oscillator. An alias for `osc({ type: 'sine', ... })` 74 | * @function 75 | * @see osc 76 | * @param {Object} config - Same as `osc` function, but without 'type' 77 | * @return {AudioNode} the oscillator 78 | * @example 79 | * sine('C4') 80 | * sine({ midi: 70, detune: -50 }) 81 | */ 82 | export const sine = create.bind(null, 'sine') 83 | 84 | /** 85 | * Create a sawtooth oscillator. An alias for `osc({ type: 'sawtooth', ... })` 86 | * @function 87 | * @see osc 88 | * @param {Object} config - Same as `osc` function, but without 'type' 89 | * @return {AudioNode} the oscillator 90 | * @example 91 | * saw('A3') 92 | * saw({ freq: 440, detune: lfo(5, 10) }) 93 | */ 94 | export const saw = create.bind(null, 'sawtooth') 95 | /** 96 | * Create a square oscillator. An alias for `osc({ type: 'square', ... })` 97 | * @function 98 | * @see osc 99 | * @param {Object} config - Same as `osc` function, but without 'type' 100 | * @return {AudioNode} the oscillator 101 | * @example 102 | * square({ note: 'c#3', context: offline() }) 103 | */ 104 | export const square = create.bind(null, 'square') 105 | 106 | /** 107 | * Create a triangle oscillator. An alias for `osc({ type: 'triangle', ... })` 108 | * @function 109 | * @see osc 110 | * @param {Object} config - Same as `osc` function, but without 'type' 111 | * @return {AudioNode} the oscillator 112 | * @example 113 | * triangle({ note: 'Bb4', detune: -10 }) 114 | */ 115 | export const triangle = osc.bind(null, 'triangle') 116 | 117 | /** 118 | * Create an LFO (low frequency oscillator). It's a standard oscillator with 119 | * some goodies to reduce the boilerplate code when used as signal modulator. 120 | * 121 | * @see osc 122 | * @param {Options} config - May include any of the `osc` function plus: 123 | * 124 | * - tempo: the tempo used to calculate the frequency (it overrides the frequency parameter) 125 | * - division: the number of subdivisions of the tempo (defaults to 1) 126 | * - amplitude: the amplitude of the oscillator 127 | * 128 | * @example 129 | * sine({ note: 'C4', detune: lfo({ freq: 5, amplitude: 50 }) }) 130 | * sine({ note: 'A4', detune: lfo({ amplitude: 10, tempo: 120, division: 3 }) 131 | */ 132 | export function lfo (opts) { 133 | opts = opts || OPTS 134 | var node = osc(opts) 135 | if (opts.tempo) node.frequency.value = tempoToFreq(opts.tempo, opts.division) 136 | return mult(opts.amplitude || 1, node) 137 | } 138 | -------------------------------------------------------------------------------- /lib/routing.js: -------------------------------------------------------------------------------- 1 | import { when } from './context' 2 | import { slice, isArray } from './utils' 3 | 4 | /** 5 | * This module provides two ways to route nodes: 6 | * 7 | * - In series: A -> B -> C -> D, using the `connect` function 8 | * - In parallel: in -> [A, B, C] -> out, using the `add` function 9 | * @module routing 10 | */ 11 | 12 | /** 13 | * Connect nodes in series: A -> B -> C -> D. 14 | * @param {Array} nodes - the list of nodes to be connected 15 | * @return {AudioNode} the resulting audio node 16 | */ 17 | export function conn (nodes) { 18 | nodes = isArray(nodes) ? nodes : slice.call(arguments) 19 | if (!nodes.length) return null 20 | else if (nodes.length === 1) return nodes[0] 21 | 22 | var node = nodes[0] 23 | if (!node.duration) node.duration = 0 24 | var last = nodes.reduce(function (src, dest) { 25 | src.connect(dest) 26 | node.duration = Math.max(node.duration, dest.duration || 0) 27 | return dest 28 | }) 29 | overrideConnect(node, last) 30 | var startables = nodes.slice(1).filter(isStartable) 31 | if (startables.length) { 32 | var _start = node.start 33 | node.start = function (time) { 34 | if (_start) _start.call(node, time) 35 | startables.forEach(function (node) { node.start(time) }) 36 | if (node.duration) node.stop(time + node.duration) 37 | } 38 | var _stop = node.stop 39 | node.stop = function (time) { 40 | var t = 0 41 | startables.reverse() 42 | startables.forEach(function (node) { 43 | t = t || when(time, null, node.context) 44 | node.stop(t) 45 | t += node.release || 0 46 | }) 47 | if (_stop) _stop.call(node, t) 48 | } 49 | } 50 | return node 51 | } 52 | 53 | // TODO: A RESOLVER: add debe tener onended cuando acaben todos sus nodos 54 | /** 55 | * Connect nodes in parallel in order to add signals. This is one of the 56 | * routing functions (the other is `connect`). 57 | * @param {...AudioNode} nodes - the nodes to be connected 58 | * @return {AudioNode} the resulting audio node 59 | * @example 60 | * add(sine(400), sine(401)).start() 61 | */ 62 | export function add (nodes) { 63 | nodes = isArray(nodes) ? nodes : slice.call(arguments) 64 | if (!nodes.length) return null 65 | else if (nodes.length === 1) return nodes[0] 66 | 67 | var context = nodes[0].context 68 | var input = context.createGain() 69 | input.id = 'ADDin' 70 | var output = context.createGain() 71 | output.id = 'ADDout' 72 | // the connection loop: connect input to all nodes. all nodes to output. 73 | nodes.forEach(function (node) { 74 | if (node.numberOfInputs) input.connect(node) 75 | node.connect(output) 76 | }) 77 | // this must be after the connection loop 78 | overrideConnect(input, output) 79 | var node = lifecycle(input, nodes) 80 | addOnEndedEvent(node) 81 | return node 82 | } 83 | 84 | // make trigger an onended event when all startable node ended 85 | function addOnEndedEvent (node) { 86 | if (!node.dependents) return 87 | var length = node.dependents.length 88 | function triggerEnded () { 89 | length-- 90 | if (!length && node.onended) node.onended() 91 | } 92 | node.dependents.forEach(function (node) { 93 | node.onended = triggerEnded 94 | }) 95 | } 96 | 97 | // overrides the node's connect function to use output node 98 | function overrideConnect (node, output) { 99 | node.output = node 100 | node.connect = function (dest) { 101 | output.connect(dest) 102 | return node 103 | } 104 | return node 105 | } 106 | 107 | /** 108 | * Return if a node is startable or note 109 | * @private 110 | */ 111 | function isStartable (node) { return node && typeof node.start === 'function' } 112 | 113 | /** 114 | * Plug something (a value, a node) into a node parameter 115 | * @param {String} name - the parameter name 116 | * @param {AudioNode|Object} value - the value (can be a signal) 117 | * @param {AudioNode} target - the target audio node 118 | * @return {AudioNode} the modulator signal if any or undefined 119 | * @private 120 | */ 121 | export function plug (name, value, node) { 122 | if (value === null || typeof value === 'undefined') { 123 | // do nothing 124 | } else if (typeof value.connect === 'function') { 125 | node[name].value = 0 126 | value.conn(node[name]) 127 | return value 128 | } else if (node[name]) { 129 | node[name].value = value 130 | } 131 | } 132 | 133 | /** 134 | * Override start and stop functions (if necessary) to handle node dependencies 135 | * lifecycle. 136 | * @private 137 | */ 138 | export function lifecycle (node, dependents) { 139 | dependents = dependents.filter(isStartable) 140 | if (dependents.length) { 141 | var _start = node.start 142 | var _stop = node.stop 143 | node.start = function (time) { 144 | var res = _start ? _start.call(node, time) : void 0 145 | dependents.forEach(function (d) { if (d.start) d.start(time) }) 146 | if (node.start.then) node.start.then(time, node, dependents) 147 | return res 148 | } 149 | node.stop = function (time) { 150 | var res = _stop ? _stop.call(node, time) : void 0 151 | dependents.forEach(function (d) { if (d.stop) d.stop(time) }) 152 | return res 153 | } 154 | node.dependents = dependents 155 | } 156 | return node 157 | } 158 | -------------------------------------------------------------------------------- /lib/signals.js: -------------------------------------------------------------------------------- 1 | /** @module signals */ 2 | 3 | import { OPTS, isA } from './utils' 4 | import { context } from './context' 5 | import { add, conn, lifecycle, plug } from './routing' 6 | import { dBToGain, levelToGain } from './units' 7 | 8 | /** 9 | * Create a GainNode 10 | * 11 | * @param {Object} config - may include: 12 | * 13 | * - value (or gain): the gain (can be a number or a signal) 14 | * - dB (or db): the gain in dB (only if gain is not specified) 15 | * - level: the gain in a logaritmic scale from 0 to 100 16 | * - context: the audio context to use to create the signal 17 | * 18 | * This funcion accepts a number with the gain value instead of a config object. 19 | * 20 | * @return {AudioNode} a GainNode 21 | * @example 22 | * gain({ dB: -3, context: }) 23 | * // with modulation (kind of tremolo) 24 | * conn(sine(400), gain({ value: sine(10) })) 25 | * // passing a number instead of an object 26 | * conn(sine('C4'), gain(0.3)) 27 | */ 28 | export function gain (opts) { 29 | opts = opts || OPTS 30 | var node = context(opts.context).createGain() 31 | return lifecycle(node, [ 32 | plug('gain', getGain(opts), node) 33 | ]) 34 | } 35 | 36 | // given an config object, return the gain 37 | function getGain (opts) { 38 | return isA('number', opts) ? opts 39 | : opts.value ? opts.value 40 | : opts.gain ? opts.gain 41 | : isA('number', opts.dB) ? dBToGain(opts.dB) 42 | : isA('number', opts.db) ? dBToGain(opts.db) 43 | : isA('number', opts.level) ? levelToGain(opts.level) 44 | : null 45 | } 46 | 47 | /** 48 | * Create a constant signal. Normally you will use it in combination with 49 | * envelopes or modulators. 50 | * 51 | * @param {Integer} value - the value of the constant 52 | * @param {Object} options - (Optional) options may include: 53 | * 54 | * - context: the audio context to use to create the signal 55 | * 56 | * @return {AudioNode} the constant audio node 57 | * @example 58 | * sine(constant(440)).start() 59 | */ 60 | export function constant (value, o) { 61 | // TODO: cache buffer 62 | var ctx = context(o ? o.context : null) 63 | var source = ctx.createBufferSource() 64 | source.loop = true 65 | source.buffer = ctx.createBuffer(1, 2, ctx.sampleRate) 66 | var data = source.buffer.getChannelData(0) 67 | data[0] = data[1] = value 68 | return source 69 | } 70 | 71 | /** 72 | * Create a signal source. You will use signals to change parameters of a 73 | * audio node after starting. See example. 74 | * @param {Integer} value - the value of the constant 75 | * @param {Object} options - (Optional) options may include: 76 | * 77 | * - context: the audio context to use to create the signal 78 | * 79 | * @return {AudioParam} the constant audio node 80 | * @example 81 | * var freq = signal(440) 82 | * sine(freq).start() 83 | * freq.value.linearRampToValueAtTime(880, after(5)) 84 | */ 85 | export function signal (value, opts) { 86 | var signal = gain(value, opts) 87 | conn(constant(1, opts), signal).start() 88 | signal.signal = signal.gain 89 | return signal 90 | } 91 | 92 | /** 93 | * Create a node that bypasses the signal 94 | * @param {Object} config - may include: 95 | * 96 | * - context: the audio context to use 97 | * 98 | * @return {AudioNode} the bypass audio node 99 | * @example 100 | * conn(sine('C4'), add(bypass(), dly(0.2))) 101 | */ 102 | export function bypass (o) { 103 | return context(o ? o.context : null).createGain() 104 | } 105 | 106 | /** 107 | * Multiply a signal. 108 | * 109 | * @param {Integer} value - the value 110 | * @param {Integer|AudioNode} signal - the signal to multiply by 111 | * @example 112 | * // a vibrato effect 113 | * sine(440, { detune: mult(500, sine(2)) }) 114 | */ 115 | export function mult (value, signal) { 116 | if (isA('number', signal)) return value * signal 117 | return conn(signal, gain({ value: value, context: signal.context })) 118 | } 119 | 120 | /** 121 | * Scale a signal. Given a signal (between -1 and 1) scale it to fit in a range. 122 | * @param {Integer} min - the minimum of the range 123 | * @param {Integer} max - the minimum of the range 124 | * @param {AudioNode} source - the signal to scale 125 | * @return {AudioNode} the scaled signal node 126 | * @example 127 | * // create a frequency envelope between 440 and 880 Hz 128 | * sine(scale(440, 880, adsr(0.1, 0.01, 1, 1))) 129 | */ 130 | export function scale (min, max, source) { 131 | var ctx = source 132 | if (source.numberOfInputs) source = conn(constant(1, ctx), source) 133 | var delta = max - min 134 | return add(constant(min, ctx), mult(delta, source)) 135 | } 136 | -------------------------------------------------------------------------------- /lib/synths.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides some typical syntetizers components, like vca, vcf 3 | * and some syntetizers 4 | * @example 5 | * // a dual osc synth sound 6 | * synth( 7 | * frequency: 440, 8 | * oscillators: { ratios: [1, 5/7, 9/8], types: 'saw' }, 9 | * attack: 0.1, release: 0.1, 10 | * filter: { frequency: 'follow' } 11 | ).connect(dest()).start() 12 | * @module synths 13 | */ 14 | import { isA, OPTS, assign, toArr } from './utils' 15 | import { dBToGain } from './units' 16 | import { conn, add } from './routing' 17 | import { gain, mult, signal } from './signals' 18 | import { osc } from './oscillators' 19 | import { filter } from './filters' 20 | import { adsr } from './envelopes' 21 | 22 | /** 23 | * Decorate a function to have default configuration 24 | */ 25 | export function withDefaults (synth, defaults) { 26 | return function (options) { 27 | return synth(assign({}, defaults, options)) 28 | } 29 | } 30 | 31 | /** 32 | * Create a VCA: an amplifier controlled by an ADSR envelope 33 | * @function 34 | * @param {Object} options - may include: 35 | * @return {AudioNode} a GainNode 36 | * 37 | * - db: the gain in decibels 38 | * - gain: the gain (will override dB param) 39 | * - attack, decay, sustain, release: the ADSR envelope parameters 40 | * - context: the audio context 41 | */ 42 | export const vca = withDefaults(function (opts) { 43 | if (!isA('number', opts.gain)) opts.gain = dBToGain(opts.dB) || 1 44 | return conn(gain(opts), adsr(opts)) 45 | }, { 46 | dB: 0, // in dB 47 | attack: 0.01, // attack time in seconds 48 | decay: 0.1, // decay time in seconds 49 | sustain: 0, 50 | release: 0.1 51 | }) 52 | 53 | /** 54 | * Create a VCF: a filter controlled by an ADSR envelope 55 | * @function 56 | * @param {Object} config - may include: 57 | * @return {AudioNode} the BiquadFilterNode 58 | * 59 | * - type: filter type 60 | * - frequency: filter frequency (can be a signal) 61 | * - octaves: amplitude of the modulation in octaves (defaults to 2) 62 | * - attack, decay, sustain, release: the adsr parameters 63 | * - context: the audio context 64 | 65 | * @example 66 | * conn(square({ frequency: 800 }), vcf({ frequency: 400, attack: 0.1 })) 67 | */ 68 | export const vcf = withDefaults(function (opts) { 69 | var mod = conn(signal(mult(opts.octaves, opts.frequency)), adsr(opts)) 70 | return filter(opts.type, add(signal(opts.frequency), mod), opts) 71 | }, { 72 | type: 'lowpass', 73 | frequency: 22000, 74 | octaves: 2, 75 | attack: 0.01, 76 | decay: 0.1, 77 | sustain: 0, 78 | release: 0.1 79 | }) 80 | 81 | /** 82 | * A subtractive synthetizer (oscillator bank -> VCF -> VCA) 83 | * 84 | * @param {AudioNode|Number|String} source - The subtractive synth source 85 | * @param {Object} options - (Optional) the configuration may include: 86 | * 87 | * - gain: the gain (defaults to 1) 88 | * - adsr: the parameters of the gain envelop in the form [attack, decay, sustain, release] 89 | * - attack, decay, sustain, release: the parameters of the gain envelope. It 90 | * will override the one provided by adsr parameter if any 91 | * - filter: an object with the filter properties 92 | * - filter.type: the filter type ('lowpass' by default) 93 | * - filter.frequency: the filter frequency (can be follow to use the options.frequency value) 94 | * - filter.adsr: the filter envelope 95 | * - filter.attack, filter.decay, filter.sustain, filter.release: the individual 96 | * filter envelope parameters 97 | * - filter.octaves: the number of the octaves of the filter envelope (1 by default) 98 | * 99 | * @function 100 | * @example 101 | * // a 300Hz sawtooth with adsr and lowass filter at 600 102 | * subtractive({ frequency: 300, type: 'sawtooth', adsr: [0.01, 0.1, 0.8, 1], filter: { type: 'lowpass', frequency: 600 } }) 103 | * // a custom source node 104 | * substractive({ source: add(white(), square(500)), attack: 0.1, duration: 1 }) 105 | * // connected in a chain 106 | * connect(sample('snare.wav', subtractive({ attack: 0.1 })) 107 | */ 108 | export function subtractive (opts) { 109 | opts = opts || OPTS 110 | return conn(bank(opts), vcf(opts.filter), vca(opts)) 111 | } 112 | 113 | /** 114 | * Create a bank of oscillators. 115 | * 116 | * @param {Array} frequencies - an array with the frequencies 117 | * @param {Object} options - (Optional) options can include: 118 | * 119 | * - frequencies: an array with the frequencies of the oscillators (will 120 | * override ratios) 121 | * - ratios: an array of relative freqnecies of the oscillators (in combination 122 | * iwth frequency paramter) 123 | * - frequency: the base frequency of the oscillators (only used if frequencies) 124 | * - types: a value or an array of oscillator types. If the array is shorter 125 | * than the frequencies array, it's assumed to be circular. 126 | * - gains: a value or an array of gain values. If the array is shorter 127 | * than the frequencies array, it's assumed to be circular. 128 | * - compensate: if true, the gain of the bank will be reduced by the number 129 | * of oscillators (true by default) 130 | * 131 | * @return {AudioNode} 132 | * 133 | * @example 134 | * // create three sines with unrelated frequencies: 135 | * bank({ frequencies: [1345.387, 435.392, 899.432] }) 136 | * // create three sawtooth with relative frequencies: 137 | * bank({ frequency: 440, ratios: [1, 2, 2.4] , types: 'sawtooth' }) 138 | * // create two squares of 400 and 800 and two sawtooths of 600 and 1200 139 | * // (the types are cyclic) 140 | * bank({ frequencies: [400, 600, 800, 1200], types: ['square', 'sawtooth'] }) 141 | * // specify gains 142 | * bank({ frequencies: [440, 660], gains: [0.6, 0.2] }) 143 | */ 144 | export function bank (opts) { 145 | opts = opts || OPTS 146 | var base = opts.ratios ? opts.frequency || 440 : 1 147 | var freqs = toArr(opts.frequencies || opts.ratios || 440) 148 | var gains = toArr(opts.gains || 1) 149 | var types = toArr(opts.types || 'sine') 150 | var N = opts.compensate === false ? 1 : freqs.length 151 | 152 | var tl = types.length 153 | var gl = gains.length 154 | return conn(add(freqs.map(function (freq, i) { 155 | var src = osc(types[i % tl], base * freq) 156 | var g = gains[i % gl] 157 | return g === 1 ? src : conn(src, gain(g)) 158 | })), gain(1 / N)) 159 | } 160 | 161 | /** 162 | * Decorate a synth to use a destination. A synth is any function that 163 | * returns an audio node 164 | * @param {Function} synth - the audio node builder 165 | * @param {AudioNode} destination - (Optional) destination (or the default destination) 166 | * @return {Function} the decorated synth function 167 | * @example 168 | * // TODO: write a more realistic example 169 | * var synth = withDest(sine, dest()) 170 | * synth(300).start() 171 | * // TODO: make this function public 172 | * @private 173 | */ 174 | export function withDest (synth, dest) { 175 | return function (value) { 176 | var node = synth ? synth(value) : value 177 | if (dest) node.connect(dest) 178 | return node 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/units.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides some conversion utilities between different units 3 | * used when dealing with audio 4 | * 5 | * @example 6 | * import { noteToFreq } from 'synth-kit' 7 | * 8 | * noteToFreq('A4') // => 440 9 | * @module units 10 | */ 11 | import Note from 'note-parser' 12 | var pow = Math.pow 13 | 14 | /** 15 | * Convert from note midi number to frequency 16 | * @param {Number} midi - the midi note number (can have decimals) 17 | * @param {Number} tuning - (Optional) the frequency of a reference A4 note (440 by default) 18 | * @return {Number} the note frequency 19 | * @example 20 | * midiToFreq(69) // => 440 21 | * midiToFreq(69.5) // => 452.8929841231365 22 | * midiToFreq(70) // => 466.1637615180899 23 | */ 24 | export function midiToFreq (value, base) { return pow(2, (+value - 69) / 12) * (base || 440) } 25 | 26 | /** 27 | * Convert from note name to frequency 28 | * @function 29 | * @param {String} note - the note name 30 | * @param {Number} tuning - (Optional) the tuning of A4 (440 by default) 31 | * @return {Number} the note frequency 32 | * @example 33 | * noteToFreq('C3') // => 130.8127826502993 34 | */ 35 | export var noteToFreq = Note.freq 36 | 37 | /** 38 | * Convert from beats per minute to hertzs 39 | * 40 | * @param {Integer} bpm - the tempo 41 | * @param {Integer} sub - (Optional) subdivision (default 1) 42 | * @return {Float} the tempo expressed in hertzs 43 | * @example 44 | * tempoToFreq(120) // => 2 45 | * tempoToFreq(120, 4) // => 8 46 | */ 47 | export function tempoToFreq (bpm, sub) { return (bpm / 60) * (sub || 1) } 48 | 49 | /** 50 | * Convert decibels into gain. 51 | * @param {Number} db 52 | * @return {Number} the gain (from 0 to 1) 53 | * @example 54 | * dBToGain(-3) // => 0.7071067811865475 55 | */ 56 | export function dBToGain (db) { return pow(2, db / 6) } 57 | 58 | /** 59 | * Convert from level (an equal power scale from 0 to 100) into gain. 60 | * @param {Number} level - from 0 to 100 61 | * @return {Number} gain (from 0 to 1) 62 | * @example 63 | * levelToGain(80) // => 0.9510565162951535 64 | */ 65 | export function levelToGain (level) { return Math.sin(0.5 * Math.PI * level / 100) } 66 | 67 | /** 68 | * Convert gain to decibels. 69 | * @param {Number} gain (0-1) 70 | * @return {Decibels} 71 | * @example 72 | * s.gainToDb(0.3) // => -10.457574905606752 73 | */ 74 | export function gainToDb (gain) { return 20 * (Math.log(gain) / Math.LN10) } 75 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple utilities shared between all the modules 3 | * @module utils 4 | * @private 5 | */ 6 | 7 | /** 8 | * Empty options object (treat is as immutable) 9 | * @private 10 | */ 11 | export var OPTS = {} 12 | 13 | /** 14 | * typeof shortcut 15 | * @private 16 | */ 17 | export function isA (t, x) { return typeof x === t } 18 | 19 | /** 20 | * Ensure the value is an array 21 | * @private 22 | */ 23 | export function toArr (arr) { return Array.isArray(arr) ? arr : [ arr ] } 24 | 25 | /** 26 | * The array.slice function unbinded 27 | * @private 28 | */ 29 | export var slice = Array.prototype.slice 30 | 31 | /** 32 | * Test if its an array 33 | * @private 34 | */ 35 | export var isArray = Array.isArray 36 | 37 | /** 38 | * Object.assign function 39 | * @private 40 | */ 41 | export var assign = Object.assign 42 | 43 | /** 44 | * Poor man's pluck 45 | * @private 46 | */ 47 | export function pluck (name, def) { return function (obj) { return obj[name] || def } } 48 | -------------------------------------------------------------------------------- /live.js: -------------------------------------------------------------------------------- 1 | var SynthKit = require('.') 2 | window.log = function (name) { 3 | return console.log.bind(console, name) 4 | } 5 | SynthKit.start = SynthKit.master.start 6 | SynthKit.stop = SynthKit.master.stop 7 | 8 | var basic = require('./instruments/basic') 9 | SynthKit.ping = basic.ping 10 | 11 | SynthKit.b3 = require('./instruments/b3') 12 | SynthKit.sf = require('./instruments/soundfont').instrument 13 | SynthKit.tr808 = require('./instruments/tr808') 14 | SynthKit.emt140 = require('./instruments/emt140') 15 | var sfnames = require('./instruments/sf-names.json') 16 | 17 | SynthKit.sf.names = function (group, num) { 18 | return !group ? Object.keys(sfnames) 19 | : num ? sfnames[group][num - 1] : sfnames[group] 20 | } 21 | SynthKit.sf.search = function (str) { 22 | str = str.toLowerCase() 23 | var results = [] 24 | Object.keys(sfnames).forEach(function (group) { 25 | sfnames[group].forEach(function (name) { 26 | if (name.toLowerCase().includes(str)) results.push(name) 27 | }) 28 | }) 29 | return results 30 | } 31 | SynthKit.live = function () { 32 | var names = [] 33 | Object.keys(SynthKit).forEach(function (name) { 34 | window[name] = SynthKit[name] 35 | names.push(name) 36 | }) 37 | window.inst = function (synth, dest, opts) { 38 | var i = SynthKit.inst(synth, dest, opts) 39 | i.on('ready', function (_, name) { 40 | console.log('Instrument ready: ', name) 41 | }) 42 | return i 43 | } 44 | console.log('SynthKit live', 200, names.length) 45 | console.log(names.sort().join(', ')) 46 | } 47 | 48 | module.exports = SynthKit 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "synth-kit", 3 | "version": "0.0.0", 4 | "description": "A (web audio) synth construction kit", 5 | "main": "dist/synth-kit.js", 6 | "jsnext:main": "index", 7 | "scripts": { 8 | "pretest": "rollup -c", 9 | "test": "tape 'test/**/*.js'", 10 | "dist": "npm run dist-core && npm run dist-live && ls -hall dist/", 11 | "dist-core": "npm run test && uglifyjs dist/synth-kit.js > dist/synth-kit.min.js", 12 | "dist-live": "browserify live.js > dist/synth-kit.live.js && uglifyjs dist/synth-kit.live.js > dist/synth-kit.live.min.js", 13 | "dev": "rollup -c --watch", 14 | "example": "budo example/nodes.js", 15 | "start": "budo example/livecoding.js --open", 16 | "prepublish": "npm test && npm run dist && npm run docs", 17 | "docs": "rm -rf docs/ && jsdoc -c jsdoc.json && open docs/index.html" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/danigb/synth-kit.git" 22 | }, 23 | "keywords": [ 24 | "synth", 25 | "webaudio", 26 | "music", 27 | "generation" 28 | ], 29 | "author": "danigb", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/danigb/synth-kit/issues" 33 | }, 34 | "homepage": "https://github.com/danigb/synth-kit#readme", 35 | "devDependencies": { 36 | "postman-jsdoc-theme": "0.0.2", 37 | "rollup": "^0.36.3", 38 | "rollup-plugin-commonjs": "^5.0.5", 39 | "rollup-plugin-node-resolve": "^2.0.0", 40 | "rollup-watch": "^2.5.0", 41 | "tape": "^4.6.2", 42 | "web-audio-test-api": "^0.5.2" 43 | }, 44 | "dependencies": { 45 | "audio-context": "^0.1.0", 46 | "note-parser": "^2.0.0" 47 | }, 48 | "standard": { 49 | "ignore": [ 50 | "dist/**", "node_modules/**" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | 4 | export default { 5 | entry: 'lib/index.js', 6 | plugins: [ 7 | nodeResolve({ jsnext: true, main: true }), 8 | commonjs() 9 | ], 10 | dest: 'dist/synth-kit.js', 11 | moduleName: 'SynthKit', 12 | format: 'umd' 13 | } 14 | -------------------------------------------------------------------------------- /test/synth-kit-test.js: -------------------------------------------------------------------------------- 1 | require('web-audio-test-api') 2 | var test = require('tape') 3 | var SynthKit = require('..') 4 | 5 | test('SynthKit', function (t) { 6 | t.assert(SynthKit) 7 | t.end() 8 | }) 9 | --------------------------------------------------------------------------------