├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── example └── src │ ├── .gitignore │ ├── audio_one.mp3 │ ├── audio_two.mp3 │ ├── example.js │ ├── example.less │ └── index.html ├── gulpfile.js ├── package.json ├── screenshots └── visualizer.png └── src └── Visualizer.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | indent_style = style 11 | indent_size = 2 12 | 13 | [*.json] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .publish/* 2 | dist/* 3 | example/dist/* 4 | lib/* 5 | node_modules/* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "curly": [2, "multi-line"], 12 | "quotes": [2, "single", "avoid-escape"], 13 | "react/display-name": 0, 14 | "react/jsx-boolean-value": 1, 15 | "react/jsx-quotes": 1, 16 | "react/jsx-no-undef": 1, 17 | "react/jsx-sort-props": 0, 18 | "react/jsx-sort-prop-types": 1, 19 | "react/jsx-uses-react": 1, 20 | "react/jsx-uses-vars": 1, 21 | "react/no-did-mount-set-state": 1, 22 | "react/no-did-update-set-state": 1, 23 | "react/no-multi-comp": 1, 24 | "react/no-unknown-property": 1, 25 | "react/prop-types": 1, 26 | "react/react-in-jsx-scope": 1, 27 | "react/self-closing-comp": 1, 28 | "react/wrap-multilines": 1, 29 | "semi": 2, 30 | "strict": 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage tools 11 | lib-cov 12 | coverage 13 | coverage.html 14 | .cover* 15 | 16 | # Dependency directory 17 | node_modules 18 | dist 19 | lib 20 | 21 | # Example build directory 22 | example/dist 23 | .publish 24 | 25 | # Editor and other tmp files 26 | *.swp 27 | *.un~ 28 | *.iml 29 | *.ipr 30 | *.iws 31 | *.sublime-* 32 | .idea/ 33 | *.DS_Store 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Lazic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visualizer 2 | 3 | React component for audio visualization using Web Audio API. 4 | ES6 compatibility. 5 | 6 | [![NPM Download Stats](https://nodei.co/npm/react-audio-visualizer.png?downloads=true)](https://www.npmjs.com/package/react-audio-visualizer) 7 | 8 | ![visualizer](/screenshots/visualizer.png "visualizer") 9 | 10 | ## Demo & Examples 11 | 12 | Example of a live app is inside _example_ directory. 13 | 14 | Live demo: [DavidLazic.github.io/react-audio-visualizer](http://DavidLazic.github.io/react-audio-visualizer/) 15 | 16 | To build the examples locally, run: 17 | 18 | ``` 19 | npm install 20 | npm start 21 | ``` 22 | 23 | Then open [`localhost:8000`](http://localhost:8000) in a browser. 24 | 25 | 26 | ## Installation 27 | 28 | The easiest way to use react-audio-visualizer is to install it from NPM and include it in your own React build process (using [Browserify](http://browserify.org), [Webpack](http://webpack.github.io/), etc). 29 | 30 | You can also use the standalone build by including `dist/react-audio-visualizer.js` in your page. If you use this, make sure you have already included React, and it is available as a global variable. 31 | 32 | ``` 33 | npm install react-audio-visualizer --save 34 | ``` 35 | 36 | 37 | ## Usage 38 | 39 | Require Visualizer component. 40 | 41 | ```javascript 42 | const Visualizer = require('react-audio-visualizer'); 43 | 44 | } /> 45 | ``` 46 | 47 | ### Properties 48 | 49 | Base Visualizer properties. 50 | 51 | | Name | Type | Required | Description 52 | | ----------- |:---------:|:--------:| ------------- 53 | | **model** | Object | Yes | Model is the only required property. It must have `path`, `author` & `title` properties. 54 | | **options** | Object | No | Set custom styling for default rendering functions, properties below. 55 | | **extensions** | Object | No | Decorator functions for overriding default rendering functions, properties below. 56 | | **className** | String | No | Modifier CSS class that will be set on visualizer's container element. 57 | | **width** | String | No | Inline canvas width. 58 | | **height** | String | No | Inline canvas height. 59 | | **onChange** | Function | No | Callback function when play state changes. 60 | 61 | 62 | 63 | *Model* properties 64 | 65 | | Name | Type | Required | Description 66 | | ----------- |:---------:|:--------:| ------------- 67 | | **path** | String | Yes | Path to audio file to be requested as an array buffer via AJAX. 68 | | **author** | String | Yes | Audio author to be rendered on canvas. 69 | | **title** | String | Yes | Audio title to be rendered on canvas. 70 | 71 | 72 | 73 | *Options* properties 74 | 75 | | Name | Type | Default | Required | Description 76 | | ------------- |:-------------:|:---------------------:|:--------:| ------------- 77 | | **autoplay** | Boolean | False | No | Flag if the file should be played automatically. 78 | | **shadowBlur** | Integer | 20 | No | Canvas shadow blur. 79 | | **shadowColor** | String | #ffffff | No | Canvas shadow color. 80 | | **barColor** | String | #cafdff | No | Canvas bar color. 81 | | **barWidth** | Integer | 2 | No | Canvas bar width. 82 | | **barHeight** | Integer | 2 | No | Canvas bar height. 83 | | **barSpacing** | Integer | 7 | No | Canvas bar spacing. 84 | | **font** | Array | ['12px', 'Helvetica'] | No | Canvas font family and size. 85 | 86 | 87 | 88 | *Extensions* properties 89 | 90 | | Name | Type | Required | Description 91 | | ------------- |:--------:|:--------:| ------------- 92 | | **renderStyle** | Function | No | Decorator fn for canvas style rendering. 93 | | **renderText** | Function | No | Decorator fn for canvas text rendering. 94 | | **renderTime** | Function | No | Decorator fn for canvas time rendering. 95 | 96 | 97 | ### Notes 98 | 99 | *Possible play states* 100 | 101 | * _ENDED_ 102 | * _PLAYING_ 103 | * _PAUSED_ 104 | * _BUFFERING_ 105 | 106 | 107 | ## Development (`src`, `lib` and the build process) 108 | 109 | **NOTE:** The source code for the component is in `src`. A transpiled CommonJS version (generated with Babel) is available in `lib` for use with node.js, browserify and webpack. A UMD bundle is also built to `dist`, which can be included without the need for any build system. 110 | 111 | To build, watch and serve the examples (which will also watch the component source), run `npm start`. If you just want to watch changes to `src` and rebuild `lib`, run `npm run watch` (this is useful if you are working with `npm link`). 112 | 113 | 114 | ## Contributing 115 | 116 | This project is an upgrade from my original experiment with Web Audio API written in vanilla JS. It's open for contributions, so, if you think something can be improved or you'd like a new feature, please submit a PR or we can discuss it via email also. 117 | 118 | 119 | ## License 120 | 121 | MIT License 122 | 123 | Copyright (c) 2017 David Lazic 124 | 125 | Permission is hereby granted, free of charge, to any person obtaining a copy 126 | of this software and associated documentation files (the "Software"), to deal 127 | in the Software without restriction, including without limitation the rights 128 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 129 | copies of the Software, and to permit persons to whom the Software is 130 | furnished to do so, subject to the following conditions: 131 | 132 | The above copyright notice and this permission notice shall be included in all 133 | copies or substantial portions of the Software. 134 | 135 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 136 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 137 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 138 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 139 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 140 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 141 | SOFTWARE. 142 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-audio-visualizer", 3 | "version": "0.0.0", 4 | "description": "Visualizer", 5 | "main": "dist/react-audio-visualizer.min.js", 6 | "homepage": "https://github.com/DavidLazic/react-audio-visualizer", 7 | "authors": [ 8 | "David Lazic" 9 | ], 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "react-component" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | ".editorconfig", 22 | ".gitignore", 23 | "package.json", 24 | "src", 25 | "node_modules", 26 | "example", 27 | "test" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /example/src/.gitignore: -------------------------------------------------------------------------------- 1 | ## This file is here to ensure it is included in the gh-pages branch, 2 | ## when `gulp deploy` is used to push updates to the demo site. 3 | 4 | # Dependency directory 5 | node_modules 6 | -------------------------------------------------------------------------------- /example/src/audio_one.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidLazic/react-audio-visualizer/1141a900ab84a087623295e3ad2930d96d7c8c44/example/src/audio_one.mp3 -------------------------------------------------------------------------------- /example/src/audio_two.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidLazic/react-audio-visualizer/1141a900ab84a087623295e3ad2930d96d7c8c44/example/src/audio_two.mp3 -------------------------------------------------------------------------------- /example/src/example.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactDOM = require('react-dom'); 3 | const Visualizer = require('react-audio-visualizer'); 4 | 5 | const DATA = [ 6 | { 7 | model: { 8 | path: './audio_one.mp3', 9 | author: 'Galimatias & Joppe', 10 | title: 'Mintaka' 11 | }, 12 | options: { autoplay: false } 13 | }, 14 | { 15 | model: { 16 | path: './audio_two.mp3', 17 | author: 'NCT', 18 | title: 'Rain Beyond The Sun' 19 | }, 20 | options: { autoplay: true }, 21 | className: 'visualizer--custom-modifier' 22 | } 23 | ]; 24 | 25 | const App = React.createClass({ 26 | 27 | getInitialState () { 28 | return { item: DATA[0] }; 29 | }, 30 | 31 | onSelect (item) { 32 | this.setState({ item }); 33 | }, 34 | 35 | onRenderStyle (context) { 36 | // Render style decorator 37 | // Write custom rendering style here 38 | }, 39 | 40 | onRenderText (context) { 41 | // Render text decorator 42 | // Write custom rendering text here 43 | }, 44 | 45 | onRenderTime (context) { 46 | // Render time decorator 47 | // Write custom rendering time here 48 | }, 49 | 50 | onPlayStateChange (state) { 51 | 52 | // Play state change notifier 53 | switch (state.status) { 54 | 55 | case 'BUFFERING': 56 | break; 57 | 58 | case 'PLAYING': 59 | break; 60 | 61 | case 'PAUSED': 62 | break; 63 | 64 | case 'ENDED': 65 | break; 66 | } 67 | }, 68 | 69 | getLinks () { 70 | return DATA.map((item, index) => { 71 | return ( 72 |
  • { this.onSelect(item); } }> 73 |
    { item.model.author } - { item.model.title }
    74 |
  • 75 | ); 76 | }); 77 | }, 78 | 79 | getExtensions () { 80 | return { 81 | renderStyle: this.onRenderStyle, 82 | renderText: this.onRenderText, 83 | renderTime: this.onRenderTime 84 | }; 85 | }, 86 | 87 | render () { 88 | const links = this.getLinks(); 89 | const extensions = this.getExtensions(); 90 | const { item } = this.state; 91 | 92 | return ( 93 |
    94 | 101 | 102 |
      { links }
    103 |
    104 | ); 105 | } 106 | }); 107 | 108 | ReactDOM.render(, document.getElementById('app')); 109 | -------------------------------------------------------------------------------- /example/src/example.less: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: 'Helvetica', 'Arial', sans-serif; 8 | font-size: 14px; 9 | background: #000; 10 | color: #fff; 11 | } 12 | 13 | h3, h4 { 14 | padding-left: 15px; 15 | } 16 | 17 | h4 { 18 | 19 | a, 20 | a:visited { 21 | color: darken(#396362, 5); 22 | } 23 | } 24 | 25 | .main { 26 | padding-top: 50px; 27 | } 28 | 29 | .visualizer { 30 | position: relative; 31 | width: 100%; 32 | background: -webkit-gradient(radial, center center, 0, center center, 460, from(#396362), to(#000000)); 33 | background: -webkit-radial-gradient(circle, #396362, #000000); 34 | background: -moz-radial-gradient(circle, #396362, #000000); 35 | background: -ms-radial-gradient(circle, #396362, #000000); 36 | box-shadow: inset 0 0 160px 0 #000; 37 | cursor: pointer; 38 | 39 | &--custom-modifier { 40 | // custom CSS modifier 41 | } 42 | 43 | &__canvas-wrapper { 44 | 45 | } 46 | 47 | &__canvas { 48 | display: block; 49 | margin: 0 auto; 50 | } 51 | } 52 | 53 | ul { 54 | display: block; 55 | margin: 140px auto 0; 56 | padding: 0; 57 | width: 400px; 58 | list-style-type: none; 59 | background: darken(#396362, 20); 60 | 61 | li { 62 | padding: 15px; 63 | transition: all 0.3s ease; 64 | -webkit-transition: all 0.3s ease; 65 | cursor: pointer; 66 | 67 | &:hover { 68 | background: darken(#396362, 5); 69 | } 70 | } 71 | } 72 | 73 | 74 | @media screen and (min-width: 420px) { 75 | .visualizer { 76 | box-shadow: inset 0 0 200px 60px #000; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Visualizer 4 | 5 | 6 | 7 |
    8 |

    React Audio Visualizer

    9 |

    View project on GitHub

    10 | 11 |
    12 |
    13 | 14 |
    15 |
    16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | /** 5 | * Tasks are added by the react-component-gulp-tasks package 6 | * 7 | * See https://github.com/JedWatson/react-component-gulp-tasks 8 | * for documentation. 9 | * 10 | * You can also add your own additional gulp tasks if you like. 11 | */ 12 | 13 | var taskConfig = { 14 | 15 | component: { 16 | name: 'Visualizer', 17 | dependencies: [ 18 | 'classnames', 19 | 'react', 20 | 'react-dom' 21 | ], 22 | lib: 'lib' 23 | }, 24 | 25 | example: { 26 | src: 'example/src', 27 | dist: 'example/dist', 28 | files: [ 29 | 'index.html', 30 | 'audio_one.mp3', 31 | 'audio_two.mp3', 32 | '.gitignore' 33 | ], 34 | scripts: [ 35 | 'example.js' 36 | ], 37 | less: [ 38 | 'example.less' 39 | ] 40 | } 41 | 42 | }; 43 | 44 | initGulpTasks(gulp, taskConfig); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-audio-visualizer", 3 | "version": "1.0.3", 4 | "description": "Canvas audio visualization using Web Audio API", 5 | "main": "lib/Visualizer.js", 6 | "author": "David Lazic", 7 | "homepage": "https://github.com/DavidLazic/react-audio-visualizer", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/DavidLazic/react-audio-visualizer.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/DavidLazic/react-audio-visualizer/issues" 14 | }, 15 | "dependencies": { 16 | "classnames": "^2.1.2" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^4.1.3", 20 | "eslint": "^1.6.0", 21 | "eslint-plugin-react": "^3.5.1", 22 | "gulp": "^3.9.0", 23 | "react": "^0.14.0", 24 | "react-component-gulp-tasks": "^0.7.6", 25 | "react-dom": "^0.14.0" 26 | }, 27 | "peerDependencies": { 28 | "react": "^0.14.0" 29 | }, 30 | "browserify-shim": { 31 | "react": "global:React" 32 | }, 33 | "scripts": { 34 | "build": "gulp clean && NODE_ENV=production gulp build", 35 | "examples": "gulp dev:server", 36 | "lint": "eslint ./; true", 37 | "publish:site": "NODE_ENV=production gulp publish:examples", 38 | "release": "NODE_ENV=production gulp release", 39 | "start": "gulp dev", 40 | "test": "echo \"no tests yet\" && exit 0", 41 | "watch": "gulp watch:lib" 42 | }, 43 | "keywords": [ 44 | "react", 45 | "react-component" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /screenshots/visualizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidLazic/react-audio-visualizer/1141a900ab84a087623295e3ad2930d96d7c8c44/screenshots/visualizer.png -------------------------------------------------------------------------------- /src/Visualizer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const STATES = [ 4 | 'ENDED', 5 | 'PLAYING', 6 | 'PAUSED', 7 | 'BUFFERING' 8 | ]; 9 | 10 | const OPTIONS_ANALYSER = { 11 | smoothingTime: 0.6, 12 | fftSize: 512 13 | }; 14 | 15 | const OPTIONS_DEFAULT = { 16 | autoplay: false, 17 | shadowBlur: 20, 18 | shadowColor: '#ffffff', 19 | barColor: '#cafdff', 20 | barWidth: 2, 21 | barHeight: 2, 22 | barSpacing: 7, 23 | font: ['12px', 'Helvetica'] 24 | }; 25 | 26 | const Visualizer = React.createClass({ 27 | 28 | getInitialState () { 29 | return { 30 | playing: false, 31 | requestAnimationFrame: null, 32 | animFrameId: null, 33 | ctx: null, 34 | analyser: null, 35 | frequencyData: 0, 36 | sourceNode: null, 37 | gradient: null, 38 | canvasCtx: null, 39 | interval: null, 40 | duration: null, 41 | minutes: '00', 42 | seconds: '00', 43 | options: OPTIONS_DEFAULT, 44 | extensions: {}, 45 | model: null 46 | }; 47 | }, 48 | 49 | componentWillMount () { 50 | this._setContext() 51 | .then(() => { 52 | this._setAnalyser(); 53 | }).then(() => { 54 | this._setFrequencyData(); 55 | }).then(() => { 56 | this._setRequestAnimationFrame(); 57 | }).catch((error) => { 58 | this._onDisplayError(error); 59 | }); 60 | }, 61 | 62 | componentDidMount () { 63 | this._extend() 64 | .then(() => { 65 | this._setBufferSourceNode(); 66 | }).then(() => { 67 | this._setCanvasContext(); 68 | }).then(() => { 69 | this._setCanvasStyles(); 70 | }).then(() => { 71 | this._onResetTimer().then(() => { 72 | this._onRender({ 73 | renderText: this.state.extensions.renderText, 74 | renderTime: this.state.extensions.renderTime 75 | }); 76 | this.state.options.autoplay && this._onResolvePlayState(); 77 | }); 78 | }).catch((error) => { 79 | this._onDisplayError(error); 80 | }); 81 | }, 82 | 83 | componentWillReceiveProps (nextProps) { 84 | if (this.state.model !== nextProps.model) { 85 | this._onAudioStop().then(() => { 86 | this.setState({ model: nextProps.model }, () => { 87 | this.componentDidMount(); 88 | }); 89 | }); 90 | } 91 | }, 92 | 93 | componentWillUnmount () { 94 | const { ctx } = this.state; 95 | 96 | ctx.close(); 97 | }, 98 | 99 | /** 100 | * @description 101 | * Display visualizer error. 102 | * 103 | * @param {Object} error 104 | * @return {Function} 105 | * @private 106 | */ 107 | _onDisplayError (error) { 108 | return window.console.table(error); 109 | }, 110 | 111 | /** 112 | * @description 113 | * Extend constructor options. 114 | * 115 | * @return {Object} 116 | * @private 117 | */ 118 | _extend () { 119 | const options = Object.assign(OPTIONS_DEFAULT, this.props.options); 120 | const extensions = Object.assign({}, this.props.extensions || { 121 | renderStyle: this._onRenderStyleDefault, 122 | renderText: this._onRenderTextDefault, 123 | renderTime: this._onRenderTimeDefault 124 | }); 125 | 126 | return new Promise((resolve, reject) => { 127 | this.setState({ 128 | options, 129 | model: this.props.model, 130 | extensions 131 | }, () => { 132 | return resolve(); 133 | }); 134 | }); 135 | }, 136 | 137 | /** 138 | * @description 139 | * Set canvas context. 140 | * 141 | * @return {Object} 142 | * @private 143 | */ 144 | _setCanvasContext () { 145 | const canvasCtx = this.refs.canvas.getContext('2d'); 146 | 147 | return new Promise((resolve, reject) => { 148 | this.setState({ canvasCtx }, () => { 149 | return resolve(); 150 | }); 151 | }); 152 | }, 153 | 154 | /** 155 | * @description 156 | * Set audio context. 157 | * 158 | * @return {Object} 159 | * @private 160 | */ 161 | _setContext () { 162 | const error = { message: 'Web Audio API is not supported.' }; 163 | 164 | return new Promise((resolve, reject) => { 165 | try { 166 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 167 | this.setState({ ctx: new window.AudioContext() }, () => { 168 | return resolve(); 169 | }); 170 | } catch (e) { 171 | return reject(error); 172 | } 173 | }); 174 | }, 175 | 176 | /** 177 | * @description 178 | * Set audio buffer analyser. 179 | * 180 | * @return {Object} 181 | * @private 182 | */ 183 | _setAnalyser () { 184 | const { ctx } = this.state; 185 | 186 | return new Promise((resolve, reject) => { 187 | let analyser = ctx.createAnalyser(); 188 | 189 | analyser.smoothingTimeConstant = OPTIONS_ANALYSER.smoothingTime; 190 | analyser.fftSize = OPTIONS_ANALYSER.fftSize; 191 | 192 | this.setState({ analyser }, () => { 193 | return resolve(); 194 | }); 195 | }); 196 | }, 197 | 198 | /** 199 | * @description 200 | * Set frequency data. 201 | * 202 | * @return {Object} 203 | * @private 204 | */ 205 | _setFrequencyData () { 206 | const { analyser } = this.state; 207 | 208 | return new Promise((resolve, reject) => { 209 | const frequencyData = new Uint8Array(analyser.frequencyBinCount); 210 | 211 | this.setState({ frequencyData }, () => { 212 | return resolve(); 213 | }); 214 | }); 215 | }, 216 | 217 | /** 218 | * @description 219 | * Set source buffer and connect processor and analyser. 220 | * 221 | * @return {Object} 222 | * @private 223 | */ 224 | _setBufferSourceNode () { 225 | const { ctx, analyser } = this.state; 226 | 227 | return new Promise((resolve, reject) => { 228 | let sourceNode = ctx.createBufferSource(); 229 | 230 | sourceNode.connect(analyser); 231 | sourceNode.connect(ctx.destination); 232 | sourceNode.onended = () => { 233 | this._onAudioStop(); 234 | }; 235 | 236 | this.setState({ sourceNode }, () => { 237 | return resolve(); 238 | }); 239 | }); 240 | }, 241 | 242 | /** 243 | * @description 244 | * Set request animation frame fn. 245 | * 246 | * @return {Object} 247 | * @private 248 | */ 249 | _setRequestAnimationFrame () { 250 | return new Promise((resolve, reject) => { 251 | const requestAnimationFrame = (() => { 252 | return window.requestAnimationFrame || 253 | window.webkitRequestAnimationFrame || 254 | window.mozRequestAnimationFrame || 255 | function (callback) { 256 | window.setTimeout(callback, 1000 / 60); 257 | }; 258 | })(); 259 | 260 | this.setState({ requestAnimationFrame }, () => { 261 | return resolve(); 262 | }); 263 | }); 264 | }, 265 | 266 | /** 267 | * @description 268 | * Set canvas gradient color. 269 | * 270 | * @return {Object} 271 | * @private 272 | */ 273 | _setCanvasStyles () { 274 | const { canvasCtx } = this.state; 275 | const { barColor, shadowBlur, shadowColor, font } = this.state.options; 276 | 277 | let gradient = canvasCtx.createLinearGradient(0, 0, 0, 300); 278 | gradient.addColorStop(1, barColor); 279 | 280 | const ctx = Object.assign(canvasCtx, { 281 | fillStyle: gradient, 282 | shadowBlur: shadowBlur, 283 | shadowColor: shadowColor, 284 | font: font.join(' '), 285 | textAlign: 'center' 286 | }); 287 | 288 | return new Promise((resolve, reject) => { 289 | this.setState({ 290 | gradient: gradient, 291 | canvasCtx: ctx 292 | }, () => { 293 | return resolve(); 294 | }); 295 | }); 296 | }, 297 | 298 | /** 299 | * @description 300 | * On playstate change. 301 | * 302 | * @param {String} state 303 | * @return {Function} 304 | * @private 305 | */ 306 | _onChange (state) { 307 | const { onChange } = this.props; 308 | 309 | return onChange && onChange.call(this, { status: state }); 310 | }, 311 | 312 | /** 313 | * @description 314 | * Resolve play state. 315 | * 316 | * @return {Function} 317 | * @private 318 | */ 319 | _onResolvePlayState () { 320 | const { ctx } = this.state; 321 | 322 | if (!this.state.playing) { 323 | return (ctx.state === 'suspended') ? 324 | this._onAudioPlay() : 325 | this._onAudioLoad(); 326 | } else { 327 | return this._onAudioPause(); 328 | } 329 | }, 330 | 331 | /** 332 | * @description 333 | * Load audio file fn. 334 | * 335 | * @return {Object} 336 | * @private 337 | */ 338 | _onAudioLoad () { 339 | const { ctx, canvasCtx, model } = this.state; 340 | const { canvas } = this.refs; 341 | 342 | canvasCtx.fillText('Loading...', canvas.width / 2 + 10, canvas.height / 2 - 25); 343 | this._onChange(STATES[3]); 344 | 345 | this._httpGet().then((response) => { 346 | ctx.decodeAudioData(response, (buffer) => { 347 | (model === this.state.model) && this._onAudioPlay(buffer); 348 | }, (error) => { 349 | this._onDisplayError(error); 350 | }); 351 | }); 352 | 353 | return this; 354 | }, 355 | 356 | /** 357 | * @description 358 | * Http GET method. 359 | * 360 | * @return {Object} 361 | * @private 362 | */ 363 | _httpGet () { 364 | const { model } = this.state; 365 | 366 | return new Promise((resolve, reject) => { 367 | let req = new XMLHttpRequest(); 368 | req.open('GET', model.path, true); 369 | req.responseType = 'arraybuffer'; 370 | 371 | req.onload = () => { 372 | return resolve(req.response); 373 | }; 374 | 375 | req.send(); 376 | }); 377 | }, 378 | 379 | /** 380 | * @description 381 | * Audio pause fn. 382 | * 383 | * @return {Object} 384 | * @private 385 | */ 386 | _onAudioPause () { 387 | const { ctx } = this.state; 388 | 389 | this.setState({ playing: false }, () => { 390 | ctx.suspend().then(() => { 391 | this._onChange(STATES[2]); 392 | }); 393 | }); 394 | 395 | return this; 396 | }, 397 | 398 | /** 399 | * @description 400 | * Audio stop fn. 401 | * 402 | * @return {Object} 403 | * @private 404 | */ 405 | _onAudioStop () { 406 | const { canvasCtx, ctx } = this.state; 407 | const { canvas } = this.refs; 408 | 409 | return new Promise((resolve, reject) => { 410 | window.cancelAnimationFrame(this.state.animFrameId); 411 | clearInterval(this.state.interval); 412 | this.state.sourceNode.disconnect(); 413 | canvasCtx.clearRect(0, 0, canvas.width, canvas.height); 414 | this._onChange(STATES[0]); 415 | 416 | this._onResetTimer().then(() => { 417 | ctx.resume(); 418 | }).then(() => { 419 | this._setBufferSourceNode(); 420 | }).then(() => { 421 | this.setState({ 422 | playing: false, 423 | animFrameId: null 424 | }, () => { 425 | return resolve(); 426 | }); 427 | }); 428 | }); 429 | }, 430 | 431 | /** 432 | * @description 433 | * Audio play fn. 434 | * 435 | * @return {Object} 436 | * @private 437 | */ 438 | _onAudioPlay (buffer) { 439 | const { ctx, sourceNode } = this.state; 440 | 441 | this.setState({ playing: true }, () => { 442 | this._onChange(STATES[1]); 443 | 444 | if (ctx.state === 'suspended') { 445 | ctx.resume(); 446 | return this._onRenderFrame(); 447 | } 448 | 449 | sourceNode.buffer = buffer; 450 | sourceNode.start(0); 451 | this._onResetTimer().then(() => { 452 | this 453 | ._onStartTimer() 454 | ._onRenderFrame(); 455 | }); 456 | }); 457 | 458 | return this; 459 | }, 460 | 461 | /** 462 | * @description 463 | * Reset audio timer fn. 464 | * 465 | * @return {Object} 466 | * @private 467 | */ 468 | _onResetTimer () { 469 | return new Promise((resolve, reject) => { 470 | this.setState({ 471 | duration: (new Date(0, 0)).getTime(), 472 | minutes: '00', 473 | seconds: '00' 474 | }, () => { 475 | return resolve(); 476 | }); 477 | }); 478 | }, 479 | 480 | /** 481 | * @description 482 | * Start audio timer fn. 483 | * 484 | * @return {Object} 485 | * @private 486 | */ 487 | _onStartTimer () { 488 | const interval = setInterval(() => { 489 | if (this.state.playing) { 490 | let now = new Date(this.state.duration); 491 | let min = now.getHours(); 492 | let sec = now.getMinutes() + 1; 493 | 494 | this.setState({ 495 | minutes: (min < 10) ? `0${min}` : min, 496 | seconds: (sec < 10) ? `0${sec}` : sec, 497 | duration: now.setMinutes(sec) 498 | }); 499 | } 500 | }, 1000); 501 | 502 | this.setState({ interval }); 503 | return this; 504 | }, 505 | 506 | /** 507 | * @description 508 | * Render canvas frame. 509 | * 510 | * @return {Object} 511 | * @private 512 | */ 513 | _onRenderFrame () { 514 | const { 515 | analyser, 516 | frequencyData, 517 | requestAnimationFrame, 518 | animFrameId 519 | } = this.state; 520 | 521 | if (this.state.playing) { 522 | const animFrameId = requestAnimationFrame(this._onRenderFrame); 523 | 524 | this.setState({ animFrameId }, () => { 525 | analyser.getByteFrequencyData(frequencyData); 526 | this._onRender(this.state.extensions); 527 | }); 528 | } 529 | 530 | return this; 531 | }, 532 | 533 | /** 534 | * @description 535 | * On render frame fn. 536 | * Invoke each of the render extensions. 537 | * 538 | * @param {Object} extensions 539 | * @return {Function} 540 | * @private 541 | */ 542 | _onRender (extensions) { 543 | const { canvasCtx } = this.state; 544 | const { canvas } = this.refs; 545 | 546 | canvasCtx.clearRect(0, 0, canvas.width, canvas.height); 547 | Object.keys(extensions).forEach((extension) => { 548 | return extensions[extension] && 549 | extensions[extension].call(this, this); 550 | }); 551 | }, 552 | 553 | /** 554 | * @description 555 | * Render audio time fn. 556 | * Default time rendering fn. 557 | * 558 | * @return {Object} 559 | * @private 560 | */ 561 | _onRenderTimeDefault () { 562 | const { canvasCtx } = this.state; 563 | const { canvas } = this.refs; 564 | 565 | let time = `${this.state.minutes}:${this.state.seconds}`; 566 | canvasCtx.fillText(time, canvas.width / 2 + 10, canvas.height / 2 + 40); 567 | return this; 568 | }, 569 | 570 | /** 571 | * @description 572 | * Render audio author and title fn. 573 | * Default text rendering fn. 574 | * 575 | * @return {Object} 576 | * @private 577 | */ 578 | _onRenderTextDefault () { 579 | const { canvasCtx } = this.state; 580 | const { canvas } = this.refs; 581 | const { model } = this.state; 582 | const { font } = this.state.options; 583 | 584 | const cx = canvas.width / 2; 585 | const cy = canvas.height / 2; 586 | const fontAdjustment = 6; 587 | const alignAdjustment = 8; 588 | 589 | canvasCtx.textBaseline = 'top'; 590 | canvasCtx.fillText(`by ${model.author}`, cx + alignAdjustment, cy); 591 | canvasCtx.font = `${parseInt(font[0], 10) + fontAdjustment}px ${font[1]}`; 592 | canvasCtx.textBaseline = 'bottom'; 593 | canvasCtx.fillText(model.title, cx + alignAdjustment, cy); 594 | canvasCtx.font = font.join(' '); 595 | 596 | return this; 597 | }, 598 | 599 | /** 600 | * @description 601 | * Render lounge style type. 602 | * Default rendering style. 603 | * 604 | * @return {Object} 605 | * @private 606 | */ 607 | _onRenderStyleDefault () { 608 | const { frequencyData, canvasCtx } = this.state; 609 | const { canvas } = this.refs; 610 | const { barWidth, barHeight, barSpacing } = this.state.options; 611 | 612 | const radiusReduction = 70; 613 | const amplitudeReduction = 6; 614 | 615 | const cx = canvas.width / 2; 616 | const cy = canvas.height / 2; 617 | const radius = Math.min(cx, cy) - radiusReduction; 618 | const maxBarNum = Math.floor((radius * 2 * Math.PI) / (barWidth + barSpacing)); 619 | const slicedPercent = Math.floor((maxBarNum * 25) / 100); 620 | const barNum = maxBarNum - slicedPercent; 621 | const freqJump = Math.floor(frequencyData.length / maxBarNum); 622 | 623 | for (let i = 0; i < barNum; i++) { 624 | const amplitude = frequencyData[i * freqJump]; 625 | const theta = (i * 2 * Math.PI ) / maxBarNum; 626 | const delta = (3 * 45 - barWidth) * Math.PI / 180; 627 | const x = 0; 628 | const y = radius - (amplitude / 12 - barHeight); 629 | const w = barWidth; 630 | const h = amplitude / amplitudeReduction + barHeight; 631 | 632 | canvasCtx.save(); 633 | canvasCtx.translate(cx + barSpacing, cy + barSpacing); 634 | canvasCtx.rotate(theta - delta); 635 | canvasCtx.fillRect(x, y, w, h); 636 | canvasCtx.restore(); 637 | } 638 | 639 | return this; 640 | }, 641 | 642 | /** 643 | * @return {Object} 644 | * @public 645 | */ 646 | render () { 647 | const { model, width, height } = this.props; 648 | const classes = ['visualizer', this.props.className].join(' '); 649 | 650 | return ( 651 |
    652 | 653 | 654 |
    655 | 660 | 661 |
    662 |
    663 | ); 664 | } 665 | }); 666 | 667 | Visualizer.PropTypes = { 668 | model: PropTypes.object.isRequired, 669 | options: PropTypes.object, 670 | className: PropTypes.string, 671 | extensions: PropTypes.object, 672 | onChange: PropTypes.func, 673 | width: PropTypes.string, 674 | height: PropTypes.string 675 | }; 676 | 677 | export default Visualizer; 678 | --------------------------------------------------------------------------------