├── .npmignore ├── .babelrc ├── js ├── getInput.js └── index.js ├── .gitignore ├── package.json ├── webpack.config.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | js/ 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-decorators-legacy", 7 | "transform-class-properties", 8 | "transform-object-rest-spread", 9 | "transform-async-to-generator", 10 | "transform-async-to-module-method", 11 | ["transform-runtime", { 12 | "polyfill": false, 13 | "regenerator": true 14 | }] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /js/getInput.js: -------------------------------------------------------------------------------- 1 | export default function getInput() { 2 | let getUserMedia = (navigator.getUserMedia || 3 | navigator.webkitGetUserMedia || 4 | navigator.mozGetUserMedia); 5 | 6 | const constraints = { 7 | audio: true, 8 | video: false 9 | }; 10 | 11 | if(!getUserMedia) 12 | Promise.reject(new Error('getUserMedia not supported!')); 13 | 14 | if(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 15 | return navigator.mediaDevices.getUserMedia(constraints); 16 | } else { 17 | return new Promise((resolve, reject) => { 18 | getUserMedia.call(navigator, constraints, constraints, resolve, reject); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | <<<<<<< 6f87c16601e1b81e9c889801c5598faf1495fe8e 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | ======= 22 | >>>>>>> First commit - Adding the whole project 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recordy", 3 | "version": "0.0.4", 4 | "description": "Record your microphone (or any other input) and add effects to it!", 5 | "main": "build/bundle.js", 6 | "scripts": { 7 | "start:dev": "webpack-dev-server", 8 | "start:prod": "webpack" 9 | }, 10 | "keywords": [], 11 | "author": "Maximilian Torggler", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "babel-core": "*", 15 | "babel-loader": "*", 16 | "babel-plugin-transform-async-to-generator": "*", 17 | "babel-plugin-transform-async-to-module-method": "*", 18 | "babel-plugin-transform-class-properties": "*", 19 | "babel-plugin-transform-decorators-legacy": "*", 20 | "babel-plugin-transform-object-rest-spread": "*", 21 | "babel-plugin-transform-runtime": "*", 22 | "babel-polyfill": "*", 23 | "babel-preset-es2015": "*", 24 | "npm-install-webpack-plugin": "*", 25 | "webpack": "*", 26 | "webpack-dev-server": "*", 27 | "webpack-merge": "*", 28 | "worker-loader": "*", 29 | "file-loader": "*" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/scriptify/Recordy.git" 34 | }, 35 | "dependencies": { 36 | "webaudio-chnl": "0.0.6", 37 | "wrecorder": "github:scriptify/Wrecorder" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import Chnl from 'webaudio-chnl'; 2 | import Recorder from 'wrecorder'; 3 | import getInput from './getInput'; 4 | 5 | export default class Recordy extends Chnl { 6 | 7 | recorder; 8 | directOutGain; 9 | 10 | constructor(audioCtx) { 11 | super(audioCtx); 12 | this.recorder = new Recorder(this); 13 | 14 | // Set direct output to speakers 15 | this.directOutGain = audioCtx.createGain(); 16 | this.directOutGain.gain.value = 0; 17 | this.connect(this.directOutGain); 18 | this.directOutGain.connect(audioCtx.destination); 19 | 20 | } 21 | 22 | toSpeaker(val) { 23 | this.directOutGain.gain.value = val; 24 | } 25 | 26 | async getInput() { 27 | const stream = await getInput(); 28 | const mediaStream = this.context.createMediaStreamSource(stream); 29 | mediaStream.connect(this); 30 | return true; 31 | } 32 | 33 | startRecording() { 34 | this.recorder.record(); 35 | } 36 | 37 | stopRecording(asAudioObject = false) { 38 | // If asAudioObject evaluates to true, a window.Audio-object will be returned; otherwise, a blob will be returned; 39 | return new Promise((resolve, reject) => { 40 | this.recorder.stop(); 41 | 42 | this.recorder.exportWAV(blob => { 43 | this.recorder.clear(); 44 | 45 | if(asAudioObject) { 46 | const url = URL.createObjectURL(blob); 47 | const audio = new Audio(url); 48 | resolve(audio); 49 | } 50 | 51 | resolve(blob); 52 | }); 53 | }); 54 | } 55 | 56 | } 57 | 58 | /*const audioCtx = new AudioContext(); 59 | const r = new Recordy(audioCtx); 60 | 61 | r.getInput() 62 | .then(val => { 63 | r.startRecording(); 64 | 65 | window.setTimeout(() => { 66 | r.stopRecording(true) 67 | .then(audio => { 68 | audio.play(); 69 | }); 70 | }, 1000); 71 | r.toSpeaker(0.4); 72 | r.effects.bitcrusher.enable(); 73 | }); 74 | 75 | */ 76 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | webpack = require('webpack'), // Da bundling modules! 3 | NpmInstallPlugin = require('npm-install-webpack-plugin'), // Install client dependencies automatically! 4 | merge = require('webpack-merge'); // Merge together configurations! 5 | 6 | const PATHS = { 7 | js: path.join(__dirname, './js/'), 8 | build: path.join(__dirname, './build') 9 | }; 10 | 11 | const TARGET = process.env.npm_lifecycle_event; 12 | 13 | const COMMON_CONFIGURATION = { 14 | entry: { 15 | app: PATHS.js 16 | }, 17 | resolve: { 18 | extensions: ['', '.js'], // Resolve these extensions 19 | }, 20 | output: { 21 | path: PATHS.build, 22 | filename: 'bundle.js', 23 | libraryTarget: 'umd', 24 | library: 'Recordy' 25 | }, 26 | module: { 27 | loaders: [ 28 | { 29 | test: /\.js$/, 30 | loaders: ['babel?cacheDirectory'], 31 | include: PATHS.js 32 | } 33 | ] 34 | } 35 | }; 36 | 37 | switch(TARGET) { 38 | // Which procedure was started? 39 | default: 40 | case 'start:dev': { 41 | module.exports = merge(COMMON_CONFIGURATION, { 42 | devServer: { 43 | contentBase: PATHS.build, 44 | historyApiFallback: true, 45 | hot: true, 46 | inline: true, 47 | progress: true, 48 | stats: 'errors-only' 49 | }, 50 | plugins: [ 51 | new webpack.HotModuleReplacementPlugin(), 52 | new NpmInstallPlugin({ 53 | save: true 54 | }) 55 | ], 56 | devtool: 'eval-source-map' 57 | }); 58 | } 59 | break; 60 | case 'start:prod': { 61 | module.exports = merge(COMMON_CONFIGURATION, { 62 | plugins: [ 63 | new webpack.DefinePlugin({ 64 | 'process.env': { 65 | 'NODE_ENV': JSON.stringify('production') 66 | } 67 | }), 68 | new webpack.optimize.UglifyJsPlugin({ 69 | compress: { warnings: false } 70 | }), 71 | new webpack.optimize.DedupePlugin() 72 | ] 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recordy 2 | ## Recording for browsers - the easy way 3 | This module abstracts away the logic needed to record audio in your browser. 4 | Since it's based on the [Chnl](https://github.com/scriptify/Chnl) module, a lot of effects can be added to the input. For information about this aspect just have a look a the documentation of Chnl. 5 | You can treat any Recordy-instance as a Chnl, because Recordy is extending Chnl. 6 | To record the input, I'm using a fork of the popular recorder.js library from Matt Diamond, [wrecorder](https://github.com/scriptify/Wrecorder), which allows us to record the output of WebAudio-nodes. Big thanks for this awesome work! 7 | 8 | __Attention__: Since the [webaudio-effect-unit](https://github.com/scriptify/webaudio-effect-unit) has reached v.1.1.0, the way how the effects work has changed. Have a look at it's repository for more details. Make sure to do this BEFORE you update. If you have difficulties or questions, just open an issue! I am always glad if I can help. :smile: 9 | 10 | ## Installation 11 | The package is hosted on npm. You can consume it with any package manager supporting npm packages. 12 | ```bash 13 | npm i recordy -S 14 | ``` 15 | 16 | ## Usage 17 | ### Creating an instance 18 | ```javascript 19 | new Recordy(audioCtx) 20 | ``` 21 | 22 | To create a Recordy-instance, you have to pass exactly one argument to the constructor: an AudioContext object. 23 | Now, you can request audio input(have a look at the example for more information). 24 | 25 | ### Getting input 26 | ```javascript 27 | .getInput() 28 | ``` 29 | 30 | This method needs to be executed before you can start recording. It asynchronously requests audio input. So the return value is a __Promise__, which returns a __boolean__ value. This value evaluates to true if the request for the microphone/audio-input was successfully and to false if it wasn't. 31 | 32 | ### Start recording 33 | ```javascript 34 | .startRecording() 35 | ``` 36 | 37 | This method is really self-explanatory. 38 | Recody will record until you call the .stopRecording(...) method. 39 | 40 | ### Stop recording 41 | ```javascript 42 | .stopRecording(asAudioObject) 43 | ``` 44 | 45 | This methods stops a previously started recording. 46 | It accepts exactly one parameter: a __boolean__. 47 | If this boolean evaluates to true, this method will return a Promise which returns an Audio-object with the recorded track. 48 | Otherwise, it returns a Promise which returns the plain binary data(blob) of the recorded track. 49 | 50 | ### Outputting to the speakers 51 | ```javascript 52 | .toSpeaker(gainValue) 53 | ``` 54 | 55 | Recordy allows you to directly output the audio-input to the speakers, so you could directly hear the effects you apply etc. The method accepts exactly one parameter: The volume of the output. This can be a number from 0 - 1. If you set a value of 0 it's muted, if you set a value of 1 it's the maximal possible volume. 56 | __ATTENTION:__ Due to the lack of support of advanced and latency-free audio protocols like ASIO(...) in the actual browsers, there's a quite high latency between input and output (it's clearly noticeable). 57 | Therefore, it's muted by default. 58 | 59 | 60 | # Example 61 | 62 | This is a simple example which records an one second long track. The track gets returned as an Audio-object so it can be directly played. Also, the input is directly outputted to the speakers with a gain of 0.4. 63 | In addition, some functionality of the Chnl module was applied: The bitcrusher effect was enabled. 64 | 65 | ```javascript 66 | const audioCtx = new AudioContext(); 67 | const r = new Recordy(audioCtx); 68 | 69 | r.getInput() 70 | .then(val => { 71 | r.startRecording(); 72 | 73 | window.setTimeout(() => { 74 | r.stopRecording(true) 75 | .then(audio => { 76 | audio.play(); 77 | }); 78 | }, 1000); 79 | r.toSpeaker(0.4); 80 | r.effects.bitcrusher.enable(); 81 | }); 82 | ``` 83 | --------------------------------------------------------------------------------