diff --git a/.gitignore b/.gitignore index 1041c72..621038f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ bower_components # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release -dist/ # Dependency directories node_modules/ diff --git a/.npmignore b/.npmignore index aa4856e..39dcd2b 100644 --- a/.npmignore +++ b/.npmignore @@ -1,12 +1,8 @@ **/node_modules/ -build/ -src/ *.tgz debug.log gulpfile.js -**/tsconfig*.* -tslint.json webpack.* **/.vscode .npmignore diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..c1ceb87 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,3 @@ +export * from './mixer'; +export * from './mixer-interleaved'; +export * from './input'; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..976c832 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,8 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +Object.defineProperty(exports, "__esModule", { value: true }); +__export(require("./mixer")); +__export(require("./mixer-interleaved")); +__export(require("./input")); diff --git a/dist/input.d.ts b/dist/input.d.ts new file mode 100644 index 0000000..da1731c --- /dev/null +++ b/dist/input.d.ts @@ -0,0 +1,32 @@ +/// +import { Writable, WritableOptions } from 'stream'; +import { Mixer } from './mixer'; +export interface InputArguments extends WritableOptions { + channels?: number; + bitDepth?: number; + sampleRate?: number; + volume?: number; + clearInterval?: number; +} +export declare class Input extends Writable { + private mixer; + private args; + private buffer; + private sampleByteLength; + private readSample; + private writeSample; + hasData: boolean; + lastDataTime: number; + lastClearTime: number; + constructor(args: InputArguments); + setMixer(mixer: Mixer): void; + read(samples: any): Buffer; + readMono(samples: any): Buffer; + readStereo(samples: any): Buffer; + availableSamples(length?: number): number; + _write(chunk: any, encoding: any, next: any): void; + setVolume(volume: number): void; + getVolume(): number; + clear(force?: boolean): void; + destroy(): void; +} diff --git a/dist/input.js b/dist/input.js new file mode 100644 index 0000000..9a319a5 --- /dev/null +++ b/dist/input.js @@ -0,0 +1,109 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const stream_1 = require("stream"); +class Input extends stream_1.Writable { + constructor(args) { + super(args); + if (args.channels !== 1 && args.channels !== 2) { + args.channels = 2; + } + if (args.sampleRate < 1) { + args.sampleRate = 44100; + } + if (args.volume < 0 || args.volume > 100) { + args.volume = 100; + } + if (args.channels === 1) { + this.readMono = this.read; + } + if (args.channels === 2) { + this.readStereo = this.read; + } + this.buffer = new Buffer(0); + if (args.bitDepth === 8) { + this.readSample = this.buffer.readInt8; + this.writeSample = this.buffer.writeInt8; + this.sampleByteLength = 1; + } + else if (args.bitDepth === 32) { + this.readSample = this.buffer.readInt32LE; + this.writeSample = this.buffer.writeInt32LE; + this.sampleByteLength = 4; + } + else { + args.bitDepth = 16; + this.readSample = this.buffer.readInt16LE; + this.writeSample = this.buffer.writeInt16LE; + this.sampleByteLength = 2; + } + this.args = args; + this.hasData = false; + this.lastClearTime = new Date().getTime(); + } + setMixer(mixer) { + this.mixer = mixer; + } + read(samples) { + let bytes = samples * (this.args.bitDepth / 8) * this.args.channels; + if (this.buffer.length < bytes) { + bytes = this.buffer.length; + } + let sample = this.buffer.slice(0, bytes); + this.buffer = this.buffer.slice(bytes); + for (let i = 0; i < sample.length; i += 2) { + sample.writeInt16LE(Math.floor(this.args.volume * sample.readInt16LE(i) / 100), i); + } + return sample; + } + readMono(samples) { + let stereoBuffer = this.read(samples); + let monoBuffer = new Buffer(stereoBuffer.length / 2); + let availableSamples = this.availableSamples(stereoBuffer.length); + for (let i = 0; i < availableSamples; i++) { + let l = this.readSample.call(stereoBuffer, i * this.sampleByteLength * 2); + let r = this.readSample.call(stereoBuffer, (i * this.sampleByteLength * 2) + this.sampleByteLength); + this.writeSample.call(monoBuffer, Math.floor((l + r) / 2), i * this.sampleByteLength); + } + return monoBuffer; + } + readStereo(samples) { + let monoBuffer = this.read(samples); + let stereoBuffer = new Buffer(monoBuffer.length * 2); + let availableSamples = this.availableSamples(monoBuffer.length); + for (let i = 0; i < availableSamples; i++) { + let m = this.readSample.call(monoBuffer, i * this.sampleByteLength); + this.writeSample.call(stereoBuffer, m, i * this.sampleByteLength * 2); + this.writeSample.call(stereoBuffer, m, (i * this.sampleByteLength * 2) + this.sampleByteLength); + } + return stereoBuffer; + } + availableSamples(length) { + length = length || this.buffer.length; + return Math.floor(length / ((this.args.bitDepth / 8) * this.args.channels)); + } + _write(chunk, encoding, next) { + if (!this.hasData) { + this.hasData = true; + } + this.buffer = Buffer.concat([this.buffer, chunk]); + next(); + } + setVolume(volume) { + this.args.volume = Math.max(Math.min(volume, 100), 0); + } + getVolume() { + return this.args.volume; + } + clear(force) { + let now = new Date().getTime(); + if (force || (this.args.clearInterval && now - this.lastClearTime >= this.args.clearInterval)) { + let length = 1024 * (this.args.bitDepth / 8) * this.args.channels; + this.buffer = this.buffer.slice(0, length); + this.lastClearTime = now; + } + } + destroy() { + this.buffer = new Buffer(0); + } +} +exports.Input = Input; diff --git a/dist/mixer-interleaved.d.ts b/dist/mixer-interleaved.d.ts new file mode 100644 index 0000000..5a35ab9 --- /dev/null +++ b/dist/mixer-interleaved.d.ts @@ -0,0 +1,4 @@ +import { Mixer } from './mixer'; +export declare class InterleavedMixer extends Mixer { + _read(): void; +} diff --git a/dist/mixer-interleaved.js b/dist/mixer-interleaved.js new file mode 100644 index 0000000..f7f6b46 --- /dev/null +++ b/dist/mixer-interleaved.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const mixer_1 = require("./mixer"); +class InterleavedMixer extends mixer_1.Mixer { + _read() { + let samples = this.getMaxSamples(); + if (samples > 0 && samples !== Number.MAX_VALUE) { + let mixedBuffer = new Buffer(samples * this.sampleByteLength * this.args.channels); + mixedBuffer.fill(0); + for (let c = 0; c < this.args.channels; c++) { + let input = this.inputs[c]; + if (input !== undefined && input.hasData) { + let inputBuffer = input.readMono(samples); + for (let i = 0; i < samples; i++) { + let sample = this.readSample.call(inputBuffer, i * this.sampleByteLength); + this.writeSample.call(mixedBuffer, sample, (i * this.sampleByteLength * this.args.channels) + (c * this.sampleByteLength)); + } + } + } + this.push(mixedBuffer); + } + else { + setImmediate(this._read.bind(this)); + } + this.clearBuffers(); + } +} +exports.InterleavedMixer = InterleavedMixer; diff --git a/dist/mixer.d.ts b/dist/mixer.d.ts new file mode 100644 index 0000000..22f0795 --- /dev/null +++ b/dist/mixer.d.ts @@ -0,0 +1,27 @@ +/// +import { Input, InputArguments } from './input'; +import { Readable, ReadableOptions } from 'stream'; +export interface MixerArguments extends ReadableOptions { + channels: number; + sampleRate: number; + bitDepth?: number; +} +export declare class Mixer extends Readable { + protected args: MixerArguments; + protected inputs: Input[]; + protected sampleByteLength: number; + protected readSample: any; + protected writeSample: any; + protected needReadable: boolean; + private static INPUT_IDLE_TIMEOUT; + private _timer; + constructor(args: MixerArguments); + _read(): void; + input(args: InputArguments, channel?: number): Input; + removeInput(input: Input): void; + addInput(input: Input, channel?: number): void; + destroy(): void; + close(): void; + protected getMaxSamples(): number; + protected clearBuffers(): void; +} diff --git a/dist/mixer.js b/dist/mixer.js new file mode 100644 index 0000000..2b7e92e --- /dev/null +++ b/dist/mixer.js @@ -0,0 +1,107 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const input_1 = require("./input"); +const stream_1 = require("stream"); +const _ = require("underscore"); +class Mixer extends stream_1.Readable { + constructor(args) { + super(args); + this.needReadable = true; + this._timer = null; + if (args.sampleRate < 1) { + args.sampleRate = 44100; + } + let buffer = new Buffer(0); + if (args.bitDepth === 8) { + this.readSample = buffer.readInt8; + this.writeSample = buffer.writeInt8; + this.sampleByteLength = 1; + } + else if (args.bitDepth === 32) { + this.readSample = buffer.readInt32LE; + this.writeSample = buffer.writeInt32LE; + this.sampleByteLength = 4; + } + else { + args.bitDepth = 16; + this.readSample = buffer.readInt16LE; + this.writeSample = buffer.writeInt16LE; + this.sampleByteLength = 2; + } + this.args = args; + this.inputs = []; + } + _read() { + let samples = this.getMaxSamples(); + if (samples > 0 && samples !== Number.MAX_VALUE) { + let mixedBuffer = new Buffer(samples * this.sampleByteLength * this.args.channels); + mixedBuffer.fill(0); + this.inputs.forEach((input) => { + if (input.hasData) { + let inputBuffer = this.args.channels === 1 ? input.readMono(samples) : input.readStereo(samples); + for (let i = 0; i < samples * this.args.channels; i++) { + let sample = this.readSample.call(mixedBuffer, i * this.sampleByteLength) + Math.floor(this.readSample.call(inputBuffer, i * this.sampleByteLength) / this.inputs.length); + this.writeSample.call(mixedBuffer, sample, i * this.sampleByteLength); + } + } + }); + this.push(mixedBuffer); + } + else if (this.needReadable) { + clearImmediate(this._timer); + this._timer = setImmediate(this._read.bind(this)); + } + this.clearBuffers(); + } + input(args, channel) { + let input = new input_1.Input({ + channels: args.channels || this.args.channels, + bitDepth: args.bitDepth || this.args.bitDepth, + sampleRate: args.sampleRate || this.args.sampleRate, + volume: args.volume || 100, + clearInterval: args.clearInterval + }); + this.addInput(input, channel); + return input; + } + removeInput(input) { + this.inputs = _.without(this.inputs, input); + } + addInput(input, channel) { + if (channel && (channel < 0 || channel >= this.args.channels)) { + throw new Error("Channel number out of range"); + } + input.setMixer(this); + this.inputs[channel || this.inputs.length] = input; + } + destroy() { + this.inputs = []; + } + close() { + this.needReadable = false; + } + getMaxSamples() { + let samples = Number.MAX_VALUE; + this.inputs.forEach((input) => { + let ias = input.availableSamples(); + if (ias > 0) { + input.lastDataTime = new Date().getTime(); + } + else if (ias <= 0 && new Date().getTime() - input.lastDataTime >= Mixer.INPUT_IDLE_TIMEOUT) { + input.hasData = false; + return; + } + if (input.hasData && ias < samples) { + samples = ias; + } + }); + return samples; + } + clearBuffers() { + this.inputs.forEach((input) => { + input.clear(); + }); + } +} +Mixer.INPUT_IDLE_TIMEOUT = 250; +exports.Mixer = Mixer; diff --git a/package-lock.json b/package-lock.json index 249cb56..2ef525c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audio-mixer", - "version": "2.0.2", + "version": "2.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0d610c7..04838e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audio-mixer", - "version": "2.1.1", + "version": "2.1.2", "description": "Allows mixing of PCM audio streams.", "main": "dist/index.js", "types": "dist/index.d.ts",