├── .babelrc ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── dist ├── sono.js ├── sono.js.map └── sono.min.js ├── docs ├── controls.md ├── effects.md ├── effects │ ├── analyser.md │ ├── compressor.md │ ├── convolver.md │ ├── distortion.md │ ├── echo.md │ ├── filter.md │ ├── flanger.md │ ├── panner.md │ ├── phaser.md │ └── reverb.md ├── getting-started.md ├── loading.md ├── sounds.md ├── utils.md └── utils │ ├── microphone.md │ ├── recorder.md │ ├── waveform.md │ └── waveformer.md ├── examples ├── analyser.html ├── audio │ ├── .gitinclude │ ├── .htaccess │ ├── bullet.mp3 │ ├── bullet.ogg │ ├── collect.mp3 │ ├── collect.ogg │ ├── dnb-loop.mp3 │ ├── dnb-loop.ogg │ ├── hit.mp3 │ ├── hit.ogg │ ├── select.mp3 │ └── select.ogg ├── background.html ├── convolver.html ├── css │ ├── control.css │ ├── index.css │ ├── normalize.css │ ├── player.css │ ├── sections.css │ └── styles.css ├── distortion.html ├── echo.html ├── favicon.ico ├── filter.html ├── flanger.html ├── img │ ├── .gitinclude │ ├── back.png │ ├── bottom.png │ ├── front.png │ ├── left.png │ ├── right.png │ └── top.png ├── js │ ├── analyser.js │ ├── base-url.js │ ├── disable.js │ ├── example.js │ ├── pixi.js │ ├── recorder.js │ ├── src │ │ ├── analyser.js │ │ ├── example.js │ │ ├── three.js │ │ └── ui.js │ ├── three.js │ └── ui.js ├── load.html ├── multi-play.html ├── multiple.html ├── oscillator.html ├── phaser.html ├── pixi.html ├── recorder.html ├── reverb.html ├── three.html ├── video.html ├── video │ └── .gitinclude ├── volume.html └── wet_dry.html ├── index.html ├── karma.conf.js ├── package.json ├── rollup.config.js ├── src ├── core │ ├── context.js │ ├── effects.js │ ├── group.js │ ├── sono.js │ ├── sound.js │ ├── source │ │ ├── audio-source.js │ │ ├── buffer-source.js │ │ ├── media-source.js │ │ ├── microphone-source.js │ │ └── oscillator-source.js │ └── utils │ │ ├── dummy.js │ │ ├── emitter.js │ │ ├── fake-context.js │ │ ├── file.js │ │ ├── firefox.js │ │ ├── iOS.js │ │ ├── isDefined.js │ │ ├── isSafeNumber.js │ │ ├── loader.js │ │ ├── log.js │ │ ├── pageVisibility.js │ │ ├── sound-group.js │ │ ├── touchLock.js │ │ └── utils.js ├── effects │ ├── abstract-direct-effect.js │ ├── abstract-effect.js │ ├── analyser.js │ ├── compressor.js │ ├── convolver.js │ ├── distortion.js │ ├── echo.js │ ├── filter.js │ ├── flanger.js │ ├── index.js │ ├── panner.js │ ├── phaser.js │ └── reverb.js ├── index.js └── utils │ ├── index.js │ ├── microphone.js │ ├── recorder.js │ ├── waveform.js │ └── waveformer.js └── test ├── api.spec.js ├── audio ├── blip.mp3 ├── blip.ogg ├── bloop.mp3 ├── bloop.ogg ├── long.mp3 └── long.ogg ├── destroy.spec.js ├── effects.spec.js ├── file.spec.js ├── group.spec.js ├── helper.js ├── is-travis.js ├── kill-wa.js ├── loader.spec.js ├── playback.spec.js ├── seek.spec.js ├── sound.spec.js ├── source.spec.js ├── utils.spec.js └── volume.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", { 5 | "loose": true 6 | } 7 | ] 8 | ], 9 | "plugins": [ 10 | ["external-helpers"], 11 | ["transform-runtime", { 12 | "helpers": true, 13 | "polyfill": false, 14 | "regenerator": false, 15 | "moduleName": "babel-runtime" 16 | }] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "globals": { 10 | "sono": true, 11 | "expect": true, 12 | "it": true 13 | }, 14 | "plugins": [], 15 | "parserOptions": { 16 | "ecmaVersion": 6, 17 | "jsx": true, 18 | "sourceType": "module" 19 | }, 20 | "rules": { 21 | "array-bracket-spacing": [2, "never"], 22 | "block-scoped-var": 0, 23 | "block-spacing": 2, 24 | "brace-style": [2, "1tbs"], 25 | "callback-return": [2, ["cb", "callback", "next"]], 26 | "camelcase": [2, {"properties": "never"}], 27 | "comma-dangle": [2, "never"], 28 | "comma-spacing": 2, 29 | "comma-style": [2, "last"], 30 | "consistent-return": 2, 31 | "curly": [2, "all"], 32 | "default-case": 2, 33 | "dot-location": [2, "property"], 34 | "dot-notation": [2, {"allowKeywords": true}], 35 | "eol-last": 2, 36 | "eqeqeq": 2, 37 | "func-style": [2, "declaration", {"allowArrowFunctions": true}], 38 | "guard-for-in": 2, 39 | "indent": [2, 4, {"SwitchCase": 1}], 40 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 41 | "keyword-spacing": 2, 42 | "max-len": [1, 120], 43 | "new-cap": 0, 44 | "new-parens": 2, 45 | "no-alert": 2, 46 | "no-array-constructor": 2, 47 | "no-caller": 2, 48 | "no-confusing-arrow": 2, 49 | "no-console": 0, 50 | "no-const-assign": 2, 51 | "no-constant-condition": 2, 52 | "no-delete-var": 2, 53 | "no-empty": ["error", { "allowEmptyCatch": true }], 54 | "no-eval": 2, 55 | "no-extend-native": 2, 56 | "no-extra-bind": 2, 57 | "no-extra-semi": 2, 58 | "no-fallthrough": 2, 59 | "no-floating-decimal": 2, 60 | "no-implied-eval": 2, 61 | "no-invalid-this": 2, 62 | "no-iterator": 2, 63 | "no-label-var": 2, 64 | "no-labels": [2, {"allowLoop": true, "allowSwitch": true}], 65 | "no-lone-blocks": 2, 66 | "no-loop-func": 2, 67 | "no-mixed-spaces-and-tabs": [2, false], 68 | "no-multi-spaces": 2, 69 | "no-multi-str": 2, 70 | "no-native-reassign": 2, 71 | "no-nested-ternary": 2, 72 | "no-new-func": 2, 73 | "no-new-object": 2, 74 | "no-new-wrappers": 2, 75 | "no-new": 2, 76 | "no-octal-escape": 2, 77 | "no-octal": 2, 78 | "no-process-exit": 2, 79 | "no-proto": 2, 80 | "no-redeclare": 2, 81 | "no-return-assign": 2, 82 | "no-script-url": 2, 83 | "no-sequences": 2, 84 | "no-shadow-restricted-names": 2, 85 | "no-shadow": 2, 86 | "no-spaced-func": 2, 87 | "no-trailing-spaces": 2, 88 | "no-undef-init": 2, 89 | "no-undef": 2, 90 | "no-undefined": 2, 91 | "no-underscore-dangle": 0, 92 | "no-unused-expressions": 0, 93 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 94 | "no-use-before-define": 0, 95 | "no-useless-concat": 2, 96 | "no-var": 2, 97 | "no-with": 2, 98 | "object-curly-spacing": [2, "never"], 99 | "prefer-const": 2, 100 | "quotes": [2, "single"], 101 | "radix": 2, 102 | "require-jsdoc": 0, 103 | "semi-spacing": [2, {"before": false, "after": true}], 104 | "semi": 2, 105 | "space-before-blocks": 2, 106 | "space-before-function-paren": [0, "never"], 107 | "space-in-parens": [2, "never"], 108 | "space-infix-ops": 2, 109 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 110 | "spaced-comment": [0, "always", {"exceptions": ["-"]}], 111 | "strict": [1, "global"], 112 | "valid-jsdoc": [0, {"prefer": { "return": "returns"}}], 113 | "wrap-iife": 2, 114 | "yoda": [2, "never"] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These files are text and should be normalized (Convert crlf => lf) 2 | *.php text 3 | *.css text 4 | *.js text 5 | *.htm text 6 | *.html text 7 | *.xml text 8 | *.txt text 9 | *.ini text 10 | *.inc text 11 | .htaccess text 12 | 13 | # These files are binary and should be left untouched 14 | # (binary is a macro for -text -diff) 15 | *.png binary 16 | *.jpg binary 17 | *.jpeg binary 18 | *.gif binary 19 | *.ico binary 20 | *.mov binary 21 | *.mp4 binary 22 | *.mp3 binary 23 | *.flv binary 24 | *.fla binary 25 | *.swf binary 26 | *.gz binary 27 | *.zip binary 28 | *.7z binary 29 | *.ttf binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | 3 | ._* 4 | .DS_Store 5 | .DS_Store? 6 | .AppleDouble 7 | Icon? 8 | .Spotlight-V100 9 | .Trashes 10 | ehthumbs.db 11 | Thumbs.db 12 | Desktop.ini 13 | 14 | # VCS 15 | 16 | .svn 17 | *.orig 18 | !.gitinclude 19 | 20 | # IDE 21 | 22 | .settings/ 23 | .project 24 | user.properties 25 | .idea 26 | .idea? 27 | .metadata/* 28 | *.ignore 29 | *.sublime-* 30 | *~ 31 | *.swp 32 | *.swo 33 | dploy.yaml 34 | 35 | # BINARIES 36 | 37 | *.mp4 38 | *.webm 39 | *.ogv 40 | *.mov 41 | *.mp3 42 | *.wav 43 | *.ogg 44 | *.air 45 | *.pyc 46 | 47 | # FOLDERS 48 | 49 | !/test/audio/*.ogg 50 | !/test/audio/*.mp3 51 | /examples/audio/*.mp3 52 | /examples/audio/*.ogg 53 | /examples/audio/other 54 | /examples/temp.html 55 | node_modules/ 56 | bower_components/ 57 | .sass-cache/ 58 | /labs/ 59 | /core/ 60 | /effects/ 61 | /utils/ 62 | /index.js 63 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /examples/ 3 | /labs/ 4 | /test/ 5 | /.babelrc 6 | /.eslintrc 7 | /.editorconfig 8 | /.gitattributes 9 | /.travis.yml 10 | /bower.json 11 | /index.html 12 | /index.js 13 | /karma.conf.js 14 | /rollup.config.js 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - 6 6 | cache: 7 | directories: 8 | - node_modules 9 | addons: 10 | chrome: stable 11 | before_install: 12 | - export CHROME_BIN=chromium-browser 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | - sleep 3 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Stinkdigital 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sono 2 | 3 | [![NPM version](https://badge.fury.io/js/sono.svg)](http://badge.fury.io/js/sono) [![Build Status](https://travis-ci.com/Stinkstudios/sono.svg?branch=master)](https://travis-ci.com/Stinkstudios/sono) 4 | 5 | A simple yet powerful JavaScript library for working with Web Audio 6 | 7 | 8 | 9 | ## Features 10 | 11 | * Full audio management including loading, playback, effects and processing 12 | * Abstracts differences across browsers such as file types and Web Audio support 13 | * Web Audio effects such as 3d positioning, reverb and frequency analysis 14 | * Handles inputs from audio files, media elements, microphone, oscillators and scripts 15 | * Falls back to HTMLAudioElement where Web Audio is not supported (e.g. IE 11 and less) 16 | * Pauses and resumes audio playback on page visibility changes 17 | * Handles initial touch to unlock media playback on mobile devices 18 | 19 | ## Installation 20 | 21 | ```shell 22 | npm i -S sono 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```javascript 28 | import sono from 'sono'; 29 | import 'sono/effects'; 30 | import 'sono/utils'; 31 | 32 | const sound = sono.create('boom.mp3'); 33 | sound.effects = [sono.echo(), sono.reverb()]; 34 | sound.play(); 35 | ``` 36 | 37 | ## Documentation 38 | 39 | ### [Getting started](docs/getting-started.md) 40 | 41 | ### [Sounds](docs/sounds.md) 42 | 43 | ### [Effects](docs/effects.md) 44 | 45 | ### [Controls](docs/controls.md) 46 | 47 | ### [Loading](docs/loading.md) 48 | 49 | ### [Utils](docs/utils.md) 50 | 51 | ## Dev setup 52 | 53 | ### Install dependencies 54 | 55 | ```shell 56 | npm i 57 | ``` 58 | 59 | ### Run tests 60 | 61 | ```shell 62 | npm i -g karma-cli 63 | npm test 64 | ``` 65 | 66 | ### Run examples 67 | 68 | ```shell 69 | npm run examples 70 | ``` 71 | 72 | ### Build bundles 73 | 74 | ```shell 75 | npm run build 76 | ``` 77 | 78 | ### Watch and test 79 | 80 | ```shell 81 | npm start 82 | ``` 83 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sono", 3 | "version": "0.1.9", 4 | "homepage": "https://github.com/Stinkdigital/sono", 5 | "authors": [ 6 | "Ian McGregor " 7 | ], 8 | "description": "A library for making stuff with Web Audio", 9 | "main": "dist/sono.min.js", 10 | "moduleType": [ 11 | "node", 12 | "amd", 13 | "globals" 14 | ], 15 | "license": "MIT", 16 | "ignore": [ 17 | "**/.*", 18 | "node_modules", 19 | "bower_components", 20 | "test" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /docs/controls.md: -------------------------------------------------------------------------------- 1 | # Controls 2 | 3 | [View source code](../src/core/sono.js) 4 | 5 | ## Play in background 6 | 7 | By default sono will pause and resume all audio when the page is hidden. This feature can be turned off by setting the `playInBackground` property: 8 | 9 | ```javascript 10 | sono.playInBackground = true; 11 | ``` 12 | 13 | ## Master volume 14 | 15 | ```javascript 16 | // mute master volume 17 | sono.mute(); 18 | 19 | // un-mute master volume 20 | sono.unMute(); 21 | 22 | // set master volume to 50% 23 | sono.volume = 0.5; 24 | 25 | // get master volume 26 | console.log(sono.volume); // 0.5 27 | 28 | // fade out master volume to 0 over 2 seconds 29 | sono.fade(0, 2); 30 | ``` 31 | 32 | ## Update individual sounds 33 | 34 | ```javascript 35 | // return instance of a sound by id 36 | sono.get('foo'); 37 | 38 | // play sound by id after a 1 second delay 39 | sono.play('foo', 1); 40 | 41 | // pause sound by id 42 | sono.pause('foo'); 43 | 44 | // stop sound by id 45 | sono.stop('foo'); 46 | 47 | // destroy a sound (by instance or id) 48 | sono.destroy(sound); 49 | sono.destroy('bar'); 50 | ``` 51 | 52 | ## Update all sounds 53 | 54 | ```javascript 55 | // pause all currently playing 56 | sono.pauseAll(); 57 | 58 | // resume all currently paused 59 | sono.resumeAll(); 60 | 61 | // stop all currently playing or paused 62 | sono.stopAll(); 63 | 64 | // destroy all sounds 65 | sono.destroyAll() 66 | ``` 67 | 68 | ## Master effects 69 | 70 | ```javascript 71 | import analyser from 'sono/effects/analyser'; 72 | 73 | const analyse = sono.effects.add(analyser()); 74 | ``` 75 | 76 | ## Check support 77 | 78 | ```javascript 79 | sono.isSupported; 80 | sono.hasWebAudio; 81 | 82 | sono.canPlay.ogg; 83 | sono.canPlay.mp3; 84 | sono.canPlay.opus 85 | sono.canPlay.wav; 86 | sono.canPlay.m4a; 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/effects.md: -------------------------------------------------------------------------------- 1 | # Effects 2 | 3 | [View source code](../src/core/effects.js) 4 | 5 | ## Add effects 6 | 7 | Use the `add` function to return the reference: 8 | ```javascript 9 | import {echo, reverb} from 'sono/effects'; 10 | 11 | const sound = sono.create('boom.mp3'); 12 | const echo = sound.effects.add(echo({feedback: 0.8})); 13 | const reverb = sound.effects.add(reverb({time: 1, decay: 5})); 14 | sound.play(); 15 | ``` 16 | 17 | Set an array of effects: 18 | ```javascript 19 | import flanger from 'sono/effects/flanger'; 20 | 21 | const sound = sono.create('boom.mp3'); 22 | sound.effects = [flanger()]; 23 | sound.play(); 24 | ``` 25 | 26 | Set within sound config: 27 | ```javascript 28 | import {distortion, filter} from 'sono/effects'; 29 | 30 | const sound = sono.create({ 31 | url: 'boom.mp3', 32 | effects: [ 33 | distortion({level: 0.2, wet: 1, dry: 0}), 34 | filter({type: 'lowpass', frequency: 400}), 35 | ] 36 | }); 37 | sound.play(); 38 | ``` 39 | 40 | ## Once imported effects are registered 41 | 42 | Import one by one: 43 | 44 | ```javascript 45 | import 'sono/effects/analyser'; 46 | 47 | const sound = sono.create('boom.mp3'); 48 | sound.effects.add(sono.analyser()); 49 | sound.play(); 50 | ``` 51 | 52 | Import everything: 53 | 54 | ```javascript 55 | import 'sono/effects'; 56 | 57 | const sound = sono.create('boom.mp3'); 58 | sound.effects = [sono.phaser(), sono.reverb()]; 59 | sound.play(); 60 | ``` 61 | 62 | ## Balance wet and dry signals 63 | 64 | ```javascript 65 | import {echo, reverb} from 'sono/effects'; 66 | 67 | const sound = sono.create('boom.mp3'); 68 | // all sound passing through the effect is distorted: 69 | const fullDistortion = sound.effects.add(distortion({ 70 | level: 0.8, wet: 1, dry: 0 71 | })); 72 | // distorted version is added to the clean version: 73 | const addDistortion = sound.effects.add(distortion({ 74 | level: 0.8, wet: 1, dry: 1 75 | })); 76 | // small amount of distorted signal is added to the clean version: 77 | const littleDistortion = sound.effects.add(distortion({ 78 | level: 0.8, wet: 0.2, dry: 1 79 | })); 80 | sound.play(); 81 | ``` 82 | 83 | ## Update effects 84 | 85 | Access by reference: 86 | ```javascript 87 | import {echo, reverb} from 'sono/effects'; 88 | 89 | const sound = sono.create('boom.mp3'); 90 | const distortion = sound.effects.add(distortion({level: 0.8})); 91 | const echo = sound.effects.add(echo({wet: 0.5})); 92 | sound.play(); 93 | 94 | distortion.level = 0.3; 95 | echo.delay = 0.8; 96 | echo.wet = 1; 97 | ``` 98 | 99 | Access by index: 100 | ```javascript 101 | import {distortion, echo} from 'sono/effects'; 102 | 103 | const sound = sono.create('boom.mp3'); 104 | sound.effects = [distortion(), echo()]; 105 | sound.play(); 106 | 107 | sound.effects[0].level = 0.3; 108 | sound.effects[0].dry = 0; 109 | 110 | sound.effects[1].delay = 0.8; 111 | ``` 112 | 113 | ## Add effects to groups 114 | 115 | Add to sono output: 116 | ```javascript 117 | import analyser from 'sono/effects/analyser'; 118 | 119 | const analyse = sono.effects.add(analyser({fftSize: 1024})); 120 | 121 | function update() { 122 | window.requestAnimationFrame(update); 123 | 124 | const frequencies = analyse.getFrequencies(); 125 | // do something cool 126 | } 127 | update(); 128 | ``` 129 | 130 | Add to a sound group: 131 | 132 | ```javascript 133 | import distortion from 'sono/effects/distortion'; 134 | 135 | const effectsBus = sono.group([ 136 | sono.create({id: 'boom', url: 'boom.mp3'}), 137 | sono.create({id: 'bang', url: 'bang.mp3'}) 138 | ]); 139 | 140 | effectsBus.effects.add(distortion()); 141 | 142 | sono.play('boom'); 143 | sono.play('bang'); 144 | ``` 145 | 146 | 147 | ## Add vanilla AudioNodes: 148 | 149 | ```javascript 150 | import sono from 'sono'; 151 | 152 | const sound = sono.create('boom.mp3'); 153 | 154 | const filter = sono.context.createBiquadFilter(); 155 | filter.type = 'lowpass'; 156 | filter.frequency.value = 1100; 157 | 158 | const gain = sono.context.createGain(); 159 | gain.gain.value = 1000; 160 | gain.connect(filter.frequency); 161 | 162 | const lfo = sono.context.createOscillator(); 163 | lfo.type = 'sine'; 164 | lfo.frequency.value = 8; 165 | lfo.connect(gain); 166 | lfo.start(0); 167 | 168 | sound.effects.add(filter); 169 | ``` 170 | 171 | ## Bypass effect (enable/disable) 172 | 173 | ```javascript 174 | import distortion from 'sono/effects/distortion'; 175 | 176 | const sound = sono.create('boom.mp3'); 177 | const distort = sound.effects.add(distortion({ 178 | level: 0.8 179 | })); 180 | 181 | distort.enable(false); // bypass 182 | distort.enable(true); // re-enable 183 | 184 | ``` 185 | 186 | ## Remove/Toggle 187 | 188 | ```javascript 189 | import distortion from 'sono/effects/distortion'; 190 | 191 | const sound = sono.create('boom.mp3'); 192 | const distort = distortion({level: 0.8}); 193 | 194 | sound.effects.add(distort); 195 | sound.effects.remove(distort); 196 | sound.effects.toggle(distort, true); 197 | sound.effects.toggle(distort, false); 198 | ``` 199 | 200 | ## Docs for individual effects 201 | 202 | [analyser](./effects/analyser.md) 203 | 204 | [compressor](./effects/compressor.md) 205 | 206 | [convolver](./effects/convolver.md) 207 | 208 | [distortion](./effects/distortion.md) 209 | 210 | [echo](./effects/echo.md) 211 | 212 | [filter](./effects/filter.md) 213 | 214 | [flanger](./effects/flanger.md) 215 | 216 | [panner](./effects/panner.md) 217 | 218 | [phaser](./effects/phaser.md) 219 | 220 | [reverb](./effects/reverb.md) 221 | -------------------------------------------------------------------------------- /docs/effects/analyser.md: -------------------------------------------------------------------------------- 1 | # Analyser 2 | 3 | [View source code](../../src/effects/analyser.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/analyser.html) 6 | 7 | Get real-time frequency and waveform information. 8 | 9 | ```javascript 10 | import analyser from 'sono/effects/analyser'; 11 | 12 | const analyse = sono.effects.add(analyser({ 13 | useFloats: false, 14 | fftSize: 2048, 15 | smoothing: 0.9, 16 | maxDecibels: 0, 17 | minDecibels: 0 18 | })); 19 | 20 | function draw() { 21 | window.requestAnimationFrame(draw); 22 | 23 | const frequencies = analyse.getFrequencies(); 24 | 25 | for (let i = 0; i < frequencies.length; i++) { 26 | const magnitude = frequencies[i]; 27 | const normalised = magnitude / 256; 28 | // do something 29 | } 30 | 31 | const waveform = analyse.getWaveform(); 32 | 33 | for (let i = 0; i < waveform.length; i++) { 34 | const magnitude = waveform[i]; 35 | const normalised = magnitude / 256; 36 | // do something 37 | } 38 | 39 | analyse.getAmplitude(amplitude => { 40 | // returns normalised amplitude 41 | }); 42 | 43 | analyse.getPitch(pitch => { 44 | const note = pitch.note; // e.g. C# 45 | const hertz = pitch.hertz; // e.g. C# 46 | // do something 47 | }); 48 | } 49 | draw(); 50 | 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/effects/compressor.md: -------------------------------------------------------------------------------- 1 | # Compressor 2 | 3 | [View source code](../../src/effects/compressor.js) 4 | 5 | Compression lowers the volume of the loudest parts of the signal and raises the volume of the softest parts. 6 | 7 | ```javascript 8 | const compress = sono.effects.add(compressor()); 9 | // min decibels to start compressing at from -100 to 0 10 | compress.threshold = -24; 11 | // decibel value to start curve to compressed value from 0 to 40 12 | compress.knee = 30; 13 | // amount of change per decibel from 1 to 20 14 | compress.ratio = 12; 15 | // seconds to reduce gain by 10db from 0 to 1 - how quickly signal adapted when volume increased 16 | compress.attack = 0.0003; 17 | // seconds to increase gain by 10db from 0 to 1 - how quickly signal adapted when volume redcuced 18 | compress.release = 0.25; 19 | 20 | // update multiple properties: 21 | compress.update({ 22 | threshold = -24, 23 | knee: 30, 24 | ratio: 12, 25 | attack: 0.0003, 26 | release: 0.25 27 | }); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/effects/convolver.md: -------------------------------------------------------------------------------- 1 | # Convolver 2 | 3 | [View source code](../../src/effects/convolver.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/convolver.html) 6 | 7 | Creates a reverb effect by applying the characteristics of one sound to another. 8 | 9 | 10 | Check out [www.openairlib.net](http://www.openairlib.net/) for a large collection of Impulse Response files to experiment with. 11 | 12 | ## Pass URL of an impulse response 13 | 14 | ```javascript 15 | import sono from 'sono'; 16 | import convolver from 'sono/effects/convolver'; 17 | 18 | const sound = sono.create({ 19 | url: 'boom.mp3', 20 | effects: [convolver({ 21 | impulse: 'large_hall.mp3', 22 | wet: 0.5, 23 | dry: 0.9 24 | })] 25 | }); 26 | sound.play(); 27 | ``` 28 | 29 | ## Pass a sono sound, AudioBuffer or ArrayBuffer 30 | 31 | ```javascript 32 | import sono from 'sono'; 33 | import convolver from 'sono/effects/convolver'; 34 | 35 | const sound = sono.create('boom.mp3'); 36 | const impulse = sono.create('large_hall.mp3'); 37 | const reverb = sound.effects.add(convolver({impulse})); 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/effects/distortion.md: -------------------------------------------------------------------------------- 1 | # Distortion 2 | 3 | [View source code](../../src/effects/distortion.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/distortion.html) 6 | 7 | ```javascript 8 | import distortion from 'sono/effects/distortion'; 9 | 10 | const sound = sono.create('boom.mp3'); 11 | const distort = sound.effects.add(distortion({ 12 | level: 1, 13 | samples: 44100, 14 | oversample: '2x' 15 | })); 16 | 17 | // update the amount of distortion: 18 | distort.level = 0.8; 19 | 20 | // or 21 | distort.update({ 22 | level: 0.8 23 | }); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/effects/echo.md: -------------------------------------------------------------------------------- 1 | # Echo 2 | 3 | [View source code](../../src/effects/echo.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/echo.html) 6 | 7 | ```javascript 8 | import echo from 'sono/effects/echo'; 9 | 10 | const sound = sono.create('boom.mp3'); 11 | const echo = sound.effects.add(echo({ 12 | delay: 0.8, 13 | feedback: 0.5 14 | })); 15 | sound.play(); 16 | 17 | // update individual properties: 18 | echo.delay = 0.5; 19 | echo.feedback = 0.8; 20 | 21 | // update multiple properties: 22 | echo.update({ 23 | delay: 0.5, 24 | feedback: 0.8 25 | }); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/effects/filter.md: -------------------------------------------------------------------------------- 1 | # Filter 2 | 3 | [View source code](../../src/effects/filter.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/filter.html) 6 | 7 | ## Let only low frequencies pass 8 | 9 | ```javascript 10 | import filter from 'sono/effects/filter'; 11 | 12 | const sound = sono.create('boom.mp3'); 13 | 14 | const lowpass = sound.effects.add(filter({ 15 | type: 'lowpass', 16 | frequency: 400, 17 | peak: 10 // peak at cutoff frequency (400hz) 18 | })); 19 | ``` 20 | 21 | ## Import lowpass from named export 22 | 23 | ```javascript 24 | import {lowpass} from 'sono/effects/filter'; 25 | 26 | const sound = sono.create('boom.mp3'); 27 | 28 | sound.effects.add(lowpass({ 29 | frequency: 600, 30 | peak: 20 31 | })); 32 | ``` 33 | 34 | ## Let only high frequencies pass 35 | 36 | ```javascript 37 | import filter from 'sono/effects/filter'; 38 | 39 | const sound = sono.create('boom.mp3'); 40 | 41 | const lowpass = sound.effects.add(filter({ 42 | type: 'highpass', 43 | frequency: 800, 44 | peak: 20 // peak at cutoff frequency (800hz) 45 | })); 46 | ``` 47 | 48 | ## Other types 49 | 50 | ```javascript 51 | import 'sono/effects/filter'; 52 | 53 | const sound = sono.create('boom.mp3'); 54 | 55 | // 20db boost on frequencies below 800hz 56 | const lowshelf = sound.effects.add(sono.lowshelf({ 57 | frequency: 800, 58 | boost: 20 59 | })); 60 | 61 | // 20db boost on frequencies above 800hz 62 | const highshelf = sound.effects.add(sono.highshelf({ 63 | frequency: 800, 64 | boost: 20 65 | })); 66 | 67 | // 20db boost on frequencies around 800hz 68 | const peaking = sound.effects.add(sono.peaking({ 69 | frequency: 800, 70 | width: 200, 71 | boost: 20 72 | })); 73 | 74 | // let frequencies around 800hz pass 75 | const bandpass = sound.effects.add(sono.bandpass({ 76 | frequency: 800, 77 | width: 20 78 | })); 79 | 80 | // let frequencies outside 800hz pass 81 | const notch = sound.effects.add(sono.notch({ 82 | frequency: 800, 83 | width: 200 84 | })); 85 | 86 | // Shift phase 87 | const allpass = sound.effects.add(sono.allpass({ 88 | frequency: 800, 89 | sharpness: 200 90 | })); 91 | ``` 92 | 93 | For a detailed explanation of what each filter type does, see 94 | -------------------------------------------------------------------------------- /docs/effects/flanger.md: -------------------------------------------------------------------------------- 1 | # Flanger 2 | 3 | [View source code](../../src/effects/flanger.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/flanger.html) 6 | 7 | Creates a sweeping filter effect 8 | 9 | ```javascript 10 | import flanger from 'sono/effects/flanger'; 11 | 12 | const sound = sono.create('boom.mp3'); 13 | 14 | const flange = sound.effects.add(flanger({ 15 | stereo: true, 16 | delay: 0.005, 17 | feedback: 0.5, 18 | frequency: 0.025, 19 | gain: 0.002 20 | })); 21 | ``` 22 | 23 | ## Stereo flanger 24 | 25 | ```javascript 26 | import {stereoFlanger} from 'sono/effects/flanger'; 27 | 28 | const sound = sono.create('boom.mp3'); 29 | 30 | const flange = sound.effects.add(stereoFlanger({ 31 | delay: 0.005, 32 | feedback: 0.5, 33 | frequency: 0.025, 34 | gain: 0.002 35 | })); 36 | ``` 37 | 38 | ## Mono flanger 39 | 40 | ```javascript 41 | import {monoFlanger} from 'sono/effects/flanger'; 42 | 43 | const sound = sono.create('boom.mp3'); 44 | 45 | const flange = sound.effects.add(monoFlanger({ 46 | delay: 0.005, 47 | feedback: 0.5, 48 | frequency: 0.025, 49 | gain: 0.002 50 | })); 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/effects/panner.md: -------------------------------------------------------------------------------- 1 | # Panner 2 | 3 | [View source code](../../src/effects/panner.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/three.html) 6 | 7 | ## Pan left/right 8 | 9 | ```javascript 10 | import panner from 'sono/effects/panner'; 11 | 12 | const sound = sono.create('boom.mp3'); 13 | 14 | const pan = sound.effects.add(panner()); 15 | 16 | // pan fully right: 17 | pan.set(1); 18 | 19 | // pan fully left: 20 | pan.set(-1); 21 | ``` 22 | 23 | ## 3d panning 24 | 25 | Pass vectors of the 'listener' (i.e. the camera) and the origin of the sound to play the sound in 3d audio. 26 | 27 | The listener is global and doesn't need to be updated for every panner object. 28 | 29 | ```javascript 30 | import panner from 'sono/effects/panner'; 31 | 32 | const sound = sono.create('boom.mp3'); 33 | 34 | const pan = sound.effects.add(panner()); 35 | 36 | function update() { 37 | window.requestAnimationFrame(update); 38 | 39 | // update the 3d position and orientation (forward vector) of the sound 40 | pan.setPosition(x, y, z); 41 | pan.setOrientation(x, y, z); 42 | 43 | // update listener position and orientation to 3d camera Vectors 44 | pan.setListenerPosition(x, y, z); 45 | pan.setListenerOrientation(x, y, z); 46 | } 47 | ``` 48 | 49 | ## Pass xyz or 3d vector objects 50 | 51 | ```javascript 52 | // accepts xyz or a 3d vector object 53 | pan.setPosition(source.position); 54 | pan.setOrientation(source.forward); 55 | 56 | pan.setListenerPosition(camera.position); 57 | pan.setListenerOrientation(camera.forward); 58 | ``` 59 | 60 | ## Set global listener position and orientation using static methods 61 | 62 | ```javascript 63 | import panner from 'sono/effects/panner'; 64 | 65 | function update() { 66 | window.requestAnimationFrame(update); 67 | 68 | panner.setListenerPosition(x, y, z); 69 | panner.setListenerOrientation(x, y, z); 70 | } 71 | ``` 72 | 73 | ## Configure how distance and angle affect the sound 74 | 75 | ```javascript 76 | import panner from 'sono/effects/panner'; 77 | 78 | const sound = sono.create('boom.mp3'); 79 | 80 | const pan = sound.effects.add(panner({ 81 | panningModel: 'HRTF', 82 | distanceModel: 'linear', 83 | refDistance: 1, 84 | maxDistance: 1000, 85 | rolloffFactor: 1, 86 | coneInnerAngle: 360, 87 | coneOuterAngle: 0, 88 | coneOuterGain: 0 89 | })); 90 | ``` 91 | 92 | ## Set defaults for all panner nodes 93 | 94 | ```javascript 95 | import panner from 'sono/effects/panner'; 96 | 97 | panner.defaults = { 98 | panningModel: 'HRTF', 99 | distanceModel: 'linear', 100 | refDistance: 1, 101 | maxDistance: 1000, 102 | rolloffFactor: 1, 103 | coneInnerAngle: 360, 104 | coneOuterAngle: 0, 105 | coneOuterGain: 0 106 | }; 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/effects/phaser.md: -------------------------------------------------------------------------------- 1 | # Phaser 2 | 3 | [View source code](../../src/effects/phaser.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/phaser.html) 6 | 7 | Creates a sweeping filter effect 8 | 9 | ```javascript 10 | import phaser from 'sono/effects/phaser'; 11 | 12 | const sound = sono.create('boom.mp3'); 13 | 14 | const phaser = sound.effects.add(phaser({ 15 | stages: 8, 16 | frequency: 0.5, 17 | gain: 300, 18 | feedback: 0.5 19 | })); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/effects/reverb.md: -------------------------------------------------------------------------------- 1 | # Reverb 2 | 3 | [View source code](../../src/effects/reverb.js) 4 | 5 | [View example](http://stinkstudios.github.io/sono/examples/reverb.html) 6 | 7 | ```javascript 8 | import reverb from 'sono/effects/reverb'; 9 | 10 | const sound = sono.create('boom.mp3'); 11 | const room = sound.effects.add(reverb({time: 1, decay: 5})); 12 | sound.play(); 13 | 14 | // update multiple properties: 15 | room.update({ 16 | time: 0.5, 17 | decay: 3, 18 | reverse: true 19 | }); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Install 4 | 5 | ```javascript 6 | npm i -S sono 7 | ``` 8 | 9 | ## Import 10 | 11 | ```javascript 12 | import sono from 'sono'; 13 | ``` 14 | 15 | ## Create a sound object 16 | 17 | Use the returned reference: 18 | ```javascript 19 | const sound = sono.create('boom.mp3'); 20 | sound.play(); 21 | ``` 22 | 23 | Or use an Id: 24 | ```javascript 25 | sono.create({ 26 | id: 'boom', 27 | url: 'boom.mp3' 28 | }); 29 | sono.play('boom'); 30 | ``` 31 | 32 | ## Add some effects 33 | 34 | Set an array of effects: 35 | ```javascript 36 | import echo from 'sono/effects/echo'; 37 | import reverb from 'sono/effects/reverb'; 38 | 39 | const sound = sono.create('boom.mp3'); 40 | sound.effects = [echo(), reverb()]; 41 | sound.play(); 42 | ``` 43 | 44 | Or use the `add` function to return the reference: 45 | ```javascript 46 | import echo from 'sono/effects/echo'; 47 | import reverb from 'sono/effects/reverb'; 48 | 49 | const sound = sono.create('boom.mp3'); 50 | const echo = sound.effects.add(echo()); 51 | const reverb = sound.effects.add(reverb()); 52 | sound.play(); 53 | ``` 54 | 55 | ## Log info on browser support 56 | 57 | ```javascript 58 | import sono from 'sono'; 59 | sono.log(); // sono 0.2.0 Supported:true WebAudioAPI:true TouchLocked:false Extensions:ogg,mp3,opus,wav,m4a 60 | ``` 61 | 62 | ## Further documentation 63 | 64 | [Sounds](./sounds.md) 65 | 66 | [Effects](./effects.md) 67 | 68 | [Controls](./controls.md) 69 | 70 | [Utils](./utils.md) 71 | -------------------------------------------------------------------------------- /docs/loading.md: -------------------------------------------------------------------------------- 1 | # Loading 2 | 3 | ## Load multiple with config and callbacks 4 | 5 | ```javascript 6 | sono.load({ 7 | url: [{ 8 | id: 'foo', 9 | url: 'foo.mp3' 10 | }, { 11 | id: 'bar', 12 | url: ['bar.ogg', 'bar.mp3'], 13 | loop: true, 14 | volume: 0.5 15 | }], 16 | onComplete: sounds => console.log(sounds), 17 | onProgress: progress => console.log(progress) 18 | }); 19 | 20 | const foo = sono.get('foo'); 21 | sono.play('bar'); 22 | ``` 23 | 24 | ## Load single with config options and callbacks 25 | 26 | ```javascript 27 | const sound = sono.load({ 28 | id: 'foo', 29 | src: ['foo.ogg', 'foo.mp3'], 30 | loop: true, 31 | volume: 0.2, 32 | onComplete: sound => console.log(sound), 33 | onProgress: progress => console.log(progress) 34 | }); 35 | ``` 36 | 37 | ## Load single 38 | 39 | ```javascript 40 | sono.load({ 41 | url: 'foo.mp3', 42 | onComplete: sound => console.log(sound), 43 | onProgress: progress => console.log(progress) 44 | }); 45 | ``` 46 | 47 | ## Load multiple 48 | 49 | ```javascript 50 | sono.load({ 51 | url: [ 52 | {url: 'foo.mp3'}, 53 | {url: 'bar.mp3'} 54 | ], 55 | onComplete: sounds => console.log(sounds), 56 | onProgress: progress => console.log(progress) 57 | }); 58 | ``` 59 | 60 | ## Check support 61 | 62 | ```javascript 63 | sono.isSupported; 64 | sono.hasWebAudio; 65 | 66 | sono.canPlay.ogg; 67 | sono.canPlay.mp3; 68 | sono.canPlay.opus 69 | sono.canPlay.wav; 70 | sono.canPlay.m4a; 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/sounds.md: -------------------------------------------------------------------------------- 1 | # Sound 2 | 3 | [View source code](../src/core/sound.js) 4 | 5 | ## Create 6 | 7 | ```javascript 8 | const sound = sono.create('boom.mp3'); 9 | ``` 10 | 11 | ## Create and configure 12 | 13 | ```javascript 14 | const sound = sono.create({ 15 | id: 'boom', 16 | url: ['boom.ogg', 'boom.mp3'], 17 | loop: true, 18 | volume: 0.5, 19 | effects: [ 20 | sono.echo() 21 | ] 22 | }); 23 | ``` 24 | 25 | ## Create an oscillator 26 | 27 | ```javascript 28 | const squareWave = sono.create('square'); 29 | squareWave.frequency = 200; 30 | ``` 31 | 32 | ## Create from HTMLMediaElement 33 | 34 | ```javascript 35 | const video = document.querySelector('video'); 36 | const videoSound = sono.create(videoEl); 37 | ``` 38 | 39 | ## Control and update 40 | 41 | ```javascript 42 | const sound = sono.create('boom.mp3'); 43 | // playback 44 | sound.play(); 45 | sound.pause(); 46 | sound.stop(); 47 | // play with 200ms delay 48 | sound.play(0.2); 49 | // set volume 50 | sound.volume = 0.5; 51 | // fade out volume to 0 over 2 seconds 52 | sound.fade(0, 2); 53 | // seek to 0.5 seconds 54 | sound.seek(0.5); 55 | // play sound at double speed 56 | sound.playbackRate = 2; 57 | // play sound at half speed 58 | sound.playbackRate = 0.5; 59 | // loop 60 | sound.loop = true; 61 | // play at 3s 62 | sound.currentTime = 3; 63 | ``` 64 | 65 | ## Get properties 66 | 67 | ```javascript 68 | const sound = sono.create('boom.mp3'); 69 | console.log(sound.context); 70 | console.log(sound.currentTime); 71 | console.log(sound.duration); 72 | console.log(sound.effects); 73 | console.log(sound.ended); 74 | console.log(sound.loop); 75 | console.log(sound.paused); 76 | console.log(sound.playing); 77 | console.log(sound.progress); 78 | console.log(sound.volume); 79 | console.log(sound.playbackRate); 80 | ``` 81 | 82 | ## Special properties 83 | 84 | ```javascript 85 | // raw sound data (AudioBuffer, MediaElement, MediaStream, Oscillator type) 86 | console.log(sound.data); 87 | // frequency for Oscillator source type 88 | console.log(sound.frequency); 89 | // output node (GainNode) 90 | console.log(sound.gain); 91 | ``` 92 | 93 | ## Methods can be chained 94 | 95 | ```javascript 96 | const sound = sono.create({ 97 | id: 'boom', 98 | url: 'boom.ogg', 99 | volume: 0 100 | }) 101 | .on('ended', sound => dispatch('ended', sound.id)) 102 | .play() 103 | .fade(1, 2) 104 | ``` 105 | 106 | ## Add and remove event listeners 107 | 108 | ```javascript 109 | sono.create('boom.ogg') 110 | .on('pause', sound => dispatch('pause', sound)) 111 | .on('play', sound => dispatch('play', sound)) 112 | .once('ended', sound => { 113 | sound.off('pause'); 114 | sound.off('play'); 115 | dispatch('ended', sound); 116 | }) 117 | .play(); 118 | ``` 119 | 120 | ## Methods 121 | 122 | ```javascript 123 | sound.play(delay, offset) 124 | sound.pause() 125 | sound.stop() 126 | sound.seek(time) 127 | sound.fade(volume, duration) 128 | sound.unload() 129 | sound.reload() 130 | sound.destroy() 131 | sound.waveform(length) 132 | ``` 133 | 134 | ## Properties 135 | 136 | ```javascript 137 | sound.context 138 | sound.currentTime 139 | sound.data 140 | sound.duration 141 | sound.effects 142 | sound.ended 143 | sound.frequency 144 | sound.gain 145 | sound.loop 146 | sound.paused 147 | sound.playbackRate 148 | sound.playing 149 | sound.progress 150 | sound.volume 151 | ``` 152 | 153 | ## Events 154 | 155 | ```javascript 156 | sound 157 | .on('loaded', (sound) => console.log('loaded')) 158 | .on('ready', (sound) => console.log('ready')) 159 | .on('play', (sound) => console.log('play')) 160 | .on('pause', (sound) => console.log('pause')) 161 | .on('stop', (sound) => console.log('stop')) 162 | .on('fade', (sound, volume) => console.log('fade')) 163 | .on('ended', (sound) => console.log('ended')) 164 | .on('unload', (sound) => console.log('unload')) 165 | .on('error', (sound, err) => console.error('error')) 166 | .on('destroy', (sound) => console.log('destroy')); 167 | ``` 168 | -------------------------------------------------------------------------------- /docs/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | ## Clone an AudioBuffer 4 | 5 | ```javascript 6 | const cloned = sono.utils.cloneBuffer(sound.data); 7 | ``` 8 | 9 | ## Reverse an AudioBuffer 10 | 11 | ```javascript 12 | const reversed = sono.utils.reverseBuffer(sound.data); 13 | ``` 14 | 15 | ## Convert currentTime seconds into time code string 16 | 17 | ```javascript 18 | const timeCode = sono.utils.timeCode(217.8); // '03:37' 19 | ``` 20 | 21 | ## Extra utils 22 | 23 | ```javascript 24 | import 'sono/utils'; 25 | ``` 26 | 27 | [microphone](./utils/microphone.md) 28 | 29 | [recorder](./utils/recorder.md) 30 | 31 | [waveform](./utils/waveform.md) 32 | 33 | [waveformer](./utils/waveformer.md) 34 | -------------------------------------------------------------------------------- /docs/utils/microphone.md: -------------------------------------------------------------------------------- 1 | # Microphone 2 | 3 | [View source code](../../src/utils/microphone.js) 4 | 5 | ```javascript 6 | import microphone from 'sono/utils'; 7 | import analyser from 'sono/effects'; 8 | 9 | const mic = microphone(stream => { 10 | // user allowed mic 11 | const sound = sono.create(stream); 12 | const analyse = sound.effects.add(analyser()); 13 | }, err => { 14 | // user denied mic 15 | }, err => { 16 | // error 17 | }); 18 | mic.connect(); 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/utils/recorder.md: -------------------------------------------------------------------------------- 1 | # Recorder 2 | 3 | [View source code](../../src/utils/recorder.js) 4 | 5 | ## Record audio from the mix or microphone to a new audio buffer 6 | 7 | ```javascript 8 | import recorder from 'sono/utils/recorder'; 9 | 10 | const record = recorder() 11 | 12 | record.start(sound) 13 | 14 | record.getDuration(); 15 | 16 | const buffer = record.stop(); 17 | ``` 18 | 19 | ## Record a microphone stream 20 | 21 | ```javascript 22 | import 'sono/utils/recorder'; 23 | import 'sono/utils/microphone'; 24 | 25 | let micSound; 26 | let recorder; 27 | 28 | function onMicConnected(stream) { 29 | micSound = sono.create(stream); 30 | // add recorder, setting passThrough to false 31 | // to avoid feedback loop between mic and speakers 32 | recorder = sono.utils.recorder(false); 33 | recorder.start(micSound); 34 | }; 35 | 36 | stopButton.addEventListener('click', function() { 37 | const buffer = recorder.stop(); 38 | const recordedSound = sono.create(buffer); 39 | recordedSound.play(); 40 | }); 41 | 42 | const mic = sono.utils.microphone(onMicConnected); 43 | mic.connect(); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/utils/waveform.md: -------------------------------------------------------------------------------- 1 | # Waveform 2 | 3 | [View source code](../../src/utils/waveform.js) 4 | 5 | ```javascript 6 | import sono from 'sono'; 7 | import 'sono/utils/waveform'; 8 | 9 | const sound = sono.create('boom.mp3'); 10 | sound.on('ready', () => { 11 | // request sound waveform 12 | const waveform = sound.waveform(640); 13 | 14 | // draw waveform 15 | for (let i = 0; i < waveform.length; i++) { 16 | const value = waveform[i]; 17 | } 18 | }); 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/utils/waveformer.md: -------------------------------------------------------------------------------- 1 | # Waveformer 2 | 3 | [View source code](../../src/utils/waveformer.js) 4 | 5 | ## Get a sound's waveform and draw it to a canvas element: 6 | 7 | ```javascript 8 | const wave = sono.utils.waveformer({ 9 | sound: sound, 10 | width: 200, 11 | height: 100, 12 | color: '#333333', 13 | bgColor: '#DDDDDD' 14 | }); 15 | document.body.appendChild(wave.canvas); 16 | ``` 17 | 18 | ## Supply your own canvas el 19 | 20 | ```javascript 21 | const canvasEl = document.querySelector('canvas'); 22 | const wave = sono.utils.waveformer({ 23 | waveform: sound.waveform(canvasEl.width), 24 | canvas: canvasEl, 25 | color: 'green' 26 | }); 27 | ``` 28 | 29 | ## Color can be a function 30 | 31 | ```javascript 32 | const waveformer = sono.utils.waveformer({ 33 | waveform: sound.waveform(canvasEl.width), 34 | canvas: canvasEl, 35 | color: (position, length) => { 36 | return position / length < sound.progress ? 'red' : 'yellow'; 37 | } 38 | }); 39 | ``` 40 | 41 | ## Shape can be circular 42 | 43 | ```javascript 44 | const waveformer = sono.utils.waveformer({ 45 | shape: 'circular', 46 | sound: sound, 47 | canvas: canvasEl, 48 | color: 'black' 49 | }); 50 | ``` 51 | 52 | ## Draw the output of an AnalyserNode to a canvas 53 | 54 | ```javascript 55 | const sound = sono.create('foo.ogg'); 56 | const analyser = sound.effects.add(sono.analyser({ 57 | fftSize: 512, 58 | smoothing: 0.7 59 | })); 60 | 61 | const waveformer = sono.utils.waveformer({ 62 | waveform: analyser.getFrequencies(), 63 | canvas: document.querySelector('canvas'), 64 | color: (position, length) => { 65 | const hue = (position / length) * 360; 66 | return `hsl(${hue}, 100%, 40%)`; 67 | }, 68 | // normalise the value from the analyser 69 | transform: value => value / 256 70 | }); 71 | 72 | // update the waveform 73 | function update() { 74 | window.requestAnimationFrame(update); 75 | // request frequencies from the analyser 76 | analyser.getFrequencies(); 77 | // update the waveformer display 78 | waveformer(); 79 | } 80 | update(); 81 | ``` 82 | -------------------------------------------------------------------------------- /examples/analyser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - 3d 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 |

Analyser

21 |
22 | 23 |

24 |     import sono from 'sono';
25 |     import 'sono/effects';
26 | 
27 |     const sound = sono.create({
28 |         url: ['beats.ogg', 'beats.mp3'],
29 |         loop: true
30 |     })
31 |     .play();
32 | 
33 |     const analyser = sound.effects.add(sono.analyser({
34 |         fftSize: 128
35 |     }));
36 | 
37 |     function averageAmplitude(wave) {
38 |         let sum = 0;
39 |         for (let i = 0; i < wave.length; i++) {
40 |             sum += wave[i];
41 |         }
42 |         return sum / wave.length / 256;
43 |     }
44 | 
45 |     function update() {
46 |         window.requestAnimationFrame(update);
47 | 
48 |         const value = averageAmplitude(analyser.getWaveform());
49 | 
50 |         if (value < min) {
51 |             min = value;
52 |         }
53 | 
54 |         if (value > max) {
55 |             max = value;
56 |         }
57 | 
58 |         const range = (max - min) || 1;
59 |         const norm = (value - min) / range;
60 | 
61 |         if (norm > threshold) {
62 |             // got a peak so animate, speed up, glow etc
63 |         }
64 |     }
65 |     update();
66 |         
67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/audio/.gitinclude: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/.gitinclude -------------------------------------------------------------------------------- /examples/audio/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Header add Access-Control-Allow-Origin "*" 3 | Header add Access-Control-Allow-Methods: "GET,POST,OPTIONS,DELETE,PUT" 4 | Header add Access-Control-Allow-Headers: "Content-Type" 5 | 6 | RewriteEngine on 7 | RewriteBase / 8 | -------------------------------------------------------------------------------- /examples/audio/bullet.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/bullet.mp3 -------------------------------------------------------------------------------- /examples/audio/bullet.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/bullet.ogg -------------------------------------------------------------------------------- /examples/audio/collect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/collect.mp3 -------------------------------------------------------------------------------- /examples/audio/collect.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/collect.ogg -------------------------------------------------------------------------------- /examples/audio/dnb-loop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/dnb-loop.mp3 -------------------------------------------------------------------------------- /examples/audio/dnb-loop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/dnb-loop.ogg -------------------------------------------------------------------------------- /examples/audio/hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/hit.mp3 -------------------------------------------------------------------------------- /examples/audio/hit.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/hit.ogg -------------------------------------------------------------------------------- /examples/audio/select.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/select.mp3 -------------------------------------------------------------------------------- /examples/audio/select.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/audio/select.ogg -------------------------------------------------------------------------------- /examples/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - play in background 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

play in background

17 |
18 | 19 |
20 |
21 |
22 |
23 | 24 |

25 |     import sono from 'sono';
26 | 
27 |     sono.playInBackground = true;
28 | 
29 |     sono.create({url: 'dnb-loop.ogg', loop: true}).play();
30 |         
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/css/index.css: -------------------------------------------------------------------------------- 1 | @import "normalize.css"; 2 | @import "sections.css"; 3 | @import "player.css"; 4 | @import "control.css"; 5 | -------------------------------------------------------------------------------- /examples/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | html { 4 | -ms-text-size-adjust: 100%; 5 | -webkit-text-size-adjust: 100%; 6 | } 7 | 8 | article, 9 | aside, 10 | details, 11 | figcaption, 12 | figure, 13 | footer, 14 | header, 15 | hgroup, 16 | main, 17 | menu, 18 | nav, 19 | section, 20 | summary { 21 | display: block; 22 | } 23 | 24 | audio, 25 | canvas, 26 | progress, 27 | video { 28 | display: inline-block; 29 | vertical-align: baseline; 30 | } 31 | 32 | audio:not([controls]) { 33 | display: none; 34 | height: 0; 35 | } 36 | 37 | [hidden], 38 | template { 39 | display: none; 40 | } 41 | 42 | a { 43 | background-color: transparent; 44 | } 45 | 46 | a:active, 47 | a:hover { 48 | outline: 0; 49 | } 50 | 51 | abbr[title] { 52 | border-bottom: 1px dotted; 53 | } 54 | 55 | b, 56 | strong { 57 | font-weight: 700; 58 | } 59 | 60 | dfn { 61 | font-style: italic; 62 | } 63 | 64 | h1 { 65 | font-size: 2em; 66 | } 67 | 68 | mark { 69 | background: #ff0; 70 | color: #000; 71 | } 72 | 73 | small { 74 | font-size: 80%; 75 | } 76 | 77 | sub, 78 | sup { 79 | font-size: 75%; 80 | line-height: 0; 81 | position: relative; 82 | vertical-align: baseline; 83 | } 84 | 85 | sup { 86 | top: -.5em; 87 | } 88 | 89 | sub { 90 | bottom: -.25em; 91 | } 92 | 93 | img { 94 | border: 0; 95 | } 96 | 97 | svg:not(:root) { 98 | overflow: hidden; 99 | } 100 | 101 | hr { 102 | -moz-box-sizing: content-box; 103 | box-sizing: content-box; 104 | height: 0; 105 | } 106 | 107 | pre { 108 | overflow: auto; 109 | } 110 | 111 | code, 112 | kbd, 113 | pre, 114 | samp { 115 | font-family: monospace,monospace; 116 | font-size: 1em; 117 | } 118 | 119 | button, 120 | input, 121 | optgroup, 122 | select, 123 | textarea { 124 | color: inherit; 125 | font: inherit; 126 | margin: 0; 127 | } 128 | 129 | button { 130 | overflow: visible; 131 | } 132 | 133 | button, 134 | select { 135 | text-transform: none; 136 | } 137 | 138 | button, 139 | html input[type=button], 140 | input[type=reset], 141 | input[type=submit] { 142 | -webkit-appearance: button; 143 | cursor: pointer; 144 | } 145 | 146 | button[disabled], 147 | html input[disabled] { 148 | cursor: default; 149 | } 150 | 151 | button::-moz-focus-inner, 152 | input::-moz-focus-inner { 153 | border: 0; 154 | padding: 0; 155 | } 156 | 157 | input { 158 | line-height: normal; 159 | } 160 | 161 | input[type=checkbox], 162 | input[type=radio] { 163 | -moz-box-sizing: border-box; 164 | box-sizing: border-box; 165 | padding: 0; 166 | } 167 | 168 | input[type=number]::-webkit-inner-spin-button, 169 | input[type=number]::-webkit-outer-spin-button { 170 | height: auto; 171 | } 172 | 173 | input[type=search] { 174 | -webkit-appearance: textfield; 175 | -moz-box-sizing: content-box; 176 | box-sizing: content-box; 177 | } 178 | 179 | input[type=search]::-webkit-search-cancel-button, 180 | input[type=search]::-webkit-search-decoration { 181 | -webkit-appearance: none; 182 | } 183 | 184 | legend { 185 | border: 0; 186 | padding: 0; 187 | } 188 | 189 | textarea { 190 | overflow: auto; 191 | } 192 | 193 | optgroup { 194 | font-weight: 700; 195 | } 196 | 197 | table { 198 | border-collapse: collapse; 199 | border-spacing: 0; 200 | } 201 | 202 | td, 203 | th { 204 | padding: 0; 205 | } 206 | 207 | 208 | 209 | html { 210 | background: inherit; 211 | color: inherit; 212 | } 213 | 214 | a { 215 | color: #069; 216 | text-decoration: none; 217 | } 218 | 219 | a:active, 220 | a:focus, 221 | a:hover { 222 | color: #069; 223 | text-decoration: underline; 224 | } 225 | 226 | blockquote, 227 | dd, 228 | dl, 229 | figure, 230 | h1, 231 | h2, 232 | h3, 233 | h4, 234 | h5, 235 | h6, 236 | p, 237 | pre { 238 | margin: 0; 239 | } 240 | 241 | button { 242 | background: 0 0; 243 | border: 0; 244 | padding: 0; 245 | } 246 | 247 | button:focus { 248 | outline: dotted 1px; 249 | outline: -webkit-focus-ring-color auto 5px; 250 | } 251 | 252 | fieldset { 253 | border: 0; 254 | margin: 0; 255 | padding: 0; 256 | } 257 | 258 | iframe { 259 | border: 0; 260 | } 261 | 262 | ol, 263 | ul { 264 | list-style: none; 265 | margin: 0; 266 | padding: 0; 267 | } 268 | 269 | [tabindex="-1"]:focus { 270 | outline: 0!important; 271 | } 272 | -------------------------------------------------------------------------------- /examples/css/player.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Player 3 | */ 4 | 5 | :root { 6 | --player-height: 760px; 7 | --player-width: 760px; 8 | --radius: 180px; 9 | --diameter: calc(var(--radius) * 2); 10 | --margin: calc((var(--player-width) - var(--diameter)) / 2); 11 | } 12 | 13 | .Player { 14 | height: var(--player-height); 15 | width: var(--player-width); 16 | position: relative; 17 | margin: 0 auto; 18 | z-index: 1; 19 | } 20 | 21 | @media (--narrow) { 22 | .Player { 23 | height: calc(var(--player-height) * 0.6); 24 | width: calc(var(--player-width) * 0.6); 25 | transform: scale(0.6) translate(-40%, -40%); 26 | } 27 | } 28 | 29 | @media (--mobile) { 30 | .Player { 31 | height: calc(var(--player-height) * 0.4); 32 | width: calc(var(--player-width) * 0.4); 33 | transform: scale(0.4) translate(-80%, -80%); 34 | } 35 | } 36 | 37 | .Player-inner { 38 | border-radius: 50%; 39 | box-shadow: inset 1px 1px 8px #aaa; 40 | height: var(--diameter); 41 | position: relative; 42 | width: var(--diameter); 43 | top: var(--margin); 44 | left: var(--margin); 45 | } 46 | 47 | .Player-canvas { 48 | left: 0; 49 | position: absolute; 50 | top: 0; 51 | z-index: 0; 52 | } 53 | 54 | .Player-control { 55 | background: #fff; 56 | border-radius: 50%; 57 | height: calc(100% - 10px); 58 | left: 5px; 59 | position: absolute; 60 | top: 5px; 61 | width: calc(100% - 10px); 62 | z-index: 1; 63 | } 64 | 65 | .Player-play { 66 | width: 0; 67 | height: 0; 68 | border-top: 60px solid transparent; 69 | border-bottom: 60px solid transparent; 70 | border-left: 80px solid var(--color-main); 71 | position: absolute; 72 | left: 50%; 73 | top: 50%; 74 | transform: translate(-40%, -50%); 75 | } 76 | 77 | .Player-pause { 78 | border-left: 36px solid var(--color-main); 79 | border-right: 36px solid var(--color-main); 80 | display: none; 81 | height: 100px; 82 | left: 50%; 83 | position: absolute; 84 | top: 50%; 85 | transform: translate(-50%, -50%); 86 | width: 90px; 87 | } 88 | 89 | .Player.is-playing .Player-play { 90 | display: none; 91 | } 92 | 93 | .Player.is-playing .Player-pause { 94 | display: block; 95 | } 96 | 97 | .Player-mask { 98 | height: 100%; 99 | position: absolute; 100 | width: 50%; 101 | overflow: hidden; 102 | } 103 | 104 | .Player-maskA { 105 | left: 50%; 106 | } 107 | 108 | .Player-half { 109 | background-color: var(--color-dark); 110 | height: 100%; 111 | position: absolute; 112 | width: 100%; 113 | } 114 | 115 | .Player-halfA { 116 | border-radius: 100% / 50%; 117 | border-top-right-radius: 0; 118 | border-bottom-right-radius: 0; 119 | left: -100%; 120 | transform-origin: right center; 121 | transform: rotate(0deg); 122 | } 123 | 124 | .Player-halfB { 125 | border-radius: 100% / 50%; 126 | border-top-left-radius: 0; 127 | border-bottom-left-radius: 0; 128 | left: 100%; 129 | transform-origin: left center; 130 | transform: rotate(0deg); 131 | } 132 | 133 | 134 | /* 135 | * player top 136 | */ 137 | 138 | .PlayerTop { 139 | background-color: #e7e9db; 140 | border-bottom: 1px solid white; 141 | display: flex; 142 | height: 60px; 143 | left: 0; 144 | position: fixed; 145 | top: 0; 146 | transform: translateY(-100%); 147 | transition: transform 0.3s ease-out; 148 | width: 100%; 149 | z-index: 2; 150 | } 151 | 152 | .PlayerTop.is-active { 153 | transform: translateY(0); 154 | } 155 | 156 | .PlayerTop-control { 157 | flex: none; 158 | height: 100%; 159 | position: relative; 160 | width: 80px; 161 | } 162 | 163 | .PlayerTop-canvas { 164 | width: 100%; 165 | height: 100%; 166 | } 167 | 168 | .PlayerTop-play { 169 | width: 0; 170 | height: 0; 171 | border-top: 20px solid transparent; 172 | border-bottom: 20px solid transparent; 173 | border-left: 30px solid var(--color-main); 174 | position: absolute; 175 | left: 50%; 176 | top: 50%; 177 | transform: translate(-40%, -50%); 178 | } 179 | 180 | .PlayerTop-pause { 181 | display: none; 182 | border-left: 12px solid var(--color-main); 183 | border-right: 12px solid var(--color-main); 184 | height: 36px; 185 | left: 50%; 186 | position: absolute; 187 | top: 50%; 188 | transform: translate(-50%, -50%); 189 | width: 30px; 190 | } 191 | 192 | .PlayerTop.is-playing .PlayerTop-play { 193 | display: none; 194 | } 195 | 196 | .PlayerTop.is-playing .PlayerTop-pause { 197 | display: block; 198 | } 199 | -------------------------------------------------------------------------------- /examples/css/sections.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-main: #bbcccc; 3 | --color-dark: #aabbbb; 4 | } 5 | 6 | @custom-media --narrow (max-width: 760px); 7 | @custom-media --mobile (max-width: 400px); 8 | 9 | html { 10 | height: 100%; 11 | } 12 | 13 | body { 14 | background-color: #FFF; 15 | color: #000; 16 | font-family: Helvetica, sans-serif; 17 | font-size: 16px; 18 | height: 100%; 19 | letter-spacing: 0.05em; 20 | line-height: 1.5; 21 | margin: 0; 22 | width: 100%; 23 | } 24 | 25 | main { 26 | overflow-x: hidden; 27 | width: 100%; 28 | } 29 | 30 | h1, 31 | h2, 32 | h3 { 33 | color: #303030; 34 | font-weight: 400; 35 | margin: 0.5em 0; 36 | } 37 | 38 | h1 { 39 | font-size: 48px; 40 | line-height: 1; 41 | margin: 0.4em 0 0.2em; 42 | } 43 | 44 | h2 { 45 | font-size: 32px; 46 | } 47 | 48 | h3 { 49 | 50 | } 51 | 52 | header { 53 | text-align: center; 54 | width: 100%; 55 | margin: 40px 0 20px; 56 | } 57 | 58 | section { 59 | 60 | } 61 | 62 | pre { 63 | margin: 50px 0; 64 | width: 100%; 65 | } 66 | 67 | @media (--mobile) { 68 | pre { 69 | margin: 20px 0; 70 | } 71 | } 72 | 73 | code { 74 | border-radius: 8px; 75 | margin: 0 auto; 76 | max-width: 800px; 77 | min-width: 320px; 78 | width: 80%; 79 | } 80 | 81 | code ul { 82 | margin-left: 3.5em; 83 | } 84 | 85 | @media (--mobile) { 86 | code { 87 | font-size: 60%; 88 | } 89 | } 90 | 91 | nav { 92 | padding: 0 0 50px; 93 | text-align: center; 94 | } 95 | 96 | nav a { 97 | text-decoration: underline; 98 | font-size: 32px; 99 | } 100 | 101 | * { 102 | box-sizing: border-box; 103 | } 104 | 105 | .dg select { 106 | color: black; 107 | } 108 | 109 | header nav { 110 | padding: 5px; 111 | position: absolute; 112 | right: 0; 113 | top: 0; 114 | } 115 | 116 | header nav ul { 117 | display: flex; 118 | } 119 | 120 | header nav a { 121 | font-size: 20px; 122 | margin: 0 10px; 123 | } 124 | 125 | @media (--mobile) { 126 | header nav { 127 | width: 100%; 128 | } 129 | 130 | header nav ul { 131 | justify-content: center; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /examples/distortion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - distortion 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

distortion

17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |

 27 |     import sono from 'sono';
 28 |     import 'sono/effects';
 29 | 
 30 |     const sound = sono.create({
 31 |         url: 'hit.mp3',
 32 |         loop: true,
 33 |         effects: [sono.distortion({
 34 |             level: 0.5,
 35 |             dry: 1,
 36 |             wet: 1
 37 |         })]
 38 |     });
 39 |         
40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /examples/echo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - echo 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

echo

17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |

 27 |     import sono from 'sono';
 28 |     import 'sono/effects';
 29 | 
 30 |     const sound = sono.create({
 31 |         url: 'hit.mp3',
 32 |         loop: true,
 33 |         effects: [sono.echo({
 34 |             delay: 0.4,
 35 |             feedback: 0.6,
 36 |             dry: 1,
 37 |             wet: 1
 38 |         })]
 39 |     });
 40 |         
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /examples/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/favicon.ico -------------------------------------------------------------------------------- /examples/img/.gitinclude: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/.gitinclude -------------------------------------------------------------------------------- /examples/img/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/back.png -------------------------------------------------------------------------------- /examples/img/bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/bottom.png -------------------------------------------------------------------------------- /examples/img/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/front.png -------------------------------------------------------------------------------- /examples/img/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/left.png -------------------------------------------------------------------------------- /examples/img/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/right.png -------------------------------------------------------------------------------- /examples/img/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/img/top.png -------------------------------------------------------------------------------- /examples/js/base-url.js: -------------------------------------------------------------------------------- 1 | window.isLocalHost = /^(?:https?:\/\/)?(?:localhost|192\.168)/.test(window.location.href); 2 | window.baseURL = window.isLocalHost ? '/examples/audio/' : 'https://ianmcgregor.co/prototypes/audio/'; 3 | -------------------------------------------------------------------------------- /examples/js/disable.js: -------------------------------------------------------------------------------- 1 | if (window.location.search.slice(1) === 'nowebaudio') { 2 | window.AudioContext = window.webkitAudioContext = undefined; 3 | } 4 | -------------------------------------------------------------------------------- /examples/js/recorder.js: -------------------------------------------------------------------------------- 1 | /* eslint no-var: 0 */ 2 | /* eslint strict: 0 */ 3 | 4 | (function() { 5 | 6 | var sono = window.sono; 7 | var ui = window.ui; 8 | 9 | sono.log(); 10 | 11 | var player, 12 | sound, 13 | recorder, 14 | analyser, 15 | canvas = document.querySelector('[data-waveform]'), 16 | context = canvas.getContext('2d'); 17 | 18 | function onConnect(stream) { 19 | sound = sono.create(stream); 20 | recorder = sono.utils.recorder(false); 21 | analyser = sound.effects.add(sono.analyser({fftSize: 1024})); 22 | analyser.maxDecibels = -60; 23 | recorder.start(sound); 24 | update(); 25 | } 26 | 27 | var mic = sono.utils.microphone(onConnect); 28 | 29 | if (!mic.isSupported) { 30 | document.querySelector('[data-warning]') 31 | .classList.add('is-visible'); 32 | } 33 | 34 | function toggle() { 35 | if (recorder && recorder.isRecording) { 36 | var recording = recorder.stop(); 37 | console.log(recording); 38 | createPlayer(recording); 39 | mic.disconnect(); 40 | } else { 41 | if (mic.stream) { 42 | // recorder.start(sound); 43 | } else { 44 | mic.connect(); 45 | } 46 | if (player) { 47 | player.destroy(); 48 | player.el.classList.remove('is-active'); 49 | } 50 | } 51 | } 52 | 53 | var control = ui.createToggle({ 54 | el: document.querySelector('[data-mic-toggle]'), 55 | name: 'Record', 56 | value: false 57 | }, function() { 58 | toggle(); 59 | }); 60 | 61 | function createPlayer(buffer) { 62 | console.log('createPlayer'); 63 | player = ui.createPlayer({ 64 | el: document.querySelector('[data-player-top]'), 65 | sound: sono.create(buffer) 66 | .play() 67 | }); 68 | player.el.classList.add('is-active'); 69 | } 70 | 71 | function update() { 72 | window.requestAnimationFrame(update); 73 | 74 | if (player) { 75 | player(); 76 | } 77 | 78 | control.setLabel(recorder.getDuration() 79 | .toFixed(1)); 80 | 81 | var width = canvas.width, 82 | height = canvas.height, 83 | frequencyBinCount = analyser.frequencyBinCount, 84 | barWidth = Math.max(1, Math.round(width / frequencyBinCount)), 85 | magnitude, 86 | percent, 87 | hue; 88 | 89 | context.fillStyle = '#ffffff'; 90 | context.fillRect(0, 0, width, height); 91 | 92 | var waveData = analyser.getFrequencies(); 93 | var freqData = analyser.getWaveform(); 94 | 95 | for (var i = 0; i < frequencyBinCount; i++) { 96 | magnitude = freqData[i]; 97 | percent = magnitude / 256; 98 | hue = i / frequencyBinCount * 360; 99 | context.fillStyle = 'hsl(' + hue + ', 100%, 30%)'; 100 | context.fillRect(barWidth * i, height, barWidth, 0 - height * percent); 101 | 102 | magnitude = waveData[i]; 103 | percent = magnitude / 512; 104 | hue = i / frequencyBinCount * 360; 105 | context.fillStyle = 'hsl(' + hue + ', 100%, 50%)'; 106 | context.fillRect(barWidth * i, height - height * percent - 1, 2, 2); 107 | } 108 | } 109 | 110 | }()); 111 | -------------------------------------------------------------------------------- /examples/multi-play.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - multiPlay 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

multi play

17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |

28 | sono.create({
29 |     id: 'sound',
30 |     url: 'hit.ogg',
31 |     multiPlay: true
32 | });
33 |     
34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /examples/oscillator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - oscillator 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

oscillator

18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |

 27 |       const sineWave = sono.create('sine');
 28 |       sineWave.frequency = 100;
 29 |       sineWave.volume = 0.1;
 30 |       sineWave.play();
 31 |     
32 | 33 |
34 | 35 | 36 | 37 | 38 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /examples/pixi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - PixiJS / Multi play 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

PixiJS

19 |
20 | 21 |
22 |

Arrow keys to move around, space to shoot.

23 |
24 | 25 |
26 |
27 | 28 |
29 |                 
30 |     import sono from 'sono';
31 |     import {loaders} from 'pixi.js';
32 | 
33 |     const ext = sono.canPlay.ogg ? 'ogg' : 'mp3';
34 | 
35 |     if (sono.hasWebAudio) {
36 |         const {Resource} = loaders;
37 |         Resource.setExtensionLoadType(ext, Resource.LOAD_TYPE.XHR);
38 |         Resource.setExtensionXhrType(ext, Resource.XHR_RESPONSE_TYPE.BUFFER);
39 |     }
40 | 
41 |     const sounds = [{
42 |         name: 'music',
43 |         url: `audio/space-shooter.${ext}`,
44 |         loop: true,
45 |         volume: 0.8
46 |     }, {
47 |         name: 'shoot',
48 |         url: `audio/shoot3.${ext}`,
49 |         volume: 0.4
50 |     }, {
51 |         name: 'explode',
52 |         url: `audio/explode2.${ext}`,
53 |         volume: 0.9
54 |     }];
55 | 
56 |     const loader = new loaders.Loader();
57 | 
58 |     loader.add(sounds);
59 | 
60 |     loader.onComplete.once(() => {
61 |         sounds.forEach(sound => {
62 |             const src = loader.resources[sound.name].data;
63 |             const config = Object.assign({}, sound, {src});
64 |             sono.create(config);
65 |         });
66 | 
67 |         sono.get('shoot').effects.add(reverb();
68 |         sono.play('music');
69 |     });
70 | 
71 |     loader.load();
72 |                 
73 |             
74 | 75 |
76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/recorder.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - recorder 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

recorder

19 |
20 | 21 |

Microphone access is not supported in this browser

22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 | 38 |

39 |         var passThrough = true,
40 |             sound,
41 |             recorder;
42 |         var onConnect = function(stream) {
43 |           // the sound coming through the micophone
44 |           sound = sono.create(stream);
45 |           // start recorder
46 |           recorder = sono.utils.recorder(passThrough);
47 |           recorder.start(sound);
48 | 
49 |           setTimeout(function() {
50 |             // recorder returns recorded sound when stopped
51 |             var recording = recorder.stop();
52 |             // create a new sound object from the recording
53 |             sono.create(recording).play();
54 |           }, 2000);
55 |         };
56 |         // propmt user to connect their mic
57 |         sono.utils.microphone(onConnect);
58 |       
59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/three.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - 3d 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |

3d Panning

20 |
21 | 22 | 23 |
24 |

Use the arrow keys to move around.

25 |
26 |
27 | 28 |
29 | 30 |

31 |     import sono from 'sono';
32 |     import 'sono/effects';
33 | 
34 |     const sound = sono.create({
35 |         url: 'pulsar.mp3',
36 |         loop: true,
37 |         effects: [
38 |             sono.panner({
39 |                 maxDistance: 1000
40 |             })
41 |         ]
42 |     });
43 |     const mesh = new THREE.Mesh(geometry, material);
44 |     // set source position to the position vector of sound source
45 |     sound.effects[0].setPosition(mesh.position);
46 | 
47 |     function update() {
48 |         window.requestAnimationFrame(update);
49 |         // set listener position to the position vector of the camera
50 |         sono.panner.setListenerPosition(camera.position);
51 |         // set listener orientation to the forward vector of the camera
52 |         const forward = new THREE.Vector3(0, 0, -1);
53 |         forward.applyQuaternion(camera.quaternion);
54 |         sono.panner.setListenerOrientation(forward.normalize());
55 |     }
56 |     update();
57 |             
58 | 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /examples/video/.gitinclude: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/examples/video/.gitinclude -------------------------------------------------------------------------------- /examples/volume.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - volume 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

volume

17 |
18 | 19 | 20 |
21 | 22 | 0.00 23 | 24 |
25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 |
42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /examples/wet_dry.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | sono - examples - wet / dry 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

wet / dry

18 |
19 | 20 | 21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 | 32 | 33 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | 3 | const files = []; 4 | 5 | if (process.env.WA === 'no') { 6 | files.push('test/kill-wa.js'); 7 | } 8 | 9 | if (process.env.TRAVIS) { 10 | files.push('test/is-travis.js'); 11 | } 12 | 13 | const configuration = { 14 | 15 | // How long to wait for a message from a browser before disconnecting 16 | browserNoActivityTimeout: 30000, 17 | 18 | // base path, that will be used to resolve files and exclude 19 | basePath: '', 20 | 21 | client: { 22 | mocha: { 23 | timeout: 20000 24 | } 25 | }, 26 | 27 | plugins: [ 28 | 'karma-mocha', 29 | 'karma-chai', 30 | 'karma-chrome-launcher', 31 | 'karma-firefox-launcher', 32 | 'karma-safari-launcher' 33 | ], 34 | 35 | // frameworks to use 36 | frameworks: ['mocha', 'chai'], 37 | 38 | // list of files / patterns to load in the browser 39 | files: files.concat([ 40 | {pattern: 'test/audio/*.{ogg,mp3}', watched: false, included: false, served: true, nocache: false}, 41 | 'test/helper.js', 42 | 'dist/sono.js', 43 | 'test/**/*.spec.js' 44 | ]), 45 | 46 | // list of files to exclude 47 | exclude: [ 48 | // 'test/playback.spec.js' 49 | ], 50 | 51 | // test results reporter to use 52 | // possible values: 'dots', 'progress' 53 | reporters: ['progress'], 54 | 55 | // web server port 56 | port: 9876, 57 | 58 | // enable / disable colors in the output (reporters and logs) 59 | colors: true, 60 | 61 | // level of logging 62 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 63 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 64 | logLevel: config.LOG_WARN, 65 | 66 | // enable / disable watching file and executing tests whenever any file changes 67 | autoWatch: true, 68 | 69 | // Start these browsers, currently available: 70 | browsers: [ 71 | 'Chrome', 72 | 'Firefox', 73 | 'Safari' 74 | ], 75 | 76 | // For Travis 77 | customLaunchers: { 78 | Chrome_travis_ci: { 79 | base: 'Chrome', 80 | flags: ['--no-sandbox'] 81 | } 82 | }, 83 | 84 | // If browser does not capture in given timeout [ms], kill it 85 | captureTimeout: 60000, 86 | 87 | // Continuous Integration mode 88 | // if true, it capture browsers, run tests and exit 89 | singleRun: false 90 | }; 91 | 92 | if (process.env.TRAVIS) { 93 | configuration.browsers = ['Chrome_travis_ci']; 94 | } 95 | 96 | config.set(configuration); 97 | }; 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sono", 3 | "version": "2.1.6", 4 | "description": "A simple yet powerful JavaScript library for working with Web Audio", 5 | "keywords": [ 6 | "WebAudio", 7 | "Web Audio", 8 | "WebAudioAPI", 9 | "Web Audio API", 10 | "audio", 11 | "sound" 12 | ], 13 | "main": "core/sono.js", 14 | "scripts": { 15 | "prepublish": "npm run lib", 16 | "test": "eslint 'src/**/*.js' && karma start --single-run --browsers Chrome && WA=no karma start --single-run --browsers Chrome", 17 | "build": "NODE_ENV=production rollup -c && rollup -c && npm run lib", 18 | "start": "rollup -c -w", 19 | "start:test": "rollup -c -w | karma start", 20 | "lint": "eslint 'src/**/*.js'; exit 0", 21 | "examples": "browser-sync start -s --files 'index.html, examples/**/*.html, examples/**/*.js, examples/**/*.css' --no-notify", 22 | "examples:js": "babel examples/js/src --out-dir examples/js/", 23 | "examples:css": "postcss examples/css/index.css --use postcss-import postcss-custom-media postcss-custom-properties postcss-calc autoprefixer -o examples/css/styles.css", 24 | "examples:watch:css": "onchange 'examples/**/*.css' -e 'examples/css/styles.css' -- npm run examples:css", 25 | "examples:watch:js": "onchange 'examples/js/src/*.js' -- npm run examples:js", 26 | "build:examples": "npm run examples:js && npm run examples:css", 27 | "lib": "rimraf core effects utils && babel src --out-dir ./" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/Stinkstudios/sono" 32 | }, 33 | "author": "ianmcgregor", 34 | "license": "MIT", 35 | "readmeFilename": "README.md", 36 | "dependencies": { 37 | "core-js": "^2.4.1", 38 | "events": "^1.1.1" 39 | }, 40 | "devDependencies": { 41 | "autoprefixer": "^6.7.7", 42 | "babel-cli": "^6.24.1", 43 | "babel-core": "^6.24.1", 44 | "babel-eslint": "^7.2.3", 45 | "babel-plugin-external-helpers": "^6.22.0", 46 | "babel-plugin-transform-runtime": "^6.23.0", 47 | "babel-preset-es2015": "^6.24.1", 48 | "browser-sync": "^2.18.8", 49 | "chai": "^3.5.0", 50 | "eslint": "^4.18.2", 51 | "karma": "^1.6.0", 52 | "karma-chai": "^0.1.0", 53 | "karma-chrome-launcher": "^2.0.0", 54 | "karma-firefox-launcher": "^1.0.1", 55 | "karma-mocha": "^1.3.0", 56 | "karma-safari-launcher": "^1.0.0", 57 | "mocha": "^3.3.0", 58 | "postcss-calc": "^5.3.1", 59 | "postcss-cli": "^3.2.0", 60 | "postcss-custom-media": "^5.0.1", 61 | "postcss-custom-properties": "^5.0.2", 62 | "postcss-import": "^9.1.0", 63 | "rimraf": "^2.6.1", 64 | "rollup": "^0.41.6", 65 | "rollup-plugin-babel": "^2.7.1", 66 | "rollup-plugin-commonjs": "^8.0.2", 67 | "rollup-plugin-node-resolve": "^3.0.0", 68 | "rollup-plugin-strip": "^1.1.1", 69 | "rollup-plugin-uglify": "^1.0.2", 70 | "rollup-watch": "^3.2.2" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import strip from 'rollup-plugin-strip'; 5 | import uglify from 'rollup-plugin-uglify'; 6 | 7 | const prod = process.env.NODE_ENV === 'production'; 8 | 9 | export default { 10 | entry: './src/index.js', 11 | format: 'umd', 12 | moduleName: 'sono', 13 | dest: (prod ? 'dist/sono.min.js' : 'dist/sono.js'), 14 | sourceMap: !prod, 15 | plugins: [ 16 | nodeResolve({ 17 | jsnext: true, 18 | main: true, 19 | preferBuiltins: false 20 | }), 21 | commonjs({ 22 | include: [ 23 | 'node_modules/core-js/**', 24 | 'node_modules/events/**' 25 | ] 26 | }), 27 | babel({ 28 | babelrc: false, 29 | exclude: 'node_modules/**', 30 | presets: [ 31 | ['es2015', {loose: true, modules: false}] 32 | ], 33 | plugins: [ 34 | 'external-helpers' 35 | ] 36 | }), 37 | (prod && strip({sourceMap: false})), 38 | (prod && uglify()) 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /src/core/context.js: -------------------------------------------------------------------------------- 1 | import dummy from './utils/dummy'; 2 | import FakeContext from './utils/fake-context'; 3 | import iOS from './utils/iOS'; 4 | 5 | const desiredSampleRate = 44100; 6 | 7 | const Ctx = window.AudioContext || window.webkitAudioContext || FakeContext; 8 | 9 | let context = new Ctx(); 10 | 11 | if (!context) { 12 | context = new FakeContext(); 13 | } 14 | 15 | // Check if hack is necessary. Only occurs in iOS6+ devices 16 | // and only when you first boot the iPhone, or play a audio/video 17 | // with a different sample rate 18 | // https://github.com/Jam3/ios-safe-audio-context/blob/master/index.js 19 | if (iOS && context.sampleRate !== desiredSampleRate) { 20 | dummy(context); 21 | context.close(); // dispose old context 22 | context = new Ctx(); 23 | } 24 | 25 | // Handles bug in Safari 9 OSX where AudioContext instance starts in 'suspended' state 26 | if (context.state === 'suspended' && typeof context.resume === 'function') { 27 | window.setTimeout(() => context.resume(), 1000); 28 | } 29 | 30 | export default context; 31 | -------------------------------------------------------------------------------- /src/core/effects.js: -------------------------------------------------------------------------------- 1 | export default class Effects { 2 | constructor(context) { 3 | this.context = context; 4 | this._destination = null; 5 | this._source = null; 6 | 7 | this._nodes = []; 8 | this._nodes.has = node => this.has(node); 9 | this._nodes.add = node => this.add(node); 10 | this._nodes.remove = node => this.remove(node); 11 | this._nodes.toggle = (node, force) => this.toggle(node, force); 12 | this._nodes.removeAll = () => this.removeAll(); 13 | 14 | Object.keys(Effects.prototype).forEach(key => { 15 | if (!this._nodes.hasOwnProperty(key) && typeof Effects.prototype[key] === 'function') { 16 | this._nodes[key] = this[key].bind(this); 17 | } 18 | }); 19 | } 20 | 21 | setSource(node) { 22 | this._source = node; 23 | this._updateConnections(); 24 | return node; 25 | } 26 | 27 | setDestination(node) { 28 | this._connectToDestination(node); 29 | return node; 30 | } 31 | 32 | has(node) { 33 | if (!node) { 34 | return false; 35 | } 36 | return this._nodes.indexOf(node) > -1; 37 | } 38 | 39 | add(node) { 40 | if (!node) { 41 | return null; 42 | } 43 | if (this.has(node)) { 44 | return node; 45 | } 46 | if (Array.isArray(node)) { 47 | let n; 48 | for (let i = 0; i < node.length; i++) { 49 | n = this.add(node[i]); 50 | } 51 | return n; 52 | } 53 | this._nodes.push(node); 54 | this._updateConnections(); 55 | return node; 56 | } 57 | 58 | remove(node) { 59 | if (!node) { 60 | return null; 61 | } 62 | if (!this.has(node)) { 63 | return node; 64 | } 65 | const l = this._nodes.length; 66 | for (let i = 0; i < l; i++) { 67 | if (node === this._nodes[i]) { 68 | this._nodes.splice(i, 1); 69 | break; 70 | } 71 | } 72 | node.disconnect(); 73 | this._updateConnections(); 74 | return node; 75 | } 76 | 77 | toggle(node, force) { 78 | force = !!force; 79 | const hasNode = this.has(node); 80 | if (arguments.length > 1 && hasNode === force) { 81 | return this; 82 | } 83 | if (hasNode) { 84 | this.remove(node); 85 | } else { 86 | this.add(node); 87 | } 88 | return this; 89 | } 90 | 91 | removeAll() { 92 | while (this._nodes.length) { 93 | const node = this._nodes.pop(); 94 | node.disconnect(); 95 | } 96 | this._updateConnections(); 97 | return this; 98 | } 99 | 100 | destroy() { 101 | this.removeAll(); 102 | this.context = null; 103 | this._destination = null; 104 | if (this._source) { 105 | this._source.disconnect(); 106 | } 107 | this._source = null; 108 | } 109 | 110 | _connect(a, b) { 111 | a.disconnect(); 112 | // console.log('> connect output', (a.name || a.constructor.name), 'to input', (b.name || b.constructor.name)); 113 | a.connect(b._in || b); 114 | } 115 | 116 | _connectToDestination(node) { 117 | const lastNode = this._nodes[this._nodes.length - 1] || this._source; 118 | 119 | if (lastNode) { 120 | this._connect(lastNode, node); 121 | } 122 | 123 | this._destination = node; 124 | } 125 | 126 | _updateConnections() { 127 | if (!this._source) { 128 | return; 129 | } 130 | 131 | // console.log('updateConnections'); 132 | 133 | let node, 134 | prev; 135 | 136 | for (let i = 0; i < this._nodes.length; i++) { 137 | node = this._nodes[i]; 138 | prev = i === 0 ? this._source : this._nodes[i - 1]; 139 | this._connect(prev, node); 140 | } 141 | 142 | if (this._destination) { 143 | this._connectToDestination(this._destination); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/core/group.js: -------------------------------------------------------------------------------- 1 | import Effects from './effects'; 2 | 3 | export default function Group(context, destination) { 4 | const sounds = []; 5 | const effects = new Effects(context); 6 | const gain = context.createGain(); 7 | let preMuteVolume = 1; 8 | let group = null; 9 | 10 | if (context) { 11 | effects.setSource(gain); 12 | effects.setDestination(destination || context.destination); 13 | } 14 | 15 | /* 16 | * Add / remove 17 | */ 18 | 19 | function find(soundOrId, callback) { 20 | let found; 21 | 22 | if (!soundOrId && soundOrId !== 0) { 23 | return found; 24 | } 25 | 26 | sounds.some(function(sound) { 27 | if (sound === soundOrId || sound.id === soundOrId) { 28 | found = sound; 29 | return true; 30 | } 31 | return false; 32 | }); 33 | 34 | if (found && callback) { 35 | return callback(found); 36 | } 37 | 38 | return found; 39 | } 40 | 41 | function remove(soundOrId) { 42 | find(soundOrId, (sound) => sounds.splice(sounds.indexOf(sound), 1)); 43 | return group; 44 | } 45 | 46 | function add(sound) { 47 | sound.gain.disconnect(); 48 | sound.gain.connect(gain); 49 | 50 | sounds.push(sound); 51 | 52 | sound.once('destroy', remove); 53 | 54 | return group; 55 | } 56 | 57 | /* 58 | * Controls 59 | */ 60 | 61 | function play(delay, offset) { 62 | sounds.forEach((sound) => sound.play(delay, offset)); 63 | return group; 64 | } 65 | 66 | function pause() { 67 | sounds.forEach((sound) => { 68 | if (sound.playing) { 69 | sound.pause(); 70 | } 71 | }); 72 | return group; 73 | } 74 | 75 | function resume() { 76 | sounds.forEach((sound) => { 77 | if (sound.paused) { 78 | sound.play(); 79 | } 80 | }); 81 | return group; 82 | } 83 | 84 | function stop() { 85 | sounds.forEach((sound) => sound.stop()); 86 | return group; 87 | } 88 | 89 | function seek(percent) { 90 | sounds.forEach((sound) => sound.seek(percent)); 91 | return group; 92 | } 93 | 94 | function mute() { 95 | preMuteVolume = group.volume; 96 | group.volume = 0; 97 | return group; 98 | } 99 | 100 | function unMute() { 101 | group.volume = preMuteVolume || 1; 102 | return group; 103 | } 104 | 105 | function setVolume(value) { 106 | group.volume = value; 107 | return group; 108 | } 109 | 110 | function fade(volume, duration) { 111 | if (context) { 112 | const param = gain.gain; 113 | const time = context.currentTime; 114 | 115 | param.cancelScheduledValues(time); 116 | param.setValueAtTime(param.value, time); 117 | // param.setValueAtTime(volume, time + duration); 118 | param.linearRampToValueAtTime(volume, time + duration); 119 | // param.setTargetAtTime(volume, time, duration); 120 | // param.exponentialRampToValueAtTime(Math.max(volume, 0.0001), time + duration); 121 | } else { 122 | sounds.forEach((sound) => sound.fade(volume, duration)); 123 | } 124 | 125 | return group; 126 | } 127 | 128 | /* 129 | * Load 130 | */ 131 | 132 | function load() { 133 | sounds.forEach((sound) => sound.load()); 134 | } 135 | 136 | /* 137 | * Unload 138 | */ 139 | 140 | function unload() { 141 | sounds.forEach((sound) => sound.unload()); 142 | } 143 | 144 | /* 145 | * Destroy 146 | */ 147 | 148 | function destroy() { 149 | while (sounds.length) { 150 | sounds.pop() 151 | .destroy(); 152 | } 153 | } 154 | 155 | /* 156 | * Api 157 | */ 158 | 159 | group = { 160 | add, 161 | find, 162 | remove, 163 | play, 164 | pause, 165 | resume, 166 | stop, 167 | seek, 168 | setVolume, 169 | mute, 170 | unMute, 171 | fade, 172 | load, 173 | unload, 174 | destroy, 175 | gain, 176 | get effects() { 177 | return effects._nodes; 178 | }, 179 | set effects(value) { 180 | effects.removeAll().add(value); 181 | }, 182 | get fx() { 183 | return this.effects; 184 | }, 185 | set fx(value) { 186 | this.effects = value; 187 | }, 188 | get sounds() { 189 | return sounds; 190 | }, 191 | get volume() { 192 | return gain.gain.value; 193 | }, 194 | set volume(value) { 195 | if (isNaN(value)) { 196 | return; 197 | } 198 | 199 | value = Math.min(Math.max(value, 0), 1); 200 | 201 | if (context) { 202 | gain.gain.cancelScheduledValues(context.currentTime); 203 | gain.gain.value = value; 204 | gain.gain.setValueAtTime(value, context.currentTime); 205 | } else { 206 | gain.gain.value = value; 207 | } 208 | sounds.forEach((sound) => { 209 | if (!sound.context) { 210 | sound.groupVolume = value; 211 | } 212 | }); 213 | } 214 | }; 215 | 216 | return group; 217 | } 218 | 219 | Group.Effects = Effects; 220 | -------------------------------------------------------------------------------- /src/core/source/buffer-source.js: -------------------------------------------------------------------------------- 1 | export default function BufferSource(buffer, context, endedCallback) { 2 | const api = {}; 3 | let ended = false; 4 | let loop = false; 5 | let paused = false; 6 | let cuedAt = 0; 7 | let playbackRate = 1; 8 | let playing = false; 9 | let sourceNode = null; 10 | let startedAt = 0; 11 | 12 | function createSourceNode() { 13 | if (!sourceNode && context) { 14 | sourceNode = context.createBufferSource(); 15 | sourceNode.buffer = buffer; 16 | } 17 | return sourceNode; 18 | } 19 | 20 | /* 21 | * Controls 22 | */ 23 | 24 | function stop() { 25 | if (sourceNode) { 26 | sourceNode.onended = null; 27 | try { 28 | sourceNode.disconnect(); 29 | sourceNode.stop(0); 30 | } catch (e) {} 31 | sourceNode = null; 32 | } 33 | 34 | paused = false; 35 | cuedAt = 0; 36 | playing = false; 37 | startedAt = 0; 38 | } 39 | 40 | function pause() { 41 | const elapsed = context.currentTime - startedAt; 42 | stop(); 43 | cuedAt = elapsed; 44 | playing = false; 45 | paused = true; 46 | } 47 | 48 | function endedHandler() { 49 | stop(); 50 | ended = true; 51 | if (typeof endedCallback === 'function') { 52 | endedCallback(api); 53 | } 54 | } 55 | 56 | function play(delay = 0, offset = 0) { 57 | if (playing) { 58 | return; 59 | } 60 | 61 | delay = delay ? context.currentTime + delay : 0; 62 | 63 | if (offset) { 64 | cuedAt = 0; 65 | } 66 | 67 | if (cuedAt) { 68 | offset = cuedAt; 69 | } 70 | 71 | while (offset > api.duration) { 72 | offset = offset % api.duration; 73 | } 74 | 75 | createSourceNode(); 76 | sourceNode.onended = endedHandler; 77 | sourceNode.start(delay, offset); 78 | 79 | sourceNode.loop = loop; 80 | sourceNode.playbackRate.value = playbackRate; 81 | 82 | startedAt = context.currentTime - offset; 83 | ended = false; 84 | paused = false; 85 | cuedAt = 0; 86 | playing = true; 87 | } 88 | 89 | 90 | /* 91 | * Destroy 92 | */ 93 | 94 | function destroy() { 95 | stop(); 96 | buffer = null; 97 | context = null; 98 | endedCallback = null; 99 | sourceNode = null; 100 | } 101 | 102 | /* 103 | * Getters & Setters 104 | */ 105 | 106 | Object.defineProperties(api, { 107 | play: { 108 | value: play 109 | }, 110 | pause: { 111 | value: pause 112 | }, 113 | stop: { 114 | value: stop 115 | }, 116 | destroy: { 117 | value: destroy 118 | }, 119 | currentTime: { 120 | get: function() { 121 | if (cuedAt) { 122 | return cuedAt; 123 | } 124 | if (startedAt) { 125 | let time = context.currentTime - startedAt; 126 | while (time > api.duration) { 127 | time = time % api.duration; 128 | } 129 | return time; 130 | } 131 | return 0; 132 | }, 133 | set: function(value) { 134 | cuedAt = value; 135 | } 136 | }, 137 | duration: { 138 | get: function() { 139 | return buffer ? buffer.duration : 0; 140 | } 141 | }, 142 | ended: { 143 | get: function() { 144 | return ended; 145 | } 146 | }, 147 | loop: { 148 | get: function() { 149 | return loop; 150 | }, 151 | set: function(value) { 152 | loop = !!value; 153 | if (sourceNode) { 154 | sourceNode.loop = loop; 155 | } 156 | } 157 | }, 158 | paused: { 159 | get: function() { 160 | return paused; 161 | } 162 | }, 163 | playbackRate: { 164 | get: function() { 165 | return playbackRate; 166 | }, 167 | set: function(value) { 168 | playbackRate = value; 169 | if (sourceNode) { 170 | sourceNode.playbackRate.value = playbackRate; 171 | } 172 | } 173 | }, 174 | playing: { 175 | get: function() { 176 | return playing; 177 | } 178 | }, 179 | progress: { 180 | get: function() { 181 | return api.duration ? api.currentTime / api.duration : 0; 182 | } 183 | }, 184 | sourceNode: { 185 | get: function() { 186 | return createSourceNode(); 187 | } 188 | } 189 | }); 190 | 191 | return Object.freeze(api); 192 | } 193 | -------------------------------------------------------------------------------- /src/core/source/microphone-source.js: -------------------------------------------------------------------------------- 1 | export default function MicrophoneSource(stream, context) { 2 | let ended = false, 3 | paused = false, 4 | cuedAt = 0, 5 | playing = false, 6 | sourceNode = null, // MicrophoneSourceNode 7 | startedAt = 0; 8 | 9 | function createSourceNode() { 10 | if (!sourceNode && context) { 11 | sourceNode = context.createMediaStreamSource(stream); 12 | // HACK: stops moz garbage collection killing the stream 13 | // see https://support.mozilla.org/en-US/questions/984179 14 | if (navigator.mozGetUserMedia) { 15 | window.mozHack = sourceNode; 16 | } 17 | } 18 | return sourceNode; 19 | } 20 | 21 | /* 22 | * Controls 23 | */ 24 | 25 | function play(delay) { 26 | delay = delay ? context.currentTime + delay : 0; 27 | 28 | createSourceNode(); 29 | sourceNode.start(delay); 30 | 31 | startedAt = context.currentTime - cuedAt; 32 | ended = false; 33 | playing = true; 34 | paused = false; 35 | cuedAt = 0; 36 | } 37 | 38 | function stop() { 39 | if (sourceNode) { 40 | try { 41 | sourceNode.stop(0); 42 | } catch (e) {} 43 | sourceNode = null; 44 | } 45 | ended = true; 46 | paused = false; 47 | cuedAt = 0; 48 | playing = false; 49 | startedAt = 0; 50 | } 51 | 52 | function pause() { 53 | const elapsed = context.currentTime - startedAt; 54 | stop(); 55 | cuedAt = elapsed; 56 | playing = false; 57 | paused = true; 58 | } 59 | 60 | /* 61 | * Destroy 62 | */ 63 | 64 | function destroy() { 65 | stop(); 66 | context = null; 67 | sourceNode = null; 68 | stream = null; 69 | window.mozHack = null; 70 | } 71 | 72 | /* 73 | * Api 74 | */ 75 | 76 | const api = { 77 | play, 78 | pause, 79 | stop, 80 | destroy, 81 | 82 | duration: 0, 83 | progress: 0 84 | }; 85 | 86 | /* 87 | * Getters & Setters 88 | */ 89 | 90 | Object.defineProperties(api, { 91 | currentTime: { 92 | get: function() { 93 | if (cuedAt) { 94 | return cuedAt; 95 | } 96 | if (startedAt) { 97 | return context.currentTime - startedAt; 98 | } 99 | return 0; 100 | }, 101 | set: function(value) { 102 | cuedAt = value; 103 | } 104 | }, 105 | ended: { 106 | get: function() { 107 | return ended; 108 | } 109 | }, 110 | paused: { 111 | get: function() { 112 | return paused; 113 | } 114 | }, 115 | playing: { 116 | get: function() { 117 | return playing; 118 | } 119 | }, 120 | sourceNode: { 121 | get: function() { 122 | return createSourceNode(); 123 | } 124 | } 125 | }); 126 | 127 | return Object.freeze(api); 128 | } 129 | -------------------------------------------------------------------------------- /src/core/source/oscillator-source.js: -------------------------------------------------------------------------------- 1 | export default function OscillatorSource(type, context) { 2 | let ended = false, 3 | paused = false, 4 | cuedAt = 0, 5 | playing = false, 6 | sourceNode = null, // OscillatorSourceNode 7 | startedAt = 0, 8 | frequency = 200, 9 | api = null; 10 | 11 | function createSourceNode() { 12 | if (!sourceNode && context) { 13 | sourceNode = context.createOscillator(); 14 | sourceNode.type = type; 15 | sourceNode.frequency.value = frequency; 16 | } 17 | return sourceNode; 18 | } 19 | 20 | /* 21 | * Controls 22 | */ 23 | 24 | function play(delay) { 25 | delay = delay || 0; 26 | if (delay) { 27 | delay = context.currentTime + delay; 28 | } 29 | 30 | createSourceNode(); 31 | sourceNode.start(delay); 32 | 33 | if (cuedAt) { 34 | startedAt = context.currentTime - cuedAt; 35 | } else { 36 | startedAt = context.currentTime; 37 | } 38 | 39 | ended = false; 40 | playing = true; 41 | paused = false; 42 | cuedAt = 0; 43 | } 44 | 45 | function stop() { 46 | if (sourceNode) { 47 | try { 48 | sourceNode.stop(0); 49 | } catch (e) {} 50 | sourceNode = null; 51 | } 52 | ended = true; 53 | paused = false; 54 | cuedAt = 0; 55 | playing = false; 56 | startedAt = 0; 57 | } 58 | 59 | function pause() { 60 | const elapsed = context.currentTime - startedAt; 61 | stop(); 62 | cuedAt = elapsed; 63 | playing = false; 64 | paused = true; 65 | } 66 | 67 | /* 68 | * Destroy 69 | */ 70 | 71 | function destroy() { 72 | stop(); 73 | context = null; 74 | sourceNode = null; 75 | } 76 | 77 | /* 78 | * Api 79 | */ 80 | 81 | api = { 82 | play: play, 83 | pause: pause, 84 | stop: stop, 85 | destroy: destroy 86 | }; 87 | 88 | /* 89 | * Getters & Setters 90 | */ 91 | 92 | Object.defineProperties(api, { 93 | currentTime: { 94 | get: function() { 95 | if (cuedAt) { 96 | return cuedAt; 97 | } 98 | if (startedAt) { 99 | return context.currentTime - startedAt; 100 | } 101 | return 0; 102 | }, 103 | set: function(value) { 104 | cuedAt = value; 105 | } 106 | }, 107 | duration: { 108 | value: 0 109 | }, 110 | ended: { 111 | get: function() { 112 | return ended; 113 | } 114 | }, 115 | frequency: { 116 | get: function() { 117 | return frequency; 118 | }, 119 | set: function(value) { 120 | frequency = value; 121 | if (sourceNode) { 122 | sourceNode.frequency.value = value; 123 | } 124 | } 125 | }, 126 | paused: { 127 | get: function() { 128 | return paused; 129 | } 130 | }, 131 | playing: { 132 | get: function() { 133 | return playing; 134 | } 135 | }, 136 | progress: { 137 | value: 0 138 | }, 139 | sourceNode: { 140 | get: function() { 141 | return createSourceNode(); 142 | } 143 | } 144 | }); 145 | 146 | return Object.freeze(api); 147 | } 148 | -------------------------------------------------------------------------------- /src/core/utils/dummy.js: -------------------------------------------------------------------------------- 1 | export default function dummy(context) { 2 | const buffer = context.createBuffer(1, 1, context.sampleRate); 3 | const source = context.createBufferSource(); 4 | source.buffer = buffer; 5 | source.connect(context.destination); 6 | source.start(0); 7 | source.stop(0); 8 | source.disconnect(); 9 | } 10 | -------------------------------------------------------------------------------- /src/core/utils/emitter.js: -------------------------------------------------------------------------------- 1 | import events from 'events'; 2 | const {EventEmitter} = events; 3 | 4 | export default class Emitter extends EventEmitter { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | off (type, listener) { 10 | if (listener) { 11 | return this.removeListener(type, listener); 12 | } 13 | if (type) { 14 | return this.removeAllListeners(type); 15 | } 16 | return this.removeAllListeners(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/utils/fake-context.js: -------------------------------------------------------------------------------- 1 | export default function FakeContext() { 2 | 3 | const startTime = Date.now(); 4 | 5 | function fn() {} 6 | 7 | function param() { 8 | return { 9 | value: 1, 10 | defaultValue: 1, 11 | linearRampToValueAtTime: fn, 12 | setValueAtTime: fn, 13 | exponentialRampToValueAtTime: fn, 14 | setTargetAtTime: fn, 15 | setValueCurveAtTime: fn, 16 | cancelScheduledValues: fn 17 | }; 18 | } 19 | 20 | function fakeNode() { 21 | return { 22 | connect: fn, 23 | disconnect: fn, 24 | // analyser 25 | frequencyBinCount: 0, 26 | smoothingTimeConstant: 0, 27 | fftSize: 0, 28 | minDecibels: 0, 29 | maxDecibels: 0, 30 | getByteTimeDomainData: fn, 31 | getByteFrequencyData: fn, 32 | getFloatTimeDomainData: fn, 33 | getFloatFrequencyData: fn, 34 | // gain 35 | gain: param(), 36 | // panner 37 | panningModel: 0, 38 | setPosition: fn, 39 | setOrientation: fn, 40 | setVelocity: fn, 41 | distanceModel: 0, 42 | refDistance: 0, 43 | maxDistance: 0, 44 | rolloffFactor: 0, 45 | coneInnerAngle: 360, 46 | coneOuterAngle: 360, 47 | coneOuterGain: 0, 48 | // filter: 49 | type: 0, 50 | frequency: param(), 51 | Q: param(), 52 | detune: param(), 53 | // delay 54 | delayTime: param(), 55 | // convolver 56 | buffer: 0, 57 | // compressor 58 | threshold: param(), 59 | knee: param(), 60 | ratio: param(), 61 | attack: param(), 62 | release: param(), 63 | reduction: param(), 64 | // distortion 65 | oversample: 0, 66 | curve: 0, 67 | // buffer 68 | sampleRate: 1, 69 | length: 0, 70 | duration: 0, 71 | numberOfChannels: 0, 72 | getChannelData: function() { 73 | return []; 74 | }, 75 | copyFromChannel: fn, 76 | copyToChannel: fn, 77 | // listener 78 | dopplerFactor: 0, 79 | speedOfSound: 0, 80 | // osc 81 | start: fn 82 | }; 83 | } 84 | 85 | // ie9 86 | if (!window.Uint8Array) { 87 | window.Uint8Array = window.Float32Array = Array; 88 | } 89 | 90 | return { 91 | isFake: true, 92 | activeSourceCount: 0, 93 | createAnalyser: fakeNode, 94 | createBuffer: fakeNode, 95 | createBufferSource: fakeNode, 96 | createMediaElementSource: fakeNode, 97 | createMediaStreamSource: fakeNode, 98 | createBiquadFilter: fakeNode, 99 | createChannelMerger: fakeNode, 100 | createChannelSplitter: fakeNode, 101 | createDynamicsCompressor: fakeNode, 102 | createConvolver: fakeNode, 103 | createDelay: fakeNode, 104 | createGain: fakeNode, 105 | createOscillator: fakeNode, 106 | createPanner: fakeNode, 107 | createScriptProcessor: fakeNode, 108 | createWaveShaper: fakeNode, 109 | decodeAudioData: fn, 110 | destination: fakeNode, 111 | listener: fakeNode(), 112 | sampleRate: 44100, 113 | state: '', 114 | get currentTime() { 115 | return (Date.now() - startTime) / 1000; 116 | } 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /src/core/utils/file.js: -------------------------------------------------------------------------------- 1 | const extensions = []; 2 | const canPlay = {}; 3 | 4 | /* 5 | * Initial tests 6 | */ 7 | 8 | const tests = [ 9 | { 10 | ext: 'ogg', 11 | type: 'audio/ogg; codecs="vorbis"' 12 | }, { 13 | ext: 'mp3', 14 | type: 'audio/mpeg;' 15 | }, { 16 | ext: 'opus', 17 | type: 'audio/ogg; codecs="opus"' 18 | }, { 19 | ext: 'wav', 20 | type: 'audio/wav; codecs="1"' 21 | }, { 22 | ext: 'm4a', 23 | type: 'audio/x-m4a;' 24 | }, { 25 | ext: 'm4a', 26 | type: 'audio/aac;' 27 | } 28 | ]; 29 | 30 | let el = document.createElement('audio'); 31 | if (el) { 32 | tests.forEach(function(test) { 33 | const canPlayType = !!el.canPlayType(test.type); 34 | if (canPlayType && extensions.indexOf(test.ext) === -1) { 35 | extensions.push(test.ext); 36 | } 37 | canPlay[test.ext] = canPlayType; 38 | }); 39 | el = null; 40 | } 41 | 42 | /* 43 | * find a supported file 44 | */ 45 | 46 | function getFileExtension(url) { 47 | if (typeof url !== 'string') { 48 | return ''; 49 | } 50 | // from DataURL 51 | if (url.slice(0, 5) === 'data:') { 52 | const match = url.match(/data:audio\/(ogg|mp3|opus|wav|m4a)/i); 53 | if (match && match.length > 1) { 54 | return match[1].toLowerCase(); 55 | } 56 | } 57 | // from Standard URL 58 | url = url.split('?')[0]; 59 | url = url.slice(url.lastIndexOf('/') + 1); 60 | 61 | const a = url.split('.'); 62 | if (a.length === 1 || (a[0] === '' && a.length === 2)) { 63 | return ''; 64 | } 65 | return a.pop().toLowerCase(); 66 | } 67 | 68 | function getSupportedFile(fileNames) { 69 | let name; 70 | 71 | if (Array.isArray(fileNames)) { 72 | // if array get the first one that works 73 | for (let i = 0; i < fileNames.length; i++) { 74 | name = fileNames[i]; 75 | const ext = getFileExtension(name); 76 | if (extensions.indexOf(ext) > -1) { 77 | break; 78 | } 79 | } 80 | } else if (typeof fileNames === 'object') { 81 | // if not array and is object 82 | Object.keys(fileNames).some(function(key) { 83 | name = fileNames[key]; 84 | const ext = getFileExtension(name); 85 | return extensions.indexOf(ext) > -1; 86 | }); 87 | } 88 | // if string just return 89 | return name || fileNames; 90 | } 91 | 92 | /* 93 | * infer file types 94 | */ 95 | 96 | function isAudioBuffer(data) { 97 | return !!(data && window.AudioBuffer && data instanceof window.AudioBuffer); 98 | } 99 | 100 | function isArrayBuffer(data) { 101 | return !!(data && window.ArrayBuffer && data instanceof window.ArrayBuffer); 102 | } 103 | 104 | function isMediaElement(data) { 105 | return !!(data && window.HTMLMediaElement && data instanceof window.HTMLMediaElement); 106 | } 107 | 108 | function isMediaStream(data) { 109 | return !!( 110 | data && typeof data.getAudioTracks === 'function' && data.getAudioTracks().length && 111 | window.MediaStreamTrack && data.getAudioTracks()[0] instanceof window.MediaStreamTrack 112 | ); 113 | } 114 | 115 | function isOscillatorType(data) { 116 | return !!( 117 | data && typeof data === 'string' && 118 | (data === 'sine' || data === 'square' || data === 'sawtooth' || data === 'triangle') 119 | ); 120 | } 121 | 122 | function isURL(data) { 123 | return !!(data && typeof data === 'string' && (data.indexOf('.') > -1 || data.slice(0, 5) === 'data:')); 124 | } 125 | 126 | function containsURL(config) { 127 | if (!config || isMediaElement(config)) { 128 | return false; 129 | } 130 | // string, array or object with src/url/data property that is string, array or arraybuffer 131 | const src = getSrc(config); 132 | return isURL(src) || isArrayBuffer(src) || (Array.isArray(src) && isURL(src[0])); 133 | } 134 | 135 | function getSrc(config) { 136 | return config.src || config.url || config.data || config; 137 | } 138 | 139 | export default { 140 | canPlay, 141 | containsURL, 142 | extensions, 143 | getFileExtension, 144 | getSrc, 145 | getSupportedFile, 146 | isAudioBuffer, 147 | isArrayBuffer, 148 | isMediaElement, 149 | isMediaStream, 150 | isOscillatorType, 151 | isURL 152 | }; 153 | -------------------------------------------------------------------------------- /src/core/utils/firefox.js: -------------------------------------------------------------------------------- 1 | export default navigator && /Firefox/i.test(navigator.userAgent); 2 | -------------------------------------------------------------------------------- /src/core/utils/iOS.js: -------------------------------------------------------------------------------- 1 | export default navigator && /(iPhone|iPad|iPod)/i.test(navigator.userAgent); 2 | -------------------------------------------------------------------------------- /src/core/utils/isDefined.js: -------------------------------------------------------------------------------- 1 | export default function isDefined(value) { 2 | return typeof value !== 'undefined'; 3 | } 4 | -------------------------------------------------------------------------------- /src/core/utils/isSafeNumber.js: -------------------------------------------------------------------------------- 1 | export default function isSafeNumber(value) { 2 | return typeof value === 'number' && !isNaN(value) && isFinite(value); 3 | } 4 | -------------------------------------------------------------------------------- /src/core/utils/log.js: -------------------------------------------------------------------------------- 1 | export default function log(api) { 2 | const title = 'sono ' + api.VERSION, 3 | info = 'Supported:' + api.isSupported + 4 | ' WebAudioAPI:' + api.hasWebAudio + 5 | ' TouchLocked:' + api.isTouchLocked + 6 | ' State:' + (api.context && api.context.state) + 7 | ' Extensions:' + api.file.extensions; 8 | 9 | if (navigator.userAgent.indexOf('Chrome') > -1) { 10 | const args = [ 11 | '%c ♫ ' + title + 12 | ' ♫ %c ' + info + ' ', 13 | 'color: #FFFFFF; background: #379F7A', 14 | 'color: #1F1C0D; background: #E0FBAC' 15 | ]; 16 | console.log.apply(console, args); 17 | } else if (window.console && window.console.log.call) { 18 | console.log.call(console, title + ' ' + info); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/core/utils/pageVisibility.js: -------------------------------------------------------------------------------- 1 | export default function pageVisibility(onHidden, onShown) { 2 | let enabled = false; 3 | let hidden = null; 4 | let visibilityChange = null; 5 | 6 | if (typeof document.hidden !== 'undefined') { 7 | hidden = 'hidden'; 8 | visibilityChange = 'visibilitychange'; 9 | } else if (typeof document.mozHidden !== 'undefined') { 10 | hidden = 'mozHidden'; 11 | visibilityChange = 'mozvisibilitychange'; 12 | } else if (typeof document.msHidden !== 'undefined') { 13 | hidden = 'msHidden'; 14 | visibilityChange = 'msvisibilitychange'; 15 | } else if (typeof document.webkitHidden !== 'undefined') { 16 | hidden = 'webkitHidden'; 17 | visibilityChange = 'webkitvisibilitychange'; 18 | } 19 | 20 | function onChange() { 21 | if (document[hidden]) { 22 | onHidden(); 23 | } else { 24 | onShown(); 25 | } 26 | } 27 | 28 | function enable(value) { 29 | enabled = value; 30 | 31 | if (enabled) { 32 | document.addEventListener(visibilityChange, onChange, false); 33 | } else { 34 | document.removeEventListener(visibilityChange, onChange); 35 | } 36 | } 37 | 38 | if (typeof visibilityChange !== 'undefined') { 39 | enable(true); 40 | } 41 | 42 | return { 43 | get enabled() { 44 | return enabled; 45 | }, 46 | set enabled(value) { 47 | enable(value); 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/core/utils/sound-group.js: -------------------------------------------------------------------------------- 1 | import Group from '../group'; 2 | 3 | export default function SoundGroup(context, destination) { 4 | const group = new Group(context, destination); 5 | const sounds = group.sounds; 6 | let playbackRate = 1, 7 | loop = false, 8 | src; 9 | 10 | function getSource() { 11 | if (!sounds.length) { 12 | return; 13 | } 14 | 15 | src = sounds.slice(0) 16 | .sort((a, b) => b.duration - a.duration)[0]; 17 | } 18 | 19 | const add = group.add; 20 | group.add = function(sound) { 21 | add(sound); 22 | getSource(); 23 | return group; 24 | }; 25 | 26 | const remove = group.remove; 27 | group.remove = function(soundOrId) { 28 | remove(soundOrId); 29 | getSource(); 30 | return group; 31 | }; 32 | 33 | Object.defineProperties(group, { 34 | currentTime: { 35 | get: function() { 36 | return src ? src.currentTime : 0; 37 | }, 38 | set: function(value) { 39 | this.stop(); 40 | this.play(0, value); 41 | } 42 | }, 43 | duration: { 44 | get: function() { 45 | return src ? src.duration : 0; 46 | } 47 | }, 48 | // ended: { 49 | // get: function() { 50 | // return src ? src.ended : false; 51 | // } 52 | // }, 53 | loop: { 54 | get: function() { 55 | return loop; 56 | }, 57 | set: function(value) { 58 | loop = !!value; 59 | sounds.forEach(function(sound) { 60 | sound.loop = loop; 61 | }); 62 | } 63 | }, 64 | paused: { 65 | get: function() { 66 | // return src ? src.paused : false; 67 | return !!src && src.paused; 68 | } 69 | }, 70 | progress: { 71 | get: function() { 72 | return src ? src.progress : 0; 73 | } 74 | }, 75 | playbackRate: { 76 | get: function() { 77 | return playbackRate; 78 | }, 79 | set: function(value) { 80 | playbackRate = value; 81 | sounds.forEach(function(sound) { 82 | sound.playbackRate = playbackRate; 83 | }); 84 | } 85 | }, 86 | playing: { 87 | get: function() { 88 | // return src ? src.playing : false; 89 | return !!src && src.playing; 90 | } 91 | } 92 | }); 93 | 94 | return group; 95 | } 96 | -------------------------------------------------------------------------------- /src/core/utils/touchLock.js: -------------------------------------------------------------------------------- 1 | import iOS from './iOS'; 2 | import dummy from './dummy'; 3 | 4 | export default function touchLock(context, callback) { 5 | const locked = iOS; 6 | 7 | function unlock() { 8 | if (context && context.state === 'suspended') { 9 | context.resume() 10 | .then(() => { 11 | dummy(context); 12 | unlocked(); 13 | }); 14 | } else { 15 | unlocked(); 16 | } 17 | } 18 | 19 | function unlocked() { 20 | document.body.removeEventListener('touchstart', unlock); 21 | document.body.removeEventListener('touchend', unlock); 22 | callback(); 23 | } 24 | 25 | function addListeners() { 26 | document.body.addEventListener('touchstart', unlock, false); 27 | document.body.addEventListener('touchend', unlock, false); 28 | } 29 | 30 | if (locked) { 31 | if (document.readyState === 'loading') { 32 | document.addEventListener('DOMContentLoaded', addListeners); 33 | } else { 34 | addListeners(); 35 | } 36 | } 37 | 38 | return locked; 39 | } 40 | -------------------------------------------------------------------------------- /src/core/utils/utils.js: -------------------------------------------------------------------------------- 1 | import context from '../context'; 2 | 3 | let offlineCtx; 4 | /* 5 | In contrast with a standard AudioContext, an OfflineAudioContext doesn't render 6 | the audio to the device hardware; 7 | instead, it generates it, as fast as it can, and outputs the result to an AudioBuffer. 8 | */ 9 | function getOfflineContext(numOfChannels, length, sampleRate) { 10 | if (offlineCtx) { 11 | return offlineCtx; 12 | } 13 | numOfChannels = numOfChannels || 2; 14 | sampleRate = sampleRate || 44100; 15 | length = sampleRate || numOfChannels; 16 | 17 | const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext; 18 | 19 | offlineCtx = (OfflineCtx ? new OfflineCtx(numOfChannels, length, sampleRate) : null); 20 | 21 | return offlineCtx; 22 | } 23 | 24 | 25 | /* 26 | * clone audio buffer 27 | */ 28 | 29 | function cloneBuffer(buffer, offset = 0, length = buffer.length) { 30 | if (!context || context.isFake) { 31 | return buffer; 32 | } 33 | const numChannels = buffer.numberOfChannels; 34 | const cloned = context.createBuffer(numChannels, length, buffer.sampleRate); 35 | for (let i = 0; i < numChannels; i++) { 36 | cloned.getChannelData(i) 37 | .set(buffer.getChannelData(i).slice(offset, offset + length)); 38 | } 39 | return cloned; 40 | } 41 | 42 | /* 43 | * reverse audio buffer 44 | */ 45 | 46 | function reverseBuffer(buffer) { 47 | const numChannels = buffer.numberOfChannels; 48 | for (let i = 0; i < numChannels; i++) { 49 | Array.prototype.reverse.call(buffer.getChannelData(i)); 50 | } 51 | return buffer; 52 | } 53 | 54 | /* 55 | * ramp audio param 56 | */ 57 | 58 | function ramp(param, fromValue, toValue, duration, linear) { 59 | if (context.isFake) { 60 | return; 61 | } 62 | 63 | param.setValueAtTime(fromValue, context.currentTime); 64 | 65 | if (linear) { 66 | param.linearRampToValueAtTime(toValue, context.currentTime + duration); 67 | } else { 68 | param.exponentialRampToValueAtTime(toValue, context.currentTime + duration); 69 | } 70 | } 71 | 72 | /* 73 | * get frequency from min to max by passing 0 to 1 74 | */ 75 | 76 | function getFrequency(value) { 77 | if (context.isFake) { 78 | return 0; 79 | } 80 | // get frequency by passing number from 0 to 1 81 | // Clamp the frequency between the minimum value (40 Hz) and half of the 82 | // sampling rate. 83 | const minValue = 40; 84 | const maxValue = context.sampleRate / 2; 85 | // Logarithm (base 2) to compute how many octaves fall in the range. 86 | const numberOfOctaves = Math.log(maxValue / minValue) / Math.LN2; 87 | // Compute a multiplier from 0 to 1 based on an exponential scale. 88 | const multiplier = Math.pow(2, numberOfOctaves * (value - 1.0)); 89 | // Get back to the frequency value between min and max. 90 | return maxValue * multiplier; 91 | } 92 | 93 | /* 94 | * Format seconds as timecode string 95 | */ 96 | 97 | function timeCode(seconds, delim = ':') { 98 | // const h = Math.floor(seconds / 3600); 99 | // const m = Math.floor((seconds % 3600) / 60); 100 | const m = Math.floor(seconds / 60); 101 | const s = Math.floor((seconds % 3600) % 60); 102 | // const hr = (h < 10 ? '0' + h + delim : h + delim); 103 | const mn = (m < 10 ? '0' + m : m) + delim; 104 | const sc = (s < 10 ? '0' + s : s); 105 | // return hr + mn + sc; 106 | return mn + sc; 107 | } 108 | 109 | export default { 110 | getOfflineContext, 111 | cloneBuffer, 112 | reverseBuffer, 113 | ramp, 114 | getFrequency, 115 | timeCode 116 | }; 117 | -------------------------------------------------------------------------------- /src/effects/abstract-direct-effect.js: -------------------------------------------------------------------------------- 1 | import context from '../core/context'; 2 | 3 | export default class AbstractDirectEffect { 4 | constructor(node) { 5 | this._node = this._in = this._out = node; 6 | } 7 | 8 | connect(node) { 9 | this._node.connect(node._in || node); 10 | } 11 | 12 | disconnect(...args) { 13 | this._node.disconnect(args); 14 | } 15 | 16 | update() { 17 | throw new Error('update must be overridden'); 18 | } 19 | 20 | get context() { 21 | return context; 22 | } 23 | 24 | get numberOfInputs() { 25 | return 1; 26 | } 27 | 28 | get numberOfOutputs() { 29 | return 1; 30 | } 31 | 32 | get channelCount() { 33 | return 1; 34 | } 35 | 36 | get channelCountMode() { 37 | return 'max'; 38 | } 39 | 40 | get channelInterpretation() { 41 | return 'speakers'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/effects/abstract-effect.js: -------------------------------------------------------------------------------- 1 | import context from '../core/context'; 2 | import isSafeNumber from '../core/utils/isSafeNumber'; 3 | 4 | export default class AbstractEffect { 5 | constructor(node = null, nodeOut = null, enabled = true) { 6 | this._node = node; 7 | this._nodeOut = nodeOut || node; 8 | this._enabled; 9 | 10 | this._in = this.context.createGain(); 11 | this._out = this.context.createGain(); 12 | this._wet = this.context.createGain(); 13 | this._dry = this.context.createGain(); 14 | 15 | this._in.connect(this._dry); 16 | this._wet.connect(this._out); 17 | this._dry.connect(this._out); 18 | 19 | this.enable(enabled); 20 | } 21 | 22 | enable(b) { 23 | if (b === this._enabled) { 24 | return; 25 | } 26 | 27 | this._enabled = b; 28 | 29 | this._in.disconnect(); 30 | 31 | if (b) { 32 | this._in.connect(this._dry); 33 | this._in.connect(this._node); 34 | this._nodeOut.connect(this._wet); 35 | } else { 36 | this._nodeOut.disconnect(); 37 | this._in.connect(this._out); 38 | } 39 | } 40 | 41 | get wet() { 42 | return this._wet.gain.value; 43 | } 44 | 45 | set wet(value) { 46 | this.setSafeParamValue(this._wet.gain, value); 47 | } 48 | 49 | get dry() { 50 | return this._dry.gain.value; 51 | } 52 | 53 | set dry(value) { 54 | this.setSafeParamValue(this._dry.gain, value); 55 | } 56 | 57 | connect(node) { 58 | this._out.connect(node._in || node); 59 | } 60 | 61 | disconnect(...args) { 62 | this._out.disconnect(args); 63 | } 64 | 65 | setSafeParamValue(param, value) { 66 | if (!isSafeNumber(value)) { 67 | console.warn(this, 'Attempt to set invalid value ' + value + ' on AudioParam'); 68 | return; 69 | } 70 | param.value = value; 71 | } 72 | 73 | update() { 74 | throw new Error('update must be overridden'); 75 | } 76 | 77 | get context() { 78 | return context; 79 | } 80 | 81 | get numberOfInputs() { 82 | return 1; 83 | } 84 | 85 | get numberOfOutputs() { 86 | return 1; 87 | } 88 | 89 | get channelCount() { 90 | return 1; 91 | } 92 | 93 | get channelCountMode() { 94 | return 'max'; 95 | } 96 | 97 | get channelInterpretation() { 98 | return 'speakers'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/effects/compressor.js: -------------------------------------------------------------------------------- 1 | import AbstractEffect from './abstract-effect'; 2 | import sono from '../core/sono'; 3 | 4 | class Compressor extends AbstractEffect { 5 | constructor({attack = 0.003, knee = 30, ratio = 12, release = 0.25, threshold = -24, wet = 1, dry = 1} = {}) { 6 | super(sono.context.createDynamicsCompressor()); 7 | 8 | this.wet = wet; 9 | this.dry = dry; 10 | this.update({threshold, knee, ratio, attack, release}); 11 | } 12 | 13 | update(options) { 14 | // min decibels to start compressing at from -100 to 0 15 | this.setSafeParamValue(this._node.threshold, options.threshold); 16 | // decibel value to start curve to compressed value from 0 to 40 17 | this.setSafeParamValue(this._node.knee, options.knee); 18 | // amount of change per decibel from 1 to 20 19 | this.setSafeParamValue(this._node.ratio, options.ratio); 20 | // seconds to reduce gain by 10db from 0 to 1 - how quickly signal adapted when volume increased 21 | this.setSafeParamValue(this._node.attack, options.attack); 22 | // seconds to increase gain by 10db from 0 to 1 - how quickly signal adapted when volume redcuced 23 | this.setSafeParamValue(this._node.release, options.release); 24 | } 25 | 26 | get threshold() { 27 | return this._node.threshold.value; 28 | } 29 | 30 | set threshold(value) { 31 | this.setSafeParamValue(this._node.threshold, value); 32 | } 33 | 34 | get knee() { 35 | return this._node.knee.value; 36 | } 37 | 38 | set knee(value) { 39 | this.setSafeParamValue(this._node.knee, value); 40 | } 41 | 42 | get ratio() { 43 | return this._node.ratio.value; 44 | } 45 | 46 | set ratio(value) { 47 | this.setSafeParamValue(this._node.ratio, value); 48 | } 49 | 50 | get attack() { 51 | return this._node.attack.value; 52 | } 53 | 54 | set attack(value) { 55 | this.setSafeParamValue(this._node.attack, value); 56 | } 57 | 58 | get release() { 59 | return this._node.release.value; 60 | } 61 | 62 | set release(value) { 63 | this.setSafeParamValue(this._node.release, value); 64 | } 65 | } 66 | 67 | export default sono.register('compressor', opts => new Compressor(opts)); 68 | -------------------------------------------------------------------------------- /src/effects/convolver.js: -------------------------------------------------------------------------------- 1 | import AbstractEffect from './abstract-effect'; 2 | import sono from '../core/sono'; 3 | import file from '../core/utils/file'; 4 | import Loader from '../core/utils/loader'; 5 | import Sound from '../core/sound'; 6 | 7 | class Convolver extends AbstractEffect { 8 | constructor({impulse, wet = 1, dry = 1} = {}) { 9 | super(sono.context.createConvolver(), null, false); 10 | 11 | this._loader = null; 12 | 13 | this.wet = wet; 14 | this.dry = dry; 15 | this.update({impulse}); 16 | } 17 | 18 | _load(src) { 19 | if (sono.context.isFake) { 20 | return; 21 | } 22 | if (this._loader) { 23 | this._loader.destroy(); 24 | } 25 | this._loader = new Loader(src); 26 | this._loader.audioContext = sono.context; 27 | this._loader.once('complete', impulse => this.update({impulse})); 28 | this._loader.once('error', error => console.error(error)); 29 | this._loader.start(); 30 | } 31 | 32 | update({impulse}) { 33 | if (!impulse) { 34 | return this; 35 | } 36 | 37 | if (file.isAudioBuffer(impulse)) { 38 | this._node.buffer = impulse; 39 | this.enable(true); 40 | return this; 41 | } 42 | 43 | if (impulse instanceof Sound) { 44 | if (impulse.data) { 45 | this.update({impulse: impulse.data}); 46 | } else { 47 | impulse.once('ready', sound => this.update({ 48 | impulse: sound.data 49 | })); 50 | } 51 | return this; 52 | } 53 | 54 | if (file.isArrayBuffer(impulse)) { 55 | this._load(impulse); 56 | return this; 57 | } 58 | 59 | if (file.isURL(file.getSupportedFile(impulse))) { 60 | this._load(file.getSupportedFile(impulse)); 61 | } 62 | 63 | return this; 64 | } 65 | 66 | get impulse() { 67 | return this._node.buffer; 68 | } 69 | 70 | set impulse(impulse) { 71 | this.update({impulse}); 72 | } 73 | } 74 | 75 | export default sono.register('convolver', opts => new Convolver(opts)); 76 | -------------------------------------------------------------------------------- /src/effects/distortion.js: -------------------------------------------------------------------------------- 1 | import AbstractEffect from './abstract-effect'; 2 | import isSafeNumber from '../core/utils/isSafeNumber'; 3 | import sono from '../core/sono'; 4 | 5 | // up-sample before applying curve for better resolution result 'none', '2x' or '4x' 6 | // oversample: '2x' 7 | // oversample: '4x' 8 | 9 | class Distortion extends AbstractEffect { 10 | constructor({level = 1, samples = 22050, oversample = 'none', wet = 1, dry = 0} = {}) { 11 | super(sono.context.createWaveShaper(), null, false); 12 | 13 | this._node.oversample = oversample || 'none'; 14 | 15 | this._samples = samples || 22050; 16 | 17 | this._curve = new Float32Array(this._samples); 18 | 19 | this._level; 20 | 21 | this._enabled = false; 22 | 23 | this.wet = wet; 24 | this.dry = dry; 25 | this.update({level}); 26 | } 27 | 28 | update({level}) { 29 | if (level === this._level || !isSafeNumber(level)) { 30 | return; 31 | } 32 | 33 | this.enable(level > 0); 34 | 35 | if (!this._enabled) { 36 | return; 37 | } 38 | 39 | const k = level * 100; 40 | const deg = Math.PI / 180; 41 | const y = 2 / this._samples; 42 | 43 | let x; 44 | for (let i = 0; i < this._samples; ++i) { 45 | x = i * y - 1; 46 | this._curve[i] = (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x)); 47 | } 48 | 49 | this._level = level; 50 | this._node.curve = this._curve; 51 | } 52 | 53 | get level() { 54 | return this._level; 55 | } 56 | 57 | set level(level) { 58 | this.update({level}); 59 | } 60 | } 61 | 62 | export default sono.register('distortion', opts => new Distortion(opts)); 63 | -------------------------------------------------------------------------------- /src/effects/echo.js: -------------------------------------------------------------------------------- 1 | import AbstractEffect from './abstract-effect'; 2 | import sono from '../core/sono'; 3 | 4 | class Echo extends AbstractEffect { 5 | constructor({delay = 0.5, feedback = 0.5, wet = 1, dry = 1} = {}) { 6 | super(sono.context.createDelay(), sono.context.createGain()); 7 | 8 | this._delay = this._node; 9 | this._feedback = this._nodeOut; 10 | 11 | this._delay.connect(this._feedback); 12 | this._feedback.connect(this._delay); 13 | 14 | this.wet = wet; 15 | this.dry = dry; 16 | this.update({delay, feedback}); 17 | } 18 | 19 | enable(value) { 20 | super.enable(value); 21 | 22 | if (this._feedback && value) { 23 | this._feedback.connect(this._delay); 24 | } 25 | } 26 | 27 | update(options) { 28 | this.delay = options.delay; 29 | this.feedback = options.feedback; 30 | } 31 | 32 | get delay() { 33 | return this._delay.delayTime.value; 34 | } 35 | 36 | set delay(value) { 37 | this.setSafeParamValue(this._delay.delayTime, value); 38 | } 39 | 40 | get feedback() { 41 | return this._feedback.gain.value; 42 | } 43 | 44 | set feedback(value) { 45 | this.setSafeParamValue(this._feedback.gain, value); 46 | } 47 | } 48 | 49 | export default sono.register('echo', opts => new Echo(opts)); 50 | -------------------------------------------------------------------------------- /src/effects/flanger.js: -------------------------------------------------------------------------------- 1 | import AbstractEffect from './abstract-effect'; 2 | import sono from '../core/sono'; 3 | 4 | class MonoFlanger extends AbstractEffect { 5 | constructor({delay = 0.005, feedback = 0.5, frequency = 0.002, gain = 0.25, wet = 1, dry = 1} = {}) { 6 | super(sono.context.createDelay()); 7 | 8 | this._delay = this._node; 9 | this._feedback = sono.context.createGain(); 10 | this._lfo = sono.context.createOscillator(); 11 | this._gain = sono.context.createGain(); 12 | this._lfo.type = 'sine'; 13 | 14 | this._delay.connect(this._feedback); 15 | this._feedback.connect(this._in); 16 | 17 | this._lfo.connect(this._gain); 18 | this._gain.connect(this._delay.delayTime); 19 | this._lfo.start(0); 20 | 21 | this.wet = wet; 22 | this.dry = dry; 23 | this.update({delay, feedback, frequency, gain}); 24 | } 25 | 26 | update(options) { 27 | this.delay = options.delay; 28 | this.frequency = options.frequency; 29 | this.gain = options.gain; 30 | this.feedback = options.feedback; 31 | } 32 | 33 | get delay() { 34 | return this._delay.delayTime.value; 35 | } 36 | 37 | set delay(value) { 38 | this.setSafeParamValue(this._delay.delayTime, value); 39 | } 40 | 41 | get frequency() { 42 | return this._lfo.frequency.value; 43 | } 44 | 45 | set frequency(value) { 46 | this.setSafeParamValue(this._lfo.frequency, value); 47 | } 48 | 49 | get gain() { 50 | return this._gain.gain.value; 51 | } 52 | 53 | set gain(value) { 54 | this.setSafeParamValue(this._gain.gain, value); 55 | } 56 | 57 | get feedback() { 58 | return this._feedback.gain.value; 59 | } 60 | 61 | set feedback(value) { 62 | this.setSafeParamValue(this._feedback.gain, value); 63 | } 64 | } 65 | 66 | sono.register('monoFlanger', opts => new MonoFlanger(opts)); 67 | 68 | class StereoFlanger extends AbstractEffect { 69 | constructor({delay = 0.003, feedback = 0.5, frequency = 0.5, gain = 0.005, wet = 1, dry = 1} = {}) { 70 | super(sono.context.createChannelSplitter(2), sono.context.createChannelMerger(2)); 71 | 72 | this._splitter = this._node; 73 | this._merger = this._nodeOut; 74 | this._feedbackL = sono.context.createGain(); 75 | this._feedbackR = sono.context.createGain(); 76 | this._lfo = sono.context.createOscillator(); 77 | this._lfoGainL = sono.context.createGain(); 78 | this._lfoGainR = sono.context.createGain(); 79 | this._delayL = sono.context.createDelay(); 80 | this._delayR = sono.context.createDelay(); 81 | 82 | this._lfo.type = 'sine'; 83 | 84 | this._splitter.connect(this._delayL, 0); 85 | this._splitter.connect(this._delayR, 1); 86 | 87 | this._delayL.connect(this._feedbackL); 88 | this._delayR.connect(this._feedbackR); 89 | 90 | this._feedbackL.connect(this._delayR); 91 | this._feedbackR.connect(this._delayL); 92 | 93 | this._delayL.connect(this._merger, 0, 0); 94 | this._delayR.connect(this._merger, 0, 1); 95 | 96 | this._lfo.connect(this._lfoGainL); 97 | this._lfo.connect(this._lfoGainR); 98 | this._lfoGainL.connect(this._delayL.delayTime); 99 | this._lfoGainR.connect(this._delayR.delayTime); 100 | this._lfo.start(0); 101 | 102 | this.wet = wet; 103 | this.dry = dry; 104 | this.update({delay, feedback, frequency, gain}); 105 | } 106 | 107 | update(options) { 108 | this.delay = options.delay; 109 | this.frequency = options.frequency; 110 | this.gain = options.gain; 111 | this.feedback = options.feedback; 112 | } 113 | 114 | get delay() { 115 | return this._delayL.delayTime.value; 116 | } 117 | 118 | set delay(value) { 119 | this.setSafeParamValue(this._delayL.delayTime, value); 120 | this._delayR.delayTime.value = this._delayL.delayTime.value; 121 | } 122 | 123 | get frequency() { 124 | return this._lfo.frequency.value; 125 | } 126 | 127 | set frequency(value) { 128 | this.setSafeParamValue(this._lfo.frequency, value); 129 | } 130 | 131 | get gain() { 132 | return this._lfoGainL.gain.value; 133 | } 134 | 135 | set gain(value) { 136 | this.setSafeParamValue(this._lfoGainL.gain, value); 137 | this._lfoGainR.gain.value = 0 - this._lfoGainL.gain.value; 138 | } 139 | 140 | get feedback() { 141 | return this._feedbackL.gain.value; 142 | } 143 | 144 | set feedback(value) { 145 | this.setSafeParamValue(this._feedbackL.gain, value); 146 | this._feedbackR.gain.value = this._feedbackL.gain.value; 147 | } 148 | } 149 | 150 | sono.register('stereoFlanger', opts => new StereoFlanger(opts)); 151 | 152 | export default sono.register('flanger', (opts = {}) => { 153 | return opts.stereo ? new StereoFlanger(opts) : new MonoFlanger(opts); 154 | }); 155 | -------------------------------------------------------------------------------- /src/effects/index.js: -------------------------------------------------------------------------------- 1 | import analyser from './analyser'; 2 | import compressor from './compressor'; 3 | import convolver from './convolver'; 4 | import distortion from './distortion'; 5 | import echo from './echo'; 6 | import filter from './filter'; 7 | import flanger from './flanger'; 8 | import panner from './panner'; 9 | import phaser from './phaser'; 10 | import reverb from './reverb'; 11 | 12 | export { 13 | analyser, 14 | compressor, 15 | convolver, 16 | distortion, 17 | echo, 18 | filter, 19 | flanger, 20 | panner, 21 | phaser, 22 | reverb 23 | }; 24 | 25 | export default { 26 | analyser, 27 | compressor, 28 | convolver, 29 | distortion, 30 | echo, 31 | filter, 32 | flanger, 33 | panner, 34 | phaser, 35 | reverb 36 | }; 37 | -------------------------------------------------------------------------------- /src/effects/phaser.js: -------------------------------------------------------------------------------- 1 | import AbstractEffect from './abstract-effect'; 2 | import sono from '../core/sono'; 3 | 4 | class Phaser extends AbstractEffect { 5 | constructor({stages = 8, feedback = 0.5, frequency = 0.5, gain = 300, wet = 0.8, dry = 0.8} = {}) { 6 | stages = stages || 8; 7 | 8 | const filters = []; 9 | for (let i = 0; i < stages; i++) { 10 | filters.push(sono.context.createBiquadFilter()); 11 | } 12 | 13 | const first = filters[0]; 14 | const last = filters[filters.length - 1]; 15 | 16 | super(first, last); 17 | 18 | this._stages = stages; 19 | this._feedback = sono.context.createGain(); 20 | this._lfo = sono.context.createOscillator(); 21 | this._lfoGain = sono.context.createGain(); 22 | this._lfo.type = 'sine'; 23 | 24 | for (let i = 0; i < filters.length; i++) { 25 | const filter = filters[i]; 26 | filter.type = 'allpass'; 27 | filter.frequency.value = 1000 * i; 28 | this._lfoGain.connect(filter.frequency); 29 | // filter.Q.value = 10; 30 | 31 | if (i > 0) { 32 | filters[i - 1].connect(filter); 33 | } 34 | } 35 | 36 | this._lfo.connect(this._lfoGain); 37 | this._lfo.start(0); 38 | 39 | this._nodeOut.connect(this._feedback); 40 | this._feedback.connect(this._node); 41 | 42 | this.wet = wet; 43 | this.dry = dry; 44 | this.update({frequency, gain, feedback}); 45 | } 46 | 47 | enable(value) { 48 | super.enable(value); 49 | 50 | if (this._feedback) { 51 | this._feedback.disconnect(); 52 | } 53 | 54 | if (value && this._feedback) { 55 | this._nodeOut.connect(this._feedback); 56 | this._feedback.connect(this._node); 57 | } 58 | } 59 | 60 | update(options) { 61 | this.frequency = options.frequency; 62 | this.gain = options.gain; 63 | this.feedback = options.feedback; 64 | } 65 | 66 | get stages() { 67 | return this._stages; 68 | } 69 | 70 | get frequency() { 71 | return this._lfo.frequency.value; 72 | } 73 | 74 | set frequency(value) { 75 | this.setSafeParamValue(this._lfo.frequency, value); 76 | } 77 | 78 | get gain() { 79 | return this._lfoGain.gain.value; 80 | } 81 | 82 | set gain(value) { 83 | this.setSafeParamValue(this._lfoGain.gain, value); 84 | } 85 | 86 | get feedback() { 87 | return this._feedback.gain.value; 88 | } 89 | 90 | set feedback(value) { 91 | this.setSafeParamValue(this._feedback.gain, value); 92 | } 93 | } 94 | 95 | export default sono.register('phaser', opts => new Phaser(opts)); 96 | -------------------------------------------------------------------------------- /src/effects/reverb.js: -------------------------------------------------------------------------------- 1 | import AbstractEffect from './abstract-effect'; 2 | import sono from '../core/sono'; 3 | import isSafeNumber from '../core/utils/isSafeNumber'; 4 | import isDefined from '../core/utils/isDefined'; 5 | 6 | function createImpulseResponse({time, decay, reverse, buffer}) { 7 | const rate = sono.context.sampleRate; 8 | const length = Math.floor(rate * time); 9 | 10 | let impulseResponse; 11 | 12 | if (buffer && buffer.length === length) { 13 | impulseResponse = buffer; 14 | } else { 15 | impulseResponse = sono.context.createBuffer(2, length, rate); 16 | } 17 | 18 | const left = impulseResponse.getChannelData(0); 19 | const right = impulseResponse.getChannelData(1); 20 | 21 | let n, e; 22 | for (let i = 0; i < length; i++) { 23 | n = reverse ? length - i : i; 24 | e = Math.pow(1 - n / length, decay); 25 | left[i] = (Math.random() * 2 - 1) * e; 26 | right[i] = (Math.random() * 2 - 1) * e; 27 | } 28 | 29 | return impulseResponse; 30 | } 31 | 32 | class Reverb extends AbstractEffect { 33 | constructor({time = 1, decay = 5, reverse = false, wet = 1, dry = 1} = {}) { 34 | super(sono.context.createConvolver()); 35 | 36 | this._convolver = this._node; 37 | 38 | this._length = 0; 39 | this._impulseResponse = null; 40 | this._opts = {}; 41 | 42 | this.wet = wet; 43 | this.dry = dry; 44 | this.update({time, decay, reverse}); 45 | } 46 | 47 | update({time, decay, reverse}) { 48 | let changed = false; 49 | if (time !== this._opts.time && isSafeNumber(time)) { 50 | this._opts.time = time; 51 | changed = true; 52 | } 53 | if (decay !== this._opts.decay && isSafeNumber(decay)) { 54 | this._opts.decay = decay; 55 | changed = true; 56 | } 57 | if (isDefined(reverse) && reverse !== this._reverse) { 58 | this._opts.reverse = reverse; 59 | changed = true; 60 | } 61 | if (!changed) { 62 | return; 63 | } 64 | 65 | this._opts.buffer = time <= 0 ? null : createImpulseResponse(this._opts); 66 | this._convolver.buffer = this._opts.buffer; 67 | } 68 | 69 | get time() { 70 | return this._opts.time; 71 | } 72 | 73 | set time(value) { 74 | this.update({time: value}); 75 | } 76 | 77 | get decay() { 78 | return this._opts.decay; 79 | } 80 | 81 | set decay(value) { 82 | this.update({decay: value}); 83 | } 84 | 85 | get reverse() { 86 | return this._opts.reverse; 87 | } 88 | 89 | set reverse(value) { 90 | this.update({reverse: value}); 91 | } 92 | } 93 | 94 | export default sono.register('reverb', opts => new Reverb(opts)); 95 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import sono from './core/sono'; 2 | import './effects'; 3 | import './utils'; 4 | 5 | export default sono; 6 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import microphone from './microphone'; 2 | import recorder from './recorder'; 3 | import waveform from './waveform'; 4 | import waveformer from './waveformer'; 5 | 6 | export { 7 | microphone, 8 | recorder, 9 | waveform, 10 | waveformer 11 | }; 12 | 13 | export default { 14 | microphone, 15 | recorder, 16 | waveform, 17 | waveformer 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/microphone.js: -------------------------------------------------------------------------------- 1 | import sono from '../core/sono'; 2 | 3 | function microphone(connected, denied, error) { 4 | navigator.getUserMedia = 5 | navigator.mediaDevices.getUserMedia || 6 | navigator.getUserMedia || 7 | navigator.webkitGetUserMedia || 8 | navigator.mozGetUserMedia || 9 | navigator.msGetUserMedia; 10 | 11 | error = error || function(err) { 12 | console.error(err); 13 | }; 14 | 15 | const isSupported = !!navigator.getUserMedia; 16 | const api = {}; 17 | let stream = null; 18 | 19 | function onConnect(micStream) { 20 | stream = micStream; 21 | connected(stream); 22 | } 23 | 24 | function onError(e) { 25 | if (denied && e.name === 'PermissionDeniedError' || e === 'PERMISSION_DENIED') { 26 | denied(); 27 | } else { 28 | error(e.message || e); 29 | } 30 | } 31 | 32 | function connect() { 33 | if (!isSupported) { 34 | return api; 35 | } 36 | 37 | if (navigator.mediaDevices.getUserMedia) { 38 | navigator.mediaDevices.getUserMedia({ 39 | audio: true 40 | }).then(onConnect).catch(onError); 41 | } else { 42 | navigator.getUserMedia({ 43 | audio: true 44 | }, onConnect, onError); 45 | } 46 | return api; 47 | } 48 | 49 | function disconnect() { 50 | if (stream.stop) { 51 | stream.stop(); 52 | } else { 53 | stream.getAudioTracks()[0].stop(); 54 | } 55 | stream = null; 56 | return api; 57 | } 58 | 59 | return Object.assign(api, { 60 | connect, 61 | disconnect, 62 | isSupported, 63 | get stream() { 64 | return stream; 65 | } 66 | }); 67 | } 68 | 69 | export default sono.register('microphone', microphone, sono.utils); 70 | -------------------------------------------------------------------------------- /src/utils/recorder.js: -------------------------------------------------------------------------------- 1 | import sono from '../core/sono'; 2 | 3 | function recorder(passThrough = false) { 4 | const bufferLength = 4096; 5 | const buffersL = []; 6 | const buffersR = []; 7 | let startedAt = 0; 8 | let stoppedAt = 0; 9 | let script = null; 10 | let isRecording = false; 11 | let soundOb = null; 12 | 13 | const input = sono.context.createGain(); 14 | const output = sono.context.createGain(); 15 | output.gain.value = passThrough ? 1 : 0; 16 | 17 | const node = { 18 | _in: input, 19 | _out: output, 20 | connect(n) { 21 | output.connect(n._in || n); 22 | }, 23 | disconnect(...args) { 24 | output.disconnect(args); 25 | } 26 | }; 27 | 28 | function mergeBuffers(buffers, length) { 29 | const buffer = new Float32Array(length); 30 | let offset = 0; 31 | for (let i = 0; i < buffers.length; i++) { 32 | buffer.set(buffers[i], offset); 33 | offset += buffers[i].length; 34 | } 35 | return buffer; 36 | } 37 | 38 | function getBuffer() { 39 | if (!buffersL.length) { 40 | return sono.context.createBuffer(2, bufferLength, sono.context.sampleRate); 41 | } 42 | const recordingLength = buffersL.length * bufferLength; 43 | const buffer = sono.context.createBuffer(2, recordingLength, sono.context.sampleRate); 44 | buffer.getChannelData(0) 45 | .set(mergeBuffers(buffersL, recordingLength)); 46 | buffer.getChannelData(1) 47 | .set(mergeBuffers(buffersR, recordingLength)); 48 | return buffer; 49 | } 50 | 51 | function destroyScriptProcessor() { 52 | if (script) { 53 | script.onaudioprocess = null; 54 | input.disconnect(); 55 | script.disconnect(); 56 | } 57 | } 58 | 59 | function createScriptProcessor() { 60 | destroyScriptProcessor(); 61 | 62 | script = sono.context.createScriptProcessor(bufferLength, 2, 2); 63 | input.connect(script); 64 | script.connect(output); 65 | script.connect(sono.context.destination); 66 | // output.connect(sono.context.destination); 67 | 68 | 69 | script.onaudioprocess = function(event) { 70 | const inputL = event.inputBuffer.getChannelData(0); 71 | const inputR = event.inputBuffer.getChannelData(1); 72 | 73 | if (passThrough) { 74 | const outputL = event.outputBuffer.getChannelData(0); 75 | const outputR = event.outputBuffer.getChannelData(1); 76 | outputL.set(inputL); 77 | outputR.set(inputR); 78 | } 79 | 80 | if (isRecording) { 81 | buffersL.push(new Float32Array(inputL)); 82 | buffersR.push(new Float32Array(inputR)); 83 | } 84 | }; 85 | } 86 | 87 | return { 88 | start(sound) { 89 | if (!sound) { 90 | return; 91 | } 92 | createScriptProcessor(); 93 | buffersL.length = 0; 94 | buffersR.length = 0; 95 | startedAt = sono.context.currentTime; 96 | stoppedAt = 0; 97 | soundOb = sound; 98 | sound.effects.add(node); 99 | isRecording = true; 100 | }, 101 | stop() { 102 | soundOb.effects.remove(node); 103 | soundOb = null; 104 | stoppedAt = sono.context.currentTime; 105 | isRecording = false; 106 | destroyScriptProcessor(); 107 | return getBuffer(); 108 | }, 109 | getDuration() { 110 | if (!isRecording) { 111 | return stoppedAt - startedAt; 112 | } 113 | return sono.context.currentTime - startedAt; 114 | }, 115 | get isRecording() { 116 | return isRecording; 117 | } 118 | }; 119 | } 120 | 121 | export default sono.register('recorder', recorder, sono.utils); 122 | -------------------------------------------------------------------------------- /src/utils/waveform.js: -------------------------------------------------------------------------------- 1 | import sono from '../core/sono'; 2 | 3 | function waveform() { 4 | let buffer, 5 | wave; 6 | 7 | return function(audioBuffer, length) { 8 | if (!window.Float32Array || !window.AudioBuffer) { 9 | return []; 10 | } 11 | 12 | const sameBuffer = buffer === audioBuffer; 13 | const sameLength = wave && wave.length === length; 14 | if (sameBuffer && sameLength) { 15 | return wave; 16 | } 17 | 18 | wave = new Float32Array(length); 19 | 20 | if (!audioBuffer) { 21 | return wave; 22 | } 23 | 24 | // cache for repeated calls 25 | buffer = audioBuffer; 26 | 27 | const chunk = Math.floor(buffer.length / length), 28 | resolution = 5, // 10 29 | incr = Math.max(Math.floor(chunk / resolution), 1); 30 | let greatest = 0; 31 | 32 | for (let i = 0; i < buffer.numberOfChannels; i++) { 33 | // check each channel 34 | const channel = buffer.getChannelData(i); 35 | for (let j = 0; j < length; j++) { 36 | // get highest value within the chunk 37 | for (let k = j * chunk, l = k + chunk; k < l; k += incr) { 38 | // select highest value from channels 39 | let a = channel[k]; 40 | if (a < 0) { 41 | a = -a; 42 | } 43 | if (a > wave[j]) { 44 | wave[j] = a; 45 | } 46 | // update highest overall for scaling 47 | if (a > greatest) { 48 | greatest = a; 49 | } 50 | } 51 | } 52 | } 53 | // scale up 54 | const scale = 1 / greatest; 55 | for (let i = 0; i < wave.length; i++) { 56 | wave[i] *= scale; 57 | } 58 | 59 | return wave; 60 | }; 61 | } 62 | 63 | export default sono.register('waveform', waveform, sono.utils); 64 | -------------------------------------------------------------------------------- /src/utils/waveformer.js: -------------------------------------------------------------------------------- 1 | import sono from '../core/sono'; 2 | 3 | const halfPI = Math.PI / 2; 4 | const twoPI = Math.PI * 2; 5 | 6 | function waveformer(config) { 7 | 8 | const style = config.style || 'fill', // 'fill' or 'line' 9 | shape = config.shape || 'linear', // 'circular' or 'linear' 10 | color = config.color || 0, 11 | bgColor = config.bgColor, 12 | lineWidth = config.lineWidth || 1, 13 | percent = config.percent || 1, 14 | originX = config.x || 0, 15 | originY = config.y || 0, 16 | transform = config.transform; 17 | 18 | let canvas = config.canvas, 19 | width = config.width || (canvas && canvas.width), 20 | height = config.height || (canvas && canvas.height); 21 | 22 | let ctx = null, currentColor, i, x, y, 23 | radius, innerRadius, centerX, centerY; 24 | 25 | if (!canvas && !config.context) { 26 | canvas = document.createElement('canvas'); 27 | width = width || canvas.width; 28 | height = height || canvas.height; 29 | canvas.width = width; 30 | canvas.height = height; 31 | } 32 | 33 | if (shape === 'circular') { 34 | radius = config.radius || Math.min(height / 2, width / 2); 35 | innerRadius = config.innerRadius || radius / 2; 36 | centerX = originX + width / 2; 37 | centerY = originY + height / 2; 38 | } 39 | 40 | ctx = config.context || canvas.getContext('2d'); 41 | 42 | function clear() { 43 | if (bgColor) { 44 | ctx.fillStyle = bgColor; 45 | ctx.fillRect(originX, originY, width, height); 46 | } else { 47 | ctx.clearRect(originX, originY, width, height); 48 | } 49 | 50 | ctx.lineWidth = lineWidth; 51 | 52 | currentColor = null; 53 | 54 | if (typeof color !== 'function') { 55 | ctx.strokeStyle = color; 56 | ctx.beginPath(); 57 | } 58 | } 59 | 60 | function updateColor(position, length, value) { 61 | if (typeof color === 'function') { 62 | const newColor = color(position, length, value); 63 | if (newColor !== currentColor) { 64 | currentColor = newColor; 65 | ctx.stroke(); 66 | ctx.strokeStyle = currentColor; 67 | ctx.beginPath(); 68 | } 69 | } 70 | } 71 | 72 | function getValue(value, position, length) { 73 | if (typeof transform === 'function') { 74 | return transform(value, position, length); 75 | } 76 | return value; 77 | } 78 | 79 | function getWaveform(value, length) { 80 | if (value && typeof value.waveform === 'function') { 81 | return value.waveform(length); 82 | } 83 | if (value) { 84 | return value; 85 | } 86 | if (config.waveform) { 87 | return config.waveform; 88 | } 89 | if (config.sound) { 90 | return config.sound.waveform(length); 91 | } 92 | return null; 93 | } 94 | 95 | function update(wave) { 96 | 97 | clear(); 98 | 99 | if (shape === 'circular') { 100 | const waveform = getWaveform(wave, 360); 101 | const length = Math.floor(waveform.length * percent); 102 | 103 | const step = twoPI / length; 104 | let angle, magnitude, sine, cosine; 105 | 106 | for (i = 0; i < length; i++) { 107 | const value = getValue(waveform[i], i, length); 108 | updateColor(i, length, value); 109 | 110 | angle = i * step - halfPI; 111 | cosine = Math.cos(angle); 112 | sine = Math.sin(angle); 113 | 114 | if (style === 'fill') { 115 | x = centerX + innerRadius * cosine; 116 | y = centerY + innerRadius * sine; 117 | ctx.moveTo(x, y); 118 | } 119 | 120 | magnitude = innerRadius + (radius - innerRadius) * value; 121 | x = centerX + magnitude * cosine; 122 | y = centerY + magnitude * sine; 123 | 124 | if (style === 'line' && i === 0) { 125 | ctx.moveTo(x, y); 126 | } 127 | 128 | ctx.lineTo(x, y); 129 | } 130 | 131 | if (style === 'line') { 132 | ctx.closePath(); 133 | } 134 | } else { 135 | 136 | const waveform = getWaveform(wave, width); 137 | const maxX = width - lineWidth / 2; 138 | let length = Math.min(waveform.length, maxX); 139 | length = Math.floor(length * percent); 140 | const stepX = maxX / length; 141 | 142 | for (i = 0; i < length; i++) { 143 | const value = getValue(waveform[i], i, length); 144 | updateColor(i, length, value); 145 | 146 | if (style === 'line' && i > 0) { 147 | ctx.lineTo(x, y); 148 | } 149 | 150 | x = originX + i * stepX; 151 | y = originY + height - Math.round(height * value); 152 | y = Math.floor(Math.min(y, originY + height - lineWidth / 2)); 153 | 154 | if (style === 'fill') { 155 | x = Math.ceil(x + lineWidth / 2); 156 | ctx.moveTo(x, y); 157 | ctx.lineTo(x, originY + height); 158 | } else { 159 | ctx.lineTo(x, y); 160 | } 161 | } 162 | } 163 | ctx.stroke(); 164 | } 165 | 166 | update.canvas = canvas; 167 | 168 | if (config.waveform || config.sound) { 169 | update(); 170 | } 171 | 172 | return update; 173 | } 174 | 175 | export default sono.register('waveformer', waveformer, sono.utils); 176 | -------------------------------------------------------------------------------- /test/api.spec.js: -------------------------------------------------------------------------------- 1 | describe('sono API', () => { 2 | 3 | describe('misc', () => { 4 | it('should exist', () => { 5 | expect(sono).to.be.an('object'); 6 | }); 7 | 8 | it('should have context property', () => { 9 | expect(sono).to.have.property('context'); 10 | }); 11 | 12 | it('should have hasWebAudio bool', () => { 13 | expect(sono.hasWebAudio).to.be.a('boolean'); 14 | }); 15 | 16 | it('should have isSupported bool', () => { 17 | expect(sono.isSupported).to.be.a('boolean'); 18 | }); 19 | 20 | it('should have VERSION string', () => { 21 | expect(sono.VERSION).to.be.a('string'); 22 | }); 23 | 24 | it('should have log func', () => { 25 | expect(sono.log).to.be.a('function'); 26 | }); 27 | 28 | it('should have log playInBackground getter/setter', () => { 29 | expect(sono.playInBackground).to.be.a('boolean'); 30 | }); 31 | }); 32 | 33 | describe('create', () => { 34 | it('should have expected api', () => { 35 | expect(sono.create).to.be.a('function'); 36 | expect(sono.create.length).to.eql(1); 37 | }); 38 | 39 | it('should return new Sound', () => { 40 | const sound = sono.createSound({ 41 | id: 'newsoundA', 42 | data: new Audio() 43 | }); 44 | expect(sound).to.exist; 45 | expect(sound.id).to.eql('newsoundA'); 46 | expect(sono.getSound(sound.id)).to.exist; 47 | }); 48 | }); 49 | 50 | describe('destroy', () => { 51 | it('should have expected api', () => { 52 | expect(sono.destroy).to.be.a('function'); 53 | expect(sono.destroy.length).to.eql(1); 54 | }); 55 | 56 | it('should destroy existing sound by id', () => { 57 | sono.createSound({ 58 | id: 'killme', 59 | data: new Audio() 60 | }); 61 | expect(sono.getSound('killme')).to.exist; 62 | sono.destroy('killme'); 63 | expect(sono.getSound('killme')).to.not.exist; 64 | }); 65 | 66 | it('should destroy existing sound by instance', () => { 67 | const sound = sono.createSound({ 68 | id: 'killmeagain', 69 | data: new Audio() 70 | }); 71 | expect(sound).to.exist; 72 | sono.destroy(sound); 73 | expect(sono.getSound('killmeagain')).to.not.exist; 74 | }); 75 | }); 76 | 77 | describe('getSound', () => { 78 | it('should have expected api', () => { 79 | expect(sono.getSound).to.be.a('function'); 80 | expect(sono.getSound.length).to.eql(1); 81 | }); 82 | 83 | it('should return existing sound', () => { 84 | sono.createSound({ 85 | id: 'yep', 86 | data: new Audio() 87 | }); 88 | expect(sono.getSound('yep')).to.exist; 89 | expect(sono.getSound('yep').id).to.eql('yep'); 90 | }); 91 | 92 | it('should return null for non-existant sound', () => { 93 | expect(sono.getSound('nope')).to.not.exist; 94 | }); 95 | }); 96 | 97 | describe('controls', () => { 98 | it('should have expected members', () => { 99 | expect(sono.mute).to.be.a('function'); 100 | expect(sono.unMute).to.be.a('function'); 101 | expect(sono.pauseAll).to.be.a('function'); 102 | expect(sono.resumeAll).to.be.a('function'); 103 | expect(sono.stopAll).to.be.a('function'); 104 | expect(sono.play).to.be.a('function'); 105 | expect(sono.pause).to.be.a('function'); 106 | expect(sono.stop).to.be.a('function'); 107 | expect(sono.volume).to.be.a('number'); 108 | expect(sono.fade).to.be.a('function'); 109 | }); 110 | 111 | it('should have get/set volume', () => { 112 | const desc = Object.getOwnPropertyDescriptor(sono, 'volume'); 113 | expect(desc.get).to.be.a('function'); 114 | expect(desc.set).to.be.a('function'); 115 | }); 116 | }); 117 | 118 | describe('load', () => { 119 | it('should have expected api', () => { 120 | expect(sono.load).to.be.a('function'); 121 | expect(sono.load.length).to.eql(1); 122 | }); 123 | }); 124 | 125 | describe('canPlay', () => { 126 | it('should have expected members', () => { 127 | expect(sono.canPlay).to.be.an('object'); 128 | expect(sono.canPlay.ogg).to.be.a('boolean'); 129 | expect(sono.canPlay.mp3).to.be.a('boolean'); 130 | expect(sono.canPlay.opus).to.be.a('boolean'); 131 | expect(sono.canPlay.wav).to.be.a('boolean'); 132 | expect(sono.canPlay.m4a).to.be.a('boolean'); 133 | }); 134 | }); 135 | 136 | describe('effects', () => { 137 | it('should have effects module', () => { 138 | expect(sono.effects).to.exist; 139 | }); 140 | }); 141 | 142 | describe('utils', () => { 143 | it('should have utils module', () => { 144 | expect(sono.utils).to.be.an('object'); 145 | }); 146 | }); 147 | 148 | }); 149 | -------------------------------------------------------------------------------- /test/audio/blip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/blip.mp3 -------------------------------------------------------------------------------- /test/audio/blip.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/blip.ogg -------------------------------------------------------------------------------- /test/audio/bloop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/bloop.mp3 -------------------------------------------------------------------------------- /test/audio/bloop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/bloop.ogg -------------------------------------------------------------------------------- /test/audio/long.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/long.mp3 -------------------------------------------------------------------------------- /test/audio/long.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stinkstudios/sono/50fbea6b51d7015496539b27b3e568d08faef0e8/test/audio/long.ogg -------------------------------------------------------------------------------- /test/destroy.spec.js: -------------------------------------------------------------------------------- 1 | describe('Destroy', () => { 2 | 3 | let sound; 4 | 5 | beforeEach(() => { 6 | sono.destroyAll(); 7 | }); 8 | 9 | it('should have one sound', () => { 10 | sound = sono.createSound({ 11 | id: 'sine', 12 | type: 'sine' 13 | }); 14 | expect(sono.sounds.length).to.eql(1); 15 | }); 16 | 17 | it('should have zero sounds', () => { 18 | sound.destroy(); 19 | expect(sono.sounds.length).to.eql(0); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/file.spec.js: -------------------------------------------------------------------------------- 1 | describe('file', function() { 2 | 3 | const el = document.createElement('audio'); 4 | const file = sono.file; 5 | 6 | it('should get audio type', function() { 7 | expect(file.isAudioBuffer(el)).to.be.false; 8 | expect(file.isMediaElement(el)).to.be.true; 9 | }); 10 | 11 | it('should get file extension', function() { 12 | expect(file.getFileExtension).to.be.a('function'); 13 | expect(file.getFileExtension('audio/foo.ogg')).to.eql('ogg'); 14 | expect(file.getFileExtension('audio/foo.ogg?foo=bar')).to.eql('ogg'); 15 | expect(file.getFileExtension('./audio/foo.ogg')).to.eql('ogg'); 16 | expect(file.getFileExtension('../audio/foo.ogg')).to.eql('ogg'); 17 | expect(file.getFileExtension('../../audio/foo')).to.eql(''); 18 | expect(file.getFileExtension('../../audio/foo.ogg')).to.eql('ogg'); 19 | expect(file.getFileExtension('http://www.example.com/audio/foo.ogg')).to.eql('ogg'); 20 | expect(file.getFileExtension('http://www.example.com/audio/foo.ogg?foo=bar')).to.eql('ogg'); 21 | expect(file.getFileExtension('data:audio/ogg;base64,T2dnUwAC')).to.eql('ogg'); 22 | expect(file.getFileExtension('data:audio/mp3;base64,T2dnUwAC')).to.eql('mp3'); 23 | }); 24 | 25 | it('should get file', function() { 26 | expect(file.extensions).to.be.an('array'); 27 | expect(file.extensions.length).to.be.at.least(1); 28 | expect(file.getSupportedFile).to.be.a('function'); 29 | expect(file.getSupportedFile(['audio/foo.ogg', 'audio/foo.mp3'])).to.be.a('string'); 30 | expect(file.getSupportedFile({foo: 'audio/foo.ogg', bar: 'audio/foo.mp3'})).to.be.a('string'); 31 | expect(file.getSupportedFile('audio/foo.ogg')).to.be.a('string'); 32 | expect(file.getSupportedFile('data:audio/ogg;base64,T2dnUwAC')).to.be.a('string'); 33 | }); 34 | 35 | it('should have canPlay hash', function() { 36 | expect(file.canPlay).to.be.an('object'); 37 | expect(file.canPlay.ogg).to.be.a('boolean'); 38 | expect(file.canPlay.mp3).to.be.a('boolean'); 39 | expect(file.canPlay.opus).to.be.a('boolean'); 40 | expect(file.canPlay.wav).to.be.a('boolean'); 41 | expect(file.canPlay.m4a).to.be.a('boolean'); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /test/group.spec.js: -------------------------------------------------------------------------------- 1 | describe('Group', () => { 2 | 3 | describe('group', () => { 4 | it('should have expected api', () => { 5 | expect(sono.group).to.be.a('function'); 6 | expect(sono.group.length).to.eql(1); 7 | }); 8 | it('should return new Group', () => { 9 | const group = sono.group(); 10 | expect(group).to.exist; 11 | }); 12 | }); 13 | 14 | describe('add sound', () => { 15 | let sound; 16 | 17 | beforeEach((done) => { 18 | sono.load({ 19 | id: 'foo', 20 | url: [ 21 | '/base/test/audio/blip.ogg', 22 | '/base/test/audio/blip.mp3' 23 | ], 24 | onComplete: function(s) { 25 | sound = s; 26 | done(); 27 | } 28 | }); 29 | }); 30 | 31 | afterEach(() => { 32 | sono.destroy(sound.id); 33 | }); 34 | 35 | it('should return new Group', () => { 36 | const group = sono.group(); 37 | expect(group).to.exist; 38 | group.add(sound); 39 | expect(group.sounds.length).to.eql(1); 40 | }); 41 | }); 42 | 43 | describe('control', () => { 44 | const group = sono.group(); 45 | 46 | it('should have zero volume', () => { 47 | group.volume = 0; 48 | expect(group.volume).to.eql(0); 49 | }); 50 | 51 | it('should have 1 volume', () => { 52 | group.volume = 1; 53 | expect(group.volume).to.eql(1); 54 | }); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | console.log('Running tests with Web Audio API?', !!(window.AudioContext || window.webkitAudioContext)); 2 | -------------------------------------------------------------------------------- /test/is-travis.js: -------------------------------------------------------------------------------- 1 | window.isTravis = true; 2 | -------------------------------------------------------------------------------- /test/kill-wa.js: -------------------------------------------------------------------------------- 1 | window.AudioContext = window.webkitAudioContext = undefined; 2 | // window.MediaElementAudioSourceNode = Object; 3 | // window.AudioNode = Object; 4 | // window.AudioBuffer = Object; 5 | -------------------------------------------------------------------------------- /test/playback.spec.js: -------------------------------------------------------------------------------- 1 | describe('sono playback', () => { 2 | 3 | describe('create', () => { 4 | const config = { 5 | id: 'playback-create', 6 | url: [ 7 | '/base/test/audio/blip.ogg', 8 | '/base/test/audio/blip.mp3' 9 | ] 10 | }; 11 | 12 | let sound; 13 | 14 | beforeEach((done) => { 15 | sound = sono.create(config) 16 | .on('error', (s, err) => console.error('error', err, s)) 17 | .on('loaded', () => console.log('loaded')) 18 | .on('ready', () => console.log('ready')) 19 | .on('play', () => console.log('play')) 20 | .on('ended', () => done()); 21 | sound.play(); 22 | }); 23 | 24 | afterEach(() => { 25 | sono.destroy(sound.id); 26 | }); 27 | 28 | it('should get ended callback', () => { 29 | expect(sound).to.exist; 30 | }); 31 | }); 32 | 33 | describe('play and end', () => { 34 | let sound, 35 | ended = false; 36 | 37 | beforeEach((done) => { 38 | function onComplete(loadedSound) { 39 | sound = loadedSound 40 | .on('error', (s, err) => console.error('error', err, s)) 41 | .on('loaded', () => console.log('loaded')) 42 | .on('ready', () => console.log('ready')) 43 | .on('play', () => console.log('play')) 44 | .on('ended', () => { 45 | ended = true; 46 | done(); 47 | }); 48 | sound.play(); 49 | } 50 | sono.load({ 51 | url: [ 52 | '/base/test/audio/blip.ogg', 53 | '/base/test/audio/blip.mp3' 54 | ], 55 | onComplete: onComplete, 56 | onError: function(err) { 57 | console.error(err); 58 | } 59 | }); 60 | }); 61 | 62 | afterEach(() => { 63 | sono.destroy(sound.id); 64 | }); 65 | 66 | it('should get ended callback', () => { 67 | expect(sound).to.exist; 68 | expect(ended).to.be.true; 69 | }); 70 | }); 71 | 72 | describe('play when ready', () => { 73 | let sound, 74 | ended = false; 75 | 76 | beforeEach((done) => { 77 | sound = sono.create({ 78 | url: [ 79 | '/base/test/audio/blip.ogg', 80 | '/base/test/audio/blip.mp3' 81 | ] 82 | }) 83 | .on('error', (s, err) => console.error('error', err, s)) 84 | .on('loaded', () => console.log('loaded')) 85 | .on('ready', () => console.log('ready')) 86 | .on('play', () => console.log('play')) 87 | .on('ended', () => { 88 | ended = true; 89 | done(); 90 | }) 91 | .play(0.1, 0.1); 92 | }); 93 | 94 | afterEach(() => { 95 | sono.destroy(sound); 96 | }); 97 | 98 | it('should have played', () => { 99 | expect(sound).to.exist; 100 | expect(ended).to.be.true; 101 | }); 102 | }); 103 | 104 | describe('currentTime and duration', () => { 105 | let sound = null; 106 | let ended = false; 107 | 108 | beforeEach((done) => { 109 | sound = sono.create({ 110 | url: [ 111 | '/base/test/audio/blip.ogg', 112 | '/base/test/audio/blip.mp3' 113 | ], 114 | loop: true 115 | }) 116 | .on('error', (s, err) => console.error('error', err, s)) 117 | .on('loaded', () => console.log('loaded')) 118 | .on('ready', () => console.log('ready')) 119 | .on('play', () => { 120 | window.setTimeout(() => done(), 1000); 121 | }) 122 | .on('ended', () => { 123 | ended = true; 124 | }) 125 | .play(); 126 | }); 127 | 128 | afterEach(() => { 129 | sono.destroy(sound); 130 | }); 131 | 132 | it('should get duration above 0 and currentTime below duration', () => { 133 | expect(sound).to.exist; 134 | expect(sound.playing).to.be.true; 135 | expect(ended).to.be.false; 136 | expect(sound.currentTime).to.be.a('number'); 137 | expect(sound.duration).to.be.a('number'); 138 | expect(sound.duration).to.be.above(0); 139 | expect(sound.currentTime).to.be.below(sound.duration + 0.01); 140 | }); 141 | }); 142 | 143 | }); 144 | -------------------------------------------------------------------------------- /test/seek.spec.js: -------------------------------------------------------------------------------- 1 | describe('seek', () => { 2 | 3 | let sound = null; 4 | 5 | describe('after load', () => { 6 | const config = { 7 | id: 'seek-after', 8 | url: [ 9 | '/base/test/audio/long.ogg', 10 | '/base/test/audio/long.mp3' 11 | ] 12 | }; 13 | beforeEach((done) => { 14 | sound = sono.create(config) 15 | .on('error', (snd, err) => console.error('error', err, snd)) 16 | .on('loaded', () => console.log('loaded')) 17 | .on('ready', () => done()); 18 | }); 19 | 20 | afterEach(() => { 21 | sono.destroy(sound); 22 | }); 23 | 24 | it('should set currentTime to 1', () => { 25 | sound.currentTime = 1; 26 | expect(sound.currentTime).to.eql(1); 27 | }); 28 | 29 | it('should set seek to 1', () => { 30 | sound.seek(1); 31 | expect(sound.currentTime).to.eql(1); 32 | }); 33 | 34 | it('should set currentTime to 0', () => { 35 | sound.currentTime = 0; 36 | expect(sound.currentTime).to.eql(0); 37 | }); 38 | 39 | it('should not auto play after seek', (done) => { 40 | sound.currentTime = 1; 41 | setTimeout(() => { 42 | expect(sound.currentTime).to.eql(1); 43 | expect(sound.playing).to.be.false; 44 | done(); 45 | }, 500); 46 | }); 47 | 48 | it('should jump to 1 and continue playing', (done) => { 49 | expect(sound.currentTime).to.eql(0); 50 | sound.play(); 51 | sound.currentTime = 1; 52 | setTimeout(() => { 53 | expect(sound.playing).to.be.true; 54 | expect(sound.currentTime).to.be.at.least(1); 55 | done(); 56 | }, 500); 57 | }); 58 | }); 59 | 60 | describe('before load', () => { 61 | const config = { 62 | id: 'seek-before', 63 | url: [ 64 | '/base/test/audio/long.ogg', 65 | '/base/test/audio/long.mp3' 66 | ], 67 | deferLoad: true 68 | }; 69 | beforeEach((done) => { 70 | sound = sono.create(config) 71 | .on('error', (snd, err) => console.error('error', err, snd)); 72 | done(); 73 | }); 74 | 75 | afterEach(() => { 76 | sono.destroy(sound); 77 | }); 78 | 79 | it('should set currentTime to 1', () => { 80 | sound.currentTime = 1; 81 | expect(sound.currentTime).to.eql(1); 82 | }); 83 | 84 | it('should set seek to 1', () => { 85 | sound.seek(1); 86 | expect(sound.currentTime).to.eql(1); 87 | }); 88 | 89 | it('should set currentTime to 0', () => { 90 | sound.currentTime = 0; 91 | expect(sound.currentTime).to.eql(0); 92 | }); 93 | 94 | it('should start from seeked time when loaded', (done) => { 95 | sound.currentTime = 1; 96 | sound.on('play', () => { 97 | expect(sound.currentTime).to.be.at.least(1); 98 | done(); 99 | }); 100 | sound.play(); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/sound.spec.js: -------------------------------------------------------------------------------- 1 | describe('Sound', () => { 2 | const sound = new sono.__test.Sound({ 3 | context: sono.context, 4 | deferLoad: true 5 | }); 6 | 7 | it('should have id property', () => { 8 | expect(sound).to.have.property('id'); 9 | sound.id = 'some-id'; 10 | expect(sound.id).to.eql('some-id'); 11 | }); 12 | 13 | it('should have data property', () => { 14 | expect(sound).to.have.property('data'); 15 | }); 16 | 17 | it('should have expected members (controls)', () => { 18 | expect(sound.play).to.be.a('function'); 19 | expect(sound.pause).to.be.a('function'); 20 | expect(sound.stop).to.be.a('function'); 21 | expect(sound.volume).to.be.a('number'); 22 | expect(sound.playbackRate).to.be.a('number'); 23 | expect(sono.fade).to.be.a('function'); 24 | }); 25 | 26 | it('should have expected members (state)', () => { 27 | expect(sound.loop).to.be.a('boolean'); 28 | expect(sound.duration).to.be.a('number'); 29 | expect(sound.currentTime).to.be.a('number'); 30 | expect(sound.progress).to.be.a('number'); 31 | expect(sound.playing).to.be.a('boolean'); 32 | expect(sound.paused).to.be.a('boolean'); 33 | }); 34 | 35 | it('should have expected members (effects)', () => { 36 | expect(sound.effects).to.exist; 37 | expect(sound.effects.add).to.be.a('function'); 38 | expect(sound.effects.remove).to.be.a('function'); 39 | expect(sound.effects.toggle).to.be.a('function'); 40 | expect(sound.effects.removeAll).to.be.a('function'); 41 | }); 42 | 43 | it('should have chainable methods', () => { 44 | const a = sound.play() 45 | .pause() 46 | .load() 47 | .stop() 48 | .fade(1) 49 | .play(); 50 | 51 | expect(a).to.be.an('object'); 52 | expect(a.currentTime).to.be.a('number'); 53 | }); 54 | 55 | it('should have event emitter', () => { 56 | expect(sound.on).to.be.a('function'); 57 | expect(sound.off).to.be.a('function'); 58 | expect(sound.once).to.be.a('function'); 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | describe('utils', () => { 2 | const utils = sono.utils; 3 | 4 | describe('buffer', () => { 5 | const expectedAudioBufferType = sono.hasWebAudio ? window.AudioBuffer : Object; 6 | const expectedValue = sono.hasWebAudio ? 1 : 0; 7 | const buffer = sono.context.createBuffer(1, 4096, sono.context.sampleRate); 8 | 9 | it('should clone buffer', () => { 10 | const cloned = utils.cloneBuffer(buffer); 11 | expect(cloned).to.be.an.instanceof(expectedAudioBufferType); 12 | expect(cloned).to.eql(buffer); 13 | }); 14 | 15 | it('should reverse buffer', () => { 16 | const data = buffer.getChannelData(0); 17 | data[0] = expectedValue; 18 | expect(data[0]).to.eql(expectedValue); 19 | utils.reverseBuffer(buffer); 20 | expect(data[0]).to.eql(0); 21 | expect(data[data.length - 1]).to.eql(expectedValue); 22 | }); 23 | }); 24 | 25 | describe('timecode', () => { 26 | it('should format timecode', () => { 27 | expect(utils.timeCode(217.8)).to.eql('03:37'); 28 | }); 29 | }); 30 | 31 | describe('recorder', () => { 32 | it('should have expected api', () => { 33 | expect(utils.recorder).to.be.a('function'); 34 | expect(utils.recorder()).to.exist; 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/volume.spec.js: -------------------------------------------------------------------------------- 1 | describe('volume', () => { 2 | 3 | describe('sound', () => { 4 | const config = { 5 | id: 'volume-test', 6 | url: [ 7 | '/base/test/audio/blip.ogg', 8 | '/base/test/audio/blip.mp3' 9 | ], 10 | volume: 0.8 11 | }; 12 | 13 | let sound; 14 | 15 | beforeEach((done) => { 16 | sound = sono.create(config) 17 | .on('error', (s, err) => console.error('error', err, s)) 18 | .on('loaded', () => console.log('loaded')) 19 | .on('ready', () => console.log('ready')) 20 | .on('play', () => console.log('play')) 21 | .on('ended', () => done()); 22 | sound.play(); 23 | }); 24 | 25 | afterEach(() => { 26 | sono.destroy(sound.id); 27 | }); 28 | 29 | it('should get initial volume', () => { 30 | expect(sound).to.exist; 31 | expect(Math.round(sound.volume * 100)).to.eql(80); 32 | }); 33 | 34 | it('should change volume', () => { 35 | sound.volume = 0.5; 36 | expect(Math.round(sound.volume * 100)).to.eql(50); 37 | }); 38 | 39 | it('should clamp value', () => { 40 | sound.volume = 2; 41 | expect(sound.volume).to.eql(1); 42 | sound.volume = -1; 43 | expect(sound.volume).to.eql(0); 44 | }); 45 | }); 46 | 47 | }); 48 | --------------------------------------------------------------------------------