├── .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 | 38 | 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 | --------------------------------------------------------------------------------