├── .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 | 
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
171 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
172 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
147 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
148 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
191 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
192 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
169 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
170 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
138 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
139 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
199 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
200 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
123 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
124 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
190 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
191 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
210 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
211 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
188 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
189 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Modules
27 |
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 |
127 | Documentation generated at Mon Nov 14 2016 12:23:13 GMT+0100 (CET)
128 |
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 |
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 |
--------------------------------------------------------------------------------