├── .gitignore
├── LICENSE
├── README.md
├── css
├── custom.css
├── normalize.css
├── nprogress.css
└── skeleton.css
├── images
├── favicon.png
├── speaker.jpg
└── speaker.original.jpg
├── index.html
├── index.js
├── lib
├── AbstractNode.js
├── effects
│ ├── Chorus.js
│ ├── Filter.js
│ └── WaveShaper.js
├── generators
│ ├── Bass.js
│ └── Sampler.js
└── util
│ ├── LFO.js
│ └── RecorderWrapper.js
├── neuro.js
├── package.json
├── plots
├── clipping.png
├── clipping.py
├── phasing.png
├── phasing.py
├── resampling.png
└── resampling.py
└── vendor
├── recorder.js
└── recorderWorker.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.swp
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Nick Thompson
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Neuro
2 |
3 | > A Web Audio sound design experiment and demonstration.
4 |
5 | Read the blog bost [here](http://nickwritesablog.com/sound-design-in-web-audio-neurofunk-bass-part-1/).
6 |
7 | ## About
8 |
9 | This repository holds a Web Audio demonstration which examines a particular sound design process for creating
10 | electronic bass sounds in the style of popular Drum n' Bass groups such as Noisia, Evol Intent, Trifonic, and KOAN
11 | Sound. The blog post which accompanies it, linked above, tries to explain the intricacies of three major parts of
12 | the process: phasing, waveshaping, and resampling, with the goal of answering the "why" kinds of questions behind
13 | each step of this well-known process.
14 |
15 | "Why is resampling important?"
16 |
17 | "Why does this distortion plugin sound better than my other distortion plugin?"
18 |
19 | The details which answer these kinds of questions are often hidden behind the implementation of popular DAW
20 | software and VST plugins. But those same details become important for shaping a sound when working with a
21 | lower-level interface such as the Web Audio API, and, frankly, are just really interesting. This project attempts
22 | to illuminate such details, at least from the perspective of a musician with little formal DSP training.
23 |
24 | ## License
25 |
26 | Copyright (c) 2015 Nick Thompson
27 |
28 | Permission is hereby granted, free of charge, to any person
29 | obtaining a copy of this software and associated documentation
30 | files (the "Software"), to deal in the Software without
31 | restriction, including without limitation the rights to use,
32 | copy, modify, merge, publish, distribute, sublicense, and/or sell
33 | copies of the Software, and to permit persons to whom the
34 | Software is furnished to do so, subject to the following
35 | conditions:
36 |
37 | The above copyright notice and this permission notice shall be
38 | included in all copies or substantial portions of the Software.
39 |
40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
42 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
43 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
44 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
45 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
46 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
47 | OTHER DEALINGS IN THE SOFTWARE.
48 |
--------------------------------------------------------------------------------
/css/custom.css:
--------------------------------------------------------------------------------
1 | html { height: 100%; }
2 | body { height: 100%; }
3 |
4 | h3.title {
5 | margin-bottom: 1rem;
6 | }
7 |
8 | h5.subtitle {
9 | color: #bbb;
10 | letter-spacing: .1rem;
11 | text-transform: uppercase;
12 | }
13 |
14 | p.description {
15 | font-family: "Open Sans";
16 | }
17 |
18 | p.disclaimer {
19 | font-family: "Open Sans";
20 | font-size: 1.2rem;
21 | font-style: italic;
22 | color: #bbb;
23 | margin-bottom: 1rem;
24 | }
25 |
26 | .main {
27 | padding: 8rem 0;
28 | }
29 |
30 | .photo {
31 | background: url('../images/speaker.jpg') center center no-repeat;
32 | background-size: cover;
33 | padding: 12rem 0;
34 | width: 100%;
35 | }
36 |
37 | .button[disabled] {
38 | border-color: #ddd;
39 | color: #ddd;
40 | }
41 |
--------------------------------------------------------------------------------
/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | -moz-box-sizing: content-box;
213 | box-sizing: content-box;
214 | height: 0;
215 | }
216 |
217 | /**
218 | * Contain overflow in all browsers.
219 | */
220 |
221 | pre {
222 | overflow: auto;
223 | }
224 |
225 | /**
226 | * Address odd `em`-unit font size rendering in all browsers.
227 | */
228 |
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 | font-family: monospace, monospace;
234 | font-size: 1em;
235 | }
236 |
237 | /* Forms
238 | ========================================================================== */
239 |
240 | /**
241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 | * styling of `select`, unless a `border` property is set.
243 | */
244 |
245 | /**
246 | * 1. Correct color not being inherited.
247 | * Known issue: affects color of disabled elements.
248 | * 2. Correct font properties not being inherited.
249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 | */
251 |
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 | color: inherit; /* 1 */
258 | font: inherit; /* 2 */
259 | margin: 0; /* 3 */
260 | }
261 |
262 | /**
263 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 | */
265 |
266 | button {
267 | overflow: visible;
268 | }
269 |
270 | /**
271 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 | * All other form control elements do not inherit `text-transform` values.
273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 | * Correct `select` style inheritance in Firefox.
275 | */
276 |
277 | button,
278 | select {
279 | text-transform: none;
280 | }
281 |
282 | /**
283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 | * and `video` controls.
285 | * 2. Correct inability to style clickable `input` types in iOS.
286 | * 3. Improve usability and consistency of cursor style between image-type
287 | * `input` and others.
288 | */
289 |
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 | -webkit-appearance: button; /* 2 */
295 | cursor: pointer; /* 3 */
296 | }
297 |
298 | /**
299 | * Re-set default cursor for disabled elements.
300 | */
301 |
302 | button[disabled],
303 | html input[disabled] {
304 | cursor: default;
305 | }
306 |
307 | /**
308 | * Remove inner padding and border in Firefox 4+.
309 | */
310 |
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 | border: 0;
314 | padding: 0;
315 | }
316 |
317 | /**
318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 | * the UA stylesheet.
320 | */
321 |
322 | input {
323 | line-height: normal;
324 | }
325 |
326 | /**
327 | * It's recommended that you don't attempt to style these elements.
328 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 | *
330 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 | * 2. Remove excess padding in IE 8/9/10.
332 | */
333 |
334 | input[type="checkbox"],
335 | input[type="radio"] {
336 | box-sizing: border-box; /* 1 */
337 | padding: 0; /* 2 */
338 | }
339 |
340 | /**
341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 | * `font-size` values of the `input`, it causes the cursor style of the
343 | * decrement button to change from `default` to `text`.
344 | */
345 |
346 | input[type="number"]::-webkit-inner-spin-button,
347 | input[type="number"]::-webkit-outer-spin-button {
348 | height: auto;
349 | }
350 |
351 | /**
352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
354 | * (include `-moz` to future-proof).
355 | */
356 |
357 | input[type="search"] {
358 | -webkit-appearance: textfield; /* 1 */
359 | -moz-box-sizing: content-box;
360 | -webkit-box-sizing: content-box; /* 2 */
361 | box-sizing: content-box;
362 | }
363 |
364 | /**
365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
366 | * Safari (but not Chrome) clips the cancel button when the search input has
367 | * padding (and `textfield` appearance).
368 | */
369 |
370 | input[type="search"]::-webkit-search-cancel-button,
371 | input[type="search"]::-webkit-search-decoration {
372 | -webkit-appearance: none;
373 | }
374 |
375 | /**
376 | * Define consistent border, margin, and padding.
377 | */
378 |
379 | fieldset {
380 | border: 1px solid #c0c0c0;
381 | margin: 0 2px;
382 | padding: 0.35em 0.625em 0.75em;
383 | }
384 |
385 | /**
386 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
388 | */
389 |
390 | legend {
391 | border: 0; /* 1 */
392 | padding: 0; /* 2 */
393 | }
394 |
395 | /**
396 | * Remove default vertical scrollbar in IE 8/9/10/11.
397 | */
398 |
399 | textarea {
400 | overflow: auto;
401 | }
402 |
403 | /**
404 | * Don't inherit the `font-weight` (applied by a rule above).
405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
406 | */
407 |
408 | optgroup {
409 | font-weight: bold;
410 | }
411 |
412 | /* Tables
413 | ========================================================================== */
414 |
415 | /**
416 | * Remove most spacing between table cells.
417 | */
418 |
419 | table {
420 | border-collapse: collapse;
421 | border-spacing: 0;
422 | }
423 |
424 | td,
425 | th {
426 | padding: 0;
427 | }
--------------------------------------------------------------------------------
/css/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #29d;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d;
26 | opacity: 1.0;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 15px;
39 | right: 15px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: #29d;
49 | border-left-color: #29d;
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% { -webkit-transform: rotate(0deg); }
68 | 100% { -webkit-transform: rotate(360deg); }
69 | }
70 | @keyframes nprogress-spinner {
71 | 0% { transform: rotate(0deg); }
72 | 100% { transform: rotate(360deg); }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/css/skeleton.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Skeleton V2.0.4
3 | * Copyright 2014, Dave Gamache
4 | * www.getskeleton.com
5 | * Free to use under the MIT license.
6 | * http://www.opensource.org/licenses/mit-license.php
7 | * 12/29/2014
8 | */
9 |
10 |
11 | /* Table of contents
12 | ––––––––––––––––––––––––––––––––––––––––––––––––––
13 | - Grid
14 | - Base Styles
15 | - Typography
16 | - Links
17 | - Buttons
18 | - Forms
19 | - Lists
20 | - Code
21 | - Tables
22 | - Spacing
23 | - Utilities
24 | - Clearing
25 | - Media Queries
26 | */
27 |
28 |
29 | /* Grid
30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
31 | .container {
32 | position: relative;
33 | width: 100%;
34 | max-width: 960px;
35 | margin: 0 auto;
36 | padding: 0 20px;
37 | box-sizing: border-box; }
38 | .column,
39 | .columns {
40 | width: 100%;
41 | float: left;
42 | box-sizing: border-box; }
43 |
44 | /* For devices larger than 400px */
45 | @media (min-width: 400px) {
46 | .container {
47 | width: 85%;
48 | padding: 0; }
49 | }
50 |
51 | /* For devices larger than 550px */
52 | @media (min-width: 550px) {
53 | .container {
54 | width: 80%; }
55 | .column,
56 | .columns {
57 | margin-left: 4%; }
58 | .column:first-child,
59 | .columns:first-child {
60 | margin-left: 0; }
61 |
62 | .one.column,
63 | .one.columns { width: 4.66666666667%; }
64 | .two.columns { width: 13.3333333333%; }
65 | .three.columns { width: 22%; }
66 | .four.columns { width: 30.6666666667%; }
67 | .five.columns { width: 39.3333333333%; }
68 | .six.columns { width: 48%; }
69 | .seven.columns { width: 56.6666666667%; }
70 | .eight.columns { width: 65.3333333333%; }
71 | .nine.columns { width: 74.0%; }
72 | .ten.columns { width: 82.6666666667%; }
73 | .eleven.columns { width: 91.3333333333%; }
74 | .twelve.columns { width: 100%; margin-left: 0; }
75 |
76 | .one-third.column { width: 30.6666666667%; }
77 | .two-thirds.column { width: 65.3333333333%; }
78 |
79 | .one-half.column { width: 48%; }
80 |
81 | /* Offsets */
82 | .offset-by-one.column,
83 | .offset-by-one.columns { margin-left: 8.66666666667%; }
84 | .offset-by-two.column,
85 | .offset-by-two.columns { margin-left: 17.3333333333%; }
86 | .offset-by-three.column,
87 | .offset-by-three.columns { margin-left: 26%; }
88 | .offset-by-four.column,
89 | .offset-by-four.columns { margin-left: 34.6666666667%; }
90 | .offset-by-five.column,
91 | .offset-by-five.columns { margin-left: 43.3333333333%; }
92 | .offset-by-six.column,
93 | .offset-by-six.columns { margin-left: 52%; }
94 | .offset-by-seven.column,
95 | .offset-by-seven.columns { margin-left: 60.6666666667%; }
96 | .offset-by-eight.column,
97 | .offset-by-eight.columns { margin-left: 69.3333333333%; }
98 | .offset-by-nine.column,
99 | .offset-by-nine.columns { margin-left: 78.0%; }
100 | .offset-by-ten.column,
101 | .offset-by-ten.columns { margin-left: 86.6666666667%; }
102 | .offset-by-eleven.column,
103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; }
104 |
105 | .offset-by-one-third.column,
106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; }
107 | .offset-by-two-thirds.column,
108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
109 |
110 | .offset-by-one-half.column,
111 | .offset-by-one-half.columns { margin-left: 52%; }
112 |
113 | }
114 |
115 |
116 | /* Base Styles
117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
118 | /* NOTE
119 | html is set to 62.5% so that all the REM measurements throughout Skeleton
120 | are based on 10px sizing. So basically 1.5rem = 15px :) */
121 | html {
122 | font-size: 62.5%; }
123 | body {
124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
125 | line-height: 1.6;
126 | font-weight: 400;
127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
128 | color: #222; }
129 |
130 |
131 | /* Typography
132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
133 | h1, h2, h3, h4, h5, h6 {
134 | margin-top: 0;
135 | margin-bottom: 2rem;
136 | font-weight: 300; }
137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
143 |
144 | /* Larger than phablet */
145 | @media (min-width: 550px) {
146 | h1 { font-size: 5.0rem; }
147 | h2 { font-size: 4.2rem; }
148 | h3 { font-size: 3.6rem; }
149 | h4 { font-size: 3.0rem; }
150 | h5 { font-size: 2.4rem; }
151 | h6 { font-size: 1.5rem; }
152 | }
153 |
154 | p {
155 | margin-top: 0; }
156 |
157 |
158 | /* Links
159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
160 | a {
161 | color: #1EAEDB; }
162 | a:hover {
163 | color: #0FA0CE; }
164 |
165 |
166 | /* Buttons
167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
168 | .button,
169 | button,
170 | input[type="submit"],
171 | input[type="reset"],
172 | input[type="button"] {
173 | display: inline-block;
174 | height: 38px;
175 | padding: 0 30px;
176 | color: #555;
177 | text-align: center;
178 | font-size: 11px;
179 | font-weight: 600;
180 | line-height: 38px;
181 | letter-spacing: .1rem;
182 | text-transform: uppercase;
183 | text-decoration: none;
184 | white-space: nowrap;
185 | background-color: transparent;
186 | border-radius: 4px;
187 | border: 1px solid #bbb;
188 | cursor: pointer;
189 | box-sizing: border-box; }
190 | .button:hover,
191 | button:hover,
192 | input[type="submit"]:hover,
193 | input[type="reset"]:hover,
194 | input[type="button"]:hover,
195 | .button:focus,
196 | button:focus,
197 | input[type="submit"]:focus,
198 | input[type="reset"]:focus,
199 | input[type="button"]:focus {
200 | color: #333;
201 | border-color: #888;
202 | outline: 0; }
203 | .button.button-primary,
204 | button.button-primary,
205 | input[type="submit"].button-primary,
206 | input[type="reset"].button-primary,
207 | input[type="button"].button-primary {
208 | color: #FFF;
209 | background-color: #33C3F0;
210 | border-color: #33C3F0; }
211 | .button.button-primary:hover,
212 | button.button-primary:hover,
213 | input[type="submit"].button-primary:hover,
214 | input[type="reset"].button-primary:hover,
215 | input[type="button"].button-primary:hover,
216 | .button.button-primary:focus,
217 | button.button-primary:focus,
218 | input[type="submit"].button-primary:focus,
219 | input[type="reset"].button-primary:focus,
220 | input[type="button"].button-primary:focus {
221 | color: #FFF;
222 | background-color: #1EAEDB;
223 | border-color: #1EAEDB; }
224 |
225 |
226 | /* Forms
227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
228 | input[type="email"],
229 | input[type="number"],
230 | input[type="search"],
231 | input[type="text"],
232 | input[type="tel"],
233 | input[type="url"],
234 | input[type="password"],
235 | textarea,
236 | select {
237 | height: 38px;
238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
239 | background-color: #fff;
240 | border: 1px solid #D1D1D1;
241 | border-radius: 4px;
242 | box-shadow: none;
243 | box-sizing: border-box; }
244 | /* Removes awkward default styles on some inputs for iOS */
245 | input[type="email"],
246 | input[type="number"],
247 | input[type="search"],
248 | input[type="text"],
249 | input[type="tel"],
250 | input[type="url"],
251 | input[type="password"],
252 | textarea {
253 | -webkit-appearance: none;
254 | -moz-appearance: none;
255 | appearance: none; }
256 | textarea {
257 | min-height: 65px;
258 | padding-top: 6px;
259 | padding-bottom: 6px; }
260 | input[type="email"]:focus,
261 | input[type="number"]:focus,
262 | input[type="search"]:focus,
263 | input[type="text"]:focus,
264 | input[type="tel"]:focus,
265 | input[type="url"]:focus,
266 | input[type="password"]:focus,
267 | textarea:focus,
268 | select:focus {
269 | border: 1px solid #33C3F0;
270 | outline: 0; }
271 | label,
272 | legend {
273 | display: block;
274 | margin-bottom: .5rem;
275 | font-weight: 600; }
276 | fieldset {
277 | padding: 0;
278 | border-width: 0; }
279 | input[type="checkbox"],
280 | input[type="radio"] {
281 | display: inline; }
282 | label > .label-body {
283 | display: inline-block;
284 | margin-left: .5rem;
285 | font-weight: normal; }
286 |
287 |
288 | /* Lists
289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
290 | ul {
291 | list-style: circle inside; }
292 | ol {
293 | list-style: decimal inside; }
294 | ol, ul {
295 | padding-left: 0;
296 | margin-top: 0; }
297 | ul ul,
298 | ul ol,
299 | ol ol,
300 | ol ul {
301 | margin: 1.5rem 0 1.5rem 3rem;
302 | font-size: 90%; }
303 | li {
304 | margin-bottom: 1rem; }
305 |
306 |
307 | /* Code
308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
309 | code {
310 | padding: .2rem .5rem;
311 | margin: 0 .2rem;
312 | font-size: 90%;
313 | white-space: nowrap;
314 | background: #F1F1F1;
315 | border: 1px solid #E1E1E1;
316 | border-radius: 4px; }
317 | pre > code {
318 | display: block;
319 | padding: 1rem 1.5rem;
320 | white-space: pre; }
321 |
322 |
323 | /* Tables
324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
325 | th,
326 | td {
327 | padding: 12px 15px;
328 | text-align: left;
329 | border-bottom: 1px solid #E1E1E1; }
330 | th:first-child,
331 | td:first-child {
332 | padding-left: 0; }
333 | th:last-child,
334 | td:last-child {
335 | padding-right: 0; }
336 |
337 |
338 | /* Spacing
339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
340 | button,
341 | .button {
342 | margin-bottom: 1rem; }
343 | input,
344 | textarea,
345 | select,
346 | fieldset {
347 | margin-bottom: 1.5rem; }
348 | pre,
349 | blockquote,
350 | dl,
351 | figure,
352 | table,
353 | p,
354 | ul,
355 | ol,
356 | form {
357 | margin-bottom: 2.5rem; }
358 |
359 |
360 | /* Utilities
361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
362 | .u-full-width {
363 | width: 100%;
364 | box-sizing: border-box; }
365 | .u-max-full-width {
366 | max-width: 100%;
367 | box-sizing: border-box; }
368 | .u-pull-right {
369 | float: right; }
370 | .u-pull-left {
371 | float: left; }
372 |
373 |
374 | /* Misc
375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
376 | hr {
377 | margin-top: 3rem;
378 | margin-bottom: 3.5rem;
379 | border-width: 0;
380 | border-top: 1px solid #E1E1E1; }
381 |
382 |
383 | /* Clearing
384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
385 |
386 | /* Self Clearing Goodness */
387 | .container:after,
388 | .row:after,
389 | .u-cf {
390 | content: "";
391 | display: table;
392 | clear: both; }
393 |
394 |
395 | /* Media Queries
396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
397 | /*
398 | Note: The best way to structure the use of media queries is to create the queries
399 | near the relevant code. For example, if you wanted to change the styles for buttons
400 | on small devices, paste the mobile query code up in the buttons section and style it
401 | there.
402 | */
403 |
404 |
405 | /* Larger than mobile */
406 | @media (min-width: 400px) {}
407 |
408 | /* Larger than phablet (also point when grid becomes active) */
409 | @media (min-width: 550px) {}
410 |
411 | /* Larger than tablet */
412 | @media (min-width: 750px) {}
413 |
414 | /* Larger than desktop */
415 | @media (min-width: 1000px) {}
416 |
417 | /* Larger than Desktop HD */
418 | @media (min-width: 1200px) {}
419 |
--------------------------------------------------------------------------------
/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nick-thompson/neuro/fa06f1de6bb38279ea360ebc19057bb2e39e5665/images/favicon.png
--------------------------------------------------------------------------------
/images/speaker.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nick-thompson/neuro/fa06f1de6bb38279ea360ebc19057bb2e39e5665/images/speaker.jpg
--------------------------------------------------------------------------------
/images/speaker.original.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nick-thompson/neuro/fa06f1de6bb38279ea360ebc19057bb2e39e5665/images/speaker.original.jpg
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Web Audio Sound Design | NeuroFunk
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
Sound Design in Web Audio
30 |
NeuroFunk Bass Demonstration
31 |
32 | This page serves to demonstrate a sound design experiment in Web Audio, which I've written about
33 | on my blog . The source code is also available on my
34 | Github profile .
35 | When you hit the "Play Demonstration" button below, you'll hear the progression of a simple bass sound through various stages of this particular sound design process. The final sound will then be available for download via the "Download Result" button. Each time you run the demonstration, the sound may change slightly, as certain parameters are scheduled with random values each iteration. The sound may be a little loud, so be careful with your speaker volume!
36 |
37 |
Play Demonstration
38 |
Download Result
39 |
40 | Can't hear anything? This project relies on Web Audio features that, at the time of writing, I have only found implemented in Chrome Canary. Give that a try, and keep an eye on your error console!
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var Bass = require('./lib/generators/Bass');
2 | var Chorus = require('./lib/effects/Chorus');
3 | var Filter = require('./lib/effects/Filter');
4 | var NProgress = require('nprogress');
5 | var RecorderWrapper = require('./lib/util/RecorderWrapper');
6 | var Sampler = require('./lib/generators/Sampler');
7 | var WaveShaper = require('./lib/effects/WaveShaper');
8 |
9 | var ctx = new AudioContext();
10 | var BPM = 168;
11 |
12 | // A simple helper function to play an arbitrary number of instruments,
13 | // and fire the callback only when each instrument has finished playing.
14 | function play() {
15 | var len = arguments.length;
16 | var callback = arguments[--len];
17 | var left = len;
18 | for (var i = 0; i < len; i++) {
19 | arguments[i].play(function(e) {
20 | if (--left === 0) {
21 | return callback(e);
22 | }
23 | });
24 | }
25 | }
26 |
27 | // Returns a random number generator weighted towards the lower end of the
28 | // provided range.
29 | function makeRandomGenerator(min, max, weight) {
30 | return function rand() {
31 | return Math.pow(Math.random(), weight) * (max - min + 1) + min;
32 | }
33 | }
34 |
35 | function uniformRandInt(min, max) {
36 | return Math.random() * (max - min + 1) + min;
37 | }
38 |
39 | function scheduleFilterAutomation(param, steps, rand) {
40 | var beats = 4;
41 | var duration = (60 / BPM) * beats;
42 | var interval = duration / steps;
43 |
44 | param.setValueAtTime(rand(), ctx.currentTime);
45 |
46 | for (var i = 0; i < steps; i++) {
47 | var delta = (i + 1) * interval;
48 | var t = ctx.currentTime + delta;
49 |
50 | param.exponentialRampToValueAtTime(rand(), t);
51 | }
52 | }
53 |
54 | function scheduleParallelFilterAutomation(p1, p2, steps, rand) {
55 | var beats = 4;
56 | var duration = (60 / BPM) * beats;
57 | var interval = duration / steps;
58 | var init = rand();
59 |
60 | p1.setValueAtTime(init, ctx.currentTime);
61 | p2.setValueAtTime(init / 2, ctx.currentTime);
62 |
63 | for (var i = 0; i < steps; i++) {
64 | var delta = (i + 1) * interval;
65 | var t = ctx.currentTime + delta;
66 | var r = rand();
67 |
68 | p1.exponentialRampToValueAtTime(r, t);
69 | p2.exponentialRampToValueAtTime(r / 2, t);
70 | }
71 | }
72 |
73 | // Step one is just the raw bass synth sent through a waveshaper and recorded
74 | // out to a buffer.
75 | function stepOne(callback) {
76 | var bass = new Bass(ctx);
77 | var recorder = new RecorderWrapper(ctx);
78 | var ws = new WaveShaper(ctx, {amount: 0.6});
79 | var notes = [
80 | 'g#1', 'g#1', '__', 'g#3',
81 | 'g#1', 'g#1', '__', 'b2',
82 | 'g#1', 'g#1', 'g#1', 'g#1',
83 | '__', 'g#2', '__', 'c#2'
84 | ];
85 |
86 | bass.connect(ws);
87 | ws.connect(recorder);
88 | ws.connect(ctx.destination);
89 |
90 | recorder.start();
91 | bass.play(BPM, 1, notes, function(e) {
92 | recorder.stop(callback);
93 | });
94 | }
95 |
96 | // In step two, we duplicate the incoming buffer and play one duplicate next
97 | // to the other but detuned +3 cents. We then send both sources through two
98 | // parallel effects racks. In the first, we have two heavily-modulated filters,
99 | // followed by a chorus. In the second, we send just the dry signal through.
100 | // The result is then merged and sent through a soft compression, slight EQ,
101 | // and additional filter modulation before being recorded out to buffer.
102 | function stepTwo(buffer, callback) {
103 | var recorder = new RecorderWrapper(ctx);
104 | var s1 = new Sampler(ctx, buffer);
105 | var s2 = new Sampler(ctx, buffer, {detune: 3});
106 | var bp = new Filter.Bandpass(ctx, {Q: 0.8});
107 | var notch = new Filter.Notch(ctx, {Q: 2.0});
108 | var ws = new WaveShaper(ctx, {amount: 0.6, drive: 0.6});
109 | var cr = new Chorus(ctx);
110 | var m1 = ctx.createGain();
111 | var m2 = ctx.createGain();
112 | var m3 = ctx.createGain();
113 | var comp = ctx.createDynamicsCompressor();
114 | var eq1 = new Filter.Peaking(ctx, {
115 | frequency: 128,
116 | gain: 2.0
117 | });
118 | var eq2 = new Filter.Peaking(ctx, {
119 | frequency: 4500,
120 | Q: 2.0,
121 | gain: 3.0
122 | });
123 | var eq3 = new Filter.Peaking(ctx, {
124 | frequency: 11000,
125 | gain: -3.0
126 | });
127 | var eq4 = new Filter.Highpass(ctx, {frequency: 20});
128 | var lp = new Filter.Lowpass(ctx, {Q: 2.0});
129 | var bp2 = new Filter.Bandpass(ctx, {Q: 0.8});
130 |
131 | // Merge the duplicate buffers
132 | s1.connect(m1);
133 | s2.connect(m1);
134 |
135 | // Connect the left side of the parallel chain
136 | m1.connect(bp.input);
137 | bp.connect(notch);
138 | notch.connect(cr);
139 | cr.connect(m2);
140 |
141 | // Connect the right side of the parallel chain
142 | m1.connect(m2);
143 |
144 | // Connect the merged result
145 | m2.connect(ws.input);
146 | ws.connect(comp);
147 | comp.connect(eq1.input);
148 | eq1.connect(eq2);
149 | eq2.connect(eq3);
150 | eq3.connect(eq4);
151 | eq4.connect(lp);
152 | eq4.connect(bp2);
153 | lp.connect(m3);
154 | bp2.connect(m3);
155 | m3.connect(recorder.input);
156 | m3.connect(ctx.destination);
157 |
158 | // Adjustments...
159 | m1.gain.value = 0.5;
160 | m2.gain.value = 0.5;
161 | m3.gain.value = 0.75;
162 | comp.ratio.value = 3.0;
163 | comp.knee.value = 25;
164 | comp.threshold.value = -16.0;
165 |
166 | scheduleFilterAutomation(
167 | bp._filter.frequency,
168 | 8,
169 | makeRandomGenerator(80, 18000, 3)
170 | );
171 |
172 | scheduleFilterAutomation(
173 | notch._filter.frequency,
174 | 4,
175 | makeRandomGenerator(160, 18000, 2)
176 | );
177 |
178 | scheduleParallelFilterAutomation(
179 | lp._filter.frequency,
180 | bp2._filter.frequency,
181 | uniformRandInt(2, 16),
182 | makeRandomGenerator(120, 18000, 2)
183 | );
184 |
185 | recorder.start();
186 | play(s1, s2, function(e) {
187 | recorder.stop(callback);
188 | });
189 | }
190 |
191 | // In step three we can't get away with too much modulation because of how
192 | // much filter movement we introduced in step two. Too much going on here really
193 | // complicates the sound, past the point of a pleasing result, in my opinion.
194 | function stepThree(buffer, callback) {
195 | var s1 = new Sampler(ctx, buffer);
196 | var s2 = new Sampler(ctx, buffer, {detune: 5});
197 | var recorder = new RecorderWrapper(ctx);
198 | var gain = ctx.createGain();
199 | var ws = new WaveShaper(ctx, {amount: 0.2});
200 | var ls = new Filter.Lowshelf(ctx, {
201 | frequency: 16000,
202 | gain: -1.0
203 | });
204 |
205 | gain.gain.value = 0.5;
206 |
207 | s1.connect(gain);
208 | s2.connect(gain);
209 | gain.connect(ws.input);
210 | ws.connect(ls);
211 | ls.connect(recorder);
212 | ls.connect(ctx.destination);
213 |
214 | recorder.start();
215 | play(s1, s2, function(e) {
216 | recorder.getDownloadFn(callback);
217 | });
218 | }
219 |
220 | // User interface stuff.
221 | document.addEventListener('DOMContentLoaded', function(e) {
222 | var playButton = document.getElementById('play');
223 | var downloadButton = document.getElementById('download');
224 | var _downloadFn = null;
225 | var _downloadNumber = 0;
226 |
227 | function enable() {
228 | playButton.removeAttribute('disabled');
229 | downloadButton.removeAttribute('disabled');
230 | }
231 |
232 | function disable() {
233 | playButton.setAttribute('disabled', 'true');
234 | downloadButton.setAttribute('disabled', 'true');
235 | }
236 |
237 | function download(e) {
238 | if (_downloadFn) {
239 | var name = 'WebAudioNeuroBass' + _downloadNumber++ + '.wav';
240 | _downloadFn(name);
241 | }
242 | }
243 |
244 | function waterfall(e) {
245 | disable();
246 | NProgress.start();
247 | stepOne(function(b1) {
248 | NProgress.inc();
249 | stepTwo(b1, function(b2) {
250 | NProgress.inc();
251 | stepThree(b2, function(downloadFn) {
252 | NProgress.done();
253 | enable();
254 | _downloadFn = downloadFn;
255 | });
256 | });
257 | });
258 | }
259 |
260 | playButton.addEventListener('click', waterfall);
261 | downloadButton.addEventListener('click', download);
262 | });
263 |
264 |
--------------------------------------------------------------------------------
/lib/AbstractNode.js:
--------------------------------------------------------------------------------
1 | /**
2 | * An abstract class for custom audio nodes.
3 | *
4 | * @param {AudioContext} context
5 | */
6 |
7 | function AbstractNode(context) {
8 | this.context = context;
9 |
10 | // Convenience assignment
11 | this.ctx = context;
12 |
13 | // Every node has an input and an output, whether or not they are used.
14 | this.input = context.createGain();
15 | this.output = context.createGain();
16 | }
17 |
18 | AbstractNode.prototype.connect = function connect(destination) {
19 | if (typeof destination.input !== 'undefined' &&
20 | destination.input instanceof AudioNode) {
21 | return this.output.connect(destination.input);
22 | }
23 | if (destination instanceof AudioNode) {
24 | return this.output.connect(destination);
25 | }
26 | };
27 |
28 | AbstractNode.prototype.disconnect = function disconnect(destination) {
29 | if (typeof destination.input !== 'undefined' &&
30 | destination.input instanceof AudioNode) {
31 | return this.output.disconnect(destination.input);
32 | }
33 | if (destination instanceof AudioNode) {
34 | return this.output.disconnect(destination);
35 | }
36 | return this.output.disconnect();
37 | };
38 |
39 | AbstractNode.prototype.disable = function disable() {
40 | this.input.disconnect();
41 | this.input.connect(this.output);
42 | };
43 |
44 | module.exports = AbstractNode;
45 |
--------------------------------------------------------------------------------
/lib/effects/Chorus.js:
--------------------------------------------------------------------------------
1 | var AbstractNode = require('../AbstractNode');
2 | var LFO = require('../util/LFO');
3 |
4 | /**
5 | * A simple chorus effect implementation.
6 | *
7 | * @param {AudioContext} ctx
8 | */
9 |
10 | function Chorus(ctx) {
11 | AbstractNode.call(this, ctx);
12 | this.attenuator = ctx.createGain();
13 | this.split = ctx.createChannelSplitter(2);
14 | this.leftDelay = ctx.createDelay();
15 | this.rightDelay = ctx.createDelay();
16 | this.leftGain = ctx.createGain();
17 | this.rightGain = ctx.createGain();
18 | this.merge = ctx.createChannelMerger(2);
19 |
20 | // Routing graph
21 | this.input.connect(this.attenuator);
22 | this.attenuator.connect(this.output);
23 | this.attenuator.connect(this.split);
24 | this.split.connect(this.leftDelay, 0);
25 | this.split.connect(this.rightDelay, 0);
26 | this.leftDelay.connect(this.leftGain);
27 | this.rightDelay.connect(this.rightGain);
28 | this.leftGain.connect(this.merge, 0, 0);
29 | this.rightGain.connect(this.merge, 0, 1);
30 | this.merge.connect(this.output);
31 |
32 | // Parameters
33 | this.depth = 0.335 / 1000;
34 | this.delay = 0.05 / 1000;
35 | this.attenuator.gain.value = 0.6934;
36 |
37 | // Modulation.
38 | // Traditionally, a chorus effect will modulate the delayTime
39 | // on both the left and right channel. For this implementation, I'll get
40 | // the desired effect only modulating one of them.
41 | this.lfo = new LFO(ctx, {
42 | freq: 3.828,
43 | scale: this.depth / 2
44 | });
45 |
46 | this.lfo.connect(this.rightDelay.delayTime);
47 | }
48 |
49 | Chorus.prototype = Object.create(AbstractNode.prototype, {
50 |
51 | delay: {
52 | enumerable: true,
53 | get: function() {
54 | return this._delay;
55 | },
56 | set: function(value) {
57 | this._delay = value;
58 | this.leftDelay.delayTime.value = value + this.depth / 2;
59 | this.rightDelay.delayTime.value = value + this.depth / 2;
60 | }
61 | }
62 |
63 | });
64 |
65 | module.exports = Chorus;
66 |
--------------------------------------------------------------------------------
/lib/effects/Filter.js:
--------------------------------------------------------------------------------
1 | var AbstractNode = require('../AbstractNode');
2 |
3 | /**
4 | * Filter constructor.
5 | *
6 | * @param {AudioContext} context
7 | * @param {object} opts
8 | * @param {number} opts.type
9 | * @param {number} opts.frequency
10 | * @param {number} opts.Q
11 | * @param {number} opts.gain
12 | * @param {number} opts.wet
13 | * @param {number} opts.dry
14 | */
15 |
16 | function Filter (context, opts) {
17 | AbstractNode.call(this, context);
18 |
19 | this._filter = context.createBiquadFilter();
20 | this._dry = context.createGain();
21 | this._wet = context.createGain();
22 |
23 | var p = this.meta.params;
24 | opts = opts || {};
25 | this._type = opts.type || p.type.defaultValue;
26 | this._filter.frequency.value = opts.frequency || p.frequency.defaultValue;
27 | this._filter.Q.value = opts.Q || p.Q.defaultValue;
28 | this._filter.gain.value = opts.gain || p.gain.defaultValue;
29 | this._wet.gain.value = opts.wet || p.wet.defaultValue;
30 | this._dry.gain.value = opts.dry || p.dry.defaultValue;
31 | this._filter.type = this._type;
32 |
33 | this.input.connect(this._filter);
34 | this._filter.connect(this._wet);
35 | this._wet.connect(this.output);
36 |
37 | this.input.connect(this._dry);
38 | this._dry.connect(this.output);
39 | }
40 |
41 | Filter.prototype = Object.create(AbstractNode.prototype, {
42 |
43 | /**
44 | * Module parameter metadata.
45 | */
46 |
47 | meta: {
48 | value: {
49 | name: "Filter",
50 | params: {
51 | type: {
52 | min: 0,
53 | max: 7,
54 | defaultValue: 0,
55 | type: "int"
56 | },
57 | frequency: {
58 | min: 0,
59 | max: 22050,
60 | defaultValue: 8000,
61 | type: "float"
62 | },
63 | Q: {
64 | min: 0.0001,
65 | max: 1000,
66 | defaultValue: 1.0,
67 | type: "float"
68 | },
69 | gain: {
70 | min: -40,
71 | max: 40,
72 | defaultValue: 1,
73 | type: "float"
74 | },
75 | wet: {
76 | min: 0,
77 | max: 1,
78 | defaultValue: 1,
79 | type: "float"
80 | },
81 | dry: {
82 | min: 0,
83 | max: 1,
84 | defaultValue: 0,
85 | type: "float"
86 | }
87 | }
88 | }
89 | },
90 |
91 | /**
92 | * Public parameters.
93 | */
94 |
95 | type: {
96 | enumerable: true,
97 | get: function () { return this._type; },
98 | set: function (value) {
99 | this._type = ~~value;
100 | this._filter.type = ~~value;
101 | }
102 | },
103 |
104 | frequency: {
105 | enumerable: true,
106 | get: function () { return this._filter.frequency.value; },
107 | set: function (value) {
108 | this._filter.frequency.setValueAtTime(value, 0);
109 | }
110 | },
111 |
112 | Q: {
113 | enumerable: true,
114 | get: function () { return this._filter.Q.value; },
115 | set: function (value) {
116 | this._filter.Q.setValueAtTime(value, 0);
117 | }
118 | },
119 |
120 | gain: {
121 | enumerable: true,
122 | get: function () { return this._filter.gain.value; },
123 | set: function (value) {
124 | this._filter.gain.setValueAtTime(value, 0);
125 | }
126 | },
127 |
128 | wet: {
129 | enumerable: true,
130 | get: function () { return this._wet.gain.value; },
131 | set: function (value) {
132 | this._wet.gain.setValueAtTime(value, 0);
133 | }
134 | },
135 |
136 | dry: {
137 | enumerable: true,
138 | get: function () { return this._dry.gain.value; },
139 | set: function (value) {
140 | this._dry.gain.setValueAtTime(value, 0);
141 | }
142 | }
143 |
144 | });
145 |
146 | /**
147 | * Convenience constructors.
148 | */
149 |
150 | Filter.Lowpass = function (context, opts) {
151 | opts = opts || {};
152 | opts.type = 'lowpass';
153 | return new Filter(context, opts);
154 | };
155 |
156 | Filter.Highpass = function (context, opts) {
157 | opts = opts || {};
158 | opts.type = 'highpass';
159 | return new Filter(context, opts);
160 | };
161 |
162 | Filter.Bandpass = function (context, opts) {
163 | opts = opts || {};
164 | opts.type = 'bandpass';
165 | return new Filter(context, opts);
166 | };
167 |
168 | Filter.Lowshelf = function (context, opts) {
169 | opts = opts || {};
170 | opts.type = 'lowshelf';
171 | return new Filter(context, opts);
172 | };
173 |
174 | Filter.Highshelf = function (context, opts) {
175 | opts = opts || {};
176 | opts.type = 'highshelf';
177 | return new Filter(context, opts);
178 | };
179 |
180 | Filter.Peaking = function (context, opts) {
181 | opts = opts || {};
182 | opts.type = 'peaking';
183 | return new Filter(context, opts);
184 | };
185 |
186 | Filter.Notch = function (context, opts) {
187 | opts = opts || {};
188 | opts.type = 'notch';
189 | return new Filter(context, opts);
190 | };
191 |
192 | Filter.Allpass = function (context, opts) {
193 | opts = opts || {};
194 | opts.type = 'allpass';
195 | return new Filter(context, opts);
196 | };
197 |
198 | /**
199 | * Exports.
200 | */
201 |
202 | module.exports = Filter;
203 |
--------------------------------------------------------------------------------
/lib/effects/WaveShaper.js:
--------------------------------------------------------------------------------
1 | var AbstractNode = require('../AbstractNode');
2 |
3 | /**
4 | * WaveShaper node for a hard clipping curve with configurable drive.
5 | *
6 | * @param {AudioContext} ctx
7 | * @param {Object} opts
8 | * @param {Number} opts.drive
9 | */
10 |
11 | function WaveShaper(ctx, opts) {
12 | AbstractNode.call(this, ctx);
13 | this._ws = ctx.createWaveShaper();
14 |
15 | this.drive = opts && opts.drive || 1.0;
16 | this.amount = opts && opts.amount || 0.5;
17 | this.input.gain.value = this.drive;
18 | this.output.gain.value = Math.pow(1.0 / this.drive, 0.6);
19 | this._setCurve();
20 |
21 | this.input.connect(this._ws);
22 | this._ws.connect(this.output);
23 | }
24 |
25 | WaveShaper.prototype = Object.create(AbstractNode.prototype, {
26 |
27 | _setCurve: {
28 | value: function() {
29 | var n = 65536;
30 | var curve = new Float32Array(n);
31 | var amt = Math.min(this.amount, 0.9999);
32 | var k = 2 * amt / (1 - amt);
33 |
34 | for (var i = 0; i < n; i++) {
35 | var x = (i - (n / 2)) / (n / 2);
36 |
37 | // Identity
38 | // curve[i] = x;
39 |
40 | curve[i] = (1 + k) * x / (1 + k * Math.abs(x))
41 |
42 | // Hard clipping
43 | // curve[i] = 0.5 * (Math.abs(x + 0.63) - Math.abs(x - 0.63));
44 |
45 | // Soft clipping
46 | // curve[i] = Math.tanh(x);
47 |
48 | // Soft clipping cubic approximation
49 | // curve[i] = x - Math.pow(x, 3) / 4;
50 | }
51 |
52 | this._ws.curve = curve;
53 | this._ws.oversample = '2x';
54 | }
55 | }
56 |
57 | });
58 |
59 | module.exports = WaveShaper;
60 |
--------------------------------------------------------------------------------
/lib/generators/Bass.js:
--------------------------------------------------------------------------------
1 | var AbstractNode = require('../AbstractNode');
2 |
3 | var teoria = require('teoria');
4 |
5 | /**
6 | * A simple detuned saw pair with a sub sine for those lows.
7 | *
8 | * @param {AudioContext} ctx
9 | */
10 |
11 | function Bass(ctx) {
12 | AbstractNode.call(this, ctx);
13 | this.detuneRatio = 2.053 / 2;
14 |
15 | this.sawOne = this.ctx.createOscillator();
16 | this.sawTwo = this.ctx.createOscillator();
17 | this.sub = this.ctx.createOscillator();
18 |
19 | this.sawOne.type = 'sawtooth';
20 | this.sawTwo.type = 'sawtooth';
21 |
22 | this.sawGain = this.ctx.createGain();
23 | this.subGain = this.ctx.createGain();
24 |
25 | this.sawGain.gain.value = 0.36;
26 | this.subGain.gain.value = 0.18;
27 |
28 | this.sawOne.connect(this.sawGain);
29 | this.sawTwo.connect(this.sawGain);
30 | this.sub.connect(this.subGain);
31 |
32 | this.sawGain.connect(this.output);
33 | this.subGain.connect(this.output);
34 | }
35 |
36 | Bass.prototype = Object.create(AbstractNode.prototype, {
37 |
38 | start: {
39 | value: function(when) {
40 | this.sawOne.start(when);
41 | this.sawTwo.start(when);
42 | this.sub.start(when);
43 | }
44 | },
45 |
46 | stop: {
47 | value: function(when) {
48 | this.sawOne.stop(when);
49 | this.sawTwo.stop(when);
50 | this.sub.stop(when);
51 | }
52 | },
53 |
54 | play: {
55 | value: function(bpm, bars, notes, callback) {
56 | var beats = 4 * bars;
57 | var duration = (60 / bpm) * beats;
58 | var interval = duration / notes.length;
59 | var timeout = window.setTimeout(callback, 1450);
60 |
61 | this.sawOne.onended = function(e) {
62 | window.clearTimeout(timeout);
63 | return callback(e);
64 | };
65 |
66 | // I'm assuming that the first note here is not a rest.
67 | var fq = teoria.note(notes[0]).fq();
68 | this.sub.frequency.setValueAtTime(fq, this.ctx.currentTime);
69 | this.sawOne.frequency.setValueAtTime(fq, this.ctx.currentTime);
70 | this.sawTwo.frequency.setValueAtTime(
71 | fq * this.detuneRatio,
72 | this.ctx.currentTime
73 | );
74 |
75 | notes.forEach(function(note, i) {
76 | if (note === '__') {
77 | return;
78 | }
79 | var fq = teoria.note(note).fq();
80 | var delta = (i + 1) * interval;
81 | var t = this.ctx.currentTime + delta;
82 |
83 | this.sub.frequency.exponentialRampToValueAtTime(fq, t);
84 | this.sawOne.frequency.exponentialRampToValueAtTime(fq, t);
85 | this.sawTwo.frequency.exponentialRampToValueAtTime(
86 | fq * this.detuneRatio,
87 | t
88 | );
89 | }, this);
90 |
91 | this.start(0);
92 | this.stop(this.ctx.currentTime + duration);
93 | }
94 | }
95 |
96 | });
97 |
98 | module.exports = Bass;
99 |
--------------------------------------------------------------------------------
/lib/generators/Sampler.js:
--------------------------------------------------------------------------------
1 | var AbstractNode = require('../AbstractNode');
2 |
3 | /**
4 | * A simple sampler instrument.
5 | *
6 | * @param {AudioContext} ctx
7 | * @param {AudioBuffer} buffer
8 | * @param {Object} opts
9 | * @param {Number} opts.detune
10 | */
11 |
12 | function Sampler(ctx, buffer, opts) {
13 | AbstractNode.call(this, ctx);
14 | this.source = ctx.createBufferSource();
15 | this.buffer = buffer;
16 | this.source.buffer = buffer;
17 | this.source.detune.value = opts && opts.detune || 0;
18 | this.source.connect(this.output);
19 | }
20 |
21 | Sampler.prototype = Object.create(AbstractNode.prototype, {
22 |
23 | start: {
24 | value: function(when) {
25 | this.source.start(when);
26 | }
27 | },
28 |
29 | stop: {
30 | value: function(when) {
31 | this.source.stop(when);
32 | }
33 | },
34 |
35 | play: {
36 | value: function(callback) {
37 | var timeout = window.setTimeout(callback, 1450);
38 |
39 | this.source.onended = function(e) {
40 | window.clearTimeout(timeout);
41 | return callback(e);
42 | };
43 |
44 | this.start(0);
45 | this.stop(this.ctx.currentTime + this.buffer.duration);
46 | }
47 | }
48 |
49 | });
50 |
51 | module.exports = Sampler;
52 |
--------------------------------------------------------------------------------
/lib/util/LFO.js:
--------------------------------------------------------------------------------
1 | var AbstractNode = require('../AbstractNode');
2 |
3 | /**
4 | * An LFO utility for scaling oscillator output appropriately.
5 | *
6 | * The output of the LFO will be on the range [-scale, scale] where scale
7 | * is the value provided in the options object.
8 | *
9 | * Assuming a sine-based LFO is connected to an AudioParam, the value of
10 | * the AudioParam, A, at time t will be
11 | * value = A.value + sin(t) * scale
12 | *
13 | * That is to say that the baseline value will be provided by the `.value`
14 | * property supplied on the target AudioParam. Thus, for example, suppose you
15 | * want your AudioParam's value to oscillate on the range [0, 100]. In that
16 | * case, you should set `.value` = 50, and supply a `scale` parameter to the
17 | * LFO of 50. The computed result then traverses the range [50 - 50, 50 + 50].
18 | *
19 | * Another option involves summing two LFOs of the same frequency and oscillator
20 | * type. Pretty simply, the range [-scale, scale] summed once with itself
21 | * becomes [0, 2 * scale].
22 | *
23 | * @param {AudioContext} context
24 | * @param {Object} opts
25 | * @param {Number} opts.scale
26 | * @param {Number} opts.freq
27 | * @param {String} opts.type
28 | */
29 |
30 | function LFO(context, opts) {
31 | AbstractNode.call(this, context);
32 | this.osc = context.createOscillator();
33 | this.scale = context.createGain();
34 |
35 | opts = opts || {};
36 | this.osc.frequency.value = opts.freq || 440;
37 | this.osc.type = opts.type || 'sine';
38 | this.scale.gain.value = opts.scale || 1.0;
39 |
40 | this.osc.connect(this.scale);
41 | this.scale.connect(this.output);
42 |
43 | this.osc.start(0);
44 | }
45 |
46 | LFO.prototype = Object.create(AbstractNode.prototype);
47 |
48 | module.exports = LFO;
49 |
--------------------------------------------------------------------------------
/lib/util/RecorderWrapper.js:
--------------------------------------------------------------------------------
1 | var AbstractNode = require('../AbstractNode');
2 |
3 | /**
4 | * A simple wrapper around window.Recorder for simplifying the procedure
5 | * for collecting the recorded buffer.
6 | *
7 | * @param {AudioContext} ctx
8 | */
9 |
10 | function RecorderWrapper(ctx) {
11 | AbstractNode.call(this, ctx);
12 | this.channels = 1;
13 | this.recorder = new Recorder(this.input, {
14 | workerPath: 'vendor/recorderWorker.js',
15 | numChannels: this.channels
16 | });
17 | }
18 |
19 | RecorderWrapper.prototype = Object.create(AbstractNode.prototype, {
20 |
21 | start: {
22 | value: function() {
23 | return this.recorder.record();
24 | }
25 | },
26 |
27 | stop: {
28 | value: function(callback) {
29 | this.recorder.stop();
30 | return this.recorder.getBuffer(function(buffers) {
31 | var arr = buffers[0];
32 | var buffer = this.ctx.createBuffer(
33 | this.channels,
34 | arr.length,
35 | this.ctx.sampleRate
36 | );
37 |
38 | buffer.getChannelData(0).set(arr);
39 | return callback(buffer);
40 | }.bind(this));
41 | }
42 | },
43 |
44 | getDownloadFn: {
45 | value: function(callback) {
46 | this.recorder.stop();
47 | return this.recorder.exportWAV(function(blob) {
48 | return callback(function(filename) {
49 | Recorder.forceDownload(blob, filename);
50 | });
51 | });
52 | }
53 | }
54 |
55 | });
56 |
57 | module.exports = RecorderWrapper;
58 |
--------------------------------------------------------------------------------
/neuro.js:
--------------------------------------------------------------------------------
1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o
'
938 | };
939 |
940 | /**
941 | * Updates configuration.
942 | *
943 | * NProgress.configure({
944 | * minimum: 0.1
945 | * });
946 | */
947 | NProgress.configure = function(options) {
948 | var key, value;
949 | for (key in options) {
950 | value = options[key];
951 | if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
952 | }
953 |
954 | return this;
955 | };
956 |
957 | /**
958 | * Last number.
959 | */
960 |
961 | NProgress.status = null;
962 |
963 | /**
964 | * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
965 | *
966 | * NProgress.set(0.4);
967 | * NProgress.set(1.0);
968 | */
969 |
970 | NProgress.set = function(n) {
971 | var started = NProgress.isStarted();
972 |
973 | n = clamp(n, Settings.minimum, 1);
974 | NProgress.status = (n === 1 ? null : n);
975 |
976 | var progress = NProgress.render(!started),
977 | bar = progress.querySelector(Settings.barSelector),
978 | speed = Settings.speed,
979 | ease = Settings.easing;
980 |
981 | progress.offsetWidth; /* Repaint */
982 |
983 | queue(function(next) {
984 | // Set positionUsing if it hasn't already been set
985 | if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
986 |
987 | // Add transition
988 | css(bar, barPositionCSS(n, speed, ease));
989 |
990 | if (n === 1) {
991 | // Fade out
992 | css(progress, {
993 | transition: 'none',
994 | opacity: 1
995 | });
996 | progress.offsetWidth; /* Repaint */
997 |
998 | setTimeout(function() {
999 | css(progress, {
1000 | transition: 'all ' + speed + 'ms linear',
1001 | opacity: 0
1002 | });
1003 | setTimeout(function() {
1004 | NProgress.remove();
1005 | next();
1006 | }, speed);
1007 | }, speed);
1008 | } else {
1009 | setTimeout(next, speed);
1010 | }
1011 | });
1012 |
1013 | return this;
1014 | };
1015 |
1016 | NProgress.isStarted = function() {
1017 | return typeof NProgress.status === 'number';
1018 | };
1019 |
1020 | /**
1021 | * Shows the progress bar.
1022 | * This is the same as setting the status to 0%, except that it doesn't go backwards.
1023 | *
1024 | * NProgress.start();
1025 | *
1026 | */
1027 | NProgress.start = function() {
1028 | if (!NProgress.status) NProgress.set(0);
1029 |
1030 | var work = function() {
1031 | setTimeout(function() {
1032 | if (!NProgress.status) return;
1033 | NProgress.trickle();
1034 | work();
1035 | }, Settings.trickleSpeed);
1036 | };
1037 |
1038 | if (Settings.trickle) work();
1039 |
1040 | return this;
1041 | };
1042 |
1043 | /**
1044 | * Hides the progress bar.
1045 | * This is the *sort of* the same as setting the status to 100%, with the
1046 | * difference being `done()` makes some placebo effect of some realistic motion.
1047 | *
1048 | * NProgress.done();
1049 | *
1050 | * If `true` is passed, it will show the progress bar even if its hidden.
1051 | *
1052 | * NProgress.done(true);
1053 | */
1054 |
1055 | NProgress.done = function(force) {
1056 | if (!force && !NProgress.status) return this;
1057 |
1058 | return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
1059 | };
1060 |
1061 | /**
1062 | * Increments by a random amount.
1063 | */
1064 |
1065 | NProgress.inc = function(amount) {
1066 | var n = NProgress.status;
1067 |
1068 | if (!n) {
1069 | return NProgress.start();
1070 | } else {
1071 | if (typeof amount !== 'number') {
1072 | amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95);
1073 | }
1074 |
1075 | n = clamp(n + amount, 0, 0.994);
1076 | return NProgress.set(n);
1077 | }
1078 | };
1079 |
1080 | NProgress.trickle = function() {
1081 | return NProgress.inc(Math.random() * Settings.trickleRate);
1082 | };
1083 |
1084 | /**
1085 | * Waits for all supplied jQuery promises and
1086 | * increases the progress as the promises resolve.
1087 | *
1088 | * @param $promise jQUery Promise
1089 | */
1090 | (function() {
1091 | var initial = 0, current = 0;
1092 |
1093 | NProgress.promise = function($promise) {
1094 | if (!$promise || $promise.state() === "resolved") {
1095 | return this;
1096 | }
1097 |
1098 | if (current === 0) {
1099 | NProgress.start();
1100 | }
1101 |
1102 | initial++;
1103 | current++;
1104 |
1105 | $promise.always(function() {
1106 | current--;
1107 | if (current === 0) {
1108 | initial = 0;
1109 | NProgress.done();
1110 | } else {
1111 | NProgress.set((initial - current) / initial);
1112 | }
1113 | });
1114 |
1115 | return this;
1116 | };
1117 |
1118 | })();
1119 |
1120 | /**
1121 | * (Internal) renders the progress bar markup based on the `template`
1122 | * setting.
1123 | */
1124 |
1125 | NProgress.render = function(fromStart) {
1126 | if (NProgress.isRendered()) return document.getElementById('nprogress');
1127 |
1128 | addClass(document.documentElement, 'nprogress-busy');
1129 |
1130 | var progress = document.createElement('div');
1131 | progress.id = 'nprogress';
1132 | progress.innerHTML = Settings.template;
1133 |
1134 | var bar = progress.querySelector(Settings.barSelector),
1135 | perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
1136 | parent = document.querySelector(Settings.parent),
1137 | spinner;
1138 |
1139 | css(bar, {
1140 | transition: 'all 0 linear',
1141 | transform: 'translate3d(' + perc + '%,0,0)'
1142 | });
1143 |
1144 | if (!Settings.showSpinner) {
1145 | spinner = progress.querySelector(Settings.spinnerSelector);
1146 | spinner && removeElement(spinner);
1147 | }
1148 |
1149 | if (parent != document.body) {
1150 | addClass(parent, 'nprogress-custom-parent');
1151 | }
1152 |
1153 | parent.appendChild(progress);
1154 | return progress;
1155 | };
1156 |
1157 | /**
1158 | * Removes the element. Opposite of render().
1159 | */
1160 |
1161 | NProgress.remove = function() {
1162 | removeClass(document.documentElement, 'nprogress-busy');
1163 | removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent');
1164 | var progress = document.getElementById('nprogress');
1165 | progress && removeElement(progress);
1166 | };
1167 |
1168 | /**
1169 | * Checks if the progress bar is rendered.
1170 | */
1171 |
1172 | NProgress.isRendered = function() {
1173 | return !!document.getElementById('nprogress');
1174 | };
1175 |
1176 | /**
1177 | * Determine which positioning CSS rule to use.
1178 | */
1179 |
1180 | NProgress.getPositioningCSS = function() {
1181 | // Sniff on document.body.style
1182 | var bodyStyle = document.body.style;
1183 |
1184 | // Sniff prefixes
1185 | var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
1186 | ('MozTransform' in bodyStyle) ? 'Moz' :
1187 | ('msTransform' in bodyStyle) ? 'ms' :
1188 | ('OTransform' in bodyStyle) ? 'O' : '';
1189 |
1190 | if (vendorPrefix + 'Perspective' in bodyStyle) {
1191 | // Modern browsers with 3D support, e.g. Webkit, IE10
1192 | return 'translate3d';
1193 | } else if (vendorPrefix + 'Transform' in bodyStyle) {
1194 | // Browsers without 3D support, e.g. IE9
1195 | return 'translate';
1196 | } else {
1197 | // Browsers without translate() support, e.g. IE7-8
1198 | return 'margin';
1199 | }
1200 | };
1201 |
1202 | /**
1203 | * Helpers
1204 | */
1205 |
1206 | function clamp(n, min, max) {
1207 | if (n < min) return min;
1208 | if (n > max) return max;
1209 | return n;
1210 | }
1211 |
1212 | /**
1213 | * (Internal) converts a percentage (`0..1`) to a bar translateX
1214 | * percentage (`-100%..0%`).
1215 | */
1216 |
1217 | function toBarPerc(n) {
1218 | return (-1 + n) * 100;
1219 | }
1220 |
1221 |
1222 | /**
1223 | * (Internal) returns the correct CSS for changing the bar's
1224 | * position given an n percentage, and speed and ease from Settings
1225 | */
1226 |
1227 | function barPositionCSS(n, speed, ease) {
1228 | var barCSS;
1229 |
1230 | if (Settings.positionUsing === 'translate3d') {
1231 | barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
1232 | } else if (Settings.positionUsing === 'translate') {
1233 | barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
1234 | } else {
1235 | barCSS = { 'margin-left': toBarPerc(n)+'%' };
1236 | }
1237 |
1238 | barCSS.transition = 'all '+speed+'ms '+ease;
1239 |
1240 | return barCSS;
1241 | }
1242 |
1243 | /**
1244 | * (Internal) Queues a function to be executed.
1245 | */
1246 |
1247 | var queue = (function() {
1248 | var pending = [];
1249 |
1250 | function next() {
1251 | var fn = pending.shift();
1252 | if (fn) {
1253 | fn(next);
1254 | }
1255 | }
1256 |
1257 | return function(fn) {
1258 | pending.push(fn);
1259 | if (pending.length == 1) next();
1260 | };
1261 | })();
1262 |
1263 | /**
1264 | * (Internal) Applies css properties to an element, similar to the jQuery
1265 | * css method.
1266 | *
1267 | * While this helper does assist with vendor prefixed property names, it
1268 | * does not perform any manipulation of values prior to setting styles.
1269 | */
1270 |
1271 | var css = (function() {
1272 | var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
1273 | cssProps = {};
1274 |
1275 | function camelCase(string) {
1276 | return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
1277 | return letter.toUpperCase();
1278 | });
1279 | }
1280 |
1281 | function getVendorProp(name) {
1282 | var style = document.body.style;
1283 | if (name in style) return name;
1284 |
1285 | var i = cssPrefixes.length,
1286 | capName = name.charAt(0).toUpperCase() + name.slice(1),
1287 | vendorName;
1288 | while (i--) {
1289 | vendorName = cssPrefixes[i] + capName;
1290 | if (vendorName in style) return vendorName;
1291 | }
1292 |
1293 | return name;
1294 | }
1295 |
1296 | function getStyleProp(name) {
1297 | name = camelCase(name);
1298 | return cssProps[name] || (cssProps[name] = getVendorProp(name));
1299 | }
1300 |
1301 | function applyCss(element, prop, value) {
1302 | prop = getStyleProp(prop);
1303 | element.style[prop] = value;
1304 | }
1305 |
1306 | return function(element, properties) {
1307 | var args = arguments,
1308 | prop,
1309 | value;
1310 |
1311 | if (args.length == 2) {
1312 | for (prop in properties) {
1313 | value = properties[prop];
1314 | if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
1315 | }
1316 | } else {
1317 | applyCss(element, args[1], args[2]);
1318 | }
1319 | }
1320 | })();
1321 |
1322 | /**
1323 | * (Internal) Determines if an element or space separated list of class names contains a class name.
1324 | */
1325 |
1326 | function hasClass(element, name) {
1327 | var list = typeof element == 'string' ? element : classList(element);
1328 | return list.indexOf(' ' + name + ' ') >= 0;
1329 | }
1330 |
1331 | /**
1332 | * (Internal) Adds a class to an element.
1333 | */
1334 |
1335 | function addClass(element, name) {
1336 | var oldList = classList(element),
1337 | newList = oldList + name;
1338 |
1339 | if (hasClass(oldList, name)) return;
1340 |
1341 | // Trim the opening space.
1342 | element.className = newList.substring(1);
1343 | }
1344 |
1345 | /**
1346 | * (Internal) Removes a class from an element.
1347 | */
1348 |
1349 | function removeClass(element, name) {
1350 | var oldList = classList(element),
1351 | newList;
1352 |
1353 | if (!hasClass(element, name)) return;
1354 |
1355 | // Replace the class name.
1356 | newList = oldList.replace(' ' + name + ' ', ' ');
1357 |
1358 | // Trim the opening and closing spaces.
1359 | element.className = newList.substring(1, newList.length - 1);
1360 | }
1361 |
1362 | /**
1363 | * (Internal) Gets a space separated list of the class names on the element.
1364 | * The list is wrapped with a single space on each end to facilitate finding
1365 | * matches within the list.
1366 | */
1367 |
1368 | function classList(element) {
1369 | return (' ' + (element.className || '') + ' ').replace(/\s+/gi, ' ');
1370 | }
1371 |
1372 | /**
1373 | * (Internal) Removes an element from the DOM.
1374 | */
1375 |
1376 | function removeElement(element) {
1377 | element && element.parentNode && element.parentNode.removeChild(element);
1378 | }
1379 |
1380 | return NProgress;
1381 | });
1382 |
1383 |
1384 | },{}],11:[function(require,module,exports){
1385 | var Note = require('./lib/note');
1386 | var Interval = require('./lib/interval');
1387 | var Chord = require('./lib/chord');
1388 | var Scale = require('./lib/scale');
1389 |
1390 | // never thought I would write this, but: Legacy support
1391 | function intervalConstructor(from, to) {
1392 | // Construct a Interval object from string representation
1393 | if (typeof from === 'string')
1394 | return Interval.toCoord(from);
1395 |
1396 | if (typeof to === 'string' && from instanceof Note)
1397 | return Interval.from(from, Interval.toCoord(to));
1398 |
1399 | if (to instanceof Interval && from instanceof Note)
1400 | return Interval.from(from, to);
1401 |
1402 | if (to instanceof Note && from instanceof Note)
1403 | return Interval.between(from, to);
1404 |
1405 | throw new Error('Invalid parameters');
1406 | }
1407 |
1408 | intervalConstructor.toCoord = Interval.toCoord;
1409 | intervalConstructor.from = Interval.from;
1410 | intervalConstructor.between = Interval.between;
1411 | intervalConstructor.invert = Interval.invert;
1412 |
1413 | function noteConstructor(name, duration) {
1414 | if (typeof name === 'string')
1415 | return Note.fromString(name, duration);
1416 | else
1417 | return new Note(name, duration);
1418 | }
1419 |
1420 | noteConstructor.fromString = Note.fromString;
1421 | noteConstructor.fromKey = Note.fromKey;
1422 | noteConstructor.fromFrequency = Note.fromFrequency;
1423 | noteConstructor.fromMIDI = Note.fromMIDI;
1424 |
1425 | function chordConstructor(name, symbol) {
1426 | if (typeof name === 'string') {
1427 | var root, octave;
1428 | root = name.match(/^([a-h])(x|#|bb|b?)/i);
1429 | if (root && root[0]) {
1430 | octave = typeof symbol === 'number' ? symbol.toString(10) : '4';
1431 | return new Chord(Note.fromString(root[0].toLowerCase() + octave),
1432 | name.substr(root[0].length));
1433 | }
1434 | } else if (name instanceof Note)
1435 | return new Chord(name, symbol);
1436 |
1437 | throw new Error('Invalid Chord. Couldn\'t find note name');
1438 | }
1439 |
1440 | function scaleConstructor(tonic, scale) {
1441 | tonic = (tonic instanceof Note) ? tonic : teoria.note(tonic);
1442 | return new Scale(tonic, scale);
1443 | }
1444 |
1445 | var teoria = {
1446 | note: noteConstructor,
1447 |
1448 | chord: chordConstructor,
1449 |
1450 | interval: intervalConstructor,
1451 |
1452 | scale: scaleConstructor,
1453 |
1454 | Note: Note,
1455 | Chord: Chord,
1456 | Scale: Scale,
1457 | Interval: Interval
1458 | };
1459 |
1460 | require('./lib/sugar')(teoria);
1461 | exports = module.exports = teoria;
1462 |
1463 | },{"./lib/chord":12,"./lib/interval":13,"./lib/note":15,"./lib/scale":16,"./lib/sugar":17}],12:[function(require,module,exports){
1464 | var daccord = require('daccord');
1465 | var knowledge = require('./knowledge');
1466 | var Note = require('./note');
1467 | var Interval = require('./interval');
1468 |
1469 | function Chord(root, name) {
1470 | if (!(this instanceof Chord)) return new Chord(root, name);
1471 | name = name || '';
1472 | this.name = root.name().toUpperCase() + root.accidental() + name;
1473 | this.symbol = name;
1474 | this.root = root;
1475 | this.intervals = [];
1476 | this._voicing = [];
1477 |
1478 | var bass = name.split('/');
1479 | if (bass.length === 2 && bass[1].trim() !== '9') {
1480 | name = bass[0];
1481 | bass = bass[1].trim();
1482 | } else {
1483 | bass = null;
1484 | }
1485 |
1486 | this.intervals = daccord(name).map(Interval.toCoord)
1487 | this._voicing = this.intervals.slice();
1488 |
1489 | if (bass) {
1490 | var intervals = this.intervals, bassInterval, note;
1491 | // Make sure the bass is atop of the root note
1492 | note = Note.fromString(bass + (root.octave() + 1)); // crude
1493 |
1494 | bassInterval = Interval.between(root, note);
1495 | bass = bassInterval.simple();
1496 | bassInterval = bassInterval.invert().direction('down');
1497 |
1498 | this._voicing = [bassInterval];
1499 | for (var i = 0, length = intervals.length; i < length; i++) {
1500 | if (!intervals[i].simple().equal(bass))
1501 | this._voicing.push(intervals[i]);
1502 | }
1503 | }
1504 | }
1505 |
1506 | Chord.prototype = {
1507 | notes: function() {
1508 | var root = this.root;
1509 | return this.voicing().map(function(interval) {
1510 | return root.interval(interval);
1511 | });
1512 | },
1513 |
1514 | simple: function() {
1515 | return this.notes().map(function(n) { return n.toString(true); });
1516 | },
1517 |
1518 | bass: function() {
1519 | return this.root.interval(this._voicing[0]);
1520 | },
1521 |
1522 | voicing: function(voicing) {
1523 | // Get the voicing
1524 | if (!voicing) {
1525 | return this._voicing;
1526 | }
1527 |
1528 | // Set the voicing
1529 | this._voicing = [];
1530 | for (var i = 0, length = voicing.length; i < length; i++) {
1531 | this._voicing[i] = Interval.toCoord(voicing[i]);
1532 | }
1533 |
1534 | return this;
1535 | },
1536 |
1537 | resetVoicing: function() {
1538 | this._voicing = this.intervals;
1539 | },
1540 |
1541 | dominant: function(additional) {
1542 | additional = additional || '';
1543 | return new Chord(this.root.interval('P5'), additional);
1544 | },
1545 |
1546 | subdominant: function(additional) {
1547 | additional = additional || '';
1548 | return new Chord(this.root.interval('P4'), additional);
1549 | },
1550 |
1551 | parallel: function(additional) {
1552 | additional = additional || '';
1553 | var quality = this.quality();
1554 |
1555 | if (this.chordType() !== 'triad' || quality === 'diminished' ||
1556 | quality === 'augmented') {
1557 | throw new Error('Only major/minor triads have parallel chords');
1558 | }
1559 |
1560 | if (quality === 'major') {
1561 | return new Chord(this.root.interval('m3', 'down'), 'm');
1562 | } else {
1563 | return new Chord(this.root.interval('m3', 'up'));
1564 | }
1565 | },
1566 |
1567 | quality: function() {
1568 | var third, fifth, seventh, intervals = this.intervals;
1569 |
1570 | for (var i = 0, length = intervals.length; i < length; i++) {
1571 | if (intervals[i].number() === 3) {
1572 | third = intervals[i];
1573 | } else if (intervals[i].number() === 5) {
1574 | fifth = intervals[i];
1575 | } else if (intervals[i].number() === 7) {
1576 | seventh = intervals[i];
1577 | }
1578 | }
1579 |
1580 | if (!third) {
1581 | return;
1582 | }
1583 |
1584 | third = (third.direction() === 'down') ? third.invert() : third;
1585 | third = third.simple().toString();
1586 |
1587 | if (fifth) {
1588 | fifth = (fifth.direction === 'down') ? fifth.invert() : fifth;
1589 | fifth = fifth.simple().toString();
1590 | }
1591 |
1592 | if (seventh) {
1593 | seventh = (seventh.direction === 'down') ? seventh.invert() : seventh;
1594 | seventh = seventh.simple().toString();
1595 | }
1596 |
1597 | if (third === 'M3') {
1598 | if (fifth === 'A5') {
1599 | return 'augmented';
1600 | } else if (fifth === 'P5') {
1601 | return (seventh === 'm7') ? 'dominant' : 'major';
1602 | }
1603 |
1604 | return 'major';
1605 | } else if (third === 'm3') {
1606 | if (fifth === 'P5') {
1607 | return 'minor';
1608 | } else if (fifth === 'd5') {
1609 | return (seventh === 'm7') ? 'half-diminished' : 'diminished';
1610 | }
1611 |
1612 | return 'minor';
1613 | }
1614 | },
1615 |
1616 | chordType: function() { // In need of better name
1617 | var length = this.intervals.length, interval, has, invert, i, name;
1618 |
1619 | if (length === 2) {
1620 | return 'dyad';
1621 | } else if (length === 3) {
1622 | has = {first: false, third: false, fifth: false};
1623 | for (i = 0; i < length; i++) {
1624 | interval = this.intervals[i];
1625 | invert = interval.invert();
1626 | if (interval.base() in has) {
1627 | has[interval.base()] = true;
1628 | } else if (invert.base() in has) {
1629 | has[invert.base()] = true;
1630 | }
1631 | }
1632 |
1633 | name = (has.first && has.third && has.fifth) ? 'triad' : 'trichord';
1634 | } else if (length === 4) {
1635 | has = {first: false, third: false, fifth: false, seventh: false};
1636 | for (i = 0; i < length; i++) {
1637 | interval = this.intervals[i];
1638 | invert = interval.invert();
1639 | if (interval.base() in has) {
1640 | has[interval.base()] = true;
1641 | } else if (invert.base() in has) {
1642 | has[invert.base()] = true;
1643 | }
1644 | }
1645 |
1646 | if (has.first && has.third && has.fifth && has.seventh) {
1647 | name = 'tetrad';
1648 | }
1649 | }
1650 |
1651 | return name || 'unknown';
1652 | },
1653 |
1654 | get: function(interval) {
1655 | if (typeof interval === 'string' && interval in knowledge.stepNumber) {
1656 | var intervals = this.intervals, i, length;
1657 |
1658 | interval = knowledge.stepNumber[interval];
1659 | for (i = 0, length = intervals.length; i < length; i++) {
1660 | if (intervals[i].number() === interval) {
1661 | return this.root.interval(intervals[i]);
1662 | }
1663 | }
1664 |
1665 | return null;
1666 | } else {
1667 | throw new Error('Invalid interval name');
1668 | }
1669 | },
1670 |
1671 | interval: function(interval) {
1672 | return new Chord(this.root.interval(interval), this.symbol);
1673 | },
1674 |
1675 | transpose: function(interval) {
1676 | this.root.transpose(interval);
1677 | this.name = this.root.name().toUpperCase() +
1678 | this.root.accidental() + this.symbol;
1679 |
1680 | return this;
1681 | },
1682 |
1683 | toString: function() {
1684 | return this.name;
1685 | }
1686 | };
1687 |
1688 | module.exports = Chord;
1689 |
1690 | },{"./interval":13,"./knowledge":14,"./note":15,"daccord":19}],13:[function(require,module,exports){
1691 | var knowledge = require('./knowledge');
1692 | var vector = require('./vector');
1693 | var toCoord = require('interval-coords');
1694 |
1695 | function Interval(coord) {
1696 | if (!(this instanceof Interval)) return new Interval(coord);
1697 | this.coord = coord;
1698 | }
1699 |
1700 | Interval.prototype = {
1701 | name: function() {
1702 | return knowledge.intervalsIndex[this.number() - 1];
1703 | },
1704 |
1705 | semitones: function() {
1706 | return vector.sum(vector.mul(this.coord, [12, 7]));
1707 | },
1708 |
1709 | number: function() {
1710 | return Math.abs(this.value());
1711 | },
1712 |
1713 | value: function() {
1714 | var without = vector.sub(this.coord,
1715 | vector.mul(knowledge.sharp, Math.floor((this.coord[1] - 2) / 7) + 1))
1716 | , i, val;
1717 |
1718 | i = knowledge.intervalFromFifth[without[1] + 5];
1719 | val = knowledge.stepNumber[i] + (without[0] - knowledge.intervals[i][0]) * 7;
1720 |
1721 | return (val > 0) ? val : val - 2;
1722 | },
1723 |
1724 | type: function() {
1725 | return knowledge.intervals[this.base()][0] <= 1 ? 'perfect' : 'minor';
1726 | },
1727 |
1728 | base: function() {
1729 | var fifth = vector.sub(this.coord, vector.mul(knowledge.sharp, this.qualityValue()))[1], name;
1730 | fifth = this.value() > 0 ? fifth + 5 : -(fifth - 5) % 7;
1731 | fifth = fifth < 0 ? knowledge.intervalFromFifth.length + fifth : fifth;
1732 |
1733 | name = knowledge.intervalFromFifth[fifth];
1734 | if (name === 'unison' && this.number() >= 8)
1735 | name = 'octave';
1736 |
1737 | return name;
1738 | },
1739 |
1740 | direction: function(dir) {
1741 | if (dir) {
1742 | var is = this.value() >= 1 ? 'up' : 'down';
1743 | if (is !== dir)
1744 | this.coord = vector.mul(this.coord, -1);
1745 |
1746 | return this;
1747 | }
1748 | else
1749 | return this.value() >= 1 ? 'up' : 'down';
1750 | },
1751 |
1752 | simple: function(ignore) {
1753 | // Get the (upwards) base interval (with quality)
1754 | var simple = knowledge.intervals[this.base()];
1755 | simple = vector.add(simple, vector.mul(knowledge.sharp, this.qualityValue()));
1756 |
1757 | // Turn it around if necessary
1758 | if (!ignore)
1759 | simple = this.direction() === 'down' ? vector.mul(simple, -1) : simple;
1760 |
1761 | return new Interval(simple);
1762 | },
1763 |
1764 | isCompound: function() {
1765 | return this.number() > 8;
1766 | },
1767 |
1768 | octaves: function() {
1769 | var without, octaves;
1770 |
1771 | if (this.direction() === 'up') {
1772 | without = vector.sub(this.coord, vector.mul(knowledge.sharp, this.qualityValue()));
1773 | octaves = without[0] - knowledge.intervals[this.base()][0];
1774 | } else {
1775 | without = vector.sub(this.coord, vector.mul(knowledge.sharp, -this.qualityValue()));
1776 | octaves = -(without[0] + knowledge.intervals[this.base()][0]);
1777 | }
1778 |
1779 | return octaves;
1780 | },
1781 |
1782 | invert: function() {
1783 | var i = this.base();
1784 | var qual = this.qualityValue();
1785 | var acc = this.type() === 'minor' ? -(qual - 1) : -qual;
1786 | var coord = knowledge.intervals[knowledge.intervalsIndex[9 - knowledge.stepNumber[i] - 1]];
1787 | coord = vector.add(coord, vector.mul(knowledge.sharp, acc));
1788 |
1789 | return new Interval(coord);
1790 | },
1791 |
1792 | quality: function(lng) {
1793 | var quality = knowledge.alterations[this.type()][this.qualityValue() + 2];
1794 |
1795 | return lng ? knowledge.qualityLong[quality] : quality;
1796 | },
1797 |
1798 | qualityValue: function() {
1799 | if (this.direction() === 'down')
1800 | return Math.floor((-this.coord[1] - 2) / 7) + 1;
1801 | else
1802 | return Math.floor((this.coord[1] - 2) / 7) + 1;
1803 | },
1804 |
1805 | equal: function(interval) {
1806 | return this.coord[0] === interval.coord[0] &&
1807 | this.coord[1] === interval.coord[1];
1808 | },
1809 |
1810 | greater: function(interval) {
1811 | var semi = this.semitones();
1812 | var isemi = interval.semitones();
1813 |
1814 | // If equal in absolute size, measure which interval is bigger
1815 | // For example P4 is bigger than A3
1816 | return (semi === isemi) ?
1817 | (this.number() > interval.number()) : (semi > isemi);
1818 | },
1819 |
1820 | smaller: function(interval) {
1821 | return !this.equal(interval) && !this.greater(interval);
1822 | },
1823 |
1824 | add: function(interval) {
1825 | return new Interval(vector.add(this.coord, interval.coord));
1826 | },
1827 |
1828 | toString: function(ignore) {
1829 | // If given true, return the positive value
1830 | var number = ignore ? this.number() : this.value();
1831 |
1832 | return this.quality() + number;
1833 | }
1834 | }
1835 |
1836 | Interval.toCoord = function(simple) {
1837 | var coord = toCoord(simple);
1838 | if (!coord)
1839 | throw new Error('Invalid simple format interval');
1840 |
1841 | return new Interval(coord);
1842 | }
1843 |
1844 | Interval.from = function(from, to) {
1845 | return from.interval(to);
1846 | }
1847 |
1848 | Interval.between = function(from, to) {
1849 | return new Interval(vector.sub(to.coord, from.coord));
1850 | }
1851 |
1852 | Interval.invert = function(sInterval) {
1853 | return Interval.toCoord(sInterval).invert().toString();
1854 | }
1855 |
1856 | module.exports = Interval;
1857 |
1858 | },{"./knowledge":14,"./vector":18,"interval-coords":23}],14:[function(require,module,exports){
1859 | // Note coordinates [octave, fifth] relative to C
1860 | module.exports = {
1861 | notes: {
1862 | c: [0, 0],
1863 | d: [-1, 2],
1864 | e: [-2, 4],
1865 | f: [1, -1],
1866 | g: [0, 1],
1867 | a: [-1, 3],
1868 | b: [-2, 5],
1869 | h: [-2, 5]
1870 | },
1871 |
1872 | intervals: {
1873 | unison: [0, 0],
1874 | second: [3, -5],
1875 | third: [2, -3],
1876 | fourth: [1, -1],
1877 | fifth: [0, 1],
1878 | sixth: [3, -4],
1879 | seventh: [2, -2],
1880 | octave: [1, 0]
1881 | },
1882 |
1883 | intervalFromFifth: ['second', 'sixth', 'third', 'seventh', 'fourth',
1884 | 'unison', 'fifth'],
1885 |
1886 | intervalsIndex: ['unison', 'second', 'third', 'fourth', 'fifth',
1887 | 'sixth', 'seventh', 'octave', 'ninth', 'tenth',
1888 | 'eleventh', 'twelfth', 'thirteenth', 'fourteenth',
1889 | 'fifteenth'],
1890 |
1891 | // linaer index to fifth = (2 * index + 1) % 7
1892 | fifths: ['f', 'c', 'g', 'd', 'a', 'e', 'b'],
1893 | accidentals: ['bb', 'b', '', '#', 'x'],
1894 |
1895 | sharp: [-4, 7],
1896 | A4: [3, 3],
1897 |
1898 | durations: {
1899 | '0.25': 'longa',
1900 | '0.5': 'breve',
1901 | '1': 'whole',
1902 | '2': 'half',
1903 | '4': 'quarter',
1904 | '8': 'eighth',
1905 | '16': 'sixteenth',
1906 | '32': 'thirty-second',
1907 | '64': 'sixty-fourth',
1908 | '128': 'hundred-twenty-eighth'
1909 | },
1910 |
1911 | qualityLong: {
1912 | P: 'perfect',
1913 | M: 'major',
1914 | m: 'minor',
1915 | A: 'augmented',
1916 | AA: 'doubly augmented',
1917 | d: 'diminished',
1918 | dd: 'doubly diminished'
1919 | },
1920 |
1921 | alterations: {
1922 | perfect: ['dd', 'd', 'P', 'A', 'AA'],
1923 | minor: ['dd', 'd', 'm', 'M', 'A', 'AA']
1924 | },
1925 |
1926 | symbols: {
1927 | 'min': ['m3', 'P5'],
1928 | 'm': ['m3', 'P5'],
1929 | '-': ['m3', 'P5'],
1930 |
1931 | 'M': ['M3', 'P5'],
1932 | '': ['M3', 'P5'],
1933 |
1934 | '+': ['M3', 'A5'],
1935 | 'aug': ['M3', 'A5'],
1936 |
1937 | 'dim': ['m3', 'd5'],
1938 | 'o': ['m3', 'd5'],
1939 |
1940 | 'maj': ['M3', 'P5', 'M7'],
1941 | 'dom': ['M3', 'P5', 'm7'],
1942 | 'ø': ['m3', 'd5', 'm7'],
1943 |
1944 | '5': ['P5']
1945 | },
1946 |
1947 | chordShort: {
1948 | 'major': 'M',
1949 | 'minor': 'm',
1950 | 'augmented': 'aug',
1951 | 'diminished': 'dim',
1952 | 'half-diminished': '7b5',
1953 | 'power': '5',
1954 | 'dominant': '7'
1955 | },
1956 |
1957 | stepNumber: {
1958 | 'unison': 1,
1959 | 'first': 1,
1960 | 'second': 2,
1961 | 'third': 3,
1962 | 'fourth': 4,
1963 | 'fifth': 5,
1964 | 'sixth': 6,
1965 | 'seventh': 7,
1966 | 'octave': 8,
1967 | 'ninth': 9,
1968 | 'eleventh': 11,
1969 | 'thirteenth': 13
1970 | },
1971 |
1972 | // Adjusted Shearer syllables - Chromatic solfege system
1973 | // Some intervals are not provided for. These include:
1974 | // dd2 - Doubly diminished second
1975 | // dd3 - Doubly diminished third
1976 | // AA3 - Doubly augmented third
1977 | // dd6 - Doubly diminished sixth
1978 | // dd7 - Doubly diminished seventh
1979 | // AA7 - Doubly augmented seventh
1980 | intervalSolfege: {
1981 | 'dd1': 'daw',
1982 | 'd1': 'de',
1983 | 'P1': 'do',
1984 | 'A1': 'di',
1985 | 'AA1': 'dai',
1986 | 'd2': 'raw',
1987 | 'm2': 'ra',
1988 | 'M2': 're',
1989 | 'A2': 'ri',
1990 | 'AA2': 'rai',
1991 | 'd3': 'maw',
1992 | 'm3': 'me',
1993 | 'M3': 'mi',
1994 | 'A3': 'mai',
1995 | 'dd4': 'faw',
1996 | 'd4': 'fe',
1997 | 'P4': 'fa',
1998 | 'A4': 'fi',
1999 | 'AA4': 'fai',
2000 | 'dd5': 'saw',
2001 | 'd5': 'se',
2002 | 'P5': 'so',
2003 | 'A5': 'si',
2004 | 'AA5': 'sai',
2005 | 'd6': 'law',
2006 | 'm6': 'le',
2007 | 'M6': 'la',
2008 | 'A6': 'li',
2009 | 'AA6': 'lai',
2010 | 'd7': 'taw',
2011 | 'm7': 'te',
2012 | 'M7': 'ti',
2013 | 'A7': 'tai',
2014 | 'dd8': 'daw',
2015 | 'd8': 'de',
2016 | 'P8': 'do',
2017 | 'A8': 'di',
2018 | 'AA8': 'dai'
2019 | }
2020 | }
2021 |
2022 | },{}],15:[function(require,module,exports){
2023 | var scientific = require('scientific-notation');
2024 | var helmholtz = require('helmholtz');
2025 | var knowledge = require('./knowledge');
2026 | var vector = require('./vector');
2027 | var Interval = require('./interval');
2028 |
2029 | function pad(str, ch, len) {
2030 | for (; len > 0; len--) {
2031 | str += ch;
2032 | }
2033 |
2034 | return str;
2035 | }
2036 |
2037 |
2038 | function Note(coord, duration) {
2039 | if (!(this instanceof Note)) return new Note(coord, duration);
2040 | duration = duration || {};
2041 |
2042 | this.duration = { value: duration.value || 4, dots: duration.dots || 0 };
2043 | this.coord = coord;
2044 | }
2045 |
2046 | Note.prototype = {
2047 | octave: function() {
2048 | return this.coord[0] + knowledge.A4[0] - knowledge.notes[this.name()][0] +
2049 | this.accidentalValue() * 4;
2050 | },
2051 |
2052 | name: function() {
2053 | return knowledge.fifths[this.coord[1] + knowledge.A4[1] - this.accidentalValue() * 7 + 1];
2054 | },
2055 |
2056 | accidentalValue: function() {
2057 | return Math.round((this.coord[1] + knowledge.A4[1] - 2) / 7);
2058 | },
2059 |
2060 | accidental: function() {
2061 | return knowledge.accidentals[this.accidentalValue() + 2];
2062 | },
2063 |
2064 | /**
2065 | * Returns the key number of the note
2066 | */
2067 | key: function(white) {
2068 | if (white)
2069 | return this.coord[0] * 7 + this.coord[1] * 4 + 29;
2070 | else
2071 | return this.coord[0] * 12 + this.coord[1] * 7 + 49;
2072 | },
2073 |
2074 | /**
2075 | * Returns a number ranging from 0-127 representing a MIDI note value
2076 | */
2077 | midi: function() {
2078 | return this.key() + 20;
2079 | },
2080 |
2081 | /**
2082 | * Calculates and returns the frequency of the note.
2083 | * Optional concert pitch (def. 440)
2084 | */
2085 | fq: function(concertPitch) {
2086 | concertPitch = concertPitch || 440;
2087 |
2088 | return concertPitch *
2089 | Math.pow(2, (this.coord[0] * 12 + this.coord[1] * 7) / 12);
2090 | },
2091 |
2092 | /**
2093 | * Returns the pitch class index (chroma) of the note
2094 | */
2095 | chroma: function() {
2096 | var value = (vector.sum(vector.mul(this.coord, [12, 7])) - 3) % 12;
2097 |
2098 | return (value < 0) ? value + 12 : value;
2099 | },
2100 |
2101 | interval: function(interval) {
2102 | if (typeof interval === 'string') interval = Interval.toCoord(interval);
2103 |
2104 | if (interval instanceof Interval)
2105 | return new Note(vector.add(this.coord, interval.coord));
2106 | else if (interval instanceof Note)
2107 | return new Interval(vector.sub(interval.coord, this.coord));
2108 | },
2109 |
2110 | transpose: function(interval) {
2111 | this.coord = vector.add(this.coord, interval.coord);
2112 | return this;
2113 | },
2114 |
2115 | /**
2116 | * Returns the Helmholtz notation form of the note (fx C,, d' F# g#'')
2117 | */
2118 | helmholtz: function() {
2119 | var octave = this.octave();
2120 | var name = this.name();
2121 | name = octave < 3 ? name.toUpperCase() : name.toLowerCase();
2122 | var padchar = octave < 3 ? ',' : '\'';
2123 | var padcount = octave < 2 ? 2 - octave : octave - 3;
2124 |
2125 | return pad(name + this.accidental(), padchar, padcount);
2126 | },
2127 |
2128 | /**
2129 | * Returns the scientific notation form of the note (fx E4, Bb3, C#7 etc.)
2130 | */
2131 | scientific: function() {
2132 | return this.name().toUpperCase() + this.accidental() + this.octave();
2133 | },
2134 |
2135 | /**
2136 | * Returns notes that are enharmonic with this note.
2137 | */
2138 | enharmonics: function(oneaccidental) {
2139 | var key = this.key(), limit = oneaccidental ? 2 : 3;
2140 |
2141 | return ['m3', 'm2', 'm-2', 'm-3']
2142 | .map(this.interval.bind(this))
2143 | .filter(function(note) {
2144 | var acc = note.accidentalValue();
2145 | var diff = key - (note.key() - acc);
2146 |
2147 | if (diff < limit && diff > -limit) {
2148 | note.coord = vector.add(note.coord, vector.mul(knowledge.sharp, diff - acc));
2149 | return true;
2150 | }
2151 | });
2152 | },
2153 |
2154 | solfege: function(scale, showOctaves) {
2155 | var interval = scale.tonic.interval(this), solfege, stroke, count;
2156 | if (interval.direction() === 'down')
2157 | interval = interval.invert();
2158 |
2159 | if (showOctaves) {
2160 | count = (this.key(true) - scale.tonic.key(true)) / 7;
2161 | count = (count >= 0) ? Math.floor(count) : -(Math.ceil(-count));
2162 | stroke = (count >= 0) ? '\'' : ',';
2163 | }
2164 |
2165 | solfege = knowledge.intervalSolfege[interval.simple(true).toString()];
2166 | return (showOctaves) ? pad(solfege, stroke, Math.abs(count)) : solfege;
2167 | },
2168 |
2169 | scaleDegree: function(scale) {
2170 | var inter = scale.tonic.interval(this);
2171 |
2172 | // If the direction is down, or we're dealing with an octave - invert it
2173 | if (inter.direction() === 'down' ||
2174 | (inter.coord[1] === 0 && inter.coord[0] !== 0)) {
2175 | inter = inter.invert();
2176 | }
2177 |
2178 | inter = inter.simple(true).coord;
2179 |
2180 | return scale.scale.reduce(function(index, current, i) {
2181 | var coord = Interval.toCoord(current).coord;
2182 | return coord[0] === inter[0] && coord[1] === inter[1] ? i + 1 : index;
2183 | }, 0);
2184 | },
2185 |
2186 | /**
2187 | * Returns the name of the duration value,
2188 | * such as 'whole', 'quarter', 'sixteenth' etc.
2189 | */
2190 | durationName: function() {
2191 | return knowledge.durations[this.duration.value];
2192 | },
2193 |
2194 | /**
2195 | * Returns the duration of the note (including dots)
2196 | * in seconds. The first argument is the tempo in beats
2197 | * per minute, the second is the beat unit (i.e. the
2198 | * lower numeral in a time signature).
2199 | */
2200 | durationInSeconds: function(bpm, beatUnit) {
2201 | var secs = (60 / bpm) / (this.duration.value / 4) / (beatUnit / 4);
2202 | return secs * 2 - secs / Math.pow(2, this.duration.dots);
2203 | },
2204 |
2205 | /**
2206 | * Returns the name of the note, with an optional display of octave number
2207 | */
2208 | toString: function(dont) {
2209 | return this.name() + this.accidental() + (dont ? '' : this.octave());
2210 | }
2211 | };
2212 |
2213 | Note.fromString = function(name, dur) {
2214 | var coord = scientific(name);
2215 | if (!coord) coord = helmholtz(name);
2216 | return new Note(coord, dur);
2217 | }
2218 |
2219 | Note.fromKey = function(key) {
2220 | var octave = Math.floor((key - 4) / 12);
2221 | var distance = key - (octave * 12) - 4;
2222 | var name = knowledge.fifths[(2 * Math.round(distance / 2) + 1) % 7];
2223 | var note = vector.add(vector.sub(knowledge.notes[name], knowledge.A4), [octave + 1, 0]);
2224 | var diff = (key - 49) - vector.sum(vector.mul(note, [12, 7]));
2225 |
2226 | return new Note(diff ? vector.add(note, vector.mul(knowledge.sharp, diff)) : note);
2227 | }
2228 |
2229 | Note.fromFrequency = function(fq, concertPitch) {
2230 | var key, cents, originalFq;
2231 | concertPitch = concertPitch || 440;
2232 |
2233 | key = 49 + 12 * ((Math.log(fq) - Math.log(concertPitch)) / Math.log(2));
2234 | key = Math.round(key);
2235 | originalFq = concertPitch * Math.pow(2, (key - 49) / 12);
2236 | cents = 1200 * (Math.log(fq / originalFq) / Math.log(2));
2237 |
2238 | return { note: Note.fromKey(key), cents: cents };
2239 | }
2240 |
2241 | Note.fromMIDI = function(note) {
2242 | return Note.fromKey(note - 20);
2243 | }
2244 |
2245 | module.exports = Note;
2246 |
2247 | },{"./interval":13,"./knowledge":14,"./vector":18,"helmholtz":20,"scientific-notation":24}],16:[function(require,module,exports){
2248 | var knowledge = require('./knowledge');
2249 | var Interval = require('./interval');
2250 |
2251 | var scales = {
2252 | aeolian: ['P1', 'M2', 'm3', 'P4', 'P5', 'm6', 'm7'],
2253 | blues: ['P1', 'm3', 'P4', 'A4', 'P5', 'm7'],
2254 | chromatic: ['P1', 'm2', 'M2', 'm3', 'M3', 'P4', 'A4', 'P5', 'm6', 'M6', 'm7', 'M7'],
2255 | dorian: ['P1', 'M2', 'm3', 'P4', 'P5', 'M6', 'm7'],
2256 | doubleharmonic: ['P1', 'm2', 'M3', 'P4', 'P5', 'm6', 'M7'],
2257 | harmonicminor: ['P1', 'M2', 'm3', 'P4', 'P5', 'm6', 'M7'],
2258 | ionian: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'M7'],
2259 | locrian: ['P1', 'm2', 'm3', 'P4', 'd5', 'm6', 'm7'],
2260 | lydian: ['P1', 'M2', 'M3', 'A4', 'P5', 'M6', 'M7'],
2261 | majorpentatonic: ['P1', 'M2', 'M3', 'P5', 'M6'],
2262 | melodicminor: ['P1', 'M2', 'm3', 'P4', 'P5', 'M6', 'M7'],
2263 | minorpentatonic: ['P1', 'm3', 'P4', 'P5', 'm7'],
2264 | mixolydian: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'm7'],
2265 | phrygian: ['P1', 'm2', 'm3', 'P4', 'P5', 'm6', 'm7']
2266 | }
2267 |
2268 | // synonyms
2269 | scales.harmonicchromatic = scales.chromatic;
2270 | scales.minor = scales.aeolian;
2271 | scales.major = scales.ionian;
2272 | scales.flamenco = scales.doubleharmonic;
2273 |
2274 | function Scale(tonic, scale) {
2275 | if (!(this instanceof Scale)) return new Scale(tonic, scale);
2276 | var scaleName, i;
2277 | if (!('coord' in tonic)) {
2278 | throw new Error('Invalid Tonic');
2279 | }
2280 |
2281 | if (typeof scale === 'string') {
2282 | scaleName = scale;
2283 | scale = scales[scale];
2284 | if (!scale)
2285 | throw new Error('Invalid Scale');
2286 | } else {
2287 | for (i in scales) {
2288 | if (scales.hasOwnProperty(i)) {
2289 | if (scales[i].toString() === scale.toString()) {
2290 | scaleName = i;
2291 | break;
2292 | }
2293 | }
2294 | }
2295 | }
2296 |
2297 | this.name = scaleName;
2298 | this.tonic = tonic;
2299 | this.scale = scale;
2300 | }
2301 |
2302 | Scale.prototype = {
2303 | notes: function() {
2304 | var notes = [];
2305 |
2306 | for (var i = 0, length = this.scale.length; i < length; i++) {
2307 | notes.push(this.tonic.interval(this.scale[i]));
2308 | }
2309 |
2310 | return notes;
2311 | },
2312 |
2313 | simple: function() {
2314 | return this.notes().map(function(n) { return n.toString(true); });
2315 | },
2316 |
2317 | type: function() {
2318 | var length = this.scale.length - 2;
2319 | if (length < 8) {
2320 | return ['di', 'tri', 'tetra', 'penta', 'hexa', 'hepta', 'octa'][length] +
2321 | 'tonic';
2322 | }
2323 | },
2324 |
2325 | get: function(i) {
2326 | i = (typeof i === 'string' && i in knowledge.stepNumber) ? knowledge.stepNumber[i] : i;
2327 |
2328 | return this.tonic.interval(this.scale[i - 1]);
2329 | },
2330 |
2331 | solfege: function(index, showOctaves) {
2332 | if (index)
2333 | return this.get(index).solfege(this, showOctaves);
2334 |
2335 | return this.notes().map(function(n) {
2336 | return n.solfege(this, showOctaves);
2337 | });
2338 | },
2339 |
2340 | interval: function(interval) {
2341 | interval = (typeof interval === 'string') ?
2342 | Interval.toCoord(interval) : interval;
2343 | return new Scale(this.tonic.interval(interval), this.scale);
2344 | },
2345 |
2346 | transpose: function(interval) {
2347 | var scale = this.interval(interval);
2348 | this.scale = scale.scale;
2349 | this.tonic = scale.tonic;
2350 |
2351 | return this;
2352 | }
2353 | };
2354 |
2355 | module.exports = Scale;
2356 |
2357 | },{"./interval":13,"./knowledge":14}],17:[function(require,module,exports){
2358 | var knowledge = require('./knowledge');
2359 |
2360 | module.exports = function(teoria) {
2361 | var Note = teoria.Note;
2362 | var Chord = teoria.Chord;
2363 | var Scale = teoria.Scale;
2364 |
2365 | Note.prototype.chord = function(chord) {
2366 | chord = (chord in knowledge.chordShort) ? knowledge.chordShort[chord] : chord;
2367 |
2368 | return new Chord(this, chord);
2369 | }
2370 |
2371 | Note.prototype.scale = function(scale) {
2372 | return new Scale(this, scale);
2373 | }
2374 | }
2375 |
2376 | },{"./knowledge":14}],18:[function(require,module,exports){
2377 | module.exports = {
2378 | add: function(note, interval) {
2379 | return [note[0] + interval[0], note[1] + interval[1]];
2380 | },
2381 |
2382 | sub: function(note, interval) {
2383 | return [note[0] - interval[0], note[1] - interval[1]];
2384 | },
2385 |
2386 | mul: function(note, interval) {
2387 | if (typeof interval === 'number')
2388 | return [note[0] * interval, note[1] * interval];
2389 | else
2390 | return [note[0] * interval[0], note[1] * interval[1]];
2391 | },
2392 |
2393 | sum: function(coord) {
2394 | return coord[0] + coord[1];
2395 | }
2396 | }
2397 |
2398 | },{}],19:[function(require,module,exports){
2399 | var SYMBOLS = {
2400 | 'm': ['m3', 'P5'],
2401 | 'mi': ['m3', 'P5'],
2402 | 'min': ['m3', 'P5'],
2403 | '-': ['m3', 'P5'],
2404 |
2405 | 'M': ['M3', 'P5'],
2406 | 'ma': ['M3', 'P5'],
2407 | '': ['M3', 'P5'],
2408 |
2409 | '+': ['M3', 'A5'],
2410 | 'aug': ['M3', 'A5'],
2411 |
2412 | 'dim': ['m3', 'd5'],
2413 | 'o': ['m3', 'd5'],
2414 |
2415 | 'maj': ['M3', 'P5', 'M7'],
2416 | 'dom': ['M3', 'P5', 'm7'],
2417 | 'ø': ['m3', 'd5', 'm7'],
2418 |
2419 | '5': ['P5'],
2420 |
2421 | '6/9': ['M3', 'P5', 'M6', 'M9']
2422 | };
2423 |
2424 | module.exports = function(symbol) {
2425 | var c, parsing = 'quality', additionals = [], name, chordLength = 2
2426 | var notes = ['P1', 'M3', 'P5', 'm7', 'M9', 'P11', 'M13'];
2427 | var explicitMajor = false;
2428 |
2429 | function setChord(name) {
2430 | var intervals = SYMBOLS[name];
2431 | for (var i = 0, len = intervals.length; i < len; i++) {
2432 | notes[i + 1] = intervals[i];
2433 | }
2434 |
2435 | chordLength = intervals.length;
2436 | }
2437 |
2438 | // Remove whitespace, commas and parentheses
2439 | symbol = symbol.replace(/[,\s\(\)]/g, '');
2440 | for (var i = 0, len = symbol.length; i < len; i++) {
2441 | if (!(c = symbol[i]))
2442 | return;
2443 |
2444 | if (parsing === 'quality') {
2445 | var sub3 = (i + 2) < len ? symbol.substr(i, 3).toLowerCase() : null;
2446 | var sub2 = (i + 1) < len ? symbol.substr(i, 2).toLowerCase() : null;
2447 | if (sub3 in SYMBOLS)
2448 | name = sub3;
2449 | else if (sub2 in SYMBOLS)
2450 | name = sub2;
2451 | else if (c in SYMBOLS)
2452 | name = c;
2453 | else
2454 | name = '';
2455 |
2456 | if (name)
2457 | setChord(name);
2458 |
2459 | if (name === 'M' || name === 'ma' || name === 'maj')
2460 | explicitMajor = true;
2461 |
2462 |
2463 | i += name.length - 1;
2464 | parsing = 'extension';
2465 | } else if (parsing === 'extension') {
2466 | c = (c === '1' && symbol[i + 1]) ? +symbol.substr(i, 2) : +c;
2467 |
2468 | if (!isNaN(c) && c !== 6) {
2469 | chordLength = (c - 1) / 2;
2470 |
2471 | if (chordLength !== Math.round(chordLength))
2472 | return new Error('Invalid interval extension: ' + c.toString(10));
2473 |
2474 | if (name === 'o' || name === 'dim')
2475 | notes[3] = 'd7';
2476 | else if (explicitMajor)
2477 | notes[3] = 'M7';
2478 |
2479 | i += c >= 10 ? 1 : 0;
2480 | } else if (c === 6) {
2481 | notes[3] = 'M6';
2482 | chordLength = Math.max(3, chordLength);
2483 | } else
2484 | i -= 1;
2485 |
2486 | parsing = 'alterations';
2487 | } else if (parsing === 'alterations') {
2488 | var alterations = symbol.substr(i).split(/(#|b|add|maj|sus|M)/i),
2489 | next, flat = false, sharp = false;
2490 |
2491 | if (alterations.length === 1)
2492 | return new Error('Invalid alteration');
2493 | else if (alterations[0].length !== 0)
2494 | return new Error('Invalid token: \'' + alterations[0] + '\'');
2495 |
2496 | var ignore = false;
2497 | alterations.forEach(function(alt, i, arr) {
2498 | if (ignore || !alt.length)
2499 | return ignore = false;
2500 |
2501 | var next = arr[i + 1], lower = alt.toLowerCase();
2502 | if (alt === 'M' || lower === 'maj') {
2503 | if (next === '7')
2504 | ignore = true;
2505 |
2506 | chordLength = Math.max(3, chordLength);
2507 | notes[3] = 'M7';
2508 | } else if (lower === 'sus') {
2509 | var type = 'P4';
2510 | if (next === '2' || next === '4') {
2511 | ignore = true;
2512 |
2513 | if (next === '2')
2514 | type = 'M2';
2515 | }
2516 |
2517 | notes[1] = type; // Replace third with M2 or P4
2518 | } else if (lower === 'add') {
2519 | if (next === '9')
2520 | additionals.push('M9');
2521 | else if (next === '11')
2522 | additionals.push('P11');
2523 | else if (next === '13')
2524 | additionals.push('M13');
2525 |
2526 | ignore = true
2527 | } else if (lower === 'b') {
2528 | flat = true;
2529 | } else if (lower === '#') {
2530 | sharp = true;
2531 | } else {
2532 | var token = +alt, quality, intPos;
2533 | if (isNaN(token) || String(token).length !== alt.length)
2534 | return new Error('Invalid token: \'' + alt + '\'');
2535 |
2536 | if (token === 6) {
2537 | if (sharp)
2538 | notes[3] = 'A6';
2539 | else if (flat)
2540 | notes[3] = 'm6';
2541 | else
2542 | notes[3] = 'M6';
2543 |
2544 | chordLength = Math.max(3, chordLength);
2545 | return;
2546 | }
2547 |
2548 | // Calculate the position in the 'note' array
2549 | intPos = (token - 1) / 2;
2550 | if (chordLength < intPos)
2551 | chordLength = intPos;
2552 |
2553 | if (token < 5 || token === 7 || intPos !== Math.round(intPos))
2554 | return new Error('Invalid interval alteration: ' + token);
2555 |
2556 | quality = notes[intPos][0];
2557 |
2558 | // Alterate the quality of the interval according the accidentals
2559 | if (sharp) {
2560 | if (quality === 'd')
2561 | quality = 'm';
2562 | else if (quality === 'm')
2563 | quality = 'M';
2564 | else if (quality === 'M' || quality === 'P')
2565 | quality = 'A';
2566 | } else if (flat) {
2567 | if (quality === 'A')
2568 | quality = 'M';
2569 | else if (quality === 'M')
2570 | quality = 'm';
2571 | else if (quality === 'm' || quality === 'P')
2572 | quality = 'd';
2573 | }
2574 |
2575 | sharp = flat = false;
2576 | notes[intPos] = quality + token;
2577 | }
2578 | });
2579 | parsing = 'ended';
2580 | } else if (parsing === 'ended') {
2581 | break;
2582 | }
2583 | }
2584 |
2585 | return notes.slice(0, chordLength + 1).concat(additionals);
2586 | }
2587 |
2588 | },{}],20:[function(require,module,exports){
2589 | var coords = require('notecoord');
2590 | var accval = require('accidental-value');
2591 |
2592 | module.exports = function helmholtz(name) {
2593 | var name = name.replace(/\u2032/g, "'").replace(/\u0375/g, ',');
2594 | var parts = name.match(/^(,*)([a-h])(x|#|bb|b?)([,\']*)$/i);
2595 |
2596 | if (!parts || name !== parts[0])
2597 | throw new Error('Invalid formatting');
2598 |
2599 | var note = parts[2];
2600 | var octaveFirst = parts[1];
2601 | var octaveLast = parts[4];
2602 | var lower = note === note.toLowerCase();
2603 | var octave;
2604 |
2605 | if (octaveFirst) {
2606 | if (lower)
2607 | throw new Error('Invalid formatting - found commas before lowercase note');
2608 |
2609 | octave = 2 - octaveFirst.length;
2610 | } else if (octaveLast) {
2611 | if (octaveLast.match(/^'+$/) && lower)
2612 | octave = 3 + octaveLast.length;
2613 | else if (octaveLast.match(/^,+$/) && !lower)
2614 | octave = 2 - octaveLast.length;
2615 | else
2616 | throw new Error('Invalid formatting - mismatch between octave ' +
2617 | 'indicator and letter case')
2618 | } else
2619 | octave = lower ? 3 : 2;
2620 |
2621 | var accidentalValue = accval.interval(parts[3].toLowerCase());
2622 | var coord = coords(note.toLowerCase());
2623 |
2624 | coord[0] += octave;
2625 | coord[0] += accidentalValue[0] - coords.A4[0];
2626 | coord[1] += accidentalValue[1] - coords.A4[1];
2627 |
2628 | return coord;
2629 | };
2630 |
2631 | },{"accidental-value":21,"notecoord":22}],21:[function(require,module,exports){
2632 | var accidentalValues = {
2633 | 'bb': -2,
2634 | 'b': -1,
2635 | '': 0,
2636 | '#': 1,
2637 | 'x': 2
2638 | };
2639 |
2640 | module.exports = function accidentalNumber(acc) {
2641 | return accidentalValues[acc];
2642 | }
2643 |
2644 | module.exports.interval = function accidentalInterval(acc) {
2645 | var val = accidentalValues[acc];
2646 | return [-4 * val, 7 * val];
2647 | }
2648 |
2649 | },{}],22:[function(require,module,exports){
2650 | // First coord is octaves, second is fifths. Distances are relative to c
2651 | var notes = {
2652 | c: [0, 0],
2653 | d: [-1, 2],
2654 | e: [-2, 4],
2655 | f: [1, -1],
2656 | g: [0, 1],
2657 | a: [-1, 3],
2658 | b: [-2, 5],
2659 | h: [-2, 5]
2660 | };
2661 |
2662 | module.exports = function(name) {
2663 | return name in notes ? [notes[name][0], notes[name][1]] : null;
2664 | };
2665 |
2666 | module.exports.notes = notes;
2667 | module.exports.A4 = [3, 3]; // Relative to C0 (scientic notation, ~16.35Hz)
2668 | module.exports.sharp = [-4, 7];
2669 |
2670 | },{}],23:[function(require,module,exports){
2671 | var pattern = /^(AA|A|P|M|m|d|dd)(-?\d+)$/;
2672 |
2673 | // The interval it takes to raise a note a semitone
2674 | var sharp = [-4, 7];
2675 |
2676 | var pAlts = ['dd', 'd', 'P', 'A', 'AA'];
2677 | var mAlts = ['dd', 'd', 'm', 'M', 'A', 'AA'];
2678 |
2679 | var baseIntervals = [
2680 | [0, 0],
2681 | [3, -5],
2682 | [2, -3],
2683 | [1, -1],
2684 | [0, 1],
2685 | [3, -4],
2686 | [2, -2],
2687 | [1, 0]
2688 | ];
2689 |
2690 | module.exports = function(simple) {
2691 | var parser = simple.match(pattern);
2692 | if (!parser) return null;
2693 |
2694 | var quality = parser[1];
2695 | var number = +parser[2];
2696 | var sign = number < 0 ? -1 : 1;
2697 |
2698 | number = sign < 0 ? -number : number;
2699 |
2700 | var lower = number > 8 ? (number % 7 || 7) : number;
2701 | var octaves = (number - lower) / 7;
2702 |
2703 | var base = baseIntervals[lower - 1];
2704 | var alts = base[0] <= 1 ? pAlts : mAlts;
2705 | var alt = alts.indexOf(quality) - 2;
2706 |
2707 | // this happens, if the alteration wasn't suitable for this type
2708 | // of interval, such as P2 or M5 (no "perfect second" or "major fifth")
2709 | if (alt === -3) return null;
2710 |
2711 | return [
2712 | sign * (base[0] + octaves + sharp[0] * alt),
2713 | sign * (base[1] + sharp[1] * alt)
2714 | ];
2715 | }
2716 |
2717 | // Copy to avoid overwriting internal base intervals
2718 | module.exports.coords = baseIntervals.slice(0);
2719 |
2720 | },{}],24:[function(require,module,exports){
2721 | var coords = require('notecoord');
2722 | var accval = require('accidental-value');
2723 |
2724 | module.exports = function scientific(name) {
2725 | var format = /^([a-h])(x|#|bb|b?)(-?\d*)/i;
2726 |
2727 | parser = name.match(format);
2728 | if (!(parser && name === parser[0] && parser[3].length)) return;
2729 |
2730 | var noteName = parser[1];
2731 | var octave = +parser[3];
2732 | var accidental = parser[2].length ? parser[2].toLowerCase() : '';
2733 |
2734 | var accidentalValue = accval.interval(accidental);
2735 | var coord = coords(noteName.toLowerCase());
2736 |
2737 | coord[0] += octave;
2738 | coord[0] += accidentalValue[0] - coords.A4[0];
2739 | coord[1] += accidentalValue[1] - coords.A4[1];
2740 |
2741 | return coord;
2742 | };
2743 |
2744 | },{"accidental-value":25,"notecoord":26}],25:[function(require,module,exports){
2745 | arguments[4][21][0].apply(exports,arguments)
2746 | },{"dup":21}],26:[function(require,module,exports){
2747 | arguments[4][22][0].apply(exports,arguments)
2748 | },{"dup":22}]},{},[1]);
2749 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "neuro",
3 | "version": "0.1.0",
4 | "description": "A web audio experiment/tutorial in bass synthesis.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "browserify index.js -o neuro.js",
9 | "watch": "watchify index.js -o neuro.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/nick-thompson/neuro"
14 | },
15 | "keywords": [
16 | "audio",
17 | "web",
18 | "audio",
19 | "synthesis",
20 | "sound",
21 | "bass"
22 | ],
23 | "author": "Nick Thompson",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/nick-thompson/neuro/issues"
27 | },
28 | "homepage": "https://github.com/nick-thompson/neuro",
29 | "dependencies": {
30 | "browserify": "^11.0.1",
31 | "nprogress": "^0.2.0",
32 | "teoria": "^1.12.0",
33 | "watchify": "^3.3.1"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/plots/clipping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nick-thompson/neuro/fa06f1de6bb38279ea360ebc19057bb2e39e5665/plots/clipping.png
--------------------------------------------------------------------------------
/plots/clipping.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import matplotlib.pyplot as plt
4 | import numpy as np
5 |
6 | x = np.linspace(-np.pi, np.pi, 201)
7 | y = np.sin(x)
8 |
9 | plt.figure()
10 | plt.subplot(121)
11 |
12 | # Color in the axes
13 | plt.axvline(linewidth=1, color='#bbbbbb')
14 | plt.axhline(linewidth=1, color='#bbbbbb')
15 |
16 | x_one_one = np.linspace(-1, 1, 201)
17 |
18 | # Soft Saturation
19 | plt.plot(x_one_one, np.arctan(x_one_one), color='m')
20 |
21 | # Soft saturation approximation
22 | plt.plot(x_one_one, np.arctan(x_one_one) - (np.arctan(x_one_one) ** 3) / 3, color='c')
23 |
24 | # Hard clipping
25 | plt.plot(x_one_one, 0.5 * (abs(x_one_one + 0.85) - abs(x_one_one - 0.85)), color='b')
26 |
27 | # S-Curve
28 | plt.plot(x_one_one, (1 + 4) * x_one_one / (1 + 4 * abs(x_one_one)), color='y')
29 |
30 | # Identity
31 | plt.plot(x_one_one, x_one_one, color='k')
32 |
33 | plt.axis('tight')
34 | plt.subplot(122)
35 |
36 | # Color in the axes
37 | plt.axvline(linewidth=1, color='#bbbbbb')
38 | plt.axhline(linewidth=1, color='#bbbbbb')
39 |
40 | # Soft saturation
41 | plt.plot(x, np.arctan(y), color='m')
42 |
43 | # Soft saturation approximation
44 | plt.plot(x, y - (y ** 3) / 3, color='c')
45 |
46 | # Hard clipping
47 | plt.plot(x, 0.5 * (abs(y + 0.85) - abs(y - 0.85)), color='b')
48 |
49 | # S-Curve
50 | plt.plot(x, (1 + 4) * y / (1 + 4 * abs(y)), color='y')
51 |
52 | # Original
53 | plt.plot(x, y, color='k')
54 |
55 | plt.axis('tight')
56 | plt.show()
57 |
--------------------------------------------------------------------------------
/plots/phasing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nick-thompson/neuro/fa06f1de6bb38279ea360ebc19057bb2e39e5665/plots/phasing.png
--------------------------------------------------------------------------------
/plots/phasing.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import matplotlib.pyplot as plt
4 | import numpy as np
5 |
6 | x = np.linspace(0, 4 * np.pi, 400)
7 |
8 | plt.axvline(linewidth=1, color='#bbbbbb')
9 | plt.axhline(linewidth=1, color='#bbbbbb')
10 |
11 | plt.plot(x, np.sin(x), color='c')
12 | plt.plot(x, np.sin(1.12 * x - 0.8 * np.pi), color='m')
13 |
14 | plt.plot(x, np.sin(x) + np.sin(1.12 * x - 0.8 * np.pi), color='k')
15 |
16 | plt.axis('tight')
17 | plt.show()
18 |
--------------------------------------------------------------------------------
/plots/resampling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nick-thompson/neuro/fa06f1de6bb38279ea360ebc19057bb2e39e5665/plots/resampling.png
--------------------------------------------------------------------------------
/plots/resampling.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import matplotlib.pyplot as plt
4 | import numpy as np
5 | import math
6 |
7 | z_one = None
8 | z_two = None
9 |
10 | def without_resampling():
11 | # Plot the axes in grey.
12 | plt.axhline(linewidth=1, color='#bbbbbb')
13 | plt.axvline(linewidth=1, color='#bbbbbb')
14 |
15 | # The X domain here is modeled with 100 sample frames per cycle which
16 | # approximates an A4 sine wave playing at 44,100Hz. In the resampling step,
17 | # we'll take this number down to show the phasing artifacts introduced.
18 | x = np.linspace(0, 8 * np.pi, 400)
19 |
20 | y = np.sin(x) # First oscillator from the generator
21 | yp = np.sin(1.0265 * x) # Second oscillator from the generator.
22 |
23 | # This is a detune factor of +24 cents. A little unrealistic for the sound we
24 | # want, but helps clarify what's happening in the graphical representation.
25 | detune_factor = math.pow(2.0, (24.0 / 1200.0))
26 |
27 | ypp = np.sin(detune_factor * x) # First oscillator detuned
28 | yppp = np.sin(detune_factor * 1.0265 * x) # Second oscillator detuned
29 |
30 | plt.plot(x, y, 'm')
31 | plt.plot(x, yp, 'c')
32 | plt.plot(x, ypp, 'y')
33 | plt.plot(x, yppp, 'g')
34 |
35 | # Now the summation to show the phasing
36 | global z_one
37 | z_one = 0.25 * (y + yp + ypp + yppp)
38 | plt.plot(x, z_one, 'k')
39 |
40 | plt.axis('tight')
41 |
42 | def with_resampling():
43 | # Plot the axes in grey.
44 | plt.axhline(linewidth=1, color='#bbbbbb')
45 | plt.axvline(linewidth=1, color='#bbbbbb')
46 |
47 | # The X domain here is modeled with 100 sample frames per cycle which
48 | # approximates an A4 sine wave playing at 44,100Hz. In the resampling step,
49 | # we'll take this number down to show the phasing artifacts introduced.
50 | x = np.linspace(0, 8 * np.pi, 400)
51 |
52 | # We'll need these two because in `stepTwo` we play the raw buffer next to
53 | # the resampled buffer. These two represent the raw buffer.
54 | y = np.sin(x) # First oscillator from the generator
55 | yp = np.sin(1.0265 * x) # Second oscillator from the generator.
56 |
57 | # Same detune factor of +24 cents
58 | detune_factor = math.pow(2.0, (24.0 / 1200.0))
59 |
60 | # The resampled buffers drop samples, thus their array size is not the same
61 | # as `x`, `yp`, or `ypp`.
62 | size = int(math.floor(400.0 / detune_factor))
63 | xp = np.linspace(0, 8 * np.pi, size)
64 | ypp = np.zeros_like(xp)
65 |
66 | # Chromium's detune implementation ported here for our experiment.
67 | yyp = 0.5 * (y + yp)
68 | virtual_read_index = 1.0
69 | playback_rate = detune_factor
70 | for i in range(size):
71 | write_index = i
72 | read_index = math.floor(virtual_read_index)
73 | read_index2 = read_index + 1
74 | interp_factor = virtual_read_index - read_index
75 |
76 | if read_index2 >= size - 1:
77 | read_index2 = read_index
78 | if read_index >= size - 1:
79 | break
80 |
81 | sample1 = yyp[read_index]
82 | sample2 = yyp[read_index2]
83 | current_sample = (1.0 - interp_factor) * sample1 + interp_factor * sample2
84 |
85 | ypp[write_index] = current_sample
86 | virtual_read_index = virtual_read_index + playback_rate
87 |
88 | plt.plot(x, y, 'm')
89 | plt.plot(x, yp, 'c')
90 | plt.plot(xp, ypp, 'y')
91 |
92 | # Now the summation to show the phasing
93 | global z_two
94 | z_two = y + yp
95 | z_two[:len(ypp)] += ypp
96 | z_two = 0.33 * z_two
97 | plt.plot(x, z_two, 'k')
98 |
99 | plt.axis('tight')
100 |
101 | def show_difference():
102 | # Plot the axes in grey.
103 | plt.axhline(linewidth=1, color='#bbbbbb')
104 | plt.axvline(linewidth=1, color='#bbbbbb')
105 |
106 | x = np.linspace(0, 8 * np.pi, 400)
107 | plt.plot(x, z_one, '#ffffff')
108 | plt.plot(x, z_two, '#ffffff')
109 |
110 | plt.fill_between(x, z_one, z_two, where=z_one>=z_two, facecolor='c', interpolate=True)
111 | plt.fill_between(x, z_one, z_two, where=z_one<=z_two, facecolor='c', interpolate=True)
112 |
113 | plt.axis('tight')
114 |
115 | if __name__ == '__main__':
116 | plt.figure()
117 |
118 | # Plot the same plot on top of itself for interesting zooming perspective
119 | plt.subplot(231)
120 | without_resampling()
121 | plt.subplot(234)
122 | without_resampling()
123 |
124 | plt.subplot(232)
125 | with_resampling()
126 | plt.subplot(235)
127 | with_resampling()
128 |
129 | plt.subplot(233)
130 | show_difference()
131 | plt.subplot(236)
132 | show_difference()
133 |
134 | plt.show()
135 |
--------------------------------------------------------------------------------
/vendor/recorder.js:
--------------------------------------------------------------------------------
1 | (function(window){
2 |
3 | var WORKER_PATH = 'recorderWorker.js';
4 |
5 | var Recorder = function(source, cfg){
6 | var config = cfg || {};
7 | var bufferLen = config.bufferLen || 4096;
8 | var numChannels = config.numChannels || 2;
9 | this.context = source.context;
10 | this.node = (this.context.createScriptProcessor ||
11 | this.context.createJavaScriptNode).call(this.context,
12 | bufferLen, numChannels, numChannels);
13 | var worker = new Worker(config.workerPath || WORKER_PATH);
14 | worker.postMessage({
15 | command: 'init',
16 | config: {
17 | sampleRate: this.context.sampleRate,
18 | numChannels: numChannels
19 | }
20 | });
21 | var recording = false,
22 | currCallback;
23 |
24 | this.node.onaudioprocess = function(e){
25 | if (!recording) return;
26 | var buffer = [];
27 | for (var channel = 0; channel < numChannels; channel++){
28 | buffer.push(e.inputBuffer.getChannelData(channel));
29 | }
30 | worker.postMessage({
31 | command: 'record',
32 | buffer: buffer
33 | });
34 | }
35 |
36 | this.configure = function(cfg){
37 | for (var prop in cfg){
38 | if (cfg.hasOwnProperty(prop)){
39 | config[prop] = cfg[prop];
40 | }
41 | }
42 | }
43 |
44 | this.record = function(){
45 | recording = true;
46 | }
47 |
48 | this.stop = function(){
49 | recording = false;
50 | }
51 |
52 | this.clear = function(){
53 | worker.postMessage({ command: 'clear' });
54 | }
55 |
56 | this.getBuffer = function(cb) {
57 | currCallback = cb || config.callback;
58 | worker.postMessage({ command: 'getBuffer' })
59 | }
60 |
61 | this.exportWAV = function(cb, type){
62 | currCallback = cb || config.callback;
63 | type = type || config.type || 'audio/wav';
64 | if (!currCallback) throw new Error('Callback not set');
65 | worker.postMessage({
66 | command: 'exportWAV',
67 | type: type
68 | });
69 | }
70 |
71 | worker.onmessage = function(e){
72 | var blob = e.data;
73 | currCallback(blob);
74 | }
75 |
76 | source.connect(this.node);
77 | this.node.connect(this.context.destination); //this should not be necessary
78 | };
79 |
80 | Recorder.forceDownload = function(blob, filename){
81 | var url = (window.URL || window.webkitURL).createObjectURL(blob);
82 | var link = window.document.createElement('a');
83 | link.href = url;
84 | link.download = filename || 'output.wav';
85 | var click = document.createEvent("Event");
86 | click.initEvent("click", true, true);
87 | link.dispatchEvent(click);
88 | }
89 |
90 | window.Recorder = Recorder;
91 |
92 | })(window);
93 |
--------------------------------------------------------------------------------
/vendor/recorderWorker.js:
--------------------------------------------------------------------------------
1 | var recLength = 0,
2 | recBuffers = [],
3 | sampleRate,
4 | numChannels;
5 |
6 | this.onmessage = function(e){
7 | switch(e.data.command){
8 | case 'init':
9 | init(e.data.config);
10 | break;
11 | case 'record':
12 | record(e.data.buffer);
13 | break;
14 | case 'exportWAV':
15 | exportWAV(e.data.type);
16 | break;
17 | case 'getBuffer':
18 | getBuffer();
19 | break;
20 | case 'clear':
21 | clear();
22 | break;
23 | }
24 | };
25 |
26 | function init(config){
27 | sampleRate = config.sampleRate;
28 | numChannels = config.numChannels;
29 | initBuffers();
30 | }
31 |
32 | function record(inputBuffer){
33 | for (var channel = 0; channel < numChannels; channel++){
34 | recBuffers[channel].push(inputBuffer[channel]);
35 | }
36 | recLength += inputBuffer[0].length;
37 | }
38 |
39 | function exportWAV(type){
40 | var buffers = [];
41 | for (var channel = 0; channel < numChannels; channel++){
42 | buffers.push(mergeBuffers(recBuffers[channel], recLength));
43 | }
44 | if (numChannels === 2){
45 | var interleaved = interleave(buffers[0], buffers[1]);
46 | } else {
47 | var interleaved = buffers[0];
48 | }
49 | var dataview = encodeWAV(interleaved);
50 | var audioBlob = new Blob([dataview], { type: type });
51 |
52 | this.postMessage(audioBlob);
53 | }
54 |
55 | function getBuffer(){
56 | var buffers = [];
57 | for (var channel = 0; channel < numChannels; channel++){
58 | buffers.push(mergeBuffers(recBuffers[channel], recLength));
59 | }
60 | this.postMessage(buffers);
61 | }
62 |
63 | function clear(){
64 | recLength = 0;
65 | recBuffers = [];
66 | initBuffers();
67 | }
68 |
69 | function initBuffers(){
70 | for (var channel = 0; channel < numChannels; channel++){
71 | recBuffers[channel] = [];
72 | }
73 | }
74 |
75 | function mergeBuffers(recBuffers, recLength){
76 | var result = new Float32Array(recLength);
77 | var offset = 0;
78 | for (var i = 0; i < recBuffers.length; i++){
79 | result.set(recBuffers[i], offset);
80 | offset += recBuffers[i].length;
81 | }
82 | return result;
83 | }
84 |
85 | function interleave(inputL, inputR){
86 | var length = inputL.length + inputR.length;
87 | var result = new Float32Array(length);
88 |
89 | var index = 0,
90 | inputIndex = 0;
91 |
92 | while (index < length){
93 | result[index++] = inputL[inputIndex];
94 | result[index++] = inputR[inputIndex];
95 | inputIndex++;
96 | }
97 | return result;
98 | }
99 |
100 | function floatTo16BitPCM(output, offset, input){
101 | for (var i = 0; i < input.length; i++, offset+=2){
102 | var s = Math.max(-1, Math.min(1, input[i]));
103 | output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
104 | }
105 | }
106 |
107 | function writeString(view, offset, string){
108 | for (var i = 0; i < string.length; i++){
109 | view.setUint8(offset + i, string.charCodeAt(i));
110 | }
111 | }
112 |
113 | function encodeWAV(samples){
114 | var buffer = new ArrayBuffer(44 + samples.length * 2);
115 | var view = new DataView(buffer);
116 |
117 | /* RIFF identifier */
118 | writeString(view, 0, 'RIFF');
119 | /* RIFF chunk length */
120 | view.setUint32(4, 36 + samples.length * 2, true);
121 | /* RIFF type */
122 | writeString(view, 8, 'WAVE');
123 | /* format chunk identifier */
124 | writeString(view, 12, 'fmt ');
125 | /* format chunk length */
126 | view.setUint32(16, 16, true);
127 | /* sample format (raw) */
128 | view.setUint16(20, 1, true);
129 | /* channel count */
130 | view.setUint16(22, numChannels, true);
131 | /* sample rate */
132 | view.setUint32(24, sampleRate, true);
133 | /* byte rate (sample rate * block align) */
134 | view.setUint32(28, sampleRate * 4, true);
135 | /* block align (channel count * bytes per sample) */
136 | view.setUint16(32, numChannels * 2, true);
137 | /* bits per sample */
138 | view.setUint16(34, 16, true);
139 | /* data chunk identifier */
140 | writeString(view, 36, 'data');
141 | /* data chunk length */
142 | view.setUint32(40, samples.length * 2, true);
143 |
144 | floatTo16BitPCM(view, 44, samples);
145 |
146 | return view;
147 | }
148 |
--------------------------------------------------------------------------------