audio-mixer
Version:
Allows mixing of PCM audio streams.
174 lines (134 loc) • 4.82 kB
text/typescript
import { Input, InputArguments } from './input';
import { Readable, ReadableOptions } from 'stream';
import * as _ from 'underscore';
export interface MixerArguments extends ReadableOptions {
channels: number;
sampleRate: number;
bitDepth?: number;
}
export class Mixer extends Readable {
protected args: MixerArguments;
protected inputs: Input[];
protected sampleByteLength: number;
protected readSample;
protected writeSample;
protected needReadable: boolean = true;
private static INPUT_IDLE_TIMEOUT = 250;
private _timer:any = null
constructor(args: MixerArguments) {
super(args);
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 = [];
}
/**
* Called when this stream is read from
*/
public _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();
}
/**
* Adds an input to this mixer
* @param args The input's arguments
*/
public input(args: InputArguments, channel?: number) {
let input = new 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;
}
/**
* Removes the specified input
* @param input The input
*/
public removeInput(input: Input) {
this.inputs = _.without(this.inputs, input);
}
/**
* Adds the specified input to this mixer
* @param input The input
*/
public addInput(input: Input, channel?: number) {
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;
}
/**
* Removes all of the inputs
*/
public destroy() {
this.inputs = [];
}
public close(){
this.needReadable = false
}
/**
* Gets the max number of samples from all inputs
*/
protected 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;
}
/**
* Clears all of the input's buffers
*/
protected clearBuffers() {
this.inputs.forEach((input) => {
input.clear();
});
}
}