├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── mic.js └── silenceTransform.js ├── package.json └── tests └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Test Outputs 7 | output.raw 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ashish Bajaj 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 | # mic 2 | A simple stream wrapper for [arecord](http://alsa-project.org/) (Linux (including Raspbian)) and [sox](http://sox.sourceforge.net/) (Mac/Windows). Returns a Mic object that supports a flexible API to control: start, stop, pause, resume functionality. Also it provides access to the audioStream object that provides evented notifications for 'startComplete', 'stopComplete', 'pauseComplete', 'resumeComplete', 'silence' and 'processExitComplete'. You can use this signals to control various states. 3 | 4 | This is a cross platform library that has been tested on both my MacbookPro as well as my Raspberry Pi and it works very well on both these platforms. I haven't tested this on Windows, but it should work as long as you have installed either sox OR alsa tools. 5 | 6 | Installation 7 | ============ 8 | This module depends on your machine having an installation of sox (Mac/Windows Users) OR ALSA tools for Linux. You can use this library to record from any microphone device. 9 | Before installing and experimenting with the mic module, you need to ensure that you are able to capture audio via the command line: 10 | 11 | ``` 12 | $ arecord temp.wav 13 | ``` 14 | OR 15 | ``` 16 | $ rec temp.wav 17 | ``` 18 | To get ALSA tools on Raspberry Pi running raspbian, try the following: 19 | ``` 20 | $ sudo apt-get update 21 | $ sudo apt-get upgrade 22 | $ sudo apt-get install alsa-base alsa-utils 23 | ``` 24 | 25 | After the above is tested and validated, you can proceed to install the module using: 26 | 27 | ``` 28 | $ npm install mic 29 | ``` 30 | 31 | API 32 | ============ 33 | Below is an example of how to use the module. 34 | ```javascript 35 | var mic = require('mic'); 36 | var fs = require('fs'); 37 | 38 | var micInstance = mic({ 39 | rate: '16000', 40 | channels: '1', 41 | debug: true, 42 | exitOnSilence: 6 43 | }); 44 | var micInputStream = micInstance.getAudioStream(); 45 | 46 | var outputFileStream = fs.WriteStream('output.raw'); 47 | 48 | micInputStream.pipe(outputFileStream); 49 | 50 | micInputStream.on('data', function(data) { 51 | console.log("Recieved Input Stream: " + data.length); 52 | }); 53 | 54 | micInputStream.on('error', function(err) { 55 | console.log("Error in Input Stream: " + err); 56 | }); 57 | 58 | micInputStream.on('startComplete', function() { 59 | console.log("Got SIGNAL startComplete"); 60 | setTimeout(function() { 61 | micInstance.pause(); 62 | }, 5000); 63 | }); 64 | 65 | micInputStream.on('stopComplete', function() { 66 | console.log("Got SIGNAL stopComplete"); 67 | }); 68 | 69 | micInputStream.on('pauseComplete', function() { 70 | console.log("Got SIGNAL pauseComplete"); 71 | setTimeout(function() { 72 | micInstance.resume(); 73 | }, 5000); 74 | }); 75 | 76 | micInputStream.on('resumeComplete', function() { 77 | console.log("Got SIGNAL resumeComplete"); 78 | setTimeout(function() { 79 | micInstance.stop(); 80 | }, 5000); 81 | }); 82 | 83 | micInputStream.on('silence', function() { 84 | console.log("Got SIGNAL silence"); 85 | }); 86 | 87 | micInputStream.on('processExitComplete', function() { 88 | console.log("Got SIGNAL processExitComplete"); 89 | }); 90 | 91 | micInstance.start(); 92 | ``` 93 | 94 | You should be able to playback the output file using. Note that arecord pipes the 44 byte WAV header, whereas SOX does NOT add any header. So we need to provide the file format details to the player: 95 | 96 | ``` 97 | $ aplay -f S16_LE -r 16000 -c 1 output.raw 98 | ``` 99 | OR 100 | ``` 101 | $ play -b 16 -e signed -c 1 -r 16000 output.raw 102 | ``` 103 | 104 | ### mic(options) 105 | Returns a microphone object instance that can be used to control the streaming samples coming in from the specified device. 106 | * `options` - JSON containing command line options. Following are valid options: 107 | * `endian`: `big` OR `little`, default: `little` 108 | * `bitwidth`: `8` OR `16` OR `24` OR anything valid supported by arecord OR sox, default: `16` 109 | * `encoding`: `signed-integer` OR `unsinged-integer` (none of the other encoding formats are supported), default:`signed-integer` 110 | * `rate`: `8000` OR `16000` OR `44100` OR anything valid supported by arecord OR sox, default: `16000` 111 | * `channels`: `1` OR `2` OR anything valid supported by arecord OR sox, default: `1` (mono) 112 | * `device`: `hw:0,0` OR `plughw:1, 0` OR anything valid supported by arecord. Ignored for sox on macOS. 113 | * `exitOnSilence`: The `'silence'` signal is raised after reaching these many consecutive frames, default: '0' 114 | * `debug`: true OR false - can be used to aide in debugging 115 | * `fileType`: string defaults to 'raw', allows you to set a valid file type such as 'wav' (for sox only) to avoid the no header issue mentioned above, see a list of types [here](http://sox.sourceforge.net/soxformat.html) 116 | 117 | ### mic.start() 118 | This instantiates the process `arecord` OR `sox` using the options specified 119 | 120 | ### mic.stop() 121 | This kills the arecord OR sox process that was started in the start() routine. It uses the `SIGTERM` signal. 122 | 123 | ### mic.pause() 124 | This pauses the arecord OR sox process using the `SIGSTOP` signal. 125 | 126 | ### mic.resume() 127 | This resumes the arecord OR sox process using the `SIGCONT` signal. 128 | 129 | ### mic.getAudioStream() 130 | This returns a simple Transform stream that contains the data from the arecord OR sox process. This sream can be directly piped to a speaker sream OR a file stream. Further this provides a number of events triggered by the state of the stream: 131 | * `'silence'`: This is emitted once when `exitOnSilence` number of consecutive frames of silence are found 132 | * `'sound'`: This is emitted if we hear something after a `silence` 133 | * `'processExitComplete'`: This is emitted once the arecord OR sox process exits 134 | * `'startComplete'`: This is emitted once the start() function is successfully executed 135 | * `'stopComplete'`: This is emitted once the stop() function is successfully executed 136 | * `'pauseComplete'`: This is emitted once the pause() function is successfully executed 137 | * `'resumeComplete'`: This is emitted once the resume() function is successfully executed 138 | * It further inherits all the Events from [stream.Transform](http://nodejs.org/api/stream.html#stream_class_stream_transform) 139 | 140 | 141 | License 142 | ========== 143 | The MIT License (MIT) 144 | 145 | Copyright (c) 2016 Ashish Bajaj bajaj.ashish@gmail.com 146 | 147 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 148 | 149 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 152 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/mic'); 2 | -------------------------------------------------------------------------------- /lib/mic.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var isMac = require('os').type() == 'Darwin'; 3 | var isWindows = require('os').type().indexOf('Windows') > -1; 4 | var IsSilence = require('./silenceTransform.js'); 5 | var PassThrough = require('stream').PassThrough; 6 | 7 | var mic = function mic(options) { 8 | options = options || {}; 9 | var that = {}; 10 | var endian = options.endian || 'little'; 11 | var bitwidth = options.bitwidth || '16'; 12 | var encoding = options.encoding || 'signed-integer'; 13 | var rate = options.rate || '16000'; 14 | var channels = options.channels || '1'; 15 | var device = options.device || 'plughw:1,0'; 16 | var exitOnSilence = options.exitOnSilence || 0; 17 | var fileType = options.fileType || 'raw'; 18 | var debug = options.debug || false; 19 | var format, formatEndian, formatEncoding; 20 | var audioProcess = null; 21 | var infoStream = new PassThrough; 22 | var audioStream = new IsSilence({debug: debug}); 23 | var audioProcessOptions = { 24 | stdio: ['ignore', 'pipe', 'ignore'] 25 | }; 26 | 27 | if(debug) { 28 | audioProcessOptions.stdio[2] = 'pipe'; 29 | } 30 | 31 | // Setup format variable for arecord call 32 | if(endian === 'big') { 33 | formatEndian = 'BE'; 34 | } else { 35 | formatEndian = 'LE'; 36 | } 37 | if(encoding === 'unsigned-integer') { 38 | formatEncoding = 'U'; 39 | } else { 40 | formatEncoding = 'S'; 41 | } 42 | format = formatEncoding + bitwidth + '_' + formatEndian; 43 | audioStream.setNumSilenceFramesExitThresh(parseInt(exitOnSilence, 10)); 44 | 45 | that.start = function start() { 46 | if(audioProcess === null) { 47 | if(isWindows){ 48 | audioProcess = spawn('sox', ['-b', bitwidth, '--endian', endian, 49 | '-c', channels, '-r', rate, '-e', encoding, 50 | '-t', 'waveaudio', 'default', '-p'], 51 | audioProcessOptions) 52 | } 53 | else if(isMac){ 54 | audioProcess = spawn('rec', ['-b', bitwidth, '--endian', endian, 55 | '-c', channels, '-r', rate, '-e', encoding, 56 | '-t', fileType, '-'], audioProcessOptions) 57 | } 58 | else { 59 | audioProcess = spawn('arecord', ['-t', fileType, '-c', channels, '-r', rate, '-f', 60 | format, '-D', device], audioProcessOptions); 61 | } 62 | 63 | audioProcess.on('exit', function(code, sig) { 64 | if(code != null && sig === null) { 65 | audioStream.emit('audioProcessExitComplete'); 66 | if(debug) console.log("recording audioProcess has exited with code = %d", code); 67 | } 68 | }); 69 | audioProcess.stdout.pipe(audioStream); 70 | if(debug) { 71 | audioProcess.stderr.pipe(infoStream); 72 | } 73 | audioStream.emit('startComplete'); 74 | } else { 75 | if(debug) { 76 | throw new Error("Duplicate calls to start(): Microphone already started!"); 77 | } 78 | } 79 | }; 80 | 81 | that.stop = function stop() { 82 | if(audioProcess != null) { 83 | audioProcess.kill('SIGTERM'); 84 | audioProcess = null; 85 | audioStream.emit('stopComplete'); 86 | if(debug) console.log("Microphone stopped"); 87 | } 88 | }; 89 | 90 | that.pause = function pause() { 91 | if(audioProcess != null) { 92 | audioProcess.kill('SIGSTOP'); 93 | audioStream.pause(); 94 | audioStream.emit('pauseComplete'); 95 | if(debug) console.log("Microphone paused"); 96 | } 97 | }; 98 | 99 | that.resume = function resume() { 100 | if(audioProcess != null) { 101 | audioProcess.kill('SIGCONT'); 102 | audioStream.resume(); 103 | audioStream.emit('resumeComplete'); 104 | if(debug) console.log("Microphone resumed"); 105 | } 106 | } 107 | 108 | that.getAudioStream = function getAudioStream() { 109 | return audioStream; 110 | } 111 | 112 | if(debug) { 113 | infoStream.on('data', function(data) { 114 | console.log("Received Info: " + data); 115 | }); 116 | infoStream.on('error', function(error) { 117 | console.log("Error in Info Stream: " + error); 118 | }); 119 | } 120 | 121 | return that; 122 | } 123 | 124 | module.exports = mic; 125 | -------------------------------------------------------------------------------- /lib/silenceTransform.js: -------------------------------------------------------------------------------- 1 | var Transform = require('stream').Transform; 2 | var util = require("util"); 3 | 4 | function IsSilence(options) { 5 | var that = this; 6 | if (options && options.debug) { 7 | that.debug = options.debug; 8 | delete options.debug; 9 | } 10 | Transform.call(that, options); 11 | var consecSilenceCount = 0; 12 | var numSilenceFramesExitThresh = 0; 13 | 14 | that.getNumSilenceFramesExitThresh = function getNumSilenceFramesExitThresh() { 15 | return numSilenceFramesExitThresh; 16 | }; 17 | 18 | that.getConsecSilenceCount = function getConsecSilenceCount() { 19 | return consecSilenceCount; 20 | }; 21 | 22 | that.setNumSilenceFramesExitThresh = function setNumSilenceFramesExitThresh(numFrames) { 23 | numSilenceFramesExitThresh = numFrames; 24 | return; 25 | }; 26 | 27 | that.incrConsecSilenceCount = function incrConsecSilenceCount() { 28 | consecSilenceCount++; 29 | return consecSilenceCount; 30 | }; 31 | 32 | that.resetConsecSilenceCount = function resetConsecSilenceCount() { 33 | consecSilenceCount = 0; 34 | return; 35 | }; 36 | }; 37 | util.inherits(IsSilence, Transform); 38 | 39 | IsSilence.prototype._transform = function(chunk, encoding, callback) { 40 | var i; 41 | var speechSample; 42 | var silenceLength = 0; 43 | var self = this; 44 | var debug = self.debug; 45 | var consecutiveSilence = self.getConsecSilenceCount(); 46 | var numSilenceFramesExitThresh = self.getNumSilenceFramesExitThresh(); 47 | var incrementConsecSilence = self.incrConsecSilenceCount; 48 | var resetConsecSilence = self.resetConsecSilenceCount; 49 | 50 | if(numSilenceFramesExitThresh) { 51 | for(i=0; i 128) { 53 | speechSample = (chunk[i+1] - 256) * 256; 54 | } else { 55 | speechSample = chunk[i+1] * 256; 56 | } 57 | speechSample += chunk[i]; 58 | 59 | if(Math.abs(speechSample) > 2000) { 60 | if (debug) { 61 | console.log("Found speech block"); 62 | } 63 | //emit 'sound' if we hear a sound after a silence 64 | if(consecutiveSilence>numSilenceFramesExitThresh) self.emit('sound'); 65 | resetConsecSilence(); 66 | break; 67 | } else { 68 | silenceLength++; 69 | } 70 | 71 | } 72 | if(silenceLength == chunk.length/2) { 73 | consecutiveSilence = incrementConsecSilence(); 74 | if (debug) { 75 | console.log("Found silence block: %d of %d", consecutiveSilence, numSilenceFramesExitThresh); 76 | } 77 | //emit 'silence' only once each time the threshold condition is met 78 | if( consecutiveSilence === numSilenceFramesExitThresh) { 79 | self.emit('silence'); 80 | } 81 | } 82 | } 83 | this.push(chunk); 84 | callback(); 85 | }; 86 | 87 | module.exports = IsSilence; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mic", 3 | "version": "2.1.2", 4 | "description": "A simple stream wrapper for arecord (Linux (including Raspbian)) and sox (Mac/Windows). Returns a Passthrough stream object so that stream control like pause(), resume(), pipe(), etc. are all available.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node tests/test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ashishbajaj99/mic.git" 12 | }, 13 | "keywords": [ 14 | "node-mic", 15 | "microphone", 16 | "sox", 17 | "alsa", 18 | "arecord", 19 | "mic", 20 | "audio", 21 | "input" 22 | ], 23 | "author": "Ashish Bajaj ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/ashishbajaj99/mic/issues" 27 | }, 28 | "homepage": "https://github.com/ashishbajaj99/mic#readme" 29 | } 30 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | var mic = require('../index.js'); 2 | var fs = require('fs'); 3 | 4 | var micInstance = mic({ 'rate': '16000', 'channels': '1', 'debug': false, 'exitOnSilence': 6 }); 5 | var micInputStream = micInstance.getAudioStream(); 6 | 7 | var outputFileStream = fs.WriteStream('output.raw'); 8 | 9 | micInputStream.pipe(outputFileStream); 10 | 11 | var chunkCounter = 0; 12 | micInputStream.on('data', function(data) { 13 | console.log("Recieved Input Stream of Size %d: %d", data.length, chunkCounter++); 14 | }); 15 | 16 | micInputStream.on('error', function(err) { 17 | cosole.log("Error in Input Stream: " + err); 18 | }); 19 | 20 | micInputStream.on('startComplete', function() { 21 | console.log("Got SIGNAL startComplete"); 22 | setTimeout(function() { 23 | micInstance.pause(); 24 | }, 5000); 25 | }); 26 | 27 | micInputStream.on('stopComplete', function() { 28 | console.log("Got SIGNAL stopComplete"); 29 | }); 30 | 31 | micInputStream.on('pauseComplete', function() { 32 | console.log("Got SIGNAL pauseComplete"); 33 | setTimeout(function() { 34 | micInstance.resume(); 35 | }, 5000); 36 | }); 37 | 38 | micInputStream.on('resumeComplete', function() { 39 | console.log("Got SIGNAL resumeComplete"); 40 | setTimeout(function() { 41 | micInstance.stop(); 42 | }, 5000); 43 | }); 44 | 45 | micInputStream.on('silence', function() { 46 | console.log("Got SIGNAL silence"); 47 | }); 48 | 49 | micInputStream.on('processExitComplete', function() { 50 | console.log("Got SIGNAL processExitComplete"); 51 | }); 52 | 53 | micInstance.start(); 54 | 55 | 56 | --------------------------------------------------------------------------------