├── .gitignore ├── .npmignore ├── ReadMe.md ├── dist ├── index.d.ts ├── index.js ├── input.d.ts ├── input.js ├── mixer-interleaved.d.ts ├── mixer-interleaved.js ├── mixer.d.ts └── mixer.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── input.ts ├── mixer-interleaved.ts └── mixer.ts ├── test.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 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 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | *.tgz 3 | debug.log 4 | 5 | gulpfile.js 6 | webpack.* 7 | **/.vscode 8 | .npmignore 9 | .gitignore -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Audio-Mixer 2 | 3 | Allows mixing of PCM audio streams. 4 | 5 | ## Installation 6 | ``` 7 | npm i audio-mixer -S 8 | ``` 9 | 10 | ## API 11 | 12 | ### `Mixer.new` 13 | ```js 14 | // @options - The options for the mixer 15 | let mixer = new Mixer(options: MixerArguments); 16 | 17 | // @channels - The number of channels this output has 18 | // @bitDepth - The bit depth of the data going to the output 19 | // @sampleRate - The sample rate of the output 20 | // @clearInterval - An interval in ms of when to dump the stream to keep the inputs in sync (when not specified the stream is not dumped) 21 | interface MixerArguments extends ReadableOptions { 22 | channels: number; 23 | bitDepth?: number; 24 | sampleRate: number; 25 | clearInterval?: number; 26 | } 27 | ``` 28 | 29 | ### `Mixer.input` 30 | ```js 31 | // @options - The options for this input 32 | let input = mixer.input(options: InputArguments); 33 | 34 | // @channels - The number of channels this input has (default uses Mixer's) 35 | // @bitDepth - The bit depth of the data coming in this input (default uses Mixer's) 36 | // @sampleRate - The sample rate of this input (default uses Mixer's) 37 | // @volume - The volume to set this input to when mixing (default is 100) 38 | interface InputArguments extends WritableOptions { 39 | channels?: number; 40 | bitDepth?: number; 41 | sampleRate?: number; 42 | volume?: number; 43 | } 44 | ``` 45 | 46 | ### `Mixer.removeInput(input)` 47 | ```js 48 | // @input - The input to remove from the mixer 49 | mixer.removeInput(input); 50 | ``` 51 | 52 | ### `Input.new` 53 | ```js 54 | // @options - The options for this input 55 | let input = new Input(options: InputArguments); 56 | 57 | // @channels - The number of channels this input has 58 | // @bitDepth - The bit depth of the data coming in this input 59 | // @sampleRate - The sample rate of this input 60 | // @volume - The volume to set this input to when mixing (default is 100) 61 | interface InputArguments extends WritableOptions { 62 | channels?: number; 63 | bitDepth?: number; 64 | sampleRate?: number; 65 | volume?: number; 66 | } 67 | ``` 68 | 69 | ### `Input.getVolume()` 70 | ```js 71 | input.getVolume(); 72 | ``` 73 | 74 | ### `Input.setVolume(volume)` 75 | ```js 76 | // @volume - The volume to set this input to 77 | input.setVolume(volume); 78 | ``` 79 | 80 | ## Code Example 81 | ```js 82 | var AudioMixer = require('audio-mixer'); 83 | 84 | // Creates a new audio mixer with the specified options 85 | let mixer = new AudioMixer.Mixer({ 86 | channels: 2, 87 | bitDepth: 16, 88 | sampleRate: 44100, 89 | clearInterval: 250 90 | }); 91 | 92 | // Creates an input that is attached to the mixer 93 | let input = mixer.input({ 94 | channels: 1, 95 | volume: 75 96 | }); 97 | 98 | // Creates a standalone input 99 | let standaloneInput = new AudioMixer.Input({ 100 | channels: 1, 101 | bitDepth: 16, 102 | sampleRate: 48000, 103 | volume: 75 104 | }); 105 | 106 | // Adds the standalone input to the mixer 107 | mixer.addInput(standaloneInput); 108 | 109 | // Pipes a readable stream into an input 110 | deviceInputStream.pipe(input); 111 | deviceInputStream2.pipe(standaloneInput); 112 | 113 | // Pipes the mixer output to an writable stream 114 | mixer.pipe(deviceOutputStream); 115 | ``` -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './mixer'; 2 | export * from './mixer-interleaved'; 3 | export * from './input'; 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | __export(require("./mixer")); 7 | __export(require("./mixer-interleaved")); 8 | __export(require("./input")); 9 | -------------------------------------------------------------------------------- /dist/input.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Writable, WritableOptions } from 'stream'; 3 | import { Mixer } from './mixer'; 4 | export interface InputArguments extends WritableOptions { 5 | channels?: number; 6 | bitDepth?: number; 7 | sampleRate?: number; 8 | volume?: number; 9 | clearInterval?: number; 10 | } 11 | export declare class Input extends Writable { 12 | private mixer; 13 | private args; 14 | private buffer; 15 | private sampleByteLength; 16 | private readSample; 17 | private writeSample; 18 | hasData: boolean; 19 | lastDataTime: number; 20 | lastClearTime: number; 21 | constructor(args: InputArguments); 22 | setMixer(mixer: Mixer): void; 23 | read(samples: any): Buffer; 24 | readMono(samples: any): Buffer; 25 | readStereo(samples: any): Buffer; 26 | availableSamples(length?: number): number; 27 | _write(chunk: any, encoding: any, next: any): void; 28 | setVolume(volume: number): void; 29 | getVolume(): number; 30 | clear(force?: boolean): void; 31 | destroy(): void; 32 | } 33 | -------------------------------------------------------------------------------- /dist/input.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const stream_1 = require("stream"); 4 | class Input extends stream_1.Writable { 5 | constructor(args) { 6 | super(args); 7 | if (args.channels !== 1 && args.channels !== 2) { 8 | args.channels = 2; 9 | } 10 | if (args.sampleRate < 1) { 11 | args.sampleRate = 44100; 12 | } 13 | if (args.volume < 0 || args.volume > 100) { 14 | args.volume = 100; 15 | } 16 | if (args.channels === 1) { 17 | this.readMono = this.read; 18 | } 19 | if (args.channels === 2) { 20 | this.readStereo = this.read; 21 | } 22 | this.buffer = new Buffer(0); 23 | if (args.bitDepth === 8) { 24 | this.readSample = this.buffer.readInt8; 25 | this.writeSample = this.buffer.writeInt8; 26 | this.sampleByteLength = 1; 27 | } 28 | else if (args.bitDepth === 32) { 29 | this.readSample = this.buffer.readInt32LE; 30 | this.writeSample = this.buffer.writeInt32LE; 31 | this.sampleByteLength = 4; 32 | } 33 | else { 34 | args.bitDepth = 16; 35 | this.readSample = this.buffer.readInt16LE; 36 | this.writeSample = this.buffer.writeInt16LE; 37 | this.sampleByteLength = 2; 38 | } 39 | this.args = args; 40 | this.hasData = false; 41 | this.lastClearTime = new Date().getTime(); 42 | } 43 | setMixer(mixer) { 44 | this.mixer = mixer; 45 | } 46 | read(samples) { 47 | let bytes = samples * (this.args.bitDepth / 8) * this.args.channels; 48 | if (this.buffer.length < bytes) { 49 | bytes = this.buffer.length; 50 | } 51 | let sample = this.buffer.slice(0, bytes); 52 | this.buffer = this.buffer.slice(bytes); 53 | for (let i = 0; i < sample.length; i += 2) { 54 | sample.writeInt16LE(Math.floor(this.args.volume * sample.readInt16LE(i) / 100), i); 55 | } 56 | return sample; 57 | } 58 | readMono(samples) { 59 | let stereoBuffer = this.read(samples); 60 | let monoBuffer = new Buffer(stereoBuffer.length / 2); 61 | let availableSamples = this.availableSamples(stereoBuffer.length); 62 | for (let i = 0; i < availableSamples; i++) { 63 | let l = this.readSample.call(stereoBuffer, i * this.sampleByteLength * 2); 64 | let r = this.readSample.call(stereoBuffer, (i * this.sampleByteLength * 2) + this.sampleByteLength); 65 | this.writeSample.call(monoBuffer, Math.floor((l + r) / 2), i * this.sampleByteLength); 66 | } 67 | return monoBuffer; 68 | } 69 | readStereo(samples) { 70 | let monoBuffer = this.read(samples); 71 | let stereoBuffer = new Buffer(monoBuffer.length * 2); 72 | let availableSamples = this.availableSamples(monoBuffer.length); 73 | for (let i = 0; i < availableSamples; i++) { 74 | let m = this.readSample.call(monoBuffer, i * this.sampleByteLength); 75 | this.writeSample.call(stereoBuffer, m, i * this.sampleByteLength * 2); 76 | this.writeSample.call(stereoBuffer, m, (i * this.sampleByteLength * 2) + this.sampleByteLength); 77 | } 78 | return stereoBuffer; 79 | } 80 | availableSamples(length) { 81 | length = length || this.buffer.length; 82 | return Math.floor(length / ((this.args.bitDepth / 8) * this.args.channels)); 83 | } 84 | _write(chunk, encoding, next) { 85 | if (!this.hasData) { 86 | this.hasData = true; 87 | } 88 | this.buffer = Buffer.concat([this.buffer, chunk]); 89 | next(); 90 | } 91 | setVolume(volume) { 92 | this.args.volume = Math.max(Math.min(volume, 100), 0); 93 | } 94 | getVolume() { 95 | return this.args.volume; 96 | } 97 | clear(force) { 98 | let now = new Date().getTime(); 99 | if (force || (this.args.clearInterval && now - this.lastClearTime >= this.args.clearInterval)) { 100 | let length = 1024 * (this.args.bitDepth / 8) * this.args.channels; 101 | this.buffer = this.buffer.slice(0, length); 102 | this.lastClearTime = now; 103 | } 104 | } 105 | destroy() { 106 | this.buffer = new Buffer(0); 107 | } 108 | } 109 | exports.Input = Input; 110 | -------------------------------------------------------------------------------- /dist/mixer-interleaved.d.ts: -------------------------------------------------------------------------------- 1 | import { Mixer } from './mixer'; 2 | export declare class InterleavedMixer extends Mixer { 3 | _read(): void; 4 | } 5 | -------------------------------------------------------------------------------- /dist/mixer-interleaved.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const mixer_1 = require("./mixer"); 4 | class InterleavedMixer extends mixer_1.Mixer { 5 | _read() { 6 | let samples = this.getMaxSamples(); 7 | if (samples > 0 && samples !== Number.MAX_VALUE) { 8 | let mixedBuffer = new Buffer(samples * this.sampleByteLength * this.args.channels); 9 | mixedBuffer.fill(0); 10 | for (let c = 0; c < this.args.channels; c++) { 11 | let input = this.inputs[c]; 12 | if (input !== undefined && input.hasData) { 13 | let inputBuffer = input.readMono(samples); 14 | for (let i = 0; i < samples; i++) { 15 | let sample = this.readSample.call(inputBuffer, i * this.sampleByteLength); 16 | this.writeSample.call(mixedBuffer, sample, (i * this.sampleByteLength * this.args.channels) + (c * this.sampleByteLength)); 17 | } 18 | } 19 | } 20 | this.push(mixedBuffer); 21 | } 22 | else { 23 | setImmediate(this._read.bind(this)); 24 | } 25 | this.clearBuffers(); 26 | } 27 | } 28 | exports.InterleavedMixer = InterleavedMixer; 29 | -------------------------------------------------------------------------------- /dist/mixer.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Input, InputArguments } from './input'; 3 | import { Readable, ReadableOptions } from 'stream'; 4 | export interface MixerArguments extends ReadableOptions { 5 | channels: number; 6 | sampleRate: number; 7 | bitDepth?: number; 8 | } 9 | export declare class Mixer extends Readable { 10 | protected args: MixerArguments; 11 | protected inputs: Input[]; 12 | protected sampleByteLength: number; 13 | protected readSample: any; 14 | protected writeSample: any; 15 | protected needReadable: boolean; 16 | private static INPUT_IDLE_TIMEOUT; 17 | private _timer; 18 | constructor(args: MixerArguments); 19 | _read(): void; 20 | input(args: InputArguments, channel?: number): Input; 21 | removeInput(input: Input): void; 22 | addInput(input: Input, channel?: number): void; 23 | destroy(): void; 24 | close(): void; 25 | protected getMaxSamples(): number; 26 | protected clearBuffers(): void; 27 | } 28 | -------------------------------------------------------------------------------- /dist/mixer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const input_1 = require("./input"); 4 | const stream_1 = require("stream"); 5 | const _ = require("underscore"); 6 | class Mixer extends stream_1.Readable { 7 | constructor(args) { 8 | super(args); 9 | this.needReadable = true; 10 | this._timer = null; 11 | if (args.sampleRate < 1) { 12 | args.sampleRate = 44100; 13 | } 14 | let buffer = new Buffer(0); 15 | if (args.bitDepth === 8) { 16 | this.readSample = buffer.readInt8; 17 | this.writeSample = buffer.writeInt8; 18 | this.sampleByteLength = 1; 19 | } 20 | else if (args.bitDepth === 32) { 21 | this.readSample = buffer.readInt32LE; 22 | this.writeSample = buffer.writeInt32LE; 23 | this.sampleByteLength = 4; 24 | } 25 | else { 26 | args.bitDepth = 16; 27 | this.readSample = buffer.readInt16LE; 28 | this.writeSample = buffer.writeInt16LE; 29 | this.sampleByteLength = 2; 30 | } 31 | this.args = args; 32 | this.inputs = []; 33 | } 34 | _read() { 35 | let samples = this.getMaxSamples(); 36 | if (samples > 0 && samples !== Number.MAX_VALUE) { 37 | let mixedBuffer = new Buffer(samples * this.sampleByteLength * this.args.channels); 38 | mixedBuffer.fill(0); 39 | this.inputs.forEach((input) => { 40 | if (input.hasData) { 41 | let inputBuffer = this.args.channels === 1 ? input.readMono(samples) : input.readStereo(samples); 42 | for (let i = 0; i < samples * this.args.channels; i++) { 43 | let sample = this.readSample.call(mixedBuffer, i * this.sampleByteLength) + Math.floor(this.readSample.call(inputBuffer, i * this.sampleByteLength) / this.inputs.length); 44 | this.writeSample.call(mixedBuffer, sample, i * this.sampleByteLength); 45 | } 46 | } 47 | }); 48 | this.push(mixedBuffer); 49 | } 50 | else if (this.needReadable) { 51 | clearImmediate(this._timer); 52 | this._timer = setImmediate(this._read.bind(this)); 53 | } 54 | this.clearBuffers(); 55 | } 56 | input(args, channel) { 57 | let input = new input_1.Input({ 58 | channels: args.channels || this.args.channels, 59 | bitDepth: args.bitDepth || this.args.bitDepth, 60 | sampleRate: args.sampleRate || this.args.sampleRate, 61 | volume: args.volume || 100, 62 | clearInterval: args.clearInterval 63 | }); 64 | this.addInput(input, channel); 65 | return input; 66 | } 67 | removeInput(input) { 68 | this.inputs = _.without(this.inputs, input); 69 | } 70 | addInput(input, channel) { 71 | if (channel && (channel < 0 || channel >= this.args.channels)) { 72 | throw new Error("Channel number out of range"); 73 | } 74 | input.setMixer(this); 75 | this.inputs[channel || this.inputs.length] = input; 76 | } 77 | destroy() { 78 | this.inputs = []; 79 | } 80 | close() { 81 | this.needReadable = false; 82 | } 83 | getMaxSamples() { 84 | let samples = Number.MAX_VALUE; 85 | this.inputs.forEach((input) => { 86 | let ias = input.availableSamples(); 87 | if (ias > 0) { 88 | input.lastDataTime = new Date().getTime(); 89 | } 90 | else if (ias <= 0 && new Date().getTime() - input.lastDataTime >= Mixer.INPUT_IDLE_TIMEOUT) { 91 | input.hasData = false; 92 | return; 93 | } 94 | if (input.hasData && ias < samples) { 95 | samples = ias; 96 | } 97 | }); 98 | return samples; 99 | } 100 | clearBuffers() { 101 | this.inputs.forEach((input) => { 102 | input.clear(); 103 | }); 104 | } 105 | } 106 | Mixer.INPUT_IDLE_TIMEOUT = 250; 107 | exports.Mixer = Mixer; 108 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-mixer", 3 | "version": "2.1.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "8.0.14", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.14.tgz", 10 | "integrity": "sha512-lrtgE/5FeTdcuxgsDbLUIFJ33dTp4TkbKkTDZt/ueUMeqmGYqJRQd908q5Yj9EzzWSMonEhMr1q/CQlgVGEt4w==", 11 | "dev": true 12 | }, 13 | "audio-cmd-stream": { 14 | "version": "1.0.3", 15 | "resolved": "https://registry.npmjs.org/audio-cmd-stream/-/audio-cmd-stream-1.0.3.tgz", 16 | "integrity": "sha512-qZah2xZ8rlam8D4Rbfi9JMHHHwtyrd+ggaUXY4UyudIOKevt7XX71AvKbDYlINWf1cNxTO7i8+qKzsCPzG5aXA==", 17 | "dev": true 18 | }, 19 | "typescript": { 20 | "version": "2.4.2", 21 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", 22 | "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=", 23 | "dev": true 24 | }, 25 | "underscore": { 26 | "version": "1.8.3", 27 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 28 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-mixer", 3 | "version": "2.1.4", 4 | "description": "Allows mixing of PCM audio streams.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc -p ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ConnorChristie/Audio-Mixer.git" 13 | }, 14 | "keywords": [ 15 | "audio", 16 | "mixer", 17 | "stream", 18 | "pcm" 19 | ], 20 | "author": "Connor Christie", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/ConnorChristie/Audio-Mixer/issues" 24 | }, 25 | "homepage": "https://github.com/ConnorChristie/Audio-Mixer#readme", 26 | "devDependencies": { 27 | "@types/node": "^8.0.14", 28 | "audio-cmd-stream": "^1.0.3", 29 | "typescript": "^2.4.2" 30 | }, 31 | "dependencies": { 32 | "underscore": "^1.8.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mixer'; 2 | export * from './mixer-interleaved'; 3 | export * from './input'; -------------------------------------------------------------------------------- /src/input.ts: -------------------------------------------------------------------------------- 1 | import { Writable, WritableOptions } from 'stream'; 2 | import { Mixer } from './mixer'; 3 | 4 | import * as _ from 'underscore'; 5 | 6 | export interface InputArguments extends WritableOptions { 7 | channels?: number; 8 | bitDepth?: number; 9 | sampleRate?: number; 10 | volume?: number; 11 | clearInterval?: number; 12 | } 13 | 14 | export class Input extends Writable { 15 | 16 | private mixer: Mixer; 17 | private args: InputArguments; 18 | 19 | private buffer: Buffer; 20 | private sampleByteLength: number; 21 | 22 | private readSample; 23 | private writeSample; 24 | 25 | public hasData: boolean; 26 | 27 | public lastDataTime: number; 28 | public lastClearTime: number; 29 | 30 | constructor(args: InputArguments) { 31 | super(args); 32 | 33 | if (args.channels !== 1 && args.channels !== 2) { 34 | args.channels = 2; 35 | } 36 | 37 | if (args.sampleRate < 1) { 38 | args.sampleRate = 44100; 39 | } 40 | 41 | if (args.volume < 0 || args.volume > 100) { 42 | args.volume = 100; 43 | } 44 | 45 | if (args.channels === 1) { 46 | this.readMono = this.read; 47 | } 48 | 49 | if (args.channels === 2) { 50 | this.readStereo = this.read; 51 | } 52 | 53 | this.buffer = new Buffer(0); 54 | 55 | if (args.bitDepth === 8) { 56 | this.readSample = this.buffer.readInt8; 57 | this.writeSample = this.buffer.writeInt8; 58 | 59 | this.sampleByteLength = 1; 60 | } else if (args.bitDepth === 32) { 61 | this.readSample = this.buffer.readInt32LE; 62 | this.writeSample = this.buffer.writeInt32LE; 63 | 64 | this.sampleByteLength = 4; 65 | } else { 66 | args.bitDepth = 16; 67 | 68 | this.readSample = this.buffer.readInt16LE; 69 | this.writeSample = this.buffer.writeInt16LE; 70 | 71 | this.sampleByteLength = 2; 72 | } 73 | 74 | this.args = args; 75 | this.hasData = false; 76 | 77 | this.lastClearTime = new Date().getTime(); 78 | } 79 | 80 | public setMixer(mixer: Mixer) { 81 | this.mixer = mixer; 82 | } 83 | 84 | /** 85 | * Reads the specified number of samples into a buffer 86 | * @param samples The number of samples to read 87 | */ 88 | public read(samples) { 89 | let bytes = samples * (this.args.bitDepth / 8) * this.args.channels; 90 | if (this.buffer.length < bytes) { 91 | bytes = this.buffer.length; 92 | } 93 | 94 | let sample = this.buffer.slice(0, bytes); 95 | this.buffer = this.buffer.slice(bytes); 96 | 97 | for (let i = 0; i < sample.length; i += 2) { 98 | sample.writeInt16LE(Math.floor(this.args.volume * sample.readInt16LE(i) / 100), i); 99 | } 100 | 101 | return sample; 102 | } 103 | 104 | /** 105 | * Reads the specified number of samples into a mono buffer 106 | * This function will be overridden by this.read, if input already is mono. 107 | * @param samples The number of samples to read 108 | */ 109 | public readMono(samples) { 110 | let stereoBuffer = this.read(samples); 111 | let monoBuffer = new Buffer(stereoBuffer.length / 2); 112 | 113 | let availableSamples = this.availableSamples(stereoBuffer.length); 114 | 115 | for (let i = 0; i < availableSamples; i++) { 116 | let l = this.readSample.call(stereoBuffer, i * this.sampleByteLength * 2); 117 | let r = this.readSample.call(stereoBuffer, (i * this.sampleByteLength * 2) + this.sampleByteLength); 118 | 119 | this.writeSample.call(monoBuffer, Math.floor((l + r) / 2), i * this.sampleByteLength); 120 | } 121 | 122 | return monoBuffer; 123 | } 124 | 125 | /** 126 | * Reads the specified number of samples into a stereo buffer 127 | * This function will be overridden by this.read, if input already is stereo. 128 | * @param samples The number of samples to read 129 | */ 130 | public readStereo(samples) { 131 | let monoBuffer = this.read(samples); 132 | let stereoBuffer = new Buffer(monoBuffer.length * 2); 133 | 134 | let availableSamples = this.availableSamples(monoBuffer.length); 135 | 136 | for (let i = 0; i < availableSamples; i++) { 137 | let m = this.readSample.call(monoBuffer, i * this.sampleByteLength); 138 | 139 | this.writeSample.call(stereoBuffer, m, i * this.sampleByteLength * 2); 140 | this.writeSample.call(stereoBuffer, m, (i * this.sampleByteLength * 2) + this.sampleByteLength); 141 | } 142 | 143 | return stereoBuffer; 144 | } 145 | 146 | /** 147 | * Gets the number of available samples in the buffer 148 | * @param length The length to get the number of samples for 149 | */ 150 | public availableSamples(length?: number) { 151 | length = length || this.buffer.length; 152 | 153 | return Math.floor(length / ((this.args.bitDepth / 8) * this.args.channels)); 154 | } 155 | 156 | /** 157 | * The method that gets called when this stream is being written to 158 | */ 159 | public _write(chunk, encoding, next) { 160 | if (!this.hasData) { 161 | this.hasData = true; 162 | } 163 | 164 | this.buffer = Buffer.concat([this.buffer, chunk]); 165 | next(); 166 | } 167 | 168 | /** 169 | * Sets the volume of this input 170 | * @param volume The volume 171 | */ 172 | public setVolume(volume: number) { 173 | this.args.volume = Math.max(Math.min(volume, 100), 0); 174 | } 175 | 176 | /** 177 | * Gets the current volume for this input 178 | */ 179 | public getVolume() { 180 | return this.args.volume; 181 | } 182 | 183 | /** 184 | * Clears the buffer but keeps 1024 samples still in the buffer to avoid a possible empty buffer 185 | */ 186 | public clear(force?: boolean) { 187 | let now = new Date().getTime(); 188 | 189 | if (force || (this.args.clearInterval && now - this.lastClearTime >= this.args.clearInterval)) { 190 | let length = 1024 * (this.args.bitDepth / 8) * this.args.channels; 191 | this.buffer = this.buffer.slice(0, length); 192 | 193 | this.lastClearTime = now; 194 | } 195 | } 196 | 197 | /** 198 | * Clears the buffer 199 | */ 200 | public destroy() { 201 | this.buffer = new Buffer(0); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/mixer-interleaved.ts: -------------------------------------------------------------------------------- 1 | import { Mixer, MixerArguments } from './mixer'; 2 | import { Input, InputArguments } from './input'; 3 | import { Readable, ReadableOptions } from 'stream'; 4 | 5 | import * as _ from 'underscore'; 6 | 7 | export class InterleavedMixer extends Mixer { 8 | 9 | /** 10 | * Called when this stream is read from 11 | */ 12 | public _read() { 13 | let samples = this.getMaxSamples(); 14 | 15 | if (samples > 0 && samples !== Number.MAX_VALUE) { 16 | let mixedBuffer = new Buffer(samples * this.sampleByteLength * this.args.channels); 17 | 18 | mixedBuffer.fill(0); 19 | 20 | for (let c = 0; c < this.args.channels; c++) { 21 | let input = this.inputs[c]; 22 | 23 | if (input !== undefined && input.hasData) { 24 | let inputBuffer = input.readMono(samples); 25 | 26 | for (let i = 0; i < samples; i++) { 27 | let sample = this.readSample.call(inputBuffer, i * this.sampleByteLength); 28 | 29 | this.writeSample.call(mixedBuffer, sample, (i * this.sampleByteLength * this.args.channels) + (c * this.sampleByteLength)); 30 | } 31 | } 32 | } 33 | 34 | this.push(mixedBuffer); 35 | } else { 36 | setImmediate(this._read.bind(this)); 37 | } 38 | 39 | this.clearBuffers(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/mixer.ts: -------------------------------------------------------------------------------- 1 | import { Input, InputArguments } from './input'; 2 | import { Readable, ReadableOptions } from 'stream'; 3 | 4 | import * as _ from 'underscore'; 5 | 6 | export interface MixerArguments extends ReadableOptions { 7 | channels: number; 8 | sampleRate: number; 9 | bitDepth?: number; 10 | } 11 | 12 | export class Mixer extends Readable { 13 | 14 | protected args: MixerArguments; 15 | protected inputs: Input[]; 16 | 17 | protected sampleByteLength: number; 18 | 19 | protected readSample; 20 | protected writeSample; 21 | protected needReadable: boolean = true; 22 | 23 | private static INPUT_IDLE_TIMEOUT = 250; 24 | private _timer:any = null 25 | 26 | constructor(args: MixerArguments) { 27 | super(args); 28 | 29 | if (args.sampleRate < 1) { 30 | args.sampleRate = 44100; 31 | } 32 | 33 | let buffer = new Buffer(0); 34 | 35 | if (args.bitDepth === 8) { 36 | this.readSample = buffer.readInt8; 37 | this.writeSample = buffer.writeInt8; 38 | 39 | this.sampleByteLength = 1; 40 | } else if (args.bitDepth === 32) { 41 | this.readSample = buffer.readInt32LE; 42 | this.writeSample = buffer.writeInt32LE; 43 | 44 | this.sampleByteLength = 4; 45 | } else { 46 | args.bitDepth = 16; 47 | 48 | this.readSample = buffer.readInt16LE; 49 | this.writeSample = buffer.writeInt16LE; 50 | 51 | this.sampleByteLength = 2; 52 | } 53 | 54 | this.args = args; 55 | this.inputs = []; 56 | } 57 | 58 | /** 59 | * Called when this stream is read from 60 | */ 61 | public _read() { 62 | let samples = this.getMaxSamples(); 63 | 64 | if (samples > 0 && samples !== Number.MAX_VALUE) { 65 | let mixedBuffer = new Buffer(samples * this.sampleByteLength * this.args.channels); 66 | 67 | mixedBuffer.fill(0); 68 | 69 | this.inputs.forEach((input) => { 70 | if (input.hasData) { 71 | let inputBuffer = this.args.channels === 1 ? input.readMono(samples) : input.readStereo(samples); 72 | 73 | for (let i = 0; i < samples * this.args.channels; i++) { 74 | let sample = this.readSample.call(mixedBuffer, i * this.sampleByteLength) + Math.floor(this.readSample.call(inputBuffer, i * this.sampleByteLength) / this.inputs.length); 75 | this.writeSample.call(mixedBuffer, sample, i * this.sampleByteLength); 76 | } 77 | } 78 | }); 79 | 80 | this.push(mixedBuffer); 81 | } else if(this.needReadable) { 82 | clearImmediate(this._timer) 83 | this._timer = setImmediate(this._read.bind(this)); 84 | } 85 | 86 | this.clearBuffers(); 87 | } 88 | 89 | /** 90 | * Adds an input to this mixer 91 | * @param args The input's arguments 92 | */ 93 | public input(args: InputArguments, channel?: number) { 94 | let input = new Input({ 95 | channels: args.channels || this.args.channels, 96 | bitDepth: args.bitDepth || this.args.bitDepth, 97 | sampleRate: args.sampleRate || this.args.sampleRate, 98 | volume: args.volume || 100, 99 | clearInterval: args.clearInterval 100 | }); 101 | 102 | this.addInput(input, channel); 103 | 104 | return input; 105 | } 106 | 107 | /** 108 | * Removes the specified input 109 | * @param input The input 110 | */ 111 | public removeInput(input: Input) { 112 | this.inputs = _.without(this.inputs, input); 113 | } 114 | 115 | /** 116 | * Adds the specified input to this mixer 117 | * @param input The input 118 | */ 119 | public addInput(input: Input, channel?: number) { 120 | if (channel && (channel < 0 || channel >= this.args.channels)) { 121 | throw new Error("Channel number out of range"); 122 | } 123 | 124 | input.setMixer(this); 125 | 126 | this.inputs[channel || this.inputs.length] = input; 127 | } 128 | 129 | /** 130 | * Removes all of the inputs 131 | */ 132 | public destroy() { 133 | this.inputs = []; 134 | } 135 | 136 | public close(){ 137 | this.needReadable = false 138 | } 139 | 140 | /** 141 | * Gets the max number of samples from all inputs 142 | */ 143 | protected getMaxSamples() { 144 | let samples = Number.MAX_VALUE; 145 | 146 | this.inputs.forEach((input) => { 147 | let ias = input.availableSamples(); 148 | 149 | if (ias > 0) { 150 | input.lastDataTime = new Date().getTime(); 151 | } else if (ias <= 0 && new Date().getTime() - input.lastDataTime >= Mixer.INPUT_IDLE_TIMEOUT) { 152 | input.hasData = false; 153 | 154 | return; 155 | } 156 | 157 | if (input.hasData && ias < samples) { 158 | samples = ias; 159 | } 160 | }); 161 | 162 | return samples; 163 | } 164 | 165 | /** 166 | * Clears all of the input's buffers 167 | */ 168 | protected clearBuffers() { 169 | this.inputs.forEach((input) => { 170 | input.clear(); 171 | }); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const { InterleavedMixer } = require('./dist/index.js'); 2 | 3 | const AudioStream = require('audio-cmd-stream'); 4 | 5 | setTimeout(() => console.log('done'), 1234567); 6 | 7 | var mixer = new InterleavedMixer({ 8 | channels: 2 9 | }); 10 | 11 | var in1 = new AudioStream.Input(1); 12 | var in2 = new AudioStream.Input(3); 13 | 14 | var out = new AudioStream.Output(5); 15 | 16 | mixer.pipe(out); 17 | 18 | var mixIn1 = mixer.input({ 19 | channels: 2, 20 | clearInterval: 250 21 | }, 0); 22 | 23 | var mixIn2 = mixer.input({ 24 | channels: 2, 25 | clearInterval: 250 26 | }, 1); 27 | 28 | in1.pipe(mixIn1); 29 | in2.pipe(mixIn2); 30 | 31 | // setTimeout(function() { 32 | // in2.unpipe(mixIn2); 33 | 34 | // setTimeout(function() { 35 | // in2.pipe(mixIn2); 36 | // in1.unpipe(mixIn1); 37 | // }, 4000); 38 | // }, 4000); 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "node" 5 | ], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "target": "es6", 9 | "removeComments": true, 10 | "preserveConstEnums": true, 11 | "stripInternal": true, 12 | "declaration": true, 13 | "outDir": "dist", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ] 17 | }, 18 | "include": [ 19 | "src/**/*.ts" 20 | ] 21 | } --------------------------------------------------------------------------------