├── .eslintignore ├── tests ├── .eslintrc └── index-test.js ├── src ├── index.js ├── libs │ ├── AudioPlayer.js │ ├── AudioContext.js │ ├── MicrophoneRecorder.js │ └── Visualizer.js └── components │ └── ReactMic.js ├── .gitignore ├── .babelrc ├── nwb.config.js ├── .travis.yml ├── CONTRIBUTING.md ├── circle.yml ├── webpack.lib.config.js ├── .eslintrc ├── webpack.demo.config.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | bin 4 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactMic from './components/ReactMic'; 2 | 3 | export { ReactMic }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo 3 | /dist 4 | /demo/dist 5 | /es 6 | /lib 7 | /node_modules 8 | /umd 9 | .DS_Store 10 | npm-debug.log* 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | "@babel/preset-env" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-syntax-dynamic-import" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'react-mic', 7 | externals: { 8 | react: 'React' 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 4 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | before_install: 12 | - npm install codecov.io coveralls 13 | 14 | after_success: 15 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 16 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 17 | 18 | branches: 19 | only: 20 | - master 21 | -------------------------------------------------------------------------------- /src/libs/AudioPlayer.js: -------------------------------------------------------------------------------- 1 | import AudioContext from './AudioContext'; 2 | 3 | let audioSource; 4 | 5 | const AudioPlayer = { 6 | 7 | create(audioElem) { 8 | const audioCtx = AudioContext.getAudioContext(); 9 | const analyser = AudioContext.getAnalyser(); 10 | 11 | if(audioSource === undefined){ 12 | const source = audioCtx.createMediaElementSource(audioElem); 13 | source.connect(analyser); 14 | audioSource = source; 15 | } 16 | 17 | analyser.connect(audioCtx.destination); 18 | } 19 | 20 | } 21 | 22 | export default AudioPlayer; -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import {render, unmountComponentAtNode} from 'react-dom' 4 | 5 | import Component from 'src/' 6 | 7 | describe('Component', () => { 8 | let node 9 | 10 | beforeEach(() => { 11 | node = document.createElement('div') 12 | }) 13 | 14 | afterEach(() => { 15 | unmountComponentAtNode(node) 16 | }) 17 | 18 | it('displays a welcome message', () => { 19 | render(, node, () => { 20 | expect(node.innerHTML).toContain('Welcome to React components') 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/libs/AudioContext.js: -------------------------------------------------------------------------------- 1 | const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); 2 | let analyser = audioCtx.createAnalyser(); 3 | 4 | const AudioContext = { 5 | 6 | getAudioContext() { 7 | return audioCtx; 8 | }, 9 | 10 | getAnalyser() { 11 | return analyser; 12 | }, 13 | 14 | resetAnalyser() { 15 | analyser = audioCtx.createAnalyser(); 16 | }, 17 | 18 | decodeAudioData() { 19 | audioCtx.decodeAudioData(audioData).then(function(decodedData) { 20 | // use the decoded data here 21 | }); 22 | } 23 | 24 | } 25 | 26 | export default AudioContext; 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the components's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.10.3 4 | environment: 5 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 6 | 7 | dependencies: 8 | override: 9 | #Cleaning caches and old yarn to prevent NOENT issues such as https://github.com/svg/svgo/issues/622 10 | - rm -rf ./node_modules ./.yarnclean ~/.yarn 11 | - nvm install 6.10.3 12 | - npm install -g npm 13 | - npm install -g yarn 14 | - yarn install 15 | 16 | compile: 17 | override: 18 | - yarn build 19 | 20 | test: 21 | override: 22 | #Until this is fixed and phantomjs has AudioContext... https://github.com/ariya/phantomjs/issues/12409 23 | #- yarn test 24 | - echo "Skipping tests due to lack of support for AudioContext in PhantomJS" 25 | 26 | deployment: 27 | production: 28 | branch: master 29 | override: 30 | - yarn pack 31 | - cp *.tgz $CIRCLE_ARTIFACTS 32 | -------------------------------------------------------------------------------- /webpack.lib.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | const PATH_ROOT = path.resolve(__dirname, './') 5 | const PATH_SRC = path.resolve(PATH_ROOT, 'src/') 6 | const PATH_OUT = path.resolve(PATH_ROOT, 'dist/') 7 | 8 | module.exports = { 9 | mode: 'production', 10 | 11 | entry: [path.resolve(PATH_SRC, 'index.js')], 12 | 13 | output: { 14 | path: PATH_OUT, 15 | filename: 'index.js', 16 | library: 'ReactMic', 17 | libraryTarget: 'umd' 18 | }, 19 | 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | loaders: ['babel-loader'], 25 | exclude: /node_modules/ 26 | } 27 | ] 28 | }, 29 | 30 | externals: { 31 | 'react': { 32 | root: 'React', 33 | commonjs: 'react', 34 | commonjs2: 'react' 35 | }, 36 | 'prop-types': { 37 | root: 'PropTypes', 38 | commonjs: 'prop-types', 39 | commonjs2: 'prop-types' 40 | }, 41 | 'react-dom': { 42 | root: 'ReactDOM', 43 | commonjs: 'react-dom', 44 | commonjs2: 'react-dom' 45 | } 46 | }, 47 | 48 | plugins: [ 49 | new webpack.DefinePlugin({ 50 | 'process.env': { 51 | NODE_ENV: 'production' 52 | }, 53 | __DEVELOPMENT__: false 54 | }) 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | 4 | "parser": "babel-eslint", 5 | "parserOptions": { 6 | "ecmaVersion": 6, 7 | "ecmaFeatures": { 8 | "jsx": true 9 | }, 10 | "sourceType": "module" 11 | }, 12 | "env": { 13 | "browser": true, 14 | "node": true, 15 | "es6": true 16 | }, 17 | "plugins": [ 18 | "react" 19 | ], 20 | "settings" : { 21 | "import/resolver": { 22 | "node": { 23 | "paths": ["src"] 24 | } 25 | } 26 | }, 27 | "rules": { // 0=off, 1=warn, 2=error. Defaults to 0. 28 | semi: [2, "never"], 29 | "no-multi-spaces": [2, { 30 | "exceptions": { 31 | "VariableDeclaration": true, 32 | "ImportDeclaration": true 33 | } 34 | }], 35 | "comma-dangle": [2, "never"], 36 | "key-spacing": [2, { 37 | "singleLine": { 38 | "beforeColon": false, 39 | "afterColon": true 40 | } 41 | }], 42 | "arrow-body-style": "off", 43 | "react/jsx-filename-extension": [0, { "extensions": [".js"] }], 44 | "import/no-extraneous-dependencies": [0, {"devDependencies": true}], 45 | "react/prefer-stateless-function": "off", 46 | "react/no-unused-prop-types": [2, { 47 | skipShapeProps: false 48 | }], 49 | "import/prefer-default-export": "off", 50 | "function-paren-newline": "off" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /webpack.demo.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const CopyWebpackPlugin = require('copy-webpack-plugin') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | 7 | const PATH_ROOT = path.resolve(__dirname, './demo') 8 | const PATH_SRC = path.resolve(PATH_ROOT, 'src/') 9 | const PATH_OUT = path.resolve(PATH_ROOT, 'dist/') 10 | 11 | 12 | module.exports = { 13 | mode: 'production', 14 | 15 | entry: [ 16 | path.resolve(PATH_SRC, 'index.js') 17 | ], 18 | 19 | output: { 20 | path: PATH_OUT, 21 | filename: 'index.js' 22 | }, 23 | 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.js$/, 28 | loaders: ['babel-loader'], 29 | exclude: /node_modules/ 30 | }, 31 | { 32 | test: /\.wasm$/, 33 | type: 'javascript/auto', 34 | loaders: ['arraybuffer-loader'] 35 | }, 36 | { 37 | test: /\.scss$|\.css$/, 38 | use: ExtractTextPlugin.extract({ 39 | fallback: 'style-loader', 40 | use: [ 41 | { loader: 'css-loader' }, 42 | { loader: 'sass-loader' } 43 | ] 44 | }) 45 | }, 46 | { 47 | test: /\.jpg$|\.gif$|\.png$|\.svg$/, 48 | use: ['url-loader'] 49 | }, 50 | { 51 | test: /\.woff$|\.woff2$|\.eot$|\.tff$|\.ttf$/, 52 | loader: 'file-loader?publicPath=../&name=./assets/fonts/[name].[ext]' 53 | } 54 | ] 55 | }, 56 | 57 | plugins: [ 58 | new webpack.DefinePlugin({ 59 | 'process.env': { 60 | NODE_ENV: "'production'" 61 | }, 62 | __DEVELOPMENT__: false 63 | }), 64 | new HtmlWebpackPlugin({ 65 | hash: true, 66 | title: 'React-Mic', 67 | filename: `${PATH_OUT}/index.html`, 68 | template: `${PATH_SRC}/index.html`, 69 | }), 70 | new ExtractTextPlugin({ filename: 'bundle.css' }) 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mic", 3 | "version": "12.4.6", 4 | "description": "Record audio from your microphone and display as a sound oscillation", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "keywords": [ 10 | "react", 11 | "react-component", 12 | "microphone", 13 | "MediaRecorder", 14 | "volume meter", 15 | "audio recording", 16 | "record audio", 17 | "web audio api", 18 | "html5 canvas", 19 | "getUserMedia", 20 | "voice recognition", 21 | "voice activation", 22 | "speech recognition", 23 | "language" 24 | ], 25 | "scripts": { 26 | "build:lib": "npm run clean && webpack --progress --verbose --colors --display-error-details --config ./webpack.lib.config.js", 27 | "build:demo": "npm run clean && webpack --progress --verbose --colors --display-error-details --config ./webpack.demo.config.js", 28 | "clean": "rimraf dist", 29 | "lint": "eslint src/**", 30 | "start": "nwb serve-react-demo", 31 | "test": "nwb test", 32 | "test:coverage": "nwb test --coverage", 33 | "test:watch": "nwb test --server", 34 | "deploy": "npm run build:lib && npm run build:demo && gh-pages -d demo/dist" 35 | }, 36 | "peerDependencies": { 37 | "prop-types": "^15.5.10", 38 | "react": "16.x" 39 | }, 40 | "devDependencies": { 41 | "gh-pages": "^1.0.0", 42 | "nwb": "^0.23.0", 43 | "nwb-sass": "^0.9.0", 44 | "@babel/core": "^7.7.7", 45 | "@babel/plugin-proposal-class-properties": "^7.7.4", 46 | "@babel/plugin-syntax-dynamic-import": "^7.7.4", 47 | "@babel/plugin-transform-runtime": "^7.7.6", 48 | "@babel/preset-env": "^7.7.7", 49 | "@babel/preset-react": "^7.7.4", 50 | "@babel/runtime": "^7.7.7", 51 | "babel-eslint": "^10.0.3", 52 | "babel-loader": "^8.0.6", 53 | "copy-webpack-plugin": "^5.1.1", 54 | "css-loader": "^3.4.0", 55 | "eslint": "^6.8.0", 56 | "eslint-config-airbnb": "^18.0.1", 57 | "eslint-plugin-import": "^2.19.1", 58 | "eslint-plugin-jsx-a11y": "^6.2.3", 59 | "eslint-plugin-react": "^7.17.0", 60 | "exports-loader": "^0.7.0", 61 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 62 | "file-loader": "^5.0.2", 63 | "html-webpack-plugin": "^3.2.0", 64 | "husky": "^3.1.0", 65 | "lint-staged": "^9.5.0", 66 | "react": "^16.12.0", 67 | "react-dom": "^16.12.0", 68 | "react-ga": "^2.7.0", 69 | "rimraf": "^3.0.0", 70 | "sass-loader": "^8.0.0", 71 | "style-loader": "^1.1.1", 72 | "url-loader": "^3.0.0", 73 | "webpack": "^4.41.4", 74 | "webpack-cli": "^3.3.10" 75 | }, 76 | "author": "Mark Muskardin", 77 | "homepage": "https://hackingbeauty.github.io/react-mic", 78 | "license": "MIT", 79 | "repository": "https://github.com/hackingbeauty/react-mic", 80 | "dependencies": { 81 | "prop-types": "^15.5.10", 82 | "react-ga": "^2.2.0" 83 | }, 84 | "lint-staged": { 85 | "./src/**/*.js": [ 86 | "eslint --fix", 87 | "git add" 88 | ] 89 | }, 90 | "husky": { 91 | "hooks": { 92 | "pre-commit": "lint-staged" 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/libs/MicrophoneRecorder.js: -------------------------------------------------------------------------------- 1 | import AudioContext from './AudioContext' 2 | 3 | let analyser 4 | let audioCtx 5 | let mediaRecorder 6 | let chunks = [] 7 | let startTime 8 | let stream 9 | let mediaOptions 10 | let onStartCallback 11 | let onStopCallback 12 | let onSaveCallback 13 | let onDataCallback 14 | let constraints 15 | 16 | navigator.getUserMedia = (navigator.getUserMedia 17 | || navigator.webkitGetUserMedia 18 | || navigator.mozGetUserMedia 19 | || navigator.msGetUserMedia) 20 | 21 | export class MicrophoneRecorder { 22 | constructor(onStart, onStop, onSave, onData, options, soundOptions) { 23 | const { 24 | echoCancellation, 25 | autoGainControl, 26 | noiseSuppression, 27 | channelCount 28 | } = soundOptions 29 | 30 | onStartCallback = onStart 31 | onStopCallback = onStop 32 | onSaveCallback = onSave 33 | onDataCallback = onData 34 | mediaOptions = options 35 | 36 | constraints = { 37 | audio: { 38 | echoCancellation, 39 | autoGainControl, 40 | noiseSuppression, 41 | channelCount 42 | }, 43 | video: false 44 | } 45 | } 46 | 47 | startRecording=() => { 48 | startTime = Date.now() 49 | 50 | if (mediaRecorder) { 51 | if (audioCtx && audioCtx.state === 'suspended') { 52 | audioCtx.resume() 53 | } 54 | 55 | if (mediaRecorder && mediaRecorder.state === 'paused') { 56 | mediaRecorder.resume() 57 | return 58 | } 59 | 60 | if (audioCtx && mediaRecorder && mediaRecorder.state === 'inactive') { 61 | mediaRecorder.start(10) 62 | const source = audioCtx.createMediaStreamSource(stream) 63 | source.connect(analyser) 64 | if (onStartCallback) { onStartCallback() } 65 | } 66 | } else if (navigator.mediaDevices) { 67 | console.log('getUserMedia supported.') 68 | 69 | navigator.mediaDevices.getUserMedia(constraints) 70 | .then((str) => { 71 | stream = str 72 | 73 | if (MediaRecorder.isTypeSupported(mediaOptions.mimeType)) { 74 | mediaRecorder = new MediaRecorder(str, mediaOptions) 75 | } else { 76 | mediaRecorder = new MediaRecorder(str) 77 | } 78 | 79 | if (onStartCallback) { onStartCallback() } 80 | 81 | mediaRecorder.onstop = this.onStop 82 | mediaRecorder.ondataavailable = (event) => { 83 | chunks.push(event.data) 84 | if (onDataCallback) { 85 | onDataCallback(event.data) 86 | } 87 | } 88 | 89 | audioCtx = AudioContext.getAudioContext() 90 | audioCtx.resume().then(() => { 91 | analyser = AudioContext.getAnalyser() 92 | mediaRecorder.start(10) 93 | const sourceNode = audioCtx.createMediaStreamSource(stream) 94 | sourceNode.connect(analyser) 95 | }) 96 | }) 97 | } else { 98 | alert('Your browser does not support audio recording') 99 | } 100 | } 101 | 102 | stopRecording() { 103 | if (mediaRecorder && mediaRecorder.state !== 'inactive') { 104 | mediaRecorder.stop() 105 | 106 | stream.getAudioTracks().forEach((track) => { 107 | track.stop() 108 | }) 109 | mediaRecorder = null 110 | AudioContext.resetAnalyser() 111 | } 112 | } 113 | 114 | onStop() { 115 | const blob = new Blob(chunks, { type: mediaOptions.mimeType }) 116 | chunks = [] 117 | 118 | const blobObject = { 119 | blob, 120 | startTime, 121 | stopTime: Date.now(), 122 | options: mediaOptions, 123 | blobURL: window.URL.createObjectURL(blob) 124 | } 125 | 126 | if (onStopCallback) { onStopCallback(blobObject) } 127 | if (onSaveCallback) { onSaveCallback(blobObject) } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/components/ReactMic.js: -------------------------------------------------------------------------------- 1 | // cool blog article on how to do this: http://www.smartjava.org/content/exploring-html5-web-audio-visualizing-sound 2 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API 3 | 4 | // distortion curve for the waveshaper, thanks to Kevin Ennis 5 | // http://stackoverflow.com/questions/22312841/waveshaper-node-in-webaudio-how-to-emulate-distortion 6 | 7 | import React, { Component } from 'react' 8 | import { 9 | string, number, bool, func 10 | } from 'prop-types' 11 | import { MicrophoneRecorder } from '../libs/MicrophoneRecorder' 12 | import AudioPlayer from '../libs/AudioPlayer' 13 | import Visualizer from '../libs/Visualizer' 14 | 15 | 16 | export default class ReactMic extends Component { 17 | constructor(props) { 18 | super(props) 19 | 20 | this.visualizerRef = React.createRef() 21 | 22 | this.state = { 23 | microphoneRecorder: null, 24 | canvas: null, 25 | canvasCtx: null 26 | } 27 | } 28 | 29 | componentDidUpdate(prevProps) { 30 | const { record, onStop } = this.props 31 | const { microphoneRecorder } = this.state 32 | if (prevProps.record !== record) { 33 | if (record) { 34 | if (microphoneRecorder) { 35 | microphoneRecorder.startRecording() 36 | } 37 | } else if (microphoneRecorder) { 38 | microphoneRecorder.stopRecording(onStop) 39 | this.clear() 40 | } 41 | } 42 | } 43 | 44 | componentDidMount() { 45 | const { 46 | onSave, 47 | onStop, 48 | onStart, 49 | onData, 50 | audioElem, 51 | audioBitsPerSecond, 52 | echoCancellation, 53 | autoGainControl, 54 | noiseSuppression, 55 | channelCount, 56 | mimeType 57 | } = this.props 58 | const visualizer = this.visualizerRef.current 59 | const canvas = visualizer 60 | const canvasCtx = canvas.getContext('2d') 61 | const options = { 62 | audioBitsPerSecond, 63 | mimeType 64 | } 65 | const soundOptions = { 66 | echoCancellation, 67 | autoGainControl, 68 | noiseSuppression 69 | } 70 | 71 | if (audioElem) { 72 | AudioPlayer.create(audioElem) 73 | 74 | this.setState({ 75 | canvas, 76 | canvasCtx 77 | }, () => { 78 | this.visualize() 79 | }) 80 | } else { 81 | this.setState({ 82 | microphoneRecorder: new MicrophoneRecorder( 83 | onStart, 84 | onStop, 85 | onSave, 86 | onData, 87 | options, 88 | soundOptions 89 | ), 90 | canvas, 91 | canvasCtx 92 | }, () => { 93 | this.visualize() 94 | }) 95 | } 96 | } 97 | 98 | visualize = () => { 99 | const { 100 | backgroundColor, strokeColor, width, height, visualSetting 101 | } = this.props 102 | const { canvas, canvasCtx } = this.state 103 | 104 | if (visualSetting === 'sinewave') { 105 | Visualizer.visualizeSineWave(canvasCtx, canvas, width, height, backgroundColor, strokeColor) 106 | } else if (visualSetting === 'frequencyBars') { 107 | Visualizer.visualizeFrequencyBars(canvasCtx, canvas, width, height, backgroundColor, strokeColor) 108 | } else if (visualSetting === 'frequencyCircles') { 109 | Visualizer.visualizeFrequencyCircles(canvasCtx, canvas, width, height, backgroundColor, strokeColor) 110 | } 111 | } 112 | 113 | clear() { 114 | const { width, height } = this.props 115 | const { canvasCtx } = this.state 116 | canvasCtx.clearRect(0, 0, width, height) 117 | } 118 | 119 | render() { 120 | const { width, height } = this.props 121 | 122 | return ( 123 | 129 | ) 130 | } 131 | } 132 | 133 | ReactMic.propTypes = { 134 | backgroundColor: string, 135 | strokeColor: string, 136 | className: string, 137 | audioBitsPerSecond: number, 138 | mimeType: string, 139 | height: number, 140 | record: bool.isRequired, 141 | onStop: func, 142 | onData: func, 143 | onSave: func 144 | } 145 | 146 | ReactMic.defaultProps = { 147 | backgroundColor: 'rgba(255, 255, 255, 0.5)', 148 | strokeColor: '#000000', 149 | className: 'visualizer', 150 | audioBitsPerSecond: 128000, 151 | mimeType: 'audio/webm;codecs=opus', 152 | record: false, 153 | width: 640, 154 | height: 100, 155 | visualSetting: 'sinewave', 156 | echoCancellation: false, 157 | autoGainControl: false, 158 | noiseSuppression: false, 159 | channelCount: 2 160 | } 161 | -------------------------------------------------------------------------------- /src/libs/Visualizer.js: -------------------------------------------------------------------------------- 1 | import AudioContext from './AudioContext'; 2 | 3 | 4 | let drawVisual; 5 | 6 | const Visualizer = { 7 | 8 | visualizeSineWave(canvasCtx, canvas, width, height, backgroundColor, strokeColor) { 9 | let analyser = AudioContext.getAnalyser(); 10 | 11 | const bufferLength = analyser.fftSize; 12 | const dataArray = new Uint8Array(bufferLength); 13 | 14 | canvasCtx.clearRect(0, 0, width, height); 15 | 16 | function draw() { 17 | 18 | drawVisual = requestAnimationFrame(draw); 19 | 20 | analyser = AudioContext.getAnalyser(); 21 | 22 | analyser.getByteTimeDomainData(dataArray); 23 | 24 | canvasCtx.fillStyle = backgroundColor; 25 | canvasCtx.fillRect(0, 0, width, height); 26 | 27 | canvasCtx.lineWidth = 2; 28 | canvasCtx.strokeStyle = strokeColor; 29 | 30 | canvasCtx.beginPath(); 31 | 32 | const sliceWidth = width * 1.0 / bufferLength; 33 | let x = 0; 34 | 35 | for(let i = 0; i < bufferLength; i++) { 36 | const v = dataArray[i] / 128.0; 37 | const y = v * height/2; 38 | 39 | if(i === 0) { 40 | canvasCtx.moveTo(x, y); 41 | } else { 42 | canvasCtx.lineTo(x, y); 43 | } 44 | 45 | x += sliceWidth; 46 | } 47 | 48 | canvasCtx.lineTo(canvas.width, canvas.height/2); 49 | canvasCtx.stroke(); 50 | }; 51 | 52 | draw(); 53 | }, 54 | 55 | visualizeFrequencyBars(canvasCtx, canvas, width, height, backgroundColor, strokeColor) { 56 | const self = this; 57 | let analyser = AudioContext.getAnalyser(); 58 | analyser.fftSize = 256; 59 | const bufferLength = analyser.frequencyBinCount; 60 | const dataArray = new Uint8Array(bufferLength); 61 | 62 | canvasCtx.clearRect(0, 0, width, height); 63 | 64 | function draw() { 65 | drawVisual = requestAnimationFrame(draw); 66 | 67 | analyser = AudioContext.getAnalyser(); 68 | analyser.getByteFrequencyData(dataArray); 69 | 70 | canvasCtx.fillStyle = backgroundColor; 71 | canvasCtx.fillRect(0, 0, width, height); 72 | 73 | const barWidth = (width / bufferLength) * 2.5; 74 | let barHeight; 75 | let x = 0; 76 | 77 | for(let i = 0; i < bufferLength; i++) { 78 | barHeight = dataArray[i]; 79 | 80 | const rgb = self.hexToRgb(strokeColor); 81 | 82 | // canvasCtx.fillStyle = `rgb(${barHeight+100},${rgb.g},${rgb.b})`; 83 | canvasCtx.fillStyle = strokeColor; 84 | canvasCtx.fillRect(x,height-barHeight/2,barWidth,barHeight/2); 85 | 86 | x += barWidth + 1; 87 | } 88 | }; 89 | 90 | draw(); 91 | }, 92 | 93 | visualizeFrequencyCircles(canvasCtx, canvas, width, height, backgroundColor, strokeColor) { 94 | const self = this; 95 | let analyser = AudioContext.getAnalyser(); 96 | analyser.fftSize = 32; 97 | const bufferLength = analyser.frequencyBinCount; 98 | 99 | const dataArray = new Uint8Array(bufferLength); 100 | canvasCtx.clearRect(0, 0, width, height); 101 | 102 | function draw() { 103 | 104 | drawVisual = requestAnimationFrame(draw); 105 | analyser = AudioContext.getAnalyser(); 106 | analyser.getByteFrequencyData(dataArray); 107 | const reductionAmount = 3; 108 | const reducedDataArray = new Uint8Array(bufferLength / reductionAmount); 109 | 110 | for (let i = 0; i < bufferLength; i += reductionAmount) { 111 | let sum = 0; 112 | for (let j = 0; j < reductionAmount; j++) { 113 | sum += dataArray[i + j]; 114 | } 115 | reducedDataArray[i/reductionAmount] = sum / reductionAmount; 116 | } 117 | 118 | canvasCtx.clearRect(0, 0, width, height); 119 | canvasCtx.beginPath(); 120 | canvasCtx.arc(width / 2, height / 2, Math.min(height, width) / 2, 0, 2 * Math.PI); 121 | canvasCtx.fillStyle = backgroundColor; 122 | canvasCtx.fill(); 123 | const stepSize = (Math.min(height, width) / 2.0) / (reducedDataArray.length); 124 | canvasCtx.strokeStyle = strokeColor; 125 | 126 | for (let i = 0; i < reducedDataArray.length; i++) { 127 | canvasCtx.beginPath(); 128 | const normalized = reducedDataArray[i] / 128; 129 | const r = (stepSize * i) + (stepSize * normalized); 130 | canvasCtx.arc(width / 2, height / 2, r, 0, 2 * Math.PI); 131 | canvasCtx.stroke(); 132 | } 133 | }; 134 | draw(); 135 | }, 136 | 137 | 138 | hexToRgb(hex) { 139 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 140 | return result ? { 141 | r: parseInt(result[1], 16), 142 | g: parseInt(result[2], 16), 143 | b: parseInt(result[3], 16) 144 | } : null; 145 | } 146 | 147 | } 148 | 149 | export default Visualizer; 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _ATTENTION_ THIS PACKAGE IS NO LONGER BEING MAINTAINED! PLEASE CONSIDER USING AN ALTERNATE PACKAGE LIKE [MAKLOV'S REACT-MIC](https://github.com/maklov/react-mic). 2 | 3 | # React-Mic 4 | 5 | Record a user's voice and display as an oscillation (or frequency bars). Plug-n-play component for React apps. 6 | 7 | Audio is saved as [WebM](https://en.wikipedia.org/wiki/WebM) audio file format. Works via the HTML5 MediaRecorder API ([currently only available in Chrome & Firefox](https://caniuse.com/#search=MediaRecorder)). 8 | 9 | 10 | ## Installation 11 | 12 | `npm install --save react-mic` 13 | 14 | `yarn add react-mic` 15 | 16 | ## Demos 17 | 18 | Check out the simple React-Mic [demo](https://hackingbeauty.github.io/react-mic/). 19 | 20 | ## Features 21 | 22 | - Record audio from microphone 23 | - Display sound wave as voice is being recorded 24 | - Save audio as BLOB 25 | 26 | ## License 27 | 28 | MIT 29 | 30 | ## Usage 31 | 32 | ```js 33 | 34 | false. Set to true to begin recording 36 | pause={boolean} // defaults -> false (available in React-Mic-Gold) 37 | visualSetting="sinewave" // defaults -> "sinewave". Other option is "frequencyBars" 38 | className={string} // provide css class name 39 | onStop={function} // required - called when audio stops recording 40 | onData={function} // optional - called when chunk of audio data is available 41 | onBlock={function} // optional - called if user selected "block" when prompted to allow microphone access (available in React-Mic-Gold) 42 | strokeColor={string} // sinewave or frequency bar color 43 | backgroundColor={string} // background color 44 | mimeType="audio/webm" // defaults -> "audio/webm". Set to "audio/wav" for WAV or "audio/mp3" for MP3 audio format (available in React-Mic-Gold) 45 | echoCancellation={boolean} // defaults -> false 46 | autoGainControl={boolean} // defaults -> false 47 | noiseSuppression={boolean} // defaults -> false 48 | channelCount={number} // defaults -> 2 (stereo). Specify 1 for mono. 49 | bitRate={256000} // defaults -> 128000 (128kbps). React-Mic-Gold only. 50 | sampleRate={96000} // defaults -> 44100 (44.1 kHz). It accepts values only in range: 22050 to 96000 (available in React-Mic-Gold) 51 | timeSlice={3000} // defaults -> 4000 milliseconds. The interval at which captured audio is returned to onData callback (available in React-Mic-Gold). 52 | /> 53 | 54 | ``` 55 | 56 | ## Example 57 | 58 | The code snippet below is just a quick example of how to use React-Mic. To see how to integrate React-Mic into a *real* application, join the [React-Mic private member's area](https://hackingbeautyllc.clickfunnels.com/optin1588882330260) for a complete tutorial. 59 | 60 | ```js 61 | import { ReactMic } from 'react-mic'; 62 | 63 | export class Example extends React.Component { 64 | constructor(props) { 65 | super(props); 66 | this.state = { 67 | record: false 68 | } 69 | } 70 | 71 | startRecording = () => { 72 | this.setState({ record: true }); 73 | } 74 | 75 | stopRecording = () => { 76 | this.setState({ record: false }); 77 | } 78 | 79 | onData(recordedBlob) { 80 | console.log('chunk of real-time data is: ', recordedBlob); 81 | } 82 | 83 | onStop(recordedBlob) { 84 | console.log('recordedBlob is: ', recordedBlob); 85 | } 86 | 87 | render() { 88 | return ( 89 |
90 | 97 | 98 | 99 |
100 | ); 101 | } 102 | } 103 | ``` 104 | 105 | # React-Mic-Gold 106 | 107 | ![Voice Record Pro](https://professionalreactapp.com/assets/images/react-mic-gold-voice-record-pro-iphone-encased-small.png) 108 | 109 | Get your copy of React-Mic-Gold, the premium enhanced version of React-Mic [here](https://react-mic-gold.professionalreactapp.com/sales-page34701298). 110 | 111 | [React-Mic-Gold](https://react-mic-gold.professionalreactapp.com/sales-page34701298) lets you record audio as either MP3 or WAV files. The MP3 audio file format is super compressed which will result in small file sizes, and is widely supported across all devices. The WAV audio file format is uncompressed and is used when you need professional quality audio; however, the file size is *significantly* larger. 112 | 113 | React-Mic-Gold is built with WebAssembly and Web Workers. The MP3/WAV encoding process takes place in the browser using WebAssembly which makes it super fast. Via Web Workers, the encoding process occurs in a separate thread in the browser so the performance of your UI won't be affected. 114 | 115 | There's no need to set up a separate backend endpoint to convert captured voice/audio into MP3 or WAV. It all happens in the browser. 116 | 117 | Plus, you can stream MP3/WAV to any endpoint as voice/audio is being captured via the onData callback. 118 | 119 | ## Demos 120 | 121 | Check out the simple demo of React-Mic-Gold in action [here](https://hackingbeauty.github.io/react-mic-gold/). 122 | 123 | Also, check out React-Mic-Gold integrated into an actual app [here](https://voice-record.firebaseapp.com/#/record-audio). 124 | 125 | ## Details 126 | 127 | In React-Mic-Gold, encoding of recorded audio into MP3 format happens in the browser, via a combination of advanced Web technologies (Web Workers and Web Assembly). 128 | 129 | You won't have to continuously stream audio data to your back-end server or API endpoint to convert captured audio into an MP3 file. Althought you can if you want to. 130 | 131 | React-Mic-Gold also comes with an optional pause feature and additional [premium enhancements](https://react-mic-gold.professionalreactapp.com/sales-page34701298). 132 | 133 |   134 |   135 | 136 | # React-Mic-Plus 137 | 138 | If you need a version of this React component that only supports the WAV audio format on every device (iOS + Android), you can purchase [React-Mic-Plus](https://react-mic-plus.professionalreactapp.com). 139 | 140 | React-Mic-Plus also comes with an optional pause feature and additional [premium enhancements](https://react-mic-plus.professionalreactapp.com). 141 | 142 | **PLEASE NOTE**: Apple does not allow audio recording from the Chrome browser on Iphone/iOS. To record audio from a web application on an Iphone, a user must use the Safari browser. There is no way around this. 143 | 144 |   145 |   146 | 147 | 148 | # Get Support 149 | 150 | Join the [Slack channel](https://hackingbeauty-slack-invite.herokuapp.com) if you have any questions or problems with React-Mic or React-Sound-Gold. I'm here to help you build amazing apps with audio recording capabilities. 151 | 152 | Customers of React-Mic-Gold and associated products develop audio recording apps, voice-activated apps, speech recognition apps, language learning apps, and much more. 153 | --------------------------------------------------------------------------------