├── .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 | }
--------------------------------------------------------------------------------